visgl / deck.gl

WebGL2 powered visualization framework
https://deck.gl
MIT License
12.15k stars 2.08k forks source link

In pydeck how do I display a colorbar or legend? #4850

Closed elizabethswkim closed 4 years ago

elizabethswkim commented 4 years ago

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.linelayers (because I couldn't figure out another way to draw multiple segments of different colors on a single linelayer):

  for segJ in range(len(routeline)):
    ll = pdk.Layer("LineLayer",
                   data=routeline.iloc[segJ],
                   get_source_position="start",
                   get_target_position="end",
                   get_color=color_list[segJ],
                   get_width=10,
                   )

How do I display a colorbar or legend?

ajduberstein commented 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.

elizabethswkim commented 4 years ago

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!

ajduberstein commented 4 years ago

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.

image

Legends should still be part of the pydeck API but hopefully these options provide you with a decent stopgap.

AdrianKriger commented 2 years ago

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. :

pydeckLeg

How would I render the background transparent or set opacity?

KS-HTK commented 9 months ago

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.

grafik

kavyajeetbora commented 4 months ago
  1. First I created the legend using matplotlib and saved it as a image file
  2. Then encoded the image to base64 format using the base64 python module

Here 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)

NDWI