dynamo/docs/blogs/flash-indexer/tools/plotly_dynamo.py

276 lines
7.3 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Dynamo Dark theme for Plotly charts.
Loads design tokens from design_tokens.yaml and produces a reusable
plotly.graph_objects.layout.Template aligned with the Dynamo visual identity.
Typography:
- Titles, headings, body: Arial (sans-serif)
- Tick labels, code-like text: Roboto Mono (monospace fallback)
Usage:
from plotly_dynamo import dynamo_template, load_tokens
fig = go.Figure(data=[...], layout=go.Layout(template=dynamo_template))
fig.write_image("chart.svg")
fig.write_image("chart.png", scale=2)
Or apply globally:
import plotly.io as pio
pio.templates.default = dynamo_template
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import plotly.graph_objects as go
import yaml
def load_tokens(path: str | Path | None = None) -> dict[str, Any]:
"""Load design tokens from YAML file.
Args:
path: Path to design_tokens.yaml. Defaults to the file
next to this module.
Returns:
Parsed token dictionary.
"""
if path is None:
path = Path(__file__).parent / "design_tokens.yaml"
with open(path) as f:
return yaml.safe_load(f)
def build_template(tokens: dict[str, Any] | None = None) -> go.layout.Template:
"""Build a Plotly Template from design tokens.
Args:
tokens: Parsed design_tokens dict. Loaded from default path if None.
Returns:
A fully configured plotly.graph_objects.layout.Template.
"""
if tokens is None:
tokens = load_tokens()
colors = tokens["colors"]
typo = tokens["typography"]
bg_primary = colors["background"]["primary"]
text_primary = colors["text"]["primary"]
text_secondary = colors["text"]["secondary"]
border_subtle = colors["border"]["subtle"]
series = colors["chart_series"]
font_sans = typo["font_family"]
font_mono = typo["font_family_mono"] # Roboto Mono for ticks/code
layout = go.Layout(
# -- Background --
paper_bgcolor=bg_primary,
plot_bgcolor=bg_primary,
# -- Typography --
font=dict(
family=font_sans,
color=text_primary,
size=typo["label"]["size"],
),
title=dict(
font=dict(
family=font_sans,
size=typo["title"]["size"],
color=text_primary,
),
x=0.02,
xanchor="left",
y=0.98,
yanchor="top",
),
# -- Color palette --
colorway=series,
# -- X axis (monospace tick labels) --
xaxis=dict(
gridcolor=border_subtle,
gridwidth=0.5,
zeroline=False,
linecolor=border_subtle,
linewidth=0.5,
tickfont=dict(
family=font_mono,
size=typo["annotation"]["size"],
color=text_secondary,
),
title=dict(
font=dict(
family=font_sans,
size=typo["label"]["size"],
color=text_secondary,
),
),
),
# -- Y axis (monospace tick labels for alignment) --
yaxis=dict(
gridcolor=border_subtle,
gridwidth=0.5,
zeroline=False,
linecolor=border_subtle,
linewidth=0.5,
tickfont=dict(
family=font_mono,
size=typo["label"]["size"],
color=text_primary,
),
title=dict(
font=dict(
family=font_sans,
size=typo["label"]["size"],
color=text_secondary,
),
),
automargin=True,
),
# -- Legend --
legend=dict(
bgcolor="rgba(0,0,0,0)",
font=dict(
family=font_sans,
size=typo["annotation"]["size"],
color=text_secondary,
),
bordercolor=border_subtle,
borderwidth=0,
),
# -- Margins --
margin=dict(l=20, r=20, t=60, b=20),
# -- Subtle outer frame (muted green, thin) --
shapes=[
dict(
type="rect",
xref="paper",
yref="paper",
x0=-0.01,
y0=-0.02,
x1=1.01,
y1=1.04,
line=dict(
color=border_subtle,
width=0.5,
),
fillcolor="rgba(0,0,0,0)",
layer="above",
),
],
)
# -- Data defaults --
template = go.layout.Template(layout=layout)
# Bar chart defaults
template.data.bar = [
go.Bar(
marker=dict(
line=dict(width=0),
opacity=0.92,
),
textfont=dict(
family=font_mono,
size=typo["annotation"]["size"],
color=text_primary,
),
textposition="inside",
insidetextanchor="start",
error_x=dict(
color=text_primary,
thickness=1.5,
width=4,
),
error_y=dict(
color=text_primary,
thickness=1.5,
width=4,
),
),
]
# Scatter / line chart defaults
template.data.scatter = [
go.Scatter(
marker=dict(size=8, line=dict(width=1, color=bg_primary)),
line=dict(width=2),
),
]
return template
# Module-level singleton -- import this directly
dynamo_template = build_template()
def apply_outer_frame(fig: go.Figure) -> go.Figure:
"""Add the Dynamo green outer frame to an existing figure.
Useful when creating a figure without the template, or when
the frame was lost during updates.
"""
tokens = load_tokens()
border_frame = tokens["colors"]["border"]["frame"]
frame_width = tokens["borders"]["frame_width"]
fig.add_shape(
type="rect",
xref="paper",
yref="paper",
x0=-0.01,
y0=-0.02,
x1=1.01,
y1=1.04,
line=dict(color=border_frame, width=frame_width),
fillcolor="rgba(0,0,0,0)",
layer="above",
)
return fig
def section_annotation(
fig: go.Figure,
text: str,
y: float,
x: float = 0.0,
) -> go.Figure:
"""Add a section header annotation (e.g., 'DISPATCH (us)').
Uses the Dynamo theme font for section headers.
Args:
fig: The Plotly figure.
text: Section header text (pre-formatted; not auto-uppercased).
y: Y position in paper coordinates (0-1).
x: X position in paper coordinates (0-1).
"""
tokens = load_tokens()
typo = tokens["typography"]
text_color = tokens["colors"]["text"]["secondary"]
fig.add_annotation(
text=text,
xref="paper",
yref="paper",
x=x,
y=y,
showarrow=False,
font=dict(
family=typo["font_family"],
size=typo["heading"]["size"],
color=text_color,
),
xanchor="left",
yanchor="bottom",
)
return fig