Report charts: re-basing-on-zoom widget (same as Equity tab); layout fixes

- per-fund report charts now use chart_widget.equity_chart_html
  (st.iframe) instead of static st.plotly_chart: every visible window
  re-bases each line to 1.0 at its left edge, so fund/reference/index
  compare in any zoom level - same semantics as the Equity curves tab
- removed 'expand all' checkbox (24 expanded sections drowned the page)
- added 'rest of Fund Lab continues below' marker after the report
  section; the other Fund Lab sections (alpha search, clusters, N-PORT,
  drawdown, tax, CEF) were never deleted and are unchanged
This commit is contained in:
Greg Pomerantz 2026-08-30 18:15:11 -04:00
parent 3fbf332b31
commit e28f8fcf85
2 changed files with 43 additions and 25 deletions

55
app.py
View File

@ -639,8 +639,6 @@ with tab_fundlab:
}) })
st.dataframe(pd.DataFrame(_ag), width="stretch", st.dataframe(pd.DataFrame(_ag), width="stretch",
hide_index=True) hide_index=True)
_rall = st.checkbox("Expand all fund reports", value=False,
key="fl_rpt_expand")
for _s in _rorder: for _s in _rorder:
_f = _rfunds[_s] _f = _rfunds[_s]
_m, _st = _f["meta"], _f.get("stats", {}) _m, _st = _f["meta"], _f.get("stats", {})
@ -649,35 +647,38 @@ with tab_fundlab:
f"{_m['name']}") f"{_m['name']}")
if _m.get("verdict"): if _m.get("verdict"):
_title += f" [{_m['verdict'][:44]}]" _title += f" [{_m['verdict'][:44]}]"
with st.expander(_title, expanded=_rall): with st.expander(_title):
if _f.get("narrative"): if _f.get("narrative"):
st.markdown("\n\n".join(_f["narrative"])) st.markdown("\n\n".join(_f["narrative"]))
# same re-basing-on-zoom widget as the Equity curves tab:
# every visible window re-bases each line to 1.0 at its
# left edge, so fund/reference/index are comparable no
# matter where you zoom
_c = _f.get("chart", {}) _c = _f.get("chart", {})
if _c.get("dates"): if _c.get("dates"):
import plotly.graph_objects as _go from chart_widget import equity_chart_html
_fig = _go.Figure() _ds = pd.to_datetime(_c["dates"])
_fig.add_trace(_go.Scatter( _series = {_m["sym"].upper():
x=pd.to_datetime(_c["dates"]), pd.Series(_c["fund"], _ds)}
y=_c["fund"], name=_m["sym"].upper(),
line=dict(width=2)))
if _c.get("ref"): if _c.get("ref"):
_fig.add_trace(_go.Scatter( _series["fitted reference"] = pd.Series(
x=pd.to_datetime(_c["ref_dates"]), _c["ref"], pd.to_datetime(_c["ref_dates"]))
y=_c["ref"], name="fitted reference",
line=dict(width=1.2, dash="dash")))
if _c.get("ivv"): if _c.get("ivv"):
_fig.add_trace(_go.Scatter( _series["S&P 500 (IVV)"] = pd.Series(
x=pd.to_datetime(_c["ivv_dates"]), _c["ivv"], pd.to_datetime(_c["ivv_dates"]))
y=_c["ivv"], name="S&P 500 (IVV)", st.iframe(equity_chart_html(
line=dict(width=1, dash="dot"), {k: s / s.iloc[0]
opacity=0.7)) for k, s in _series.items()},
_fig.update_layout( _ds, height=460, ytitle="growth (1.0 = start)",
height=420, margin=dict(l=10, r=10, t=25, b=10), styles={
legend=dict(orientation="h", y=1.1), _m["sym"].upper(): {"width": 2},
hovermode="x unified", "fitted reference":
title=f"total return since " {"width": 1, "dash": "dash",
f"{_c['dates'][0][:4]} (rebased 100)") "opacity": 0.8},
st.plotly_chart(_fig, width="stretch") "S&P 500 (IVV)":
{"width": 1, "dash": "dot",
"opacity": 0.5},
}), height=490)
if _f.get("perf"): if _f.get("perf"):
st.dataframe(pd.DataFrame( st.dataframe(pd.DataFrame(
_f["perf"], _f["perf"],
@ -713,6 +714,10 @@ with tab_fundlab:
"R² 5y", "alpha 5y", "tax"]), "R² 5y", "alpha 5y", "tax"]),
width="stretch", hide_index=True) width="stretch", hide_index=True)
st.caption(_pe["proscons"]) st.caption(_pe["proscons"])
st.caption("← the rest of the Fund Lab continues below: "
"alpha search, return-driver clusters, N-PORT "
"cross-check, drawdown resilience, tax location, "
"and the CEF ranking.")
# --- alpha search: all screened funds (shortlist + longlist + harvest) # --- alpha search: all screened funds (shortlist + longlist + harvest)
with st.expander("Alpha search — all screened funds, ranked"): with st.expander("Alpha search — all screened funds, ranked"):

View File

@ -1136,3 +1136,16 @@ chart, period table, drivers, reference mix, tax, peers) + an "expand
all" checkbox. Static build so the page stays fast; regenerate with all" checkbox. Static build so the page stays fast; regenerate with
`python -m fundlab.reportdata` (~8 s) and `python -m fundlab.report` `python -m fundlab.reportdata` (~8 s) and `python -m fundlab.report`
(~11 s for the HTML). (~11 s for the HTML).
**Report charts: re-basing-on-zoom widget (2nd pass).** The first report
build used static st.plotly_chart with pre-rebased series — zooming to a
window left the lines starting at different levels (no comparison
possible). Replaced with the SAME widget as the Equity curves tab
(`chart_widget.equity_chart_html` + st.iframe): on every relayout each
visible line is re-based to 1.0 at its left edge and y fits the visible
data, so fund/reference/index are comparable in any window. report_data
still stores the full-history series; the app divides by each series'
first point before handing it to the widget (per-series 1.0 start, as
the widget expects). Also removed the "expand all" checkbox (24 expanded
sections made the page unusable and hid the sections below) and added a
"rest of the Fund Lab continues below" marker.