feat: multi-page navigation with st.navigation and Material icons#29
feat: multi-page navigation with st.navigation and Material icons#29
Conversation
Restructures visualize.py from a single-file monolith into a proper multi-page Streamlit app using the 2024 st.navigation + st.Page API. New structure: - visualize.py — thin entrypoint (~40 lines); calls render_sidebar() then pg.run() with 6 pages across 5 nav groups - components/sidebar.py — all data loading, caching, and global filter logic extracted from main(); stores result in st.session_state['df'] - pages/overview.py — metrics + top charts (render_top_charts) - pages/music.py — timeline + cumulative growth (render_timeline_analysis) - pages/places.py — 3D geographic map (render_spatial_analysis) - pages/insights.py — patterns, narrative, granular filters (render_insights_and_narrative) - pages/fitness.py — empty state (Strava/Garmin/Runkeeper not yet wired) - pages/culture.py — empty state (Letterboxd/Goodreads not yet wired) - pages/beer.py — empty state (Untappd not yet wired) All render functions re-exported from visualize.py for backward compat. Navigation persists loaded data across page switches via session_state. Pages show a friendly empty state when no data source is configured. Tests updated to import from new page module locations; main() test simplified to assert set_page_config + navigation + pg.run() are called. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Architectural note: download-then-display for all source pluginsA decision has been made (see #30) that no
Impact on this PRPlease audit
This may require updating the Flagging before merge so the pattern is locked in consistently across all future plugins. |
Second architectural note: data sovereigntyBuilding on the download-then-display constraint (previous comment), a second principle applies to all Each plugin is the sole authority over its own data and must be completely unaware of any other source. Concretely, a plugin must not:
All cross-source logic — joins, merges, correlations, shared time-range filtering — belongs exclusively in Why this matters for this PRPlease check
If the |
… dark theme - components/sidebar.py: replace text inputs with native file/dir pickers (tkinter dialog in a dedicated thread to avoid main-thread-loop error); paths persisted to data/config.json and reloaded on startup so users don't re-select files each session; sidebar iterates plugin registry so each plugin gets its own labelled selector section - plugins/sources/base.py: document file_path / dir_path field types and optional file_types key in get_config_fields() contract - plugins/sources/lastfm/loader.py: type → file_path, add file_types - plugins/sources/swarm/loader.py: swarm_dir → dir_path, assumptions_file → file_path with JSON file_types - components/theme.py: new single source of truth for the dark palette (TEAL #00C8C8, AMBER #FFA014, COLORWAY, SEQUENTIAL_SCALE, map RGBA constants, apply_dark_theme() helper) - pages/overview.py, music.py, insights.py: apply_dark_theme() on every Plotly figure; heatmap uses SEQUENTIAL_SCALE so all charts share the teal-to-amber identity instead of Plotly's default blues - pages/places.py: map_style dark, column spectrum teal→amber, country overlays and borders reference theme constants; inline RGBA literals removed - .gitignore: explicit data/config.json entry - tests: coverage for config persistence, path-input widget, new field types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lugin Each source plugin now renders as a collapsible expander in the sidebar with its Material icon and display name matching the nav panel's visual style. Unconfigured plugins auto-expand so first-time users see what needs filling in; configured ones collapse to keep the sidebar clean. - SourcePlugin ABC: add ICON class attribute (default :material/database:) - LastFmPlugin: ICON = :material/headphones: - SwarmPlugin: ICON = :material/location_on: - sidebar: replace header+subheader with markdown "DATA SOURCES" section header and st.sidebar.expander per plugin, expanded state driven by whether the primary path field is already configured Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inject CSS that strips expander box-borders and matches Streamlit's st.navigation typography for both section headers and list items: - visualize.py: inject _SIDEBAR_CSS once at startup via st.markdown; .autobio-section-header class mirrors st.navigation group-label (0.75rem, 600 weight, uppercase, 55% opacity); expander details rendered borderless/transparent so plugins read as flat list items at nav-item font size (0.875rem) with matching hover highlight - sidebar.py: replace st.sidebar.header/subheader calls with st.sidebar.markdown using .autobio-section-header class for Data Sources, Cache Management, and Global Filters sections Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_path_input used st.sidebar.columns() and st.sidebar.text_input() which explicitly address the sidebar container, bypassing the active expander context and causing file selectors to render outside their plugin section. Replace with st.columns() and st.text_input() so widgets are placed in whatever container is currently active (the expander when called from _render_plugin_config). Update tests to patch streamlit.columns / streamlit.button / streamlit.text_input instead of streamlit.sidebar. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- lastfm/loader.py: guard against None return from load_listening_data before calling .empty / indexing (union-attr / index errors) - sidebar.py: annotate json.load result as dict[str, str] to satisfy the no-any-return check on _read_config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes #28
Summary
visualize.pyis now a ~40-line entrypoint; all chart and data logic extracted into focused modulescomponents/sidebar.py— shared data loading, caching, and global date filter; stores result inst.session_state['df']so navigation doesn't reload data from diskpages/overview.py— metrics + top chartspages/music.py— listening timeline + cumulative growthpages/places.py— 3D geographic map (fly-through, export)pages/insights.py— patterns, narrative, granular filterspages/fitness.py,pages/culture.py,pages/beer.py— friendly empty states ready for future source pluginsst.navigation+st.Pagewith Material icons; pages grouped into Overview / Music / Places / Health / Culture nav sectionsSource plugin contract (must hold before merge)
All source plugins — including those wired through
components/sidebar.pyandDataBroker— must satisfy two architectural principles:1. Download-then-display
No plugin may make outbound network or API calls at Streamlit runtime.
SourcePlugin.fetch()reads only from a previously downloaded local file. If the file is absent it raisesFileNotFoundErrorwith a clear message; it does not fall back to a live fetch.2. Data sovereignty
Each plugin is the sole authority over its own data and has no knowledge of any other source.
fetch()returns only that source's canonical DataFrame. No cross-source filtering, join hints, or foreign schema references belong inside a plugin. All joining and merging is the exclusive responsibility ofDataBroker.Checklist for this PR
components/sidebar.pydoes not pass cross-source context into any plugin'sfetch()callis_data_available() -> boolto theSourcePluginABC so pages can branch cleanly without catching exceptionsTest plan
streamlit run visualize.pylaunches with sidebar nav and all existing charts intact🤖 Generated with Claude Code