Plotly go:如何将图像添加到悬停功能?

Plotly go: how to add an image to the hover feature?

下面的第一段代码(代码 #1)生成了一个图表,其中 1) 当您将鼠标悬停在每个点上时,将显示与每个点关联的数据;2) 当您单击每个点时,将显示与每个点关联的数据与每个点相关联的信息被保存到一个列表中。对于此代码,我还想显示与每个点关联的图像。假设数据帧 df 有一列 'image',其中包含每个点的图像 pixel/array 数据。我在网上找到了实现此图像悬停功能但没有点击功能的代码(代码#2)。我很难将图像悬停功能与点击功能结合起来。所以,基本上,我正在尝试将代码 # 2 的点击功能(点击点,它的数据保存到列表中)合并到代码 # 1 中。

CODE # 1(带点击功能):

import json
from textwrap import dedent as d
import pandas as pd
import plotly.graph_objects as go
import numpy as np
import dash
from dash import dcc
import dash_html_components as html
import plotly.express as px
from dash.dependencies import Input, Output
from jupyter_dash import JupyterDash
import warnings

warnings.simplefilter(action='ignore', category=FutureWarning)

# app info
app = JupyterDash(__name__)
styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}

# data
df = px.data.gapminder().query("continent=='Oceania'")

# plotly figure
fig = px.line(df, x="year", y="lifeExp", color="country", title="No label selected")
fig.update_traces(mode="markers+lines")

app.layout = html.Div([
    dcc.Graph(
        id='figure1',
        figure=fig,
    ),

    html.Div(className
             ='row', children=[
        html.Div([
            dcc.Markdown(d("""Hoverdata using figure references""")),
            html.Pre(id='hoverdata2', style=styles['pre']),
        ], className='three columns'),
                 
                     html.Div([
            dcc.Markdown(d("""
              
              Full hoverdata
            """)),
            html.Pre(id='hoverdata1', style=styles['pre']),
        ], className='three columns')   
    ]),
    
])

# container for clicked points in callbacks
store = []

@app.callback(
    Output('figure1', 'figure'),
    Output('hoverdata1', 'children'),
    Output('hoverdata2', 'children'),
    [Input('figure1', 'clickData')])
def display_hover_data(hoverData):
    
    if hoverData is not None:
        traceref = hoverData['points'][0]['curveNumber']
        pointref = hoverData['points'][0]['pointNumber']
        store.append([fig.data[traceref]['name'],
                      fig.data[traceref]['x'][pointref],
                     fig.data[traceref]['y'][pointref]])
        fig.update_layout(title = 'Last label was ' + fig.data[traceref]['name'])
        return fig, json.dumps(hoverData, indent=2), str(store)
    else:
        return fig, 'None selected', 'None selected'

app.run_server(mode='external', port = 7077, dev_tools_ui=True,
          dev_tools_hot_reload =True, threaded=True)

代码 # 2(包括图像悬停功能):

from jupyter_dash import JupyterDash
from dash import Dash, dcc, html, Input, Output, no_update
import plotly.graph_objects as go
import pandas as pd

## create sample random data
df = pd.DataFrame({
    'x': [1,2,3],
    'y': [2,3,4],
    'z': [3,4,5],
    'color': ['red','green','blue'],
    'img_url': [
        "https://upload.wikimedia.org/wikipedia/commons/thumb/0/02/Stack_Overflow_logo.svg/2880px-Stack_Overflow_logo.svg.png",
        "https://upload.wikimedia.org/wikipedia/commons/3/37/Plotly-logo-01-square.png",
        "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ed/Pandas_logo.svg/2880px-Pandas_logo.svg.png"
    ]
})

fig = go.Figure(data=[
    go.Scatter3d(
        x=df['x'], 
        y=df['y'], 
        z=df['z'],
        mode='markers',
        marker=dict(color=df['color'])
    )
])

# turn off native plotly.js hover effects - make sure to use
# hoverinfo="none" rather than "skip" which also halts events.
fig.update_traces(hoverinfo="none", hovertemplate=None)
fig.update_layout(
    scene = dict(
        xaxis = dict(range=[-1,8],),
                     yaxis = dict(range=[-1,8],),
                     zaxis = dict(range=[-1,8],),
    ),
)

app = JupyterDash(__name__)

server = app.server

app.layout = html.Div([
    dcc.Graph(id="graph-basic-2", figure=fig, clear_on_unhover=True),
    dcc.Tooltip(id="graph-tooltip"),
])


@app.callback(
    Output("graph-tooltip", "show"),
    Output("graph-tooltip", "bbox"),
    Output("graph-tooltip", "children"),
    Input("graph-basic-2", "hoverData"),
)
def display_hover(hoverData):
    if hoverData is None:
        return False, no_update, no_update

    # demo only shows the first point, but other points may also be available
    pt = hoverData["points"][0]
    bbox = pt["bbox"]
    num = pt["pointNumber"]

    df_row = df.iloc[num]
    img_src = df_row['img_url']

    children = [
        html.Div([
            html.Img(src=img_src, style={"width": "100%"}),
        ], style={'width': '100px', 'white-space': 'normal'})
    ]

    return True, bbox, children

