Font-size for dcc.Graph annotations do not accept viewport units

Currently I can set a header such as html.H1('Test', style={'font-size': '2vw'}), etc. to scale with the viewport width. However, when I attempt to do the same with a dcc.Graph annotation, I receive the following error:

ValueError: Invalid element(s) received for the ‘size’ property of scatter.textfont Invalid elements include: [‘1vw’, ‘1vw’, ‘1vw’, ‘1vw’, ‘1vw’, ‘1vw’, ‘1vw’, ‘1vw’, ‘1vw’, ‘1vw’]

The 'size' property is a number and may be specified as:
  - An int or float in the interval [1, inf]
  - A tuple, list, or one-dimensional numpy array of the above

Is there a plan to implement viewport units as a valid option? Or is there a current acceptable work-around for this implementation? When I scale my viewport down to half my screen (width-wise), my text annotations tend to dominate my graphs, which is not desirable.

Looking to get some more visibility to this question. Here is the associated question on SO: https://stackoverflow.com/q/53951814/8146556

There isn’t a plan on supporting this right now. Feel free to create an issue in the official plotly.js github though: https://github.com/plotly/plotly.js/issues

Thanks for the reply @chriddyp. I will go ahead and open up an issue.

Are their any acceptable workarounds for this that you are aware of? Resizing the window tends to result in the text annotations dominating the graph itself…

To start, make the margins of your graph really small, that way your chart isn’t dominated bymargin space on smaller screens.

Other than that, there aren’t any other workarounds if you have several annotations. Might be cool if we had a Viewport component that could render different components depending on the width of the screen

That’s what I’ve done :confused:

The margins are near zero, and the axis are actually turned off. I am recreating specially-designed charts using SVG paths as inputs to the shape attribute of the layout.

I just find it interesting that I can set the font-size to scale with viewport units for a dcc.Dropdown component by modifying the style of the parent Div, but the dcc.Graph components do not function in the same manner with text annotations.

Appreciate your time thus far on this topic!

It might have to do with how the SVG is rendered in plotly.js, or perhaps there are some pixel units that are being hardcoded. I’m not too familiar with the underlying plotly.js font code, so I’d recommend making an issue and discussing with the enginers on that project. There might be a simple fix or a deeper reason why this is difficult.

I’ve opened an issue with plotly.js github: https://github.com/plotly/plotly.js/issues/3420

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 :slight_smile: (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)