calculating width in embedded Leaflet map

In this thread, @j-f1 created an HTML template of @toja’s Embed notebooks with custom width notebook. The script works beautifully for the Bivariate Choropleth, but I am getting an error when trying to embed Tom’s Leaflet map: map = RuntimeError: width could not be resolved

Curiously, I get this error even where I defined a width=800 cell in the source notebook (after forking Tom’s example and replacing the code, of course), as well as in my attempt at explicitly defining the div element width as <div width="800"> in the HTML document. The browser console returns no errors.

Any ideas how to get around this?

Here’s my draft code, as it currently stands:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
        <link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" crossorigin=""/>

    </head>
    <body>
  <div class="container">
    <div class="col-sm-6">
	  <div id="chart" style="max-width: 800px"></div>
  </div>
  </div>

  <div class="container">
    <div class="col-sm-6">
  <div id="grandCentralMap" style="max-width: 800px"></div>
  </div>
  </div>

        <script type="module">
        // use static imports because top-level `await` isn’t yet supported in browsers
        import notebook from "https://api.observablehq.com/@d3/bivariate-choropleth.js?v=3";
        // use a URL instead of a package reference because bare specifiers (like `@observablehq/runtime`) aren’t supported in browsers
        import { Runtime, Inspector, Library } from "https://unpkg.com/@observablehq/runtime@4/dist/runtime.js";

        // only initialize the standard library once
        const library = new Library()
        function customWidth() {
          return library.Generators.observe(function(change) {
            let width = change(target.clientWidth);

            function resized() {
              let w = target.clientWidth;
              if (w !== width) change(width = w);
            }
            window.addEventListener("resize", resized);
            return function() {
              window.removeEventListener("resize", resized);
            };
          });
        }
        library.width = customWidth

        function renderNotebook(notebook, cellNames) {
          new Runtime(library).module(notebook, name => {
            if (cellNames.includes(name)) {
              return new Inspector(document.querySelector(`#${name}`));
            }
          });
        }

        renderNotebook(notebook, ["chart"])

		</script>

    <script type="module">
    // use static imports because top-level `await` isn’t yet supported in browsers
    import notebook from "https://api.observablehq.com/d/310b9c62aec0e434.js?v=3";
    // use a URL instead of a package reference because bare specifiers (like `@observablehq/runtime`) aren’t supported in browsers
    import { Runtime, Inspector, Library } from "https://unpkg.com/@observablehq/runtime@4/dist/runtime.js";


    // only initialize the standard library once
    const library = new Library()
    function customWidth() {
      return library.Generators.observe(function(change) {
        let width = change(target.clientWidth);

        function resized() {
          let w = target.clientWidth;
          if (w !== width) change(width = w);
        }
        window.addEventListener("resize", resized);
        return function() {
          window.removeEventListener("resize", resized);
        };
      });
    }
    library.width = customWidth


function renderNotebook(notebook, cellNames) {
new Runtime(library).module(notebook, name => {
if (cellNames.includes(name)) {
  return new Inspector(document.querySelector(`#${name}`));
}
});
}

renderNotebook(notebook, ["grandCentralMap"])

</script>

    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
    </body>
  </html>

And here’s a picture of the rendered output:

image

Sorry for not trying this earlier (and it doesn’t exactly answer my question), but:

if I change in the source notebook this style definition:

style: `width:${width}px;height:${width/1.6}px` })

to

style: `width:${800}px;height:${800/1.6}px` })

… then the map will render. It also will render if I start out the map cell by defining width explicitly as let width = 800. But in neither case does the map render responsively :frowning:

Also a bit confusing for me (and probably part of the issue) is that in the choropleth example, there is no defined width aside from the scale (unless it’s getting this from viewBox?). So is it not also getting the width from window.innerWidth as per the standard library?

Any insights?


I eventually found this related discussion on overriding the standard library, which resulted in Mike creating this demo:

Mike’s resize pattern is very similar to Torben’s and results in the same error on ‘width’ when I try to update the code to fit the new embed pattern (the gist was returning the same Uncaught TypeError: r is not iterable error addressed here):

    <script type="module">
    // use static imports because top-level `await` isn’t yet supported in browsers
    import notebook from "https://api.observablehq.com/@tmcw/leaflet.js?v=3";
    // use a URL instead of a package reference because bare specifiers (like `@observablehq/runtime`) aren’t supported in browsers
    import { Runtime, Inspector, Library } from "https://unpkg.com/@observablehq/runtime@4/dist/runtime.js";


     const stdlib = new Library;

    const grandCentralMap = document.querySelector("#grandCentralMap");

    const library = Object.assign({}, stdlib, {width});

    function width() {
      return stdlib.Generators.observe(notify => {
        let width = notify(test.clientWidth);

        function resized() {
          let width1 = test.clientWidth;
          if (width1 !== width) notify(width = width1);
        }

        window.addEventListener("resize", resized);
        return () => window.removeEventListener("resize", resized);
      });
    }

function renderNotebook(notebook, cellNames) {
new Runtime(library).module(notebook, name => {
if (cellNames.includes(name)) {
  return new Inspector(document.querySelector(`#${name}`));
}
});
}

renderNotebook(notebook, ["grandCentralMap"])

</script>


I also remain a bit confused about what does and doesn’t carry-over into an embedded cell. CSS won’t carryover, as noted in Jeremy’s primer on embedding (when I learned to read it more carefully thanks to @mootari ), but charts generated as the amalgams of multiple individual cells will render… except apparently when that cell is width?


Happy if someone can shed some light on what’s going on, and would greatly appreciate help in arriving at a responsive Leaflet map element. :pray:

Looks like I might have been going about this the wrong way, and that the height and width would be better controlled using CSS. Or, at least a few Internet articles are leading me to this conclusion… such as this one:

I did, however, find some JavaScript from @mourner that appears promising:

…oh, and there’s also this project related to getting leaflet to play nicely with bootstrap’s grid:

… thought it seems like overkill.

I’ll keep hacking away… but still would be very appreciative for any help, guidance and insights!

I’d avoid phrasing it like that. Whatever a cell that you explicitely render returns/outputs gets added to the page:

  1. If a cell outputs CSS (be it as <style> or <link> element) that gets added as well.
  2. CSS can always apply to the whole page. Wether it does depends solely on the rules inside a stylesheet.
  3. If a library displays some sort of UI (like leaflet does) it often requires an additional stylesheet to be embedded.
  4. Such a stylesheet will affect the whole page, and with it all of your rendered leaflet instances.

If you plan to produce embeddable cells I’d recommend to include the required <link> element in the cell’s output. Having multiple references to a stylesheet on the same page won’t hurt, but having none will. :slight_smile:

1 Like

Thanks @mootari, though I fear that I don’t follow most of this… and probably it’s how I phrased it:

If I were to reference a named cell with a style element independently into an embedded notebook, then yes - it is there. However, if I don’t explicitly call in this cell, it is not there (as you pointed out in my leaflet map CSS question). But isn’t a rendered cell the culmination of all cells in a notebook that affect it?

I suppose that I understand that the CSS in a notebook affects all elements in that page without being strictly ‘needed’ for (or referenced into) a particular cell [though necessary to make it look right], but I remain stymied why I can defined a width=800 as a separate cell in the Leaflet map, have it affect the map dimension, and yet this still returns a missing width error in the embedded cell… while setting setting let width = 800 within the Leaflet cell will allow it to render. This suggests width is special (which, of course, it is…)… but I don’t get how to work around this ‘special’ effect to make a responsive embedded Leaflet map.

OK… so maybe I do get this part, but is this to say that in order to control the presentation of a Leaflet map within a responsive gridspace, I must use CSS? That is, the approach @mbostock and @toja used for the choropleth chart simply won’t work for Leaflet?

I am eager to try this solution (I attributed it to @mourner, but actually it might have come from @omgan):

$(window).on("resize", function () { $("#map").height($(window).height()); map.invalidateSize(); }).trigger("resize");

… I’ll update again if I make any progress.

:pray: would be excited to find an Observable-centric solution to this one, though maybe I ought just to give up and take the standard approach to writing out an HTML / JS / CSS page and stop trying to embed this as a cell.

[To wit: I am trying to embed just b/c I really enjoy building out everything with Observable; my life was soo much more painful before this platform - so thanks again Observable team!] :heart:

If the cell being embedded or imported references another cell that returns a STYLE or LINK element, either directly or indirectly, then that element will exist (i.e., the referenced cell will be evaluated), but the referenced styles won’t affect the styles on the page unless the STYLE or LINK element is actually inserted into the DOM. If it’s not inserted into the DOM, then it’s a detached element, which has no effect on the page’s styles.

On Observable, a STYLE or LINK element is implicitly inserted into the DOM by the inspector because the cell is visible on the page. That won’t necessarily be true with embedding (and imports) because the inspector only runs on the cells you specify; if you want the styles, you must explicitly embed the cell that returns a STYLE or LINK element, or explicitly insert that element into the DOM yourself.

1 Like

As for the other problem, here is a simple demonstration of how to embed the two notebooks on the page and limit the max-width to 800px (without even needing a custom width definition).

https://bl.ocks.org/mbostock/raw/7f03057b2b4db398b9d0988bbef2f409/

You can view the source here:

Note that I needed to embed the Leaflet stylesheet explicitly because your notebook doesn’t expose it as a named cell. (I also corrected the version of the stylesheet to be consistent with the library.)

1 Like

Thanks for this, Mike. It’s so much easier to create, fork, and tweak independent Observable notebooks and embed them into a webpage than it is to write this out as Vanilla HTML, JS & CSS. Here are two versions of essentially the same concept (hosted via GitHub), 1) using the solution you provide here, and 2) using traditional HTML (which still is not working perfectly, as the map disappears when I shrink down the viewing window too far).

What I appreciate the most is that, when stacking map scripts in traditional HTML, I have to rename all my ‘map’ variables, layer variables, control.layers, etc. to ensure that they don’t all show up on my first or last map. Using embed, I have comparatively far fewer places to edit, and also a ‘master’ source file that I can adjust and have instantly update if I end up using the same map in many locations. Really handy!

… I still have a lot to learn for styling, etc. The CSS remains mystifying in the sense that I can’t specify style="max-width:800px" in-line for one div container and style="max-width:600px" for another; it seems all these adjustment need to be declared in an up-front CSS variable in the document head? I also noticed that in the standard HTML approach, when I started trying to adjust the Leaflet on Mobile to work within a Bootstrap container, I also had to define the document body height at the outset as well as the map height, and then define the container with exactly h-100 for the map to appear (with adjustments to height being controlled not inline, but rather in the head CSS definition). All of this is dizzying…

Thanks to everyone for your help and guidance! I really appreciate it. :star_struck:

Can you elaborate how you arrived at that conclusion?

Trial and error (mostly error):

Here I try to use the inline CSS <div id="map1" style="max-width:800px"></div>, but to no avail:

https://aaronkyle.github.io/concept/data-visualization/sandbox/leaflet-responsive-inline-css.html

Here I define the CSS in the header for each div with a specific map ID, as part of the main style definition:

<style>
@import url("https://cdn.jsdelivr.net/npm/leaflet@1.3.0/dist/leaflet.css");
body {  margin: auto; }
#map1 div { margin: auto; max-width: 800px; }
#map2 div { margin: auto; max-width: 600px; }
#map3 div { margin: auto; max-width: 400px; }
</style>

and it works swimmingly:

https://aaronkyle.github.io/concept/data-visualization/sandbox/leaflet-responsive-upfront-css.html

:slight_smile: Thanks for continuing to follow this & for helping me out!!!

It looks like you’re actively forcing an inline width in your inner container:

  let container = DOM.element('div', { style: `width:${width}px;height:${width/1.6}px` });

Remove that, and the outer inline style will apply as expected. Also, be aware that referencing width will force your map to reinitialize on every resize.

In general I’d avoid using rules that modify the internal CSS of maps (leaflet or otherwise) like your #mapX div rules, as those can throw off both the map UI and interactions. If you want to only override the inner container’s inline CSS you can use:

#mapX > div {max-width: 123px !important}
1 Like