diff --git a/app.py b/app.py index 9e0c752..3cb02bd 100644 --- a/app.py +++ b/app.py @@ -639,8 +639,6 @@ with tab_fundlab: }) st.dataframe(pd.DataFrame(_ag), width="stretch", hide_index=True) - _rall = st.checkbox("Expand all fund reports", value=False, - key="fl_rpt_expand") for _s in _rorder: _f = _rfunds[_s] _m, _st = _f["meta"], _f.get("stats", {}) @@ -649,35 +647,38 @@ with tab_fundlab: f"— {_m['name']}") if _m.get("verdict"): _title += f" [{_m['verdict'][:44]}]" - with st.expander(_title, expanded=_rall): + with st.expander(_title): if _f.get("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", {}) if _c.get("dates"): - import plotly.graph_objects as _go - _fig = _go.Figure() - _fig.add_trace(_go.Scatter( - x=pd.to_datetime(_c["dates"]), - y=_c["fund"], name=_m["sym"].upper(), - line=dict(width=2))) + from chart_widget import equity_chart_html + _ds = pd.to_datetime(_c["dates"]) + _series = {_m["sym"].upper(): + pd.Series(_c["fund"], _ds)} if _c.get("ref"): - _fig.add_trace(_go.Scatter( - x=pd.to_datetime(_c["ref_dates"]), - y=_c["ref"], name="fitted reference", - line=dict(width=1.2, dash="dash"))) + _series["fitted reference"] = pd.Series( + _c["ref"], pd.to_datetime(_c["ref_dates"])) if _c.get("ivv"): - _fig.add_trace(_go.Scatter( - x=pd.to_datetime(_c["ivv_dates"]), - y=_c["ivv"], name="S&P 500 (IVV)", - line=dict(width=1, dash="dot"), - opacity=0.7)) - _fig.update_layout( - height=420, margin=dict(l=10, r=10, t=25, b=10), - legend=dict(orientation="h", y=1.1), - hovermode="x unified", - title=f"total return since " - f"{_c['dates'][0][:4]} (rebased 100)") - st.plotly_chart(_fig, width="stretch") + _series["S&P 500 (IVV)"] = pd.Series( + _c["ivv"], pd.to_datetime(_c["ivv_dates"])) + st.iframe(equity_chart_html( + {k: s / s.iloc[0] + for k, s in _series.items()}, + _ds, height=460, ytitle="growth (1.0 = start)", + styles={ + _m["sym"].upper(): {"width": 2}, + "fitted reference": + {"width": 1, "dash": "dash", + "opacity": 0.8}, + "S&P 500 (IVV)": + {"width": 1, "dash": "dot", + "opacity": 0.5}, + }), height=490) if _f.get("perf"): st.dataframe(pd.DataFrame( _f["perf"], @@ -713,6 +714,10 @@ with tab_fundlab: "R² 5y", "alpha 5y", "tax"]), width="stretch", hide_index=True) 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) with st.expander("Alpha search — all screened funds, ranked"): diff --git a/fundlab/RESEARCH.md b/fundlab/RESEARCH.md index 9ba7a49..0ab20ef 100644 --- a/fundlab/RESEARCH.md +++ b/fundlab/RESEARCH.md @@ -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 `python -m fundlab.reportdata` (~8 s) and `python -m fundlab.report` (~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.