mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 14:38:04 +00:00
101 lines
5.0 KiB
Markdown
101 lines
5.0 KiB
Markdown
# 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
|
|
|
|
1. `_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. Colorscales `bt_diverging`,
|
|
`bt_sequential`, `bt_correlation` become plain colorscale lists.
|
|
|
|
2. `_utils.py` -- `finalize(fig, show, save)` routes: `.html` via `write_html`
|
|
(responsive full-window CSS, `displayModeBar: False`), image extensions via
|
|
`write_image` with a clear error if kaleido is missing, `show` via
|
|
`fig.show()`. `get_or_create_ax` replaced by `new_figure(figsize, title)`.
|
|
`format_pct`, `format_currency`, `auto_title` unchanged.
|
|
|
|
3. `_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.
|
|
|
|
4. `backtest.py` -- port all 10 functions to plotly. Equity gets the gradient
|
|
fill + crosshair look validated in research/plotting_bench/equity/.
|
|
`monthly_returns` becomes `go.Heatmap` with annotations, `annual_returns`
|
|
a colored bar, histograms are prebinned with numpy then drawn as `go.Bar`
|
|
so per-bin green/red coloring is preserved, `summary` is a 3-row
|
|
`make_subplots` with shared x. Rolling charts keep index x (as today).
|
|
|
|
5. `chart.py` -- `_chart_interactive` (already plotly) becomes the only path;
|
|
`_draw_candles` and the matplotlib branch are deleted. `interactive=` kept
|
|
and ignored. The `n_bars` default can later be raised now that candles are
|
|
vectorized, out of scope here.
|
|
|
|
6. `research.py` -- `heatmap_2d` and `correlation_matrix` become `go.Heatmap`;
|
|
`surface_3d` becomes `go.Surface` (camera/lighting tuned in
|
|
research/plotting_bench/equity/plot_surface_plotly.py); `walk_forward`
|
|
keeps its three modes on `make_subplots`; `stability` line + band;
|
|
`monte_carlo` / `stochastic_paths` keep 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.
|
|
|
|
7. `tearsheet.py` -- the report keeps its layout and CSS but each chart slot
|
|
embeds `fig.to_html(full_html=False, include_plotlyjs=False)` instead of a
|
|
base64 PNG; plotly.js is included once (param `plotlyjs="cdn"|"inline"`,
|
|
default cdn; inline gives a fully offline report at +4.4 MB).
|
|
`research_report` returns plotly figures and saves `.html` per figure.
|
|
|
|
8. Packaging and stragglers -- `pyproject.toml`: `plot = ["plotly>=5.0"]`
|
|
(kaleido documented for static export, not forced: it ships Chromium,
|
|
portability-first). `all`/`dev` extras updated. `plot/__init__.py` import
|
|
guard checks plotly. `sweep.py:plot_metric` rewritten to delegate to
|
|
`plot.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).
|