So that a user does not get lost in my huge plot, I want to create a smaller one that displays a frame of the view in my original plot. To save memory, I want to achieve this by creating an image of the original plot and use this as background for the overview plot. This works quite well except for the fact, that coordinates of my view window frame and the actual frame in my original plot are a bit shifted due to the fact, that .to_image()
also puts the plot’s heatmap, legend and axis into the output image. I’m searching for a way, to exclude these but there does not seem to be a direct option. Another way would be to fiddle with xref, yref, x, y, sizex, sizey
but so far I could not find a way to make it work in such a way that the overview and the original plot align nicely.
I’ve created a minimal working example and hope that someone might look into the code and is keen to play a bit with it.
from dash import Dash, html, dcc, callback
from dash.dependencies import Output, Input
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc
import plotly.express as px
import plotly.graph_objects as go
import numpy as np
from base64 import b64encode
app = Dash()
app.layout = html.Div([
dcc.Graph(id='fig-main'),
dcc.Graph(id='fig-overview'),
dcc.Store(id='fig-main-props'),
dbc.Button(id='btn-new-data', children='new data')
])
@callback(
Output('fig-main', 'figure'),
Output('fig-main-props', 'data'),
Input('btn-new-data', 'n_clicks'),
prevent_inital_call=True
)
def _create_new_figure(n):
if n:
# create figure
x = np.random.sample(150)
y = np.random.sample(150)
colors = np.random.randint(0, 3, 150)
fig = px.scatter(x=x, y=y, color=colors)
# create img from figure
img_bytes = fig.to_image(format='png', scale=1)
encoding = b64encode(img_bytes).decode()
# get x/y ranges from figure
xrange = fig.full_figure_for_development().layout.xaxis.range
yrange = fig.full_figure_for_development().layout.yaxis.range
return fig, {'img': encoding, 'x': xrange, 'y': yrange}
raise PreventUpdate
@callback(
Output('fig-overview', 'figure'),
Input('fig-main', 'relayoutData'),
Input('fig-main-props', 'data'),
prevent_inital_call=True
)
def _show_overview_figure(main_figure_relayout, main_figure_props):
if main_figure_relayout and main_figure_props:
# get img and ranges from main figure
main_figure_img = main_figure_props['img']
main_figure_x_range = main_figure_props['x']
main_figure_y_range = main_figure_props['y']
# get zoom coordinates from main figure
if 'xaxis.range[0]' in main_figure_relayout and 'yaxis.range[0]' in main_figure_relayout:
xmin_view, xmax_view = main_figure_relayout['xaxis.range[0]'], main_figure_relayout['xaxis.range[1]']
ymin_view, ymax_view = main_figure_relayout['yaxis.range[0]'], main_figure_relayout['yaxis.range[1]']
else:
xmin_view, xmax_view = np.inf, -np.inf
ymin_view, ymax_view = np.inf, -np.inf
# create overview figure
fig_overview = go.Figure()
# paste background img from main figure
fig_overview.add_layout_image(dict(source='data:image/png;base64,{}'.format(main_figure_img),
xref='x',
yref='y',
x=main_figure_x_range[0],
y=main_figure_y_range[1],
sizex=abs(main_figure_x_range[0])+main_figure_x_range[1],
sizey=abs(main_figure_y_range[0])+main_figure_y_range[1],
sizing='stretch',
layer='below')
)
fig_overview.update_layout(template='plotly_white')
# set its boundaries
fig_overview.update_xaxes(visible=False, fixedrange=True, range=main_figure_x_range)
fig_overview.update_yaxes(visible=False, fixedrange=True, range=main_figure_y_range)
# set some props
fig_overview.update_layout(showlegend=False)
fig_overview.update_layout(margin=dict(l=0, r=0, t=0, b=0))
# and draw overview window
fig_overview.add_shape(name='1', type='rect',
x0=xmin_view, x1=xmax_view, y0=ymin_view, y1=ymax_view,
line=dict(color='black', width=3))
return fig_overview
raise PreventUpdate
if __name__ == '__main__':
app.run_server(debug=True)
#fig-main {
height: 40vh;
}
#fig-overview {
width: 30vw;
}