I ran into this recently and it was haunting me for some time (years). The font-size of annotations was absolute (e.g., 20px), which meant the size of the annotation grew larger or smaller relative to the rest of the graph (scatter lines etc) as the container size changed in Dash. Luckily Gemini was able to craft the following fix which I thought I’d share. Essentially you first design and render the image at some fixed known size, allowing you to size all annotations how you like. Then you turn off all dash dynamic resizing and use only css transform scale to allow the graph to dynamically fit in your layout, which seems to work well.
The graph shrinks proportionally like a static image would (preserving exact text/shape ratios) while keeping some interactivity. Keep in mind the scale also affects aspects such as hovertext but does solve the window resizing layout issues. (However does not resolve font behaviour when using plotly zooming on the graph itself.
Here is an MWE comparing the default behavior vs. the CSS scaling approach. There’s a bool toggle to allow comparison of before vs after. Hope this helps
(Resize the browser window to see the effect)
import dash
import plotly.graph_objects as go
from dash import Input, Output, dcc, html
app = dash.Dash(__name__)
# Toggle: True = CSS Scaling (Image-like), False = Native (Reflow)
EXAMPLE_CSS_SCALING_ON = True
TARGET_W = 1000
TARGET_H = 500
fig = go.Figure()
fig.add_shape(
type="rect", x0=1, y0=1, x1=9, y1=9, line=dict(color="RoyalBlue", width=4)
)
fig.add_annotation(
x=5,
y=5,
text="TEXT CONTAINED INSIDE A BOX",
showarrow=False,
font=dict(size=45),
)
layout_props = dict(
xaxis=dict(range=[0, 10], visible=False),
yaxis=dict(range=[0, 10], visible=False),
margin=dict(l=0, r=0, t=0, b=0),
)
if EXAMPLE_CSS_SCALING_ON:
fig.update_layout(width=TARGET_W, height=TARGET_H, autosize=False, **layout_props)
config = {"responsive": False}
graph_style = {}
container_style = {
"width": "80%",
"border": "2px solid red",
"overflow": "hidden",
"margin": "0 auto",
}
else:
fig.update_layout(autosize=True, **layout_props)
config = {"responsive": True}
graph_style = {"height": "100%", "width": "100%"}
# Force 2:1 Aspect Ratio via CSS so the comparison is fair
container_style = {
"width": "80%",
"aspect-ratio": "2 / 1",
"border": "2px solid red",
"margin": "0 auto",
}
app.layout = html.Div(
[
html.H3(
f"Mode: {'CSS Scaling' if EXAMPLE_CSS_SCALING_ON else 'Native Scaling'}"
),
html.Div(
id="responsive-container",
style=container_style,
children=[
html.Div(
id="scaler-wrapper",
style={
"transformOrigin": "top left",
"height": "100%",
"width": "100%",
},
children=[
dcc.Graph(
id="my-graph", figure=fig, config=config, style=graph_style
)
],
)
],
),
]
)
if EXAMPLE_CSS_SCALING_ON:
app.clientside_callback(
"""
function(trigger) {
const resizeGraph = () => {
const container = document.getElementById('responsive-container');
const scaler = document.getElementById('scaler-wrapper');
if (!container || !scaler) return;
const targetW = 1000;
const targetH = 500;
const scale = container.clientWidth / targetW;
scaler.style.transform = `scale(${scale})`;
scaler.style.width = `${targetW}px`;
scaler.style.height = `${targetH}px`;
container.style.height = `${targetH * scale}px`;
}
window.removeEventListener('resize', resizeGraph);
window.addEventListener('resize', resizeGraph);
resizeGraph();
return window.dash_clientside.no_update;
}
""",
Output("scaler-wrapper", "style"),
Input("responsive-container", "id"),
)
if __name__ == "__main__":
app.run(debug=True)