3

I am trying to create a 3D plot with plotly. In the plotting area, I want a grid of floating text annotations. This is my MWE:

import plotly.graph_objs as go
import dash
from dash import html, dcc, Input, Output
import random
import itertools

def generate_figure(data):
    text_positions = [[[-1,-1,0], [-1,1,0]], [[1,-1,0], [1,1,0]]]
    
    fig = go.Figure()

    fig.add_trace(go.Scatter3d(
        x=[v[0] for rows in text_positions for v in rows],
        y=[v[1] for rows in text_positions for v in rows],
        z=[v[2] for rows in text_positions for v in rows],
        text=[f"{f'{data['row'][row]}' if data['row'][row] is not None else 'N/A'} x {f'{data['col'][col]}' if data['col'][col] is not None else 'N/A'}" for row, rows in enumerate(text_positions) for col, _ in enumerate(rows)],
        mode='text',
        textposition="middle center",
        textfont=dict(size=12, color='black'),
    ))

    fig.update_layout(
        scene=dict(
            xaxis=dict(range=[-2, 2]),
            yaxis=dict(range=[-2, 2]),
            zaxis=dict(range=[-1, 1])
        )
    )
    return fig

app = dash.Dash(__name__)
app.layout = html.Div([
    dcc.Graph(id='graph'),
    html.Button('Refresh data', id='btn')
])

@app.callback(
    Output('graph', 'figure'),
    Input('btn', 'n_clicks')
)
def update_graph(n_clicks):
    data = [0,1,2,3]
    col = list(random.choice(list(itertools.combinations(data, 2))))
    row = [item for item in data if item not in col]

    return generate_figure({'col': col, 'row': row})

if __name__ == '__main__':
    app.run(debug=True)

I've added a button to randomize the order of the numbers. The plot updates as expected, but the font size of some of the annotations gets smaller. Obviously, this is not what I want.

Example image of issue

I've already tried to use different traces for every annotation, which didn't work. In addition, I've tried to use different text formats, but that didn't help either.

Any help would be very much appreciated.

1
  • I haven't been able to figure out a solution, but I think this has something to do with how plotly renders text in the Scatter3d graph object. I noticed that the text is the correct size when the app initially renders, but then subsequent updates are not the correct size (unless the text is at the same location as the beginning) Commented Jul 23 at 13:45

1 Answer 1

2

I wasn't able to identify the reason why this problem is happening, but I was able to find a workaround. Instead of adding a 3D scatter trace with text data you can add annotations to a figure with the fig.update_layout() call. Now, every time you click "Refresh data" button all the annotations should have the same size. Below is the code that tested on Python 3.12.11 with plotly==6.2.0 and dash==3.2.0.

import plotly.graph_objs as go
import dash
from dash import html, dcc, Input, Output
import random
import itertools


def generate_figure(data):
    text_positions = [
        [[-1, -1, 0], [-1, 1, 0]],
        [[1, -1, 0], [1, 1, 0]]
    ]
    
    text=[
        f"{f'{data['row'][row]}' if data['row'][row] is not None else 'N/A'} "
        "x "
        f"{f'{data['col'][col]}' if data['col'][col] is not None else 'N/A'}"
        for row, rows in enumerate(text_positions) for col, _ in enumerate(rows)
    ]
    x_coords = [v[0] for rows in text_positions for v in rows]
    y_coords = [v[1] for rows in text_positions for v in rows]
    z_coords = [v[2] for rows in text_positions for v in rows]

    # Create a list of annotations separately
    annotations=[
        dict(
            showarrow=False,
            x=x,
            y=y,
            z=z,
            text=t,
            font=dict(
                family='Arial, sans-serif',
                size=18,
                color='black'
            ),
        ) for x, y, z, t in zip(x_coords, y_coords, z_coords, text)
    ]

    fig = go.Figure()
    fig.update_layout(
        scene=dict(
            xaxis=dict(range=[-2, 2]),
            yaxis=dict(range=[-2, 2]),
            zaxis=dict(range=[-1, 1]),
            annotations=annotations,    # add annotations to the figure
        )
    )
    return fig


app = dash.Dash(__name__)
app.layout = html.Div([
    dcc.Graph(id='graph'),
    html.Button('Refresh data', id='btn')
])


@app.callback(
    Output('graph', 'figure'),
    Input('btn', 'n_clicks')
)
def update_graph(n_clicks):
    data = [0, 1, 2, 3]
    col = list(random.choice(list(itertools.combinations(data, 2))))
    row = [item for item in data if item not in col]

    return generate_figure({"col": col, "row": row})


if __name__ == '__main__':
    app.run(debug=True)
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.