Closed elizabethswkim closed 4 years ago
@elizabethswkim appreciate the question! You currently have to use an external library like matplotlib to make a legend and then render it beside your pydeck visualization rather than within it. I think we'd benefit from incorporating something within pydeck, since this feature has come up a few times. I've created an issue (#4851) to track this feature and I'll update this thread when it's completed.
Thanks @ajduberstein! I feared you were saying: I should identify the color values displayed in my pydeck figure, create another figure in matplotlib that uses the same colors, generate the matplotlib figure's colorbar, hide the matplotlib figure, and display the pydeck figure over it but not over the matplotlib colorbar?
But it appears that one can display a matplotlib colorbar without having to generate a figure. Thanks for the guidance!
And thank you for adding the issue!
Matplotlib is great for this kind of thing. You can also embed the HTML for a legend directly if you're in Jupyter. You can give it a try with this mybinder example.
Legends should still be part of the pydeck API but hopefully these options provide you with a decent stopgap.
This is a wonderful visualization tool. Thank you.
If you change the create_legend
function like so:
import jinja2
from ipywidgets import HTML
def create_legend(labels: list) -> HTML:
"""Creates an HTML legend from a list dictionary of the format {'text': str, 'color': [r, g, b]}"""
labels = list(labels)
for label in labels:
assert label['color'] and label['text']
assert len(label['color']) in (3, 4)
label['color'] = ', '.join([str(c) for c in label['color']])
legend_template = jinja2.Template('''
<style>
.legend {
width: 300px;
}
.square {
height: 10px;
width: 10px;
border: 1px solid grey;
}
.left {
float: left;
}
.right {
float: right;
}
</style>
<h2>Taxi Trip Type</h2>
{% for label in labels %}
<div class='legend'>
<div class="square left" style="background:rgba({{ label['color'] }})"></div>
<span class="right">{{label['text']}}</span>
<br />
</div>
{% endfor %}
<br />
(Data from <a href="https://www.kaggle.com/c/pkdd-15-predict-taxi-service-trajectory-i/data">Kaggle</a>)
''')
html_str = legend_template.render(labels=labels)
return html_str #HTML(html_str)
and add description=legend
to .Deck
like so:
pdk.Deck(
layers,
initial_view_state=view_state,
description=legend
).to_html()
you'll get the Legend in the html
. :
How would I render the background transparent or set opacity?
For anyone stumbeling upon this, here is an example using matplotlib.colors
to get the gradient.
I think the output is a bit closer to the example given eventhough it is a bit more complex.
The cmap_discrete and cmap_continuous functions have been taken from pandapower.
import dash
import dash_deck
from dash import html
import dash_bootstrap_components as dbc
from matplotlib.colors import rgb2hex, ListedColormap, BoundaryNorm, LinearSegmentedColormap, Normalize
mapbox_token = ""
color_legends = None
# functions taken from pandapower.plotting.colormaps
# https://github.com/e2nIEE/pandapower/blob/develop/pandapower/plotting/colormaps.py
def cmap_discrete(cmap_list: list):
"""
- cmap_list (list) - list of tuples, where each tuple represents one color. Each tuple has
the form of (center, color). The colorbar is a linear segmentation of
the colors between the centers.
"""
cmap_colors = []
boundaries = []
last_upper = None
for (lower, upper), color in cmap_list:
if last_upper is not None and lower != last_upper:
raise ValueError(f"The lower boundary of {lower} is not equal to the upper boundary of {last_upper}.")
cmap_colors.append(color)
boundaries.append(lower)
last_upper = upper
boundaries.append(last_upper)
cmap = ListedColormap(cmap_colors)
norm = BoundaryNorm(boundaries, cmap.N)
return cmap, norm
def cmap_continuous(cmap_list: list):
_min = cmap_list[0][0]
_max = cmap_list[-1][0]
cmap_colors = [((loading-_min)/(_max-_min), color) for (loading, color) in cmap_list]
cmap = LinearSegmentedColormap.from_list('name', cmap_colors)
norm = Normalize(_min, _max)
return cmap, norm
def add_color_legend(
name: str,
cmap,
norm,
ticks: list,
tick_text_color: str = "black",
text_offset: int = 0,
top: str = "50%"
):
global color_legends
if color_legends is None:
color_legends = {}
color_legends[name] = {
"cmap": cmap,
"norm": norm,
"ticks": ticks,
"tick_text_color": tick_text_color,
"text_offset": text_offset,
"top": top,
}
def plot_color_legends():
color_bars = []
if not color_legends or color_legends == {}:
return
for name, color_bar in color_legends.items():
cmap, norm, ticks, tick_text_color, text_offset, top = color_bar.values()
norm_range = norm.vmax - norm.vmin
norm_start = norm.vmin
gradient = ", ".join(
[
rgb2hex(cmap(norm(norm_start + i * norm_range / 100)))
for i in range(100)
]
)
tick_divs = []
for tick in ticks:
tick_pos = (tick - norm_start) / norm_range * 100
tick_div = html.Div(
className="tick",
style={
"bottom": str(tick_pos) + "%",
"color": tick_text_color,
# styles added from css
"position": "absolute",
"left": "60%",
"transform": "translate(0, 50%)",
"backgroundColor": "white",
"borderRadius": "3px",
"boxShadow": "0 0 2px black",
"padding": "0 3px",
},
children=str(tick),
)
tick_divs.append(tick_div)
label = html.Div(
className="bar-label",
children=name,
# styles added from css
style={
"position": "absolute",
"transformOrigin": "top left 0",
"top": "50%",
"transform": "rotate(90deg) translate(-50%, -110%)",
"textAlign": "center",
"whiteSpace": "nowrap",
"textShadow": "0 0 2px white",
}
)
color_bar_div = html.Div(
id=name,
className="color-bar-container",
style={
"right": str(20 + text_offset) + "px",
"top": top,
"backgroundImage": f"linear-gradient(to top, {gradient})",
# styles added from css
"position": "absolute",
"height": "40%",
"width": "30px",
"transform": "translate(0, -50%)",
"zIndex": "1000",
"borderRadius": "3px",
},
children=tick_divs + [label],
)
color_bars.append(color_bar_div)
return color_bars
# Creating the colormaps
cmap_list_lines = [(0, "#179c7d"), (25, "#b2d235"), (50, "#b2d235"), (75, "#f58220"),
(100, "#bb0056")]
cmap_lines, norm_lines = cmap_continuous(cmap_list_lines)
cmap_list_buses = [
((0.9, 0.97), "#005b7f"),
((0.97, 1.03), "#b2d235"),
((1.03, 1.1), "#bb0056"),
]
cmap_buses, norm_buses = cmap_discrete(cmap_list_buses)
# Adding two colorbars
add_color_legend("Paths", cmap_lines, norm_lines, [0, 25, 50, 75, 100])
add_color_legend("Points", cmap_buses, norm_buses, [0.9, 0.97, 1.03, 1.1], text_offset=60)
app = dash.Dash(
external_stylesheets=[dbc.themes.BOOTSTRAP],
title="colorbar example",
meta_tags=[{"name": "viewport", "content": "width=device-width"}],
)
data = {
"description": "A minimal deck.gl example rendering a circle with text",
"initialViewState": {"longitude": -122.45, "latitude": 37.8, "zoom": 12},
"layers": [
{
"@@type": "TextLayer",
"data": [{"position": [-122.45, 37.8], "text": "Hello World"}],
},
],
}
map_component = dash_deck.DeckGL(data=data, id="deck-gl", mapboxKey=mapbox_token)
color_legend_div = html.Div(
plot_color_legends(),
style={
# background should be transparent
# if you want to set a background, use "width" and "backgroundColor" here.
"position": "absolute",
"height": "60%",
"top": "20%",
"right": "10px"
}
)
app.layout = html.Div([
map_component,
color_legend_div
])
app.run_server(debug=True)
For this example I added the css into the python code, but I recommend using a css for styling attributes that are not data driven.
.tick {
position: absolute;
left: 60%;
transform: translate(0, 50%);
background-color: white;
border-radius: 3px;
box-shadow: 0 0 2px black;
padding: 0 3px;
}
.bar-label {
position: absolute;
transform-origin: top left 0;
top: 50%;
transform: rotate(90deg) translate(-50%, -110%);
text-align: center;
white-space: nowrap;
text-shadow: 0 0 2px white;
}
.color-bar-container {
position: absolute;
height: 40%;
width: 30px;
transform: translate(0, -50%);
z-index: 1000;
border-radius: 3px;
}
This should function independent of what map you use, Leaflet, deck.gl with or without dash, should not matter.
To see a map in the example add you're mapbox token.
base64
python moduleHere is the code:
import base64
def generate_html_from_fig():
# Convert the image to base64 format
with open("temp.png", "rb") as f:
encoded_image = base64.b64encode(f.read())
html = "<img src='data:image/png;base64,{}' height=50px>".format(encoded_image.decode("utf-8"))
return html
This html can be used as description
parameter while rendering the pydeck chart:
# Render
r = pdk.Deck(layers=[layer], initial_view_state=view_state, tooltip=tooltip, description=html)
In pydeck how do I display a colorbar (ie, something like the bar to the right of the image in this question) or a legend? Can't find either anywhere in the documentation (or via concerted google and github searches).
I'm specifically trying to generate one for multiple
pdk.linelayer
s (because I couldn't figure out another way to draw multiple segments of different colors on a single linelayer):How do I display a colorbar or legend?