Creating map widgets #292
-
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 import anywidget
import traitlets
class MapWidget(anywidget.AnyWidget):
# Widget front-end JavaScript code
_esm = """
import * as L from "https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
export function render(view) {
// create a div element
const container = document.createElement("div");
container.id = "map";
container.style.height = "300px";
// 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);
canvas.appendChild(map)
view.el.appendChild(canvas);
}
"""
MapWidget() |
Beta Was this translation helpful? Give feedback.
Replies: 7 comments
-
Hi, thanks for trying out anywidget! Widgets not showing up typically means there are errors in your JavaScript, which generally fall into 3 categories:
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 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. |
Beta Was this translation helpful? Give feedback.
-
@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. |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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 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() |
Beta Was this translation helpful? Give feedback.
-
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. RecommendationTraitlets should be preferred over custom messages since widget state can be fully serialized and recreated without Python running. Write logic that treats your For your 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 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() |
Beta Was this translation helpful? Give feedback.
-
@manzt Thank you very much again for the detailed explanation. This is exactly what I was looking for. |
Beta Was this translation helpful? Give feedback.
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.
Recommendation
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 themap
objec…