Search code examples
pythonplotly-pythonipywidgets

ipywidget to replace Plotly legend item


I am trying to replace the Plotly provided legend with a custom legend using ipywidgets. A simple example is below where I have a checkbox for each trace that can be used to "highlight" the trace from the legend.

This seems to work fine, but I would like to reproduce something similar to what Plotly would normally show for the given trace. I can get all the info I need, line color, dash type, marker symbol, etc, but I can't see a easy way to reproduce a legend entry graphic such that it looks like what Plotly would have produced.

Here is what I have so far, which should be a self contained example:

import ipywidgets as widgets
from ipywidgets import interactive, fixed
import plotly.graph_objects as go

line = {'name': 'line','data': ((1,1), (2,2), (3,3)), 'color':'red', 'dash':'solid', 'symbol':'circle'}
square = {'name': 'square','data': ((1,1), (2,4), (3,9)), 'color':'blue', 'dash':'dash', 'symbol':'square'}
traces = (line, square)

def get_values(func, index):
    return [e[index] for e in func['data']]

def toggle_highlight(highlight, fig, index):
    new_width = 4 if fig.data[index].line.width == 2 else 2
    fig.data[index].line.width = new_width

fig = go.FigureWidget()
legend_items = []    
    
for i, t in enumerate(traces):
    highlight_chk = interactive(toggle_highlight, highlight=False, fig=fixed(fig), index=fixed(i)).children[0]
    item = widgets.HBox([widgets.Label(value=f"{t['name']}, c = {t['color']}, d = {t['dash']}, s = {t['symbol']}"), highlight_chk])
    s = go.Scatter(name=t['name'], x=get_values(t, 0), y=get_values(t, 1), line=dict(width=2, color=t['color'], dash=t['dash']), marker=dict(symbol=t['symbol']), customdata=[{'index':i}])
    fig.add_trace(s)    
    legend_items.append(item)
    
legend = widgets.VBox(legend_items)
display(legend)
display(fig)

This is what it looks like now: enter image description here

this is what I would like to get it to look like: enter image description here

Any thoughts on how to create such a "label"?


Solution

  • I was able to accomplish my goal using an HTML widget containing an SVG line. Below is an example where I replaced the Plotly legend with widgets that allow me to make any trace "normal", "highlighted" or invisible.

    from ipywidgets import interactive, fixed
    import plotly.graph_objects as go
    import re
    
    line = {'name': 'line','data': ((1,1), (2,2), (3,3)), 'color':'red', 'dash':'solid'}
    squared = {'name': 'squared','data': ((1,1), (2,2**2), (3,3**2)), 'color':'blue', 'dash':'4,4'}
    cubed = {'name': 'cubed','data': ((1,1), (2,2**3), (3,3**3)), 'color':'green', 'dash':'solid'}
    n4 = {'name': 'n4','data': ((1,1), (2,2**4), (3,3**4)), 'color':'purple', 'dash':'solid'}
    traces = (line, squared, cubed, n4)
    
    def get_values(func, index):
        return [e[index] for e in func['data']]
    
    def mode_fn(mode, fig, legend, index):
        html_value = legend[index].children[1].value
        if mode == 0:
            fig.data[index].line.width = 2
            fig.data[index].visible = True
            legend[index].children[1].value = re.sub(r'stroke-width:\d+', 'stroke-width:2', html_value)
        elif mode == 1:
            fig.data[index].line.width = 6
            fig.data[index].visible = True
            legend[index].children[1].value = re.sub(r'stroke-width:\d+', 'stroke-width:6', html_value)
        else:
            fig.data[index].line.width = 2
            fig.data[index].visible = False
            legend[index].children[1].value = re.sub(r'stroke-width:\d+', 'stroke-width:0', html_value)
    
    fig = go.FigureWidget()
    legend_items = []    
        
    for i, t in enumerate(traces):
        mode = interactive(mode_fn, mode=widgets.Dropdown(options=[("Normal", 0), ("Highlight", 1), ("Invisible", 2)], value=0, rows=1), fig=fixed(fig), legend=fixed(legend_items), index=fixed(i)).children[0]
        mode.layout.width = "max-content"
        mode.description = ""
        html = widgets.HTML(
            value = f'<svg height="15" width="30"><line x1="0" y1="10" x2="30" y2="10" stroke-dasharray="{t["dash"]}" style="stroke:{t["color"]};stroke-width:2" /></svg>'
        )
        item = widgets.HBox(height="15px",spacing=0, children=[mode, html, widgets.Label(height="15px", value=f"{t['name']}")])
        s = go.Scatter(mode="lines", name=t['name'], x=get_values(t, 0), y=get_values(t, 1), line=dict(width=2, color=t['color'], dash=t['dash']), customdata=[{'index':i}])
        fig.add_trace(s) 
        legend_items.append(item)
        
    fig.layout.showlegend=False 
    legend = widgets.VBox(legend_items)
    display(legend)
    display(fig)
    

    Here is a screenshot of what it looks like.

    enter image description here