app.run_server(mode="inline")
  • 您想要执行 悬停点击 的回调
    • 悬停时 显示与点和完整悬停信息关联的图像
    • 点击 更新点击点列表和图形标题
  • 假设数据帧 df 有一列 'image' 已经创建了一个 b64 编码图像
  • 已使用 customdatahover_data 参数在 px)
  • 已添加额外的 div 图片
  • 已将 callback 更改为与以前一样的行为,并且还包含新的 div。这使用 b64 编码图像,扩展必要 "data:image/png;base64,"
  • 需要注意这个https://dash.plotly.com/vtk/click-hover and https://dash.plotly.com/advanced-callbacks
import json
from textwrap import dedent as d
import pandas as pd
import plotly.graph_objects as go
import numpy as np
import dash
import plotly.express as px
from dash.dependencies import Input, Output
from jupyter_dash import JupyterDash
import warnings
import base64, io, requests
from PIL import Image
from pathlib import Path

warnings.simplefilter(action="ignore", category=FutureWarning)

# app info
app = JupyterDash(__name__)
styles = {"pre": {"border": "thin lightgrey solid", "overflowX": "scroll"}}

# data for whare images can be found
df_flag = pd.read_csv(
    io.StringIO(
        """country,Alpha-2 code,Alpha-3 code,URL
Australia,AU,AUS,https://www.worldometers.info//img/flags/small/tn_as-flag.gif
New Zealand,NZ,NZL,https://www.worldometers.info//img/flags/small/tn_nz-flag.gif"""
    )
)

# ensure that images exist on your file system...
f = Path.cwd().joinpath("flags")
if not f.exists():
    f.mkdir()

# download some images and use easy to use filenames...
for r in df_flag.iterrows():
    flag_file = f.joinpath(f'{r[1]["Alpha-3 code"]}.gif')
    if not flag_file.exists():
        r = requests.get(r[1]["URL"], stream=True, headers={"User-Agent": "XY"})
        with open(flag_file, "wb") as fd:
            for chunk in r.iter_content(chunk_size=128):
                fd.write(chunk)

# encode
def b64image(country):
    b = io.BytesIO()
    im = Image.open(Path.cwd().joinpath("flags").joinpath(f"{country}.gif"))
    im.save(b, format="PNG")
    b64 = base64.b64encode(b.getvalue())
    return b64.decode("utf-8")


df_flag["image"] = df_flag["Alpha-3 code"].apply(b64image)

# data
df = px.data.gapminder().query("continent=='Oceania'")
df = df.merge(df_flag, on="country")  # include URL and b64 encoded image

# plotly figure.  Include URL and image columns in customdata by using hover_data
fig = px.line(
    df,
    x="year",
    y="lifeExp",
    color="country",
    title="No label selected",
    hover_data={"URL": True, "image": False},
)
fig.update_traces(mode="markers+lines")

app.layout = dash.html.Div(
    [
        dash.dcc.Graph(
            id="figure1",
            figure=fig,
        ),
        dash.html.Div(
            className="row",
            children=[
                dash.html.Div(id="image"),
                dash.html.Div(
                    [
                        dash.dcc.Markdown(d("""Hoverdata using figure references""")),
                        dash.html.Pre(id="hoverdata2", style=styles["pre"]),
                    ],
                    className="three columns",
                ),
                dash.html.Div(
                    [
                        dash.dcc.Markdown(
                            d(
                                """
              
              Full hoverdata
            """
                            )
                        ),
                        dash.html.Pre(id="hoverdata1", style=styles["pre"]),
                    ],
                    className="three columns",
                ),
            ],
        ),
    ]
)

# container for clicked points in callbacks
store = []


@app.callback(
    Output("figure1", "figure"),
    Output("hoverdata1", "children"),
    Output("hoverdata2", "children"),
    Output("image", "children"),
    [Input("figure1", "clickData"), Input("figure1", "hoverData")],
)
def display_hover_data(clickData, hoverData):
    # is it a click or hover event?
    ctx = dash.callback_context

    if ctx.triggered[0]["prop_id"] == "figure1.clickData":
        traceref = clickData["points"][0]["curveNumber"]
        pointref = clickData["points"][0]["pointNumber"]
        store.append(
            [
                fig.data[traceref]["name"],
                fig.data[traceref]["x"][pointref],
                fig.data[traceref]["y"][pointref],
            ]
        )
        fig.update_layout(title="Last label was " + fig.data[traceref]["name"])

        return fig, dash.no_update, str(store), dash.no_update
    elif ctx.triggered[0]["prop_id"] == "figure1.hoverData":
        # simpler case of just use a URL...
        # dimg = dash.html.Img(src=hoverData["points"][0]["customdata"][0], style={"width": "30%"})
        # question wanted image encoded in dataframe....
        dimg = dash.html.Img(
            src="data:image/png;base64," + hoverData["points"][0]["customdata"][1],
            style={"width": "30%"},
        )

        return fig, json.dumps(hoverData, indent=2), dash.no_update, dimg
    else:
        return fig, "None selected", "None selected", "no image"


# app.run_server(mode='external', port = 7077, dev_tools_ui=True,
#           dev_tools_hot_reload =True, threaded=True)
app.run_server(mode="inline")