From 6e8287c4a9a1539cbf629b56c580bb23aa6c8c6c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 16 Dec 2020 15:13:55 +0100 Subject: [PATCH 1/4] Allow per layer tooltips on DeckGL pane --- panel/models/deckgl.py | 2 +- panel/models/deckgl.ts | 2 +- panel/models/tooltips.ts | 18 +++++++++++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/panel/models/deckgl.py b/panel/models/deckgl.py index 55679fceb8..48b1f60e60 100644 --- a/panel/models/deckgl.py +++ b/panel/models/deckgl.py @@ -66,7 +66,7 @@ def __js_skip__(cls): mapbox_api_key = String() - tooltip = Either(Bool, Dict(String, String)) + tooltip = Either(Bool, Dict(Any, Any)) clickState = Dict(String, Any) diff --git a/panel/models/deckgl.ts b/panel/models/deckgl.ts index a9dbeceb5b..c51edfd37a 100644 --- a/panel/models/deckgl.ts +++ b/panel/models/deckgl.ts @@ -144,7 +144,7 @@ export class DeckGLPlotView extends PanelHTMLBoxView { let deckgl; try { const props = this.jsonConverter.convert(jsonInput); - const getTooltip = makeTooltip(tooltip); + const getTooltip = makeTooltip(tooltip, props.layers); deckgl = new (window as any).deck.DeckGL({ ...props, map: (window as any).mapboxgl, diff --git a/panel/models/tooltips.ts b/panel/models/tooltips.ts index b3b5eb2e0f..bf6027b3c1 100644 --- a/panel/models/tooltips.ts +++ b/panel/models/tooltips.ts @@ -144,23 +144,35 @@ export function substituteIn(template: any, json: any) { return output; } -export function makeTooltip(tooltip: any) { +export function makeTooltip(tooltips: any, layers: any[]) { /* * If explictly no tooltip passed by user, return null * If a JSON object passed, return a tooltip based on that object * We expect the user has passed a string template that will take pickedInfo keywords * If a boolean passed, return the default tooltip */ - if (!tooltip) { + if (!tooltips) { return null; } - if (tooltip.html || tooltip.text) { + let per_layer = false + const layer_tooltips: any = {} + for (let i = 0; i < layers.length; i++) { + const layer = layers[i] + const layer_id = (layer.id as string) + if (i.toString() in tooltips || layer_id in tooltips) { + layer_tooltips[layer_id] = layer_id in tooltips ? tooltips[layer_id]: tooltips[i.toString()] + per_layer = true + } + } + + if (tooltips.html || tooltips.text || per_layer) { return (pickedInfo: any) => { if (!pickedInfo.picked) { return null; } + const tooltip = (per_layer) ? layer_tooltips[pickedInfo.layer.id]: tooltips const formattedTooltip: any = { style: tooltip.style || DEFAULT_STYLE }; From 8161e7afd31fcfa6fcc1ae3101c1977b28c2e796 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 16 Dec 2020 17:19:18 +0100 Subject: [PATCH 2/4] Fix docs --- examples/reference/panes/DeckGL.ipynb | 137 +++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 5 deletions(-) diff --git a/examples/reference/panes/DeckGL.ipynb b/examples/reference/panes/DeckGL.ipynb index f59aac8830..a1cdccc361 100644 --- a/examples/reference/panes/DeckGL.ipynb +++ b/examples/reference/panes/DeckGL.ipynb @@ -25,7 +25,7 @@ "\n", "* **``mapbox_api_key``** (string): The MapBox API key if not supplied by a PyDeck object.\n", "* **``object``** (object, dict or string): The deck.GL JSON or PyDeck object being displayed\n", - "* **``tooltips``** (boolean, default=True): Whether to enable tooltips\n", + "* **``tooltips``** (bool or dict, default=True): Whether to enable tooltips or custom tooltip formatters\n", "\n", "In addition to parameters which control how the object is displayed the DeckGL pane also exposes a number of parameters which receive updates from the plot:\n", "\n", @@ -72,10 +72,13 @@ " \"elevationScale\": 50,\n", " \"extruded\": True,\n", " \"getPosition\": \"@@=[lng, lat]\",\n", - " \"id\": \"8a553b25-ef3a-489c-bbe2-e102d18a3211\", \"pickable\": True\n", + " \"id\": \"8a553b25-ef3a-489c-bbe2-e102d18a3211\",\n", + " \"pickable\": True\n", " }],\n", " \"mapStyle\": \"mapbox://styles/mapbox/dark-v9\",\n", - " \"views\": [{\"@@type\": \"MapView\", \"controller\": True}]\n", + " \"views\": [\n", + " {\"@@type\": \"MapView\", \"controller\": True}\n", + " ]\n", "}\n", "\n", "deck_gl = pn.pane.DeckGL(json_spec, mapbox_api_key=MAPBOX_KEY, sizing_mode='stretch_width', height=600)\n", @@ -114,7 +117,129 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Alternatively the `DeckGL` pane can also be given a PyDeck object to render:" + "## Tooltips\n", + "\n", + "By default tooltips can be disabled and enabled by setting `tooltips=True/False`. For more customization it is possible to pass in a dictionary defining the keys. Let us start by declaring a plot with two layers:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "DATA_URL = 'https://raw.githubusercontent.com/uber-common/deck.gl-data/master/examples/geojson/vancouver-blocks.json'\n", + "\n", + "LAND_COVER = [[[-123.0, 49.196], [-123.0, 49.324], [-123.306, 49.324], [-123.306, 49.196]]]\n", + "\n", + "json_spec = {\n", + " \"initialViewState\": {\n", + " 'latitude': 49.254,\n", + " 'longitude': -123.13,\n", + " 'zoom': 11,\n", + " 'maxZoom': 16,\n", + " 'pitch': 45,\n", + " 'bearing': 0\n", + " },\n", + " \"layers\": [{\n", + " '@@type': 'GeoJsonLayer',\n", + " 'id': 'geojson',\n", + " 'data': DATA_URL,\n", + " 'opacity': 0.8,\n", + " 'stroked': True,\n", + " 'filled': True,\n", + " 'extruded': True,\n", + " 'wireframe': True,\n", + " 'fp64': True,\n", + " 'getLineColor': [255, 255, 255],\n", + " 'getElevation': \"@@=properties.valuePerSqm / 20\",\n", + " 'getFillColor': \"@@=[255, 255, properties.growth * 255]\",\n", + " 'pickable': True,\n", + " }, {\n", + " '@@type': 'PolygonLayer',\n", + " 'id': 'landcover',\n", + " 'data': LAND_COVER,\n", + " 'stroked': True,\n", + " 'pickable': True,\n", + " # processes the data as a flat longitude-latitude pair\n", + " 'getPolygon': '@@=-',\n", + " 'getFillColor': [0, 0, 0, 20]\n", + " }],\n", + " \"mapStyle\": \"mapbox://styles/mapbox/dark-v9\",\n", + " \"views\": [\n", + " {\"@@type\": \"MapView\", \"controller\": True}\n", + " ]\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have explicitly given these layers the `id` `'landcover'` and `'geojson'`. Ordinarily we wouldn't enable `pickable` property on the 'landcover' polygon and if we only have a single `pickable` layer it is sufficient to declare a tooltip like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geojson_tooltip = {\n", + " \"html\": \"\"\"\n", + " Value per Square meter: {properties.valuePerSqm}
\n", + " Growth: {properties.growth}\n", + " \"\"\",\n", + " \"style\": {\n", + " \"backgroundColor\": \"steelblue\",\n", + " \"color\": \"white\"\n", + " }\n", + "}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here we created an HTML template which is populated by the `properties` in the GeoJSON and then has the `style` applied. \n", + "\n", + "If we have multiple pickable layers we can declare distinct tooltips by nesting the tooltips dictionary, indexed by the layer `id` or the index of the layer in the list of layers (note that the dictionary must be either integer indexed or string indexed not both)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "tooltip = {\n", + " \"geojson\": geojson_tooltip,\n", + " \"landcover\": {\n", + " \"html\": \"The background\",\n", + " \"style\": {\n", + " \"backgroundColor\": \"red\",\n", + " \"color\": \"white\"\n", + " }\n", + " }\n", + "}\n", + "\n", + "pn.pane.DeckGL(json_spec, tooltips=tooltip, mapbox_api_key=MAPBOX_KEY, sizing_mode='stretch_width', height=600)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When hovering on the area around Vancouver you should now see a tooltip saying `'The background'` colored red, while the hover tooltip should show information about each property when hovering over one of the property polygons." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### PyDeck\n", + "\n", + "Instead of writing out raw JSON-like dictionaries the `DeckGL` pane may also be given a PyDeck object to render:" ] }, { @@ -184,7 +309,9 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "pn.Row(deck_gl.controls(), deck_gl)" + ] } ], "metadata": { From 927a3f89ab863c34e5eedd96494445ba46cc2270 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 16 Dec 2020 17:19:35 +0100 Subject: [PATCH 3/4] Improvements for DeckGL tooltips --- panel/models/tooltips.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/panel/models/tooltips.ts b/panel/models/tooltips.ts index bf6027b3c1..03656d460f 100644 --- a/panel/models/tooltips.ts +++ b/panel/models/tooltips.ts @@ -138,9 +138,12 @@ export function toText(jsonValue: any) { export function substituteIn(template: any, json: any) { let output = template; for (const key in json) { + if (typeof json[key] === 'object') { + for (const subkey in json[key]) + output = output.replace(`{${key}.${subkey}}`, json[key][subkey]); + } output = output.replace(`{${key}}`, json[key]); } - return output; } @@ -160,7 +163,7 @@ export function makeTooltip(tooltips: any, layers: any[]) { for (let i = 0; i < layers.length; i++) { const layer = layers[i] const layer_id = (layer.id as string) - if (i.toString() in tooltips || layer_id in tooltips) { + if (typeof tooltips !== "boolean" && (i.toString() in tooltips || layer_id in tooltips)) { layer_tooltips[layer_id] = layer_id in tooltips ? tooltips[layer_id]: tooltips[i.toString()] per_layer = true } @@ -173,6 +176,11 @@ export function makeTooltip(tooltips: any, layers: any[]) { } const tooltip = (per_layer) ? layer_tooltips[pickedInfo.layer.id]: tooltips + if (tooltip == null) + return + else if (typeof tooltip === "boolean") + return tooltip ? getTooltipDefault(pickedInfo) : null + const formattedTooltip: any = { style: tooltip.style || DEFAULT_STYLE }; From 8235a9b9468c8e68dee3f733c5515da0fdf4e5aa Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 16 Dec 2020 17:37:48 +0100 Subject: [PATCH 4/4] Allow overriding DeckGL tooltips --- examples/reference/panes/DeckGL.ipynb | 15 ++++++++++++--- panel/pane/deckgl.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/examples/reference/panes/DeckGL.ipynb b/examples/reference/panes/DeckGL.ipynb index a1cdccc361..4db3c0e2c2 100644 --- a/examples/reference/panes/DeckGL.ipynb +++ b/examples/reference/panes/DeckGL.ipynb @@ -119,7 +119,7 @@ "source": [ "## Tooltips\n", "\n", - "By default tooltips can be disabled and enabled by setting `tooltips=True/False`. For more customization it is possible to pass in a dictionary defining the keys. Let us start by declaring a plot with two layers:" + "By default tooltips can be disabled and enabled by setting `tooltips=True/False`. For more customization it is possible to pass in a dictionary defining the formatting. Let us start by declaring a plot with two layers:" ] }, { @@ -201,7 +201,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here we created an HTML template which is populated by the `properties` in the GeoJSON and then has the `style` applied. \n", + "Here we created an HTML template which is populated by the `properties` in the GeoJSON and then has the `style` applied. In general the dictionary may contain:\n", + "\n", + "- `html` - Set the innerHTML of the tooltip.\n", + "\n", + "- `text` - Set the innerText of the tooltip.\n", + "\n", + "- `style` - A dictionary of CSS styles that will modify the default style of the tooltip.\n", "\n", "If we have multiple pickable layers we can declare distinct tooltips by nesting the tooltips dictionary, indexed by the layer `id` or the index of the layer in the list of layers (note that the dictionary must be either integer indexed or string indexed not both)." ] @@ -292,7 +298,10 @@ " initial_view_state=INITIAL_VIEW_STATE\n", ")\n", "\n", - "pn.pane.DeckGL(r, sizing_mode='stretch_width', height=600)" + "# Tooltip (you can get the id directly from the layer object)\n", + "tooltips = {geojson.id: geojson_tooltip}\n", + "\n", + "pn.pane.DeckGL(r, sizing_mode='stretch_width', tooltips=tooltips, height=600)" ] }, { diff --git a/panel/pane/deckgl.py b/panel/pane/deckgl.py index af18cdc94e..fd2cbb0d90 100644 --- a/panel/pane/deckgl.py +++ b/panel/pane/deckgl.py @@ -124,7 +124,7 @@ def _get_properties(self, layout=True): data = dict(self.object.__dict__) mapbox_api_key = data.pop('mapbox_key', self.mapbox_api_key) deck_widget = data.pop('deck_widget', None) - tooltip = deck_widget.tooltip + tooltip = self.tooltips if isinstance(self.tooltips, dict) else deck_widget.tooltip data = {k: v for k, v in recurse_data(data).items() if v is not None} # Delete undefined width and height