Issue using MapLibre with ReactiveHTML

Hi,
I’m trying to create a ReactiveHTML component that uses MapLibre.

I cannot get to work at all using the latest version of Panel.

Here is a simple code to reproduce the issue:

from panel.reactive import ReactiveHTML


class MapLibre(ReactiveHTML):
    _template = """
        <div id="map-container" style="width: 100%; height: 100%;"></div>
    """

    _scripts = {
        "init": """
            var map = new maplibregl.Map({
                container: "map-container" ,
                style: 'https://demotiles.maplibre.org/style.json', // stylesheet location
                center: [-74.5, 40], // starting position [lng, lat]
                zoom: 9 // starting zoom
            });

        """,
        "render": """
            self.init()
        """,
    }

    _extension_name = "maplibre"

    __javascript__ = ["https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.js"]
    __css__ = ["https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.css"]


if __name__.startswith("bokeh"):
    import panel as pn

    pn.extension("maplibre")

    map = MapLibre(sizing_mode="stretch_both")
    app = pn.Column(map, sizing_mode="stretch_both")
    app.servable()

when running the file via panel serve, I’m getting the following error:

Error rendering Bokeh items: Error: Container 'map-container' not found.

If I try with Panel 0.14, I get the same error by default, but I can get it to work by getting the element manually onto which to attach the MapLibre instance:

from panel.reactive import ReactiveHTML


class MapLibre(ReactiveHTML):
    _template = """
        <div id="map-container" style="width: 100%; height: 100%;"></div>
    """

    _scripts = {
        "init": """
            var e = Array.from(document.querySelectorAll("div")).filter(x=> x.id.includes("map"))[0]

            var map = new maplibregl.Map({
                container: e,
                style: 'https://demotiles.maplibre.org/style.json', // stylesheet location
                center: [-74.5, 40], // starting position [lng, lat]
                zoom: 9 // starting zoom
            });

        """,
        "render": """
            self.init()
        """,
    }

    _extension_name = "maplibre"

    __javascript__ = ["https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.js"]
    __css__ = ["https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.css"]


if __name__.startswith("bokeh"):
    import panel as pn

    pn.extension("maplibre")

    map = MapLibre(sizing_mode="stretch_both")
    app = pn.Column(map, sizing_mode="stretch_both")
    app.servable()

But this hack does not work with Panel 1.x as the elements are in a shadow dom that I can’t access.

Any idea on how to make it work?

Hi @Julien

Elements in your _template that have an id can be referred to by name. So you can refer to the element with id map-container via the variable map_container.

I.e. this works

import panel as pn
from panel.reactive import ReactiveHTML


class MapLibre(ReactiveHTML):
    _template = """
        <div id="map-container" style="width: 100%; height: 100%;"></div>
    """

    _scripts = {
        "init": """
        var map = new maplibregl.Map({
            container: map_container,
            style: 'https://demotiles.maplibre.org/style.json', // stylesheet location
            center: [-74.5, 40], // starting position [lng, lat]
            zoom: 9 // starting zoom
        });
        """,
        "render": """
        console.log(map_container)
        self.init()
        """,
    }

    _extension_name = "maplibre"

    __javascript__ = ["https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.js"]
    __css__ = ["https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.css"]


if pn.state.served:
    import panel as pn

    pn.extension("maplibre")

    map = MapLibre(sizing_mode="stretch_both")
    app = pn.Column(map, sizing_mode="stretch_both")
    app.servable()

@Marc, awesome, thanks.

I did not knew it worked that way.
Looking at the documentation, I assumed it was only true for the tag name not the ids.

1 Like

If you can be specific about where we could/ should improve things, please let us know.

I’m working on an extended example. There are a few issues to fix like getting the map to resize.

1 Like

Hi @Julien

Here is an extended example that shows how to

  • Get the map to size to the full container.
  • Send data from Python to JS.
  • Send click_events from JS to Python

Its inspired by Create a time slider | MapLibre GL JS Docs | MapLibre

If you like this example please consider liking or sharing this tweet or LinkedIn post. Thanks.

maplibre

import datetime

import panel as pn
import param
import requests
from panel.reactive import ReactiveHTML

url = "https://maplibre.org/maplibre-gl-js-docs/assets/significant-earthquakes-2015.geojson"


# Workaround. See https://github.com/holoviz/panel/issues/5189
pn.FloatPanel.param.offsetx.bounds = (None, None)


@pn.cache()
def get_data():
    response = requests.get(url)
    data = response.json()
    for item in data["features"]:
        properties = item["properties"]
        time = datetime.datetime.fromtimestamp(properties["time"] / 1000)
        properties["month"] = time.month
    return data


