Skip to content

feat(adapters): add a light and dark house theme - #137

Merged
jvdd merged 20 commits into
mainfrom
feat/theme
Oct 1, 2026
Merged

jvdd merged 20 commits into
mainfrom
feat/theme

Conversation

@jvdd

@jvdd jvdd commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Gives the Plotly renderer one house theme with a light and a dark mode. Before, the plots used the Plotly defaults, and a dark system showed white plots inside a dark page.

Change

  • Theme and mode (ec6d15b). theme.css holds one set of tokens in :root and one in :root[data-fv-mode="dark"]. plotly/theme.js builds a Plotly layout.template for each figure from the tokens, so update_layout() values still win. theme-mode.js runs inline in <head> before the first paint: the mode comes from localStorage fv-mode, then the OS. A new Mode button in the toolbar cycles Auto, Light and Dark; ToolbarConfig cannot hide it. A switch redraws each figure inside fvRunProgrammaticPlotlyOp, so it sends no request and does not change the spec state.
  • Palette. Okabe-Ito replaces the Plotly colors, with the same hex values in both modes, so the hex that state.group_domains stores stays right after a switch. Slots 7-8 are #9a8700 and #8a8a8a (3.60 and 3.45:1 on white, 5.38 and 5.60:1 on #0b0e12).
  • Dark map (0117ca1). Light mode keeps open-street-map. Dark mode uses the same OSM tile source with a MapLibre raster paint that inverts and desaturates the tiles. No CARTO tiles: free CARTO use needs a CARTO API key, and commercial use needs a paid license. The adapter no longer sets map.style in Python, so the template supplies it and update_layout(map={"style": ...}) wins.
  • Dark Viridis (d00aaa2). In dark mode, "Viridis" starts at #424086 instead of #440154, so sparse cells stay visible. The colorbar uses the same scale.
  • Hover labels (662aaab). A series hover label uses the neutral tooltip colors with a border in the series color. Linked-hover guides use ink on a light plot and paper on a dark one.
  • Legend (72fde4b). In overlay mode, legend swatches no longer fade with the background layer.
  • Tick labels (b366801). yaxis.automargin in the template: the left margin grows past margin.l=60 only when a tick label does not fit.
  • Button font (a3d803a). Toolbar and panel buttons use the page font.
  • Docs (4b1c3e2, 80fe9e3, 25b8dca, 6e1a74f). Architecture.md (theme section), docs/guides/customizing.md (new "Light and dark mode" section), CHANGELOG.md.
  • Rebase on main after fix(cube): brush binned sources on their own bin grid #134 and refactor: remove the ECharts renderer #136 merged. edd6193: four theme browser test calls no longer pass "plotly" to the test helpers that refactor: remove the ECharts renderer #136 changed. cca4e35: _wait_for_init loses its unused renderer argument in all four browser test files, also in the calls that fix(cube): brush binned sources on their own bin grid #134 added. The CHANGELOG.md conflict with fix(cube): brush binned sources on their own bin grid #134 keeps both entries.
  • Review fixes:
    • 988ad95: a map in a figure with its own template gets open-street-map when neither the layout nor that template sets map.style. Before this fix, Plotly used its default basic style, which loads CARTO tiles.
    • 8969d0b: a per-figure step, called by buildTracesForFigure, sets the series border. It skips a figure that sets hoverlabel.bordercolor in its layout or its own template. Before this fix, the border on the trace beat both.
    • 38db4e4: the template leaves out each key that the figure's own font sets, so the font reaches tick labels, axis titles, the title and the legend, as it does without a template. The hover label keeps the theme font.
    • d1caf0d: a null or invalid font or template counts as unset. Python None arrives as null. Before this fix, update_layout(font=None), font="serif" or template=None on a map threw in fvApplyTheme while the bundle loaded, and the whole dashboard stayed blank.
    • 201e833: fvApplyThemeToTraces holds the per-trace theme parts (the dark Viridis and the series border) and skips a figure with its own template. Before this fix, the dark Viridis lift also reached such a figure. A mode switch no longer redraws a figure with its own template.
    • ac00739: the linked-hover overlay takes the mode of its own figure's plot background (fvPlotSurfaceMode), so the guides use ink on a light plot and paper on a dark one. Before this fix, they followed the page mode: on a figure with its own template or a plot_bgcolor apart from the page mode, the contrast fell to about 1.12:1.
    • 280ddc3: a font key set to None keeps the theme default. Before this fix, font={"size": None} set the title to 17 px instead of 14 px.

Impact

  • Every page gets the new look and follows the OS mode by default. Spec and wire format do not change.
  • update_layout() wins over the theme, also for font and hoverlabel.bordercolor. A figure that sets its own template gets no house theme. template=None and font=None count as unset.
  • Hover guide contrast: 6.97:1 on a light plot, 8.25:1 on a dark plot, also when a figure's own template or plot_bgcolor differs from the page mode.
  • Known limits:
    • The spec stores only the scale name, so the dark Viridis lift also applies to a Viridis that the user picked.
    • The lowest lifted Viridis color is 2.13:1 on the dark plot (before: 1.27:1).
    • In light mode, Okabe-Ito orange and sky blue are about 2.3:1 on white. The name box that Plotly draws beside a hover label has the same contrast.
    • With yaxis.automargin, the plot area moves when a tick label does not fit in 60 px. In the tests this happened only on an extreme y zoom.
    • A mode switch rebuilds the traces of each themed figure from the layer caches. For a 1-million-cell heatmap, the reviews measured about 95 ms and 105 ms per switch. A template-only update would not apply the dark Viridis scale.
  • The toolbar gets one more button.

Checks

  • pytest tests flexviz_polars/tests (what make test runs): 2,397 passed, 2 skipped, 8 failed. The 8 are the line-envelope kernel tests of fix(cube): brush binned sources on their own bin grid #134: they fail the same way on main, because the local flexviz-polars build predates that kernel change. CI builds the plugin from source.
  • pytest -m browser -p no:randomly tests (what make test-browser runs): 340 passed, with 17 TestThemeBrowser tests.
  • Each review-fix test fails on the commit before its fix and passes after it.
  • ruff format --check, ruff check flexviz tests, mkdocs build --strict, git diff --check: pass.

@codspeed

codspeed Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 32 untouched benchmarks


Comparing feat/theme (cca4e35) with main (18b95bf)

Open in CodSpeed

@jvdd
jvdd added this pull request to stack #138 September 30, 2026 13:58
Base automatically changed from refactor/remove-echarts to main October 1, 2026 09:08
jvdd added 20 commits October 1, 2026 11:09
@jvdd
jvdd merged commit c3d788a into main Oct 1, 2026
18 checks passed
@jvdd
jvdd deleted the feat/theme branch October 1, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant