Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow per layer tooltips on DeckGL pane #1846

Merged
merged 4 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 142 additions & 6 deletions examples/reference/panes/DeckGL.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -114,7 +117,135 @@
"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 formatting. 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",
" <b>Value per Square meter:</b> {properties.valuePerSqm}<br>\n",
" <b>Growth:</b> {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. 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)."
]
},
{
"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:"
]
},
{
Expand Down Expand Up @@ -167,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)"
]
},
{
Expand All @@ -184,7 +318,9 @@
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
"source": [
"pn.Row(deck_gl.controls(), deck_gl)"
]
}
],
"metadata": {
Expand Down
2 changes: 1 addition & 1 deletion panel/models/deckgl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion panel/models/deckgl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
28 changes: 24 additions & 4 deletions panel/models/tooltips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,29 +138,49 @@ 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;
}

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 (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
}
}

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
if (tooltip == null)
return
else if (typeof tooltip === "boolean")
return tooltip ? getTooltipDefault(pickedInfo) : null

const formattedTooltip: any = {
style: tooltip.style || DEFAULT_STYLE
};
Expand Down
2 changes: 1 addition & 1 deletion panel/pane/deckgl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down