earthquake_data = get_data()


class MapLibre(ReactiveHTML):
    _template = """
        <div id="map-container" style="width: 100%; height: 100%;"></div>
    """

    data = param.Dict(earthquake_data)
    click_event = param.Dict()
    month = param.Integer(1, bounds=(1, 12))

    _scripts = {
        "init": """
        state.map = new maplibregl.Map({
            container: map_container,
            style:
            'https://api.maptiler.com/maps/streets/style.json?key=get_your_own_OpIi9ZULNHzrESv6T2vL',
            center: [31.4606, 20.7927],
            zoom: 0.5
        });
        state.map.on('click', function(e) {
            data.click_event = {"lng": e.lngLat.lng, "lat": e.lngLat.lat, "time": Date.now()};
        });
        state.map.on('load', function () {
            self.add_layers()
        })
        """,
        "add_layers": """
        state.map.addSource('earthquakes', {
            'type': 'geojson',
            data: data.data
        });
        state.map.addLayer({
        'id': 'earthquake-circles',
        'type': 'circle',
        'source': 'earthquakes',
        'paint': {
        'circle-color': [
        'interpolate',
        ['linear'],
        ['get', 'mag'],
        6,
        '#FCA107',
        8,
        '#7F3121'
        ],
        'circle-opacity': 0.75,
        'circle-radius': [
        'interpolate',
        ['linear'],
        ['get', 'mag'],
        6,
        20,
        8,
        40
        ]
        }
        });
        state.map.addLayer({
        'id': 'earthquake-labels',
        'type': 'symbol',
        'source': 'earthquakes',
        'layout': {
        'text-field': [
        'concat',
        ['to-string', ['get', 'mag']],
        'm'
        ],
        'text-font': [
        'Open Sans Bold',
        'Arial Unicode MS Bold'
        ],
        'text-size': 12
        },
        'paint': {
        'text-color': 'rgba(0,0,0,0.5)'
        }
        });
        self.month()
        """,
        "render": """
        self.init()
        """,
        "after_layout": """
state.map.resize()
""",
        "month": """
        var filters = ['==', 'month', data.month];
        state.map.setFilter('earthquake-circles', filters);
        state.map.setFilter('earthquake-labels', filters);
        """,
    }

    _extension_name = "maplibre"

    __javascript__ = ["https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.js"]
    __css__ = ["https://unpkg.com/maplibre-gl@3.1.0/dist/maplibre-gl.css"]


if pn.state.served:
    import panel as pn

    pn.extension("floatpanel", "maplibre")

    map = MapLibre(sizing_mode="stretch_both")
    settings = pn.FloatPanel(
        map.param.month,
        map.param.click_event,
        name="Settings",
        contained=False,
        offsetx=-45,
        offsety=110,
    )
    app = pn.Column(map, settings, styles={"background": "blue"}, sizing_mode="stretch_both")
    pn.template.FastListTemplate(
        site="MapLibre + Panel", title="Earthquakes 2015 using custom Panel component", main=[app]
    ).servable()
panel serve app.py

That is a great demo, thanks.
I’ll definitely take inspirations from it.

1 Like

For the documentation, I think the paragraph about the <node> could be improved/clarified. Even for older version of Panel, I always found unclear what it represents and how to use it. The example uses the html tag input. Reading from the documentation, it was not evident to understand that one can use the id name as a variable in the _scripts object.

link to the specific part in the documentation: https://panel.holoviz.org/explanation/components/components_custom.html#scripts

Also, maybe it just me, but I find it harder to find the documentation about the ReactiveHTML in the new documentation layout/site. That being said, for the rest the new documentation site is much better than before.

Finally, it may be nice to know how the ReactiveHTML plays with the new Bokeh rendering engine:

  • it is still advised to bypass the bokeh rendering engine?
  • if so, in which situation?
  • what are the limits for the ReactiveHTML? Are they scenario where they should not be used or where a panel components/layout is prefered? (in term of performance for example).

For the last point, as an example, in one of the applications of my company built with Panel 0.14, I used the ReactiveHTML as a container for other elements, as it was easier to get the style we wanted to have as opposed to do it via a panel widget or components. I have yet to migrate it to Panel 1.x and I wondering about all those custom ReactiveHTML components and if they are necessary with the new Bokeh rendering engine.

Hope that helps.

2 Likes

Thanks. In Panel 1.x layouts are much faster and you will need ReactiveHTML components a lot less to speed up layouts.