276 lines
7.3 KiB
Python
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
|