5.0 KiB
Plan: replace matplotlib with plotly in manifoldbt.plot
Branch: feat/plot-plotly-backend
Why
Decision from the plotting benchmark (research/plotting_bench/): plotly is the
single interactive renderer going forward. It has a native Python API (already
an optional dependency and already used by chart(interactive=True)), covers
2D and 3D, has no watermark or attribution constraint (MIT), and its rendered
output is markedly more modern than the current matplotlib charts. Every chart
gains crosshair, hover tooltips, wheel-zoom and pan for free, and the tearsheet
upgrades from static base64 PNGs to fully interactive embedded charts.
Scope
crates/bt-python/python/manifoldbt/plot/ (2,922 lines, 19 public functions)
plus sweep.py:plot_metric and packaging metadata. The Rust side is untouched.
Public API contract (kept)
Every public function keeps its name, module, required arguments and data semantics. What changes:
| Aspect | Before | After |
|---|---|---|
| Return type | matplotlib Figure |
plotly go.Figure |
show=True |
plt.show() window |
browser tab (plotly fig.show()) |
save= |
.png via Agg |
.html (interactive, responsive) or .png/.svg/.pdf via kaleido |
ax= param |
draw into given Axes | accepted, ignored (deprecation note in docstring) |
figsize= |
inches | accepted, mapped to pixels (x80) for the default layout size |
| Theme | rcParams dict | plotly template registered as manifoldbt |
Composition changes: tearsheet() no longer renders sub-charts through ax=;
it embeds each chart's interactive div directly (see below).
File-by-file
-
_theme.py-- keep the palette constants (they are imported across the module and by user code). Replace the rcParams THEME with a plotly layout template (go.layout.Template) using the same colors, fonts and grid alpha.apply_theme()registers it and sets it as default;theme_context()kept as a no-op context manager for backcompat. Colorscalesbt_diverging,bt_sequential,bt_correlationbecome plain colorscale lists. -
_utils.py--finalize(fig, show, save)routes:.htmlviawrite_html(responsive full-window CSS,displayModeBar: False), image extensions viawrite_imagewith a clear error if kaleido is missing,showviafig.show().get_or_create_axreplaced bynew_figure(figsize, title).format_pct,format_currency,auto_titleunchanged. -
_decimate.py(new) -- min/max per pixel-column decimation (pure numpy, from research/plotting_bench/decimate.py, measured: 1.16 ms at 1M points, exact on extremes). Applied to equity/drawdown/benchmark series above ~20k points so saved HTML stays light at 1m resolution. -
backtest.py-- port all 10 functions to plotly. Equity gets the gradient fill + crosshair look validated in research/plotting_bench/equity/.monthly_returnsbecomesgo.Heatmapwith annotations,annual_returnsa colored bar, histograms are prebinned with numpy then drawn asgo.Barso per-bin green/red coloring is preserved,summaryis a 3-rowmake_subplotswith shared x. Rolling charts keep index x (as today). -
chart.py--_chart_interactive(already plotly) becomes the only path;_draw_candlesand the matplotlib branch are deleted.interactive=kept and ignored. Then_barsdefault can later be raised now that candles are vectorized, out of scope here. -
research.py--heatmap_2dandcorrelation_matrixbecomego.Heatmap;surface_3dbecomesgo.Surface(camera/lighting tuned in research/plotting_bench/equity/plot_surface_plotly.py);walk_forwardkeeps its three modes onmake_subplots;stabilityline + band;monte_carlo/stochastic_pathskeep their simulation logic (including the Community 1,000-sim cap) and render the fan with one batched trace for sample paths (None-separated) plus percentile fills and a stats annotation. -
tearsheet.py-- the report keeps its layout and CSS but each chart slot embedsfig.to_html(full_html=False, include_plotlyjs=False)instead of a base64 PNG; plotly.js is included once (paramplotlyjs="cdn"|"inline", default cdn; inline gives a fully offline report at +4.4 MB).research_reportreturns plotly figures and saves.htmlper figure. -
Packaging and stragglers --
pyproject.toml:plot = ["plotly>=5.0"](kaleido documented for static export, not forced: it ships Chromium, portability-first).all/devextras updated.plot/__init__.pyimport guard checks plotly.sweep.py:plot_metricrewritten to delegate toplot.heatmap_2d/ a plotly bar.
Testing
Smoke script (scratchpad) renders every public function against a real
backtest (RSI long-only, 10 perps, 2021-2026) and the real 156k sweep grid,
saving .html + .png for each; PNGs eyeballed before commit. Existing
pytest suite run to catch import regressions.
Out of scope
Window mode (--app) helper, decimation inside the Rust core, raising the
candlestick n_bars default, removing matplotlib from the dev extra while
other tooling still uses it (bench scripts).