manzt / anywidget

reusable widgets made easy
https://anywidget.dev
MIT License
500 stars 39 forks source link

Different behaviors among Colab, Jupyter Lab, and VS Code #89

Closed giswqs closed 1 year ago

giswqs commented 1 year ago

I am exploring the maplibre library with anywidget. The following code works perfectly with Colab. Jupyter Lab can display the map, but the map size is always fixed no matter what div.style.width and div.style.height values. VS Code throws errors. Any advice?

import anywidget
import traitlets

class MapWidget(anywidget.AnyWidget):
    _esm = """
    function loadScript(src) {
      return new Promise((resolve, reject) => {
        let script = Object.assign(document.createElement("script"), {
          type: "text/javascript",
          async: true,
          src: src,
        });
        script.addEventListener("load", resolve);
        script.addEventListener("error", reject);
        document.body.appendChild(script);
      });
    };

    await loadScript("https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js");

    export function render(view) {
      const div = document.createElement("div");
      div.style.width = "100%";
      div.style.height = "500px";

        var map = new maplibregl.Map({
        container: div,
        style: 'https://demotiles.maplibre.org/style.json', // stylesheet location
        center: [0, 20], // starting position [lng, lat]
        zoom: 2 // starting zoom
        });
      view.el.appendChild(div);
    }
    """
    # make sure to include styles
    _css = "https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css"
    token = traitlets.Unicode().tag(sync=True)

m = MapWidget()
m

Colab: image

Jupyter Lab: image

VS Code: image

manzt commented 1 year ago

I don't believe this is an anywidget bug. JupyterLab and Google Colab are related to how the the output areas in the DOM that JupyterLab and Google Colab provide (i.e., the DOM Node view.el). This is not something that anywidget has control over, and the same results would occur if you created a custom Jupyter widget with one of the cookiecutter templates.

The VS Code issue seems to be with your Python environment, and likely ipywidgets. Can you create a fresh environment?

manzt commented 1 year ago

actually these errors in VS Code are just adding noise. The issue is that the loadScript fails to add maplibregl as a global in the VSCode context leading to a ReferenceError because it isn't defined and you try to access maplibregl in the render function. Again, this issue stems from the the finicky nature of trying to load JavaScript that is not a standard module (ESM).

It's a hack and there will always be edge cases due to naming conflicts and environment globals in the various front-end environments (Colab, JupyterLab, VSCode). ESM is an official standard and avoids these issues all together by not allowing the import to rely on globals. anywidget can't fix this problem for you, but using ESM will.

I notice that maplibregl is on NPM. I'd suggest not using unpkg for this code and instead pick a modern CDN like esm.sh which mirrors NPM like unpkg, but additionally compiles/translates any non ESM JavaScript to ESM. This means you can ditch the loadScript code and use a valid ESM import.

I highly recommend learning more about ESM and reading the "getting started" section of our documentation: https://anywidget.dev/en/jupyter-widgets-the-good-parts/#tips-for-beginners

class MapWidget(anywidget.AnyWidget):
    _esm = """
    import maplibregl from "https://esm.sh/maplibre-gl@2.4.0";

    export function render(view) {
      const div = document.createElement("div");
      div.style.width = "100%";
      div.style.height = "500px";
      const map = new maplibregl.Map({
        container: div,
        style: 'https://demotiles.maplibre.org/style.json', // stylesheet location
        center: [0, 20], // starting position [lng, lat]
        zoom: 2 // starting zoom
      });
      view.el.appendChild(div);
    }
    """
    _css = "https://esm.sh/maplibre-gl@2.4.0?css"
MapWidget()
image
giswqs commented 1 year ago

@manzt Perfect! It works like a charm. Thank you.

How do you find out if a JS library is included in esm.sh? The esm.sh site does not seem to provide any search functionality like NPM. For now, I simply enter https://esm.sh/maplibre-gl and it redirects to https://esm.sh/maplibre-gl@2.4.0, and add ?css to get https://esm.sh/maplibre-gl@2.4.0?css. Is this the correct way?

manzt commented 1 year ago

Sorry for the delay in my response. esm.sh is a CDN that mirrors the npm registry, so packages that are on npm should be available via esm.sh under https://esm.sh/<package-name>. (read: you should be able to look up a package on npm and at least try to request it from esm.sh). An easy way to check is to copy and paste the esm.sh url in the browser.

The "magic" of esm.sh is that it will try to transform any JS that is not valid ESM to be compatible. This means that packages that don't publish ESM to npm can still be used via import from esm.sh domain.

The other thing you mentioned is with regard to the REST api that esm.sh exposes for requesting specific assets or versions from the CDN. The URL esm.sh/maplibre-gl automatically links to the latest version. It is best practice to check in the version in your package, however if you scroll down on the esm.sh landing page there are a lot of details about how to request a specific semver range, for example. The ?css is also described further down on the page, as a way to request styles. As a fallback, you can always request the exact files like on unpkg: https://esm.sh/maplibre-gl@2.4.0/dist/maplibre-gl.css

giswqs commented 1 year ago

Thanks for the advice. That's very helpful.

After switch to ESM, now the map widget works perfectly on both VS Code and Google Colab. However, this map size is always fixed with Jupyter Lab and Jupyter Notebook no matter what width and height values are specified. Any advice?

import anywidget
import traitlets

class MapWidget(anywidget.AnyWidget):
    _esm = """
    import maplibregl from "https://esm.sh/maplibre-gl@2.4.0";

    export function render(view) {

        let center = view.model.get("center");
        center.reverse();
        let zoom = view.model.get("zoom");
        let width = view.model.get("width");
        let height = view.model.get("height");

        const div = document.createElement("div");
        div.style.width = width;
        div.style.height = height;

        const map = new maplibregl.Map({
          container: div,
          style: 'https://demotiles.maplibre.org/style.json', // stylesheet location
          center: center, // starting position [lng, lat]
          zoom: zoom // starting zoom
        });
        view.el.appendChild(div);
    }
    """
    _css = "https://esm.sh/maplibre-gl@2.4.0/dist/maplibre-gl.css"

    center = traitlets.List([20, 0]).tag(sync=True, o=True)
    zoom = traitlets.Int(2).tag(sync=True, o=True)
    width = traitlets.Unicode("100%").tag(sync=True, o=True)
    height = traitlets.Unicode("600px").tag(sync=True, o=True)

m = MapWidget(center=[20, 0], zoom=2)
m

image