Closed giswqs closed 1 year ago
Hi, thanks for trying out anywidget! Widgets not showing up typically means there are errors in your JavaScript, which generally fall into 3 categories:
import
statements (i.e., attempting to import JavaScript that is not an ECMAScript module)There is probably a way that anywidget could try to communicate these errors, minus 4, but it is intentionally just a wrapper around Jupyter Widgets (so it inherits the same error handling mechanism).
With that said, the snippet you shared contains issues 1, 2, and 4 above. First, the Leaflet JavaScript you are attempting to import is not ESM, but a UMD script that adds Leaflet
as a global. Second, your render
function contains a reference to a variable named canvas
which does not exist. Third, map
is not a JavaScript element and cannot be appended to view.el
. Finally, Leaflet's docs state that you must "include Leaflet CSS file in the head section of your document", which you also have not done. Here is a working snippet,
import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
# Widget front-end JavaScript code
_esm = """
// import ESM version of Leaflet
import * as L from "https://unpkg.com/leaflet@1.9.3/dist/leaflet-src.esm.js";
export function render(view) {
// create a div element
const container = document.createElement("div");
container.id = "map";
container.style.height = "300px";
container.style.width = "500px";
// create a Leaflet map
const map = L.map(container).setView([51.505, -0.09], 13);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors',
maxZoom: 18,
}).addTo(map);
view.el.appendChild(container);
}
"""
# make sure to include styles
_css = "https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
MapWidget()
I know that you, and many potential anywidget users, are likely not JavaScript experts but it's a great way learn some front end skills! I have answered this question in detail so that I can point others to the thread, but GitHub issues should be used to surface bugs/issues with anywidget and not for debugging custom JavaScript.
@manzt Many thanks for the detailed explanation and the working solution. Much appreciated.
I did not intend to open an issue for this. Since the Discussion board is not enabled for the repo, that's why I had to open an issue for asking this question. Appreciate your help.
You bet! It might be worth learning more about ESM if you are unfamiliar, since you are trying to use a third-party dependency in your project and knowing the basics would be helpful. Second, I recommend opening the browser console when developing a widget (right click webpage + Inspect)! You can see errors on the page and console.log()
from your _esm
and see values right there:
Awesome! Thank you for the tips. Your widget inspires me to learn the front-end stuff. It has opened a new door for me to explore all kinds of JavaScript libraries in Python. Hope to contribute to Anywidget in the near future.
Would you mind me asking a follow-up question? Is it possible to add a method to the widget class that can modify the javascript object initiated through the esm module? For example, I would like to add an add_layer()
method to MapWidget
so that I can add a layer to the map
after the map creation. Is this possible? I have been searching for a solution for hours without much success. Any advice would be appreciated.
import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
# Widget front-end JavaScript code
_esm = """
// import ESM version of Leaflet
import * as L from "https://unpkg.com/leaflet@1.9.3/dist/leaflet-src.esm.js";
export function render(view) {
// create a div element
let center = view.model.get("center");
let zoom = view.model.get("zoom");
const container = document.createElement("div");
container.id = "map";
container.style.height = "600px";
// container.style.width = "500px";
// create a Leaflet map
const map = L.map(container).setView(center, zoom);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors',
maxZoom: 18,
}).addTo(map);
view.el.appendChild(container);
}
"""
# make sure to include styles
_css = "https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
center = traitlets.List([40, -100]).tag(sync=True, o=True)
zoom = traitlets.Int(4).tag(sync=True, o=True)
# add a layer
def add_layer(self, url=None):
if url is None:
url = 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}'
self.send({"type": "add_layer", "url": url, "attribution": "Google"})
m = MapWidget(center=[40, -100], zoom=4)
m
# m.add_layer()
Would you mind me asking a follow-up question?
Sure thing.
EDIT: I have moved and edited the prior content from here to Jupyter Widgets: The Good Parts in the anywidget documentation. I have left the recommendation for your use case below, but recommend reading the new docs.
Traitlets should be preferred over custom messages since widget state can be fully serialized and recreated without Python running. Write logic that treats your view.model
as the source of truth (see the anywidget Two-Way Data-Binding Example.
For your MapWidget
I'd recommend thinking about representing the layers as a traitlet and then writing a render function that creates and modifies the map
object in the client. e.g.,
class MapWidget(anywidget.AnyWidget):
# ...
_layers = traitlets.List(traitlets.Dict()).tag(sync=True)
def add_layer(self, url = "...", attribution = "... "):
self._layers = self._layers + dict(url=url, attribution=attribution) # must re-assign to trigger update (cannot mutate)
An alternative (and probably more simple JavaScript) is to use custom messages like you have before, but just note
that add_layer
must be called after the widget is displayed.
import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
# Widget front-end JavaScript code
_esm = """
// import ESM version of Leaflet
import * as L from "https://unpkg.com/leaflet@1.9.3/dist/leaflet-src.esm.js";
export function render(view) {
// create a div element
let center = view.model.get("center");
let zoom = view.model.get("zoom");
const container = document.createElement("div");
container.style.height = "600px";
// create a Leaflet map
const map = L.map(container).setView(center, zoom);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors',
maxZoom: 18,
}).addTo(map);
view.model.on("msg:custom", (msg) => {
switch (msg.type) {
case "add_layer":
L.tileLayer(msg.url, { attribution: msg.attribution }).addTo(map);
break;
default:
console.err(`Unsupported message '${msg.type}'.`);
}
console.log(data);
});
view.el.appendChild(container);
}
"""
# make sure to include styles
_css = "https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
center = traitlets.List([40, -100]).tag(sync=True, o=True)
zoom = traitlets.Int(4).tag(sync=True, o=True)
# add a layer
def add_layer(self, url=None):
if url is None:
url = 'https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}'
self.send({"type": "add_layer", "url": url, "attribution": "Google"})
m = MapWidget(center=[40, -100], zoom=4)
m
# m.add_layer()
@manzt Thank you very much again for the detailed explanation. This is exactly what I was looking for. I have created a mapwidget Python package that builds upon anywidget. Looking forward to exploring anywidget more and adding new features to mapwidget. Really appreciate your awesome work and patience in answering questions!
I am interested in creating a map widget based on the leaflet javascript library. Here is an example. I am not a JavaScript expert. Do you know why the map widget does not show up?
https://leafletjs.com/examples/quick-start/example-overlays.html