diff --git a/shiny/shiny-python-dashboard/SKILL.md b/shiny/shiny-python-dashboard/SKILL.md new file mode 100644 index 0000000..f5fecf7 --- /dev/null +++ b/shiny/shiny-python-dashboard/SKILL.md @@ -0,0 +1,268 @@ +--- +name: shiny-python-dashboard +description: Build polished Shiny for Python dashboards and analytical apps. Use when creating or refactoring Shiny for Python apps, dashboards, KPI layouts, sidebar or navbar navigation, cards, value boxes, responsive grids, reactive filtering, charts, data tables, or when choosing between Core and Express APIs. +--- + +# Modern Shiny for Python Dashboards + +Build professional dashboards with Shiny for Python's layout primitives, reactive graph, and card-based composition patterns. This skill is the Python equivalent of a modern bslib dashboard workflow: use `ui.page_sidebar()` or `ui.page_navbar()` for page structure, `ui.layout_columns()` or `ui.layout_column_wrap()` for responsive grids, `ui.card()` and `ui.value_box()` for content containers, and `@reactive.calc` plus render decorators for data flow. + +## Critical rules + +1. Never use emoji characters as icons. Use `faicons.icon_svg()` or Bootstrap Icons SVG instead. +2. Keep value boxes to 3 to 4 per row and wrap their container with `fill=False` so they do not stretch vertically. +3. Use `fillable=True` on dashboard page layouts and `full_screen=True` on chart or table cards. +4. Give every card a title with `ui.card_header("Title")`. +5. Prefer `ui.layout_column_wrap()` for uniform cards and `ui.layout_columns()` when you need explicit proportions. +6. Handle missing data explicitly with `dropna()`, `pd.to_numeric(..., errors="coerce")`, and `req()`. +7. Keep imports at module scope except `matplotlib.pyplot`, which should be imported inside `@render.plot` to avoid backend issues. +8. Format all displayed numbers for readability with commas, currency symbols, percentages, or compact abbreviations. +9. Keep the default dashboard hierarchy: value boxes at the top, charts in the middle, and the detailed table below. +10. Use breakpoint-aware `col_widths` so the layout still works on mobile. + +## Quick Start + +**Single-page dashboard with a shared sidebar:** + +```python +from pathlib import Path + +import pandas as pd +from faicons import icon_svg +from shiny import App, reactive, render, ui + +app_dir = Path(__file__).parent +df = pd.read_csv(app_dir / "data.csv") + +app_ui = ui.page_sidebar( + ui.sidebar( + ui.input_select("metric", "Metric", choices=list(df.columns)), + open="desktop", + ), + ui.layout_columns( + ui.value_box( + "Rows", + ui.output_text("row_count"), + showcase=icon_svg("database"), + theme="primary", + ), + ui.value_box( + "Average", + ui.output_text("avg_value"), + showcase=icon_svg("chart-line"), + theme="info", + ), + fill=False, + ), + ui.layout_columns( + ui.card( + ui.card_header("Distribution"), + ui.output_plot("distribution"), + full_screen=True, + ), + ui.card( + ui.card_header("Preview"), + ui.output_data_frame("preview"), + full_screen=True, + ), + col_widths=[6, 6], + ), + title="Dashboard", + fillable=True, +) + + +def server(input, output, session): + @reactive.calc + def series(): + return pd.to_numeric(df[input.metric()], errors="coerce").dropna() + + @render.text + def row_count(): + return f"{len(df):,}" + + @render.text + def avg_value(): + return f"{series().mean():,.1f}" + + @render.plot + def distribution(): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(8, 4)) + ax.hist(series(), bins=20, color="#0d6efd", edgecolor="white") + ax.set_xlabel(input.metric()) + ax.set_ylabel("Count") + fig.tight_layout() + return fig + + @render.data_frame + def preview(): + return render.DataGrid(df.head(20), filters=True) + + +app = App(app_ui, server) +``` + +**Multi-page dashboard with a navbar:** + +```python +from shiny import App, ui + +overview = ui.layout_columns( + ui.card(ui.card_header("Summary"), "Overview content", full_screen=True), + ui.card(ui.card_header("Recent activity"), "More content", full_screen=True), + col_widths=[6, 6], +) + +details = ui.navset_card_underline( + ui.nav_panel("Plot", ui.output_plot("plot")), + ui.nav_panel("Table", ui.output_data_frame("table")), + title="Analysis", +) + +app_ui = ui.page_navbar( + ui.nav_panel("Overview", overview), + ui.nav_panel("Analysis", details), + title="Analytics Platform", + fillable=True, +) +``` + +## Core Concepts + +### Page layouts + +- `ui.page_sidebar()` is the default for single-page dashboards with one shared set of controls. +- `ui.page_navbar()` is the default for multi-page apps with distinct sections. +- In Express, use `ui.page_opts(title=..., fillable=True)` and then declare `with ui.nav_panel(...):` blocks. + +See [references/layout-and-navigation.md](references/layout-and-navigation.md) for layout, sidebar, navigation, and responsive grid patterns. + +### Grids + +- `ui.layout_column_wrap()` is the simplest way to build uniform KPI rows or evenly sized cards. +- `ui.layout_columns()` gives you a 12-column grid with breakpoint-aware `col_widths`. +- Negative column widths create intentional gaps when needed. + +See [references/layout-and-navigation.md](references/layout-and-navigation.md) and [references/components.md](references/components.md) for detailed layout guidance. + +### Cards + +Cards are the primary dashboard container. Use `ui.card_header()` for titles, `ui.card_footer()` for context, and `full_screen=True` for plots, maps, and tables. + +See [references/components.md](references/components.md) for card composition, tabbed card patterns, and inline controls. + +### Value boxes + +Use `ui.value_box()` for KPIs, summaries, and status indicators. Place them in a non-filling row so they stay compact and scannable. + +See [references/components.md](references/components.md) for showcase icons, layouts, and dynamic output patterns. + +### Reactivity + +The primary reactive primitive is `@reactive.calc`. Chain calculations for derived data, and pair `@reactive.effect` with `@reactive.event` for reset buttons or imperative updates. + +See [references/reactivity-and-rendering.md](references/reactivity-and-rendering.md) for reactive graph patterns and guardrails. + +### Rendering outputs + +- Use `@render.plot` for Matplotlib or Seaborn. +- Use `@render.data_frame` with `render.DataGrid(...)` for tables. +- Use `shinywidgets.output_widget()` plus `@render_plotly` for Plotly outputs. +- Use `@render.ui` for small dynamic UI fragments, including dynamic icons. + +See [references/reactivity-and-rendering.md](references/reactivity-and-rendering.md) for output-specific guidance. + +### Styling and data + +Use a `shared.py` module or top-level data-loading block to keep data access, constants, and app directory logic out of reactive code. Pair this with a small `styles.css` file rather than inline styling everywhere. + +See [references/styling-and-data.md](references/styling-and-data.md) for project structure, formatting, and styling guidance. + +### Icons and maps + +Use `faicons.icon_svg()` for dashboard icons and treat map widgets as full-screen cards with clear loading and empty-state behavior. + +See [references/icons-and-maps.md](references/icons-and-maps.md) for icon, accessibility, and map patterns. + +### Core and Express APIs + +Both APIs are first-class. Pick one style for the app and keep it consistent. Core is explicit and easy to factor; Express is concise and works well for smaller apps. + +See [references/core-vs-express.md](references/core-vs-express.md) for side-by-side equivalents and selection guidance. + +## Common Workflows + +### Building a dashboard + +1. Choose `ui.page_sidebar()` for one-page analytics or `ui.page_navbar()` for multiple sections. +2. Load the data once at module scope or in `shared.py`, then compute reusable constants for inputs. +3. Put primary filters in a sidebar with `open="desktop"` so mobile starts collapsed. +4. Add KPI value boxes in a `fill=False` row near the top. +5. Arrange cards with `ui.layout_column_wrap()` or `ui.layout_columns()`. +6. Enable `full_screen=True` on visualizations and detailed tables. +7. Build the data pipeline through `@reactive.calc` functions, not mutated globals. +8. Finish with a `render.DataGrid(...)` card and minimal CSS overrides. + +### Translating or modernizing an existing app + +If you are replacing an older Shiny for Python layout or translating an R bslib design into Python: + +1. Replace ad hoc `ui.div()` grid wiring with `ui.page_sidebar()`, `ui.page_navbar()`, `ui.layout_columns()`, or `ui.layout_column_wrap()`. +2. Wrap charts and tables in cards with headers and full-screen support. +3. Convert top-line metrics into `ui.value_box()` components. +4. Move repeated data transformations into `@reactive.calc` or `shared.py`. +5. Replace emoji icons with `faicons.icon_svg()`. +6. Audit charts for explicit sizes, axis labels, and missing-data handling. + +## Guidelines + +1. Prefer Shiny's page and layout primitives over raw `ui.div()` composition when building dashboards. +2. Keep one API style per app unless you have a compelling reason to mix Core and Express. +3. Use `ui.layout_column_wrap()` for uniform groups and `ui.layout_columns()` for precise layout control. +4. Wrap dashboard outputs in cards and default to `full_screen=True` on charts, maps, and tables. +5. Set `fill=False` on KPI rows so value boxes do not consume spare vertical space. +6. Use responsive `col_widths` such as `{"sm": 12, "md": [6, 6]}` for mobile-safe layouts. +7. Use named Bootstrap theme colors like `"primary"`, `"success"`, `"info"`, `"warning"`, and `"danger"` for value boxes. +8. Place imports at the top of the file, except `matplotlib.pyplot` inside `@render.plot`. +9. Never display raw numbers when a formatted value is more readable. +10. Guard empty selections and filtered datasets with `req()` before rendering. +11. Never pass duplicate keys when unpacking Plotly dicts; merge overrides with `{**base, "key": value}` instead. + +## Avoid Common Errors + +1. Do not omit `fill=False` on KPI rows; otherwise value boxes stretch awkwardly. +2. Do not wrap `ui.navset_card_*()` content in another `ui.card()`; the navset is already the card container. +3. Do not import `matplotlib.pyplot` at module scope in dashboard apps. +4. Do not pass the same key through both `**base_dict` and a keyword override in Plotly layout dictionaries. +5. Do not assume a sidebar on `ui.page_navbar()` should drive every page; use page-specific controls when sections need different filters. +6. Do not leave cards untitled or charts unlabeled. + +## Number formatting + +Always format numbers for display: + +```python +f"{count:,}" # 12,345 +f"${amount:,.2f}" # $1,234.56 +f"{ratio:.1%}" # 78.3% +f"{value:,.1f}" # 1,234.6 + + +def fmt_large(number): + if number >= 1_000_000: + return f"{number / 1_000_000:.1f}M" + if number >= 1_000: + return f"{number / 1_000:.1f}K" + return f"{number:,.0f}" +``` + +## Reference Files + +- [references/layout-and-navigation.md](references/layout-and-navigation.md) — page layouts, grids, nav panels, sidebars, and fill behavior +- [references/components.md](references/components.md) — cards, value boxes, accordions, tooltips, popovers, and inline controls +- [references/reactivity-and-rendering.md](references/reactivity-and-rendering.md) — reactive graph design and output rendering patterns +- [references/styling-and-data.md](references/styling-and-data.md) — project structure, data loading, formatting, styling, and themes +- [references/icons-and-maps.md](references/icons-and-maps.md) — icon rules, accessibility, and map container patterns +- [references/core-vs-express.md](references/core-vs-express.md) — API selection and side-by-side equivalents diff --git a/shiny/shiny-python-dashboard/references/components.md b/shiny/shiny-python-dashboard/references/components.md new file mode 100644 index 0000000..876c5d4 --- /dev/null +++ b/shiny/shiny-python-dashboard/references/components.md @@ -0,0 +1,184 @@ +# Components for Shiny for Python Dashboards + +This reference covers the card-level building blocks of a Shiny for Python dashboard: cards, value boxes, accordions, and lightweight contextual UI such as tooltips and popovers. + +## Cards + +Cards are the default container for dashboard content. + +```python +ui.card( + ui.card_header("Revenue by month"), + ui.output_plot("revenue_plot"), + ui.card_footer("Updated daily at 6am"), + full_screen=True, +) +``` + +Use cards for: + +- charts +- tables +- maps +- summaries that need supporting text +- module-like dashboard sections + +Guidelines: + +- Add `ui.card_header()` to every card. +- Default to `full_screen=True` for charts, maps, and tables. +- Use `ui.card_footer()` for provenance, notes, or small actions. +- Keep static text cards shorter than plot cards; large paragraphs belong in scrolling pages, not dashboard grids. + +### Card headers with inline controls + +Card headers can hold small secondary controls. + +```python +ui.card_header( + "Bill vs tip", + ui.popover( + icon_svg("ellipsis"), + ui.input_radio_buttons("color_var", None, ["none", "sex", "day"], inline=True), + title="Color by", + placement="top", + ), + class_="d-flex justify-content-between align-items-center", +) +``` + +Use this for secondary options that do not deserve a full sidebar section. + +## Value Boxes + +Use `ui.value_box()` for KPIs and headline metrics. + +**Core API:** + +```python +ui.layout_columns( + ui.value_box( + "Total users", + ui.output_ui("total_users"), + showcase=icon_svg("users"), + theme="primary", + ), + ui.value_box( + "Average order", + ui.output_ui("avg_order"), + showcase=icon_svg("wallet"), + theme="success", + ), + fill=False, +) +``` + +**Express API:** + +```python +with ui.layout_columns(fill=False): + with ui.value_box(showcase=icon_svg("users"), theme="primary"): + "Total users" + + @render.express + def total_users(): + f"{len(df):,}" +``` + +Guidelines: + +- Keep the row to 3 or 4 boxes. +- Use named themes like `"primary"`, `"success"`, `"info"`, `"warning"`, and `"danger"`. +- Use icons that reinforce the metric; do not use emojis. +- Format the displayed value instead of passing a raw float or integer. + +### Dynamic showcase icons + +Use `@render.ui` when the icon depends on the data. + +```python +@render.ui +def change_icon(): + icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") + icon.add_class("text-success" if get_change() >= 0 else "text-danger") + return icon +``` + +In Express, `with ui.hold():` is useful when an output is referenced before it is defined. + +## Accordions + +Accordions are most useful in sidebars with many controls. + +```python +ui.sidebar( + ui.accordion( + ui.accordion_panel( + "Filters", + ui.input_selectize("species", "Species", choices=species), + ui.input_date_range("dates", "Dates", start=start, end=end), + ), + ui.accordion_panel( + "Display", + ui.input_switch("show_trend", "Show trend", value=True), + ui.input_slider("bins", "Bins", min=5, max=50, value=20), + ), + open=["Filters"], + ), + open="desktop", +) +``` + +Use accordions when: + +- the sidebar has more than a handful of inputs +- some controls are clearly advanced or optional +- you want to reduce visual clutter on smaller screens + +Keep related inputs together and leave only the most important panel open by default. + +## Tooltips and Popovers + +Tooltips provide quick read-only help. Popovers hold small interactive controls. + +```python +ui.tooltip( + icon_svg("circle-info", title="More information", a11y="sem"), + "Shows how the metric is calculated", +) +``` + +```python +ui.popover( + icon_svg("gear", title="Chart options", a11y="sem"), + ui.input_select("palette", "Palette", choices=palettes), + ui.input_switch("show_trend", "Show trend line", value=True), + title="Chart options", +) +``` + +Guidelines: + +- Use tooltips for one short explanation. +- Use popovers for 2 to 4 small controls. +- Give icon-only triggers accessible titles so screen readers have a useful label. +- Do not use popovers as a substitute for a real form or a full advanced-settings workflow. + +## Toast-like Feedback + +Use the framework's notification helpers for lightweight success, warning, or error feedback instead of permanently allocating screen space to ephemeral status messages. + +Guidelines: + +- Use notifications for completion, failure, or "export started" states. +- Keep them specific instead of saying only "Done". +- Reserve persistent on-page status areas for information users need to keep reading. + +## Best Practices + +1. Treat cards as the default dashboard container. +2. Use value boxes for headline metrics, not for long explanations. +3. Keep chart options in card-header popovers when they are secondary. +4. Group busy sidebars with accordions. +5. Give icon-only triggers accessible titles. +6. Avoid card-within-card compositions unless you are intentionally creating a nested layout. diff --git a/shiny/shiny-python-dashboard/references/core-vs-express.md b/shiny/shiny-python-dashboard/references/core-vs-express.md new file mode 100644 index 0000000..d096109 --- /dev/null +++ b/shiny/shiny-python-dashboard/references/core-vs-express.md @@ -0,0 +1,126 @@ +# Core vs Express in Shiny for Python + +Both Core and Express are first-class APIs in Shiny for Python. This reference helps you choose between them and translate patterns cleanly from one style to the other. + +## High-Level Difference + +- Core separates UI declaration from server outputs. +- Express colocates layout blocks and render functions. + +Neither is more "correct" for dashboards. Pick the style that matches the app's size, expected complexity, and the team's preference for explicit structure versus local readability. + +## When to Choose Core + +Choose Core when: + +- the app is large or likely to grow +- you want a clearly separated `app_ui` and `server` +- outputs need to be referenced across modules or helper functions +- you want the UI tree to be easy to inspect at a glance + +Core is often the safer default for production dashboards with several sections. + +## When to Choose Express + +Choose Express when: + +- the app is small to medium sized +- the main advantage is colocating a card and its renderer +- the team prefers a more linear, notebook-like authoring style +- rapid iteration matters more than strict separation + +Express works well for focused dashboards where the layout is easy to read top to bottom. + +## Equivalent Patterns + +| Pattern | Core | Express | +|---|---|---| +| Page title and fill | `ui.page_sidebar(..., title="App", fillable=True)` | `ui.page_opts(title="App", fillable=True)` | +| Sidebar | `ui.sidebar(...)` inside page layout | `with ui.sidebar(...):` | +| Card output | `ui.card(ui.card_header("Plot"), ui.output_plot("plot"), full_screen=True)` | `with ui.card(full_screen=True): ui.card_header("Plot")` plus `@render.plot` | +| Data table | `ui.output_data_frame("table")` plus `@render.data_frame` | `@render.data_frame` inside the card block | +| Value box | `ui.value_box("Title", ui.output_ui("value"), ...)` | `with ui.value_box(...): "Title"` plus `@render.express` | +| Section tabs | `ui.navset_card_underline(...)` assigned to a variable or inserted directly | `with ui.navset_card_underline(...):` | + +## Skeletons + +### Core skeleton + +```python +from shiny import App, reactive, render, ui + +app_ui = ui.page_sidebar( + ui.sidebar(ui.input_select("metric", "Metric", choices=metrics), open="desktop"), + ui.card(ui.card_header("Plot"), ui.output_plot("plot"), full_screen=True), + title="Dashboard", + fillable=True, +) + + +def server(input, output, session): + @reactive.calc + def filtered(): + return df + + @render.plot + def plot(): + ... + + +app = App(app_ui, server) +``` + +### Express skeleton + +```python +from shiny import reactive +from shiny.express import input, render, ui + +ui.page_opts(title="Dashboard", fillable=True) + +with ui.sidebar(open="desktop"): + ui.input_select("metric", "Metric", choices=metrics) + + +@reactive.calc +def filtered(): + return df + + +with ui.card(full_screen=True): + ui.card_header("Plot") + + @render.plot + def plot(): + ... +``` + +## Important Express Detail + +`ui.hold()` exists in the Express namespace and is useful when an output is referenced before it is defined. + +```python +from shiny.express import render, ui + +with ui.value_box(showcase=ui.output_ui("trend_icon")): + "Trend" + +with ui.hold(): + @render.ui + def trend_icon(): + return icon_svg("arrow-up") +``` + +## Rules for Mixing Styles + +- Do not casually mix Core and Express in the same file. +- If you need to translate one style into the other, translate the full local pattern: layout block, reactive helpers, and render functions. +- Keep helper modules, data loading, and formatting utilities reusable regardless of API choice. + +## Best Practices + +1. Pick one API per app file and stay consistent. +2. Use Core when structure and maintainability matter most. +3. Use Express when localized readability matters most. +4. Translate patterns between APIs mechanically rather than inventing a third style. +5. Treat both APIs as equally valid for production dashboards. diff --git a/shiny/shiny-python-dashboard/references/icons-and-maps.md b/shiny/shiny-python-dashboard/references/icons-and-maps.md new file mode 100644 index 0000000..1e8ac78 --- /dev/null +++ b/shiny/shiny-python-dashboard/references/icons-and-maps.md @@ -0,0 +1,105 @@ +# Icons and Maps in Shiny for Python Dashboards + +This reference covers two dashboard details that strongly affect quality: icon usage and geographic views. Both should feel intentional, readable, and easy to expand. + +## Icons + +Use `faicons.icon_svg()` for dashboard icons. + +```python +from faicons import icon_svg + +icon_svg("chart-line") +icon_svg("users") +icon_svg("dollar-sign") +icon_svg("globe") +``` + +Guidelines: + +- Never use emoji characters as icons. +- Use icons to reinforce a metric or action, not to decorate every label. +- Keep icon choice semantically close to the content. +- Reuse a small icon vocabulary across the app. + +### Accessible icon-only triggers + +When an icon is the only visible trigger for a tooltip, popover, or action, mark it as semantic and provide a meaningful title. + +```python +ui.tooltip( + icon_svg("circle-info", title="More information", a11y="sem"), + "Explains how this value is calculated", +) +``` + +The title should describe the purpose of the trigger, not the icon itself. + +### Common dashboard icons + +- `"chart-line"` for trends and time series +- `"chart-bar"` for comparisons +- `"users"` or `"user"` for people counts +- `"dollar-sign"` or `"wallet"` for money metrics +- `"percent"` for ratios and conversion +- `"globe"` or `"map"` for geographic views +- `"table"` for tabular drill-downs +- `"filter"` for filter controls + +## Maps + +The repo already contains Python dashboard examples with geographic outputs, so the guidance here should be concrete: maps belong in full-screen cards, need explicit height, and depend on clean numeric coordinates. + +### Clean coordinates first + +```python +df["latitude"] = pd.to_numeric(df["latitude"], errors="coerce") +df["longitude"] = pd.to_numeric(df["longitude"], errors="coerce") +map_data = df.dropna(subset=["latitude", "longitude"]) +``` + +Do not defer coordinate cleanup to the rendering function if the whole dashboard depends on the map. + +### Put maps in full-screen cards + +```python +ui.card( + ui.card_header("Market map"), + output_widget("market_map", height="540px"), + full_screen=True, +) +``` + +Guidelines: + +- Give the map an explicit height. +- Use `full_screen=True` so users can inspect dense point layers. +- Keep map controls in the sidebar or a small popover, not spread across the page. +- Remove extra padding when the visual should run edge to edge. + +### Match the map to the question + +Use the map when users need to answer geographic questions such as: + +- where listings cluster +- which neighborhoods command higher prices +- how a filtered subset changes by location +- which points deserve drill-down inspection + +If the geography is incidental, use a ranked table or bar chart instead. + +### Widget choices + +Shiny for Python dashboards in this repo use widget-style geographic outputs, so keep the guidance library-agnostic: + +- if the map library returns an interactive widget or figure, place it in a card with explicit height +- if the map shares filters with charts, drive all of them from the same `@reactive.calc` dataset +- keep legends, color scales, and marker size encodings simple enough to read at dashboard scale + +## Best Practices + +1. Use icons sparingly and consistently. +2. Add `a11y="sem"` and a useful `title` for icon-only triggers. +3. Clean latitude and longitude before building map outputs. +4. Put maps in full-screen cards with explicit height. +5. Use maps only when location materially changes the analysis. diff --git a/shiny/shiny-python-dashboard/references/layout-and-navigation.md b/shiny/shiny-python-dashboard/references/layout-and-navigation.md new file mode 100644 index 0000000..fbfb00e --- /dev/null +++ b/shiny/shiny-python-dashboard/references/layout-and-navigation.md @@ -0,0 +1,255 @@ +# Layout and Navigation in Shiny for Python + +This reference covers the page-level and section-level layout patterns for Shiny for Python dashboards. The goal is the same as a modern bslib dashboard in R: keep high-level structure explicit, use cards as the main content containers, and let the layout primitives handle responsive behavior instead of wiring grids by hand with `ui.div()`. + +## Page Layouts + +### `ui.page_sidebar()` + +Use `ui.page_sidebar()` for single-page dashboards where one sidebar controls the whole page. + +**Core API:** + +```python +app_ui = ui.page_sidebar( + ui.sidebar( + ui.input_selectize("ticker", "Stock", choices=stocks), + ui.input_date_range("dates", "Dates", start=start, end=end), + open="desktop", + ), + ui.card(ui.card_header("Price history"), ui.output_plot("price"), full_screen=True), + title="Market Dashboard", + fillable=True, +) +``` + +**Express API:** + +```python +ui.page_opts(title="Market Dashboard", fillable=True) + +with ui.sidebar(open="desktop"): + ui.input_selectize("ticker", "Stock", choices=stocks) + ui.input_date_range("dates", "Dates", start=start, end=end) +``` + +Best practices: + +- Keep inputs in `ui.sidebar()` and outputs in the main page body. +- Use `open="desktop"` so the sidebar is visible on large screens and collapses on mobile. +- Treat the page body as a sequence of value-box rows, chart cards, and table cards. + +### `ui.page_navbar()` + +Use `ui.page_navbar()` for multi-page apps with distinct sections. + +**Core API:** + +```python +overview = ui.layout_columns( + ui.card(ui.card_header("Overview"), ui.output_plot("overview_plot"), full_screen=True), + ui.card(ui.card_header("Summary"), ui.output_data_frame("overview_table"), full_screen=True), + col_widths=[6, 6], +) + +app_ui = ui.page_navbar( + ui.nav_panel("Overview", overview), + ui.nav_panel("Details", ui.output_data_frame("details")), + title="Analytics Platform", + fillable=True, +) +``` + +**Express API:** + +```python +ui.page_opts(title="Analytics Platform", fillable=True) +ui.nav_spacer() + +with ui.nav_panel("Overview"): + with ui.layout_columns(col_widths=[6, 6]): + with ui.card(full_screen=True): + ui.card_header("Overview") + @render.plot + def overview_plot(): + ... + +with ui.nav_panel("Details"): + with ui.card(full_screen=True): + ui.card_header("Details") + @render.data_frame + def details(): + ... +``` + +Best practices: + +- Use a navbar when pages serve different workflows, not just because there is a lot of content. +- `ui.nav_spacer()` is useful when you want later nav items aligned to the right. +- Do not force one shared sidebar onto every page if the pages need different filters. + +## Grid Layouts + +### `ui.layout_column_wrap()` + +Use `ui.layout_column_wrap()` for uniform cards or KPI boxes. + +```python +ui.layout_column_wrap( + ui.value_box("Users", ui.output_text("users"), showcase=icon_svg("users")), + ui.value_box("Revenue", ui.output_text("revenue"), showcase=icon_svg("dollar-sign")), + ui.value_box("Growth", ui.output_text("growth"), showcase=icon_svg("chart-line")), + width=1 / 3, + fill=False, +) +``` + +Use a CSS width like `"280px"` when you want the column count to adapt to screen size automatically. + +```python +ui.layout_column_wrap(card_a, card_b, card_c, width="280px") +``` + +Guidelines: + +- Prefer `width=1 / 3` or `width="280px"` instead of hand-calculated percent strings. +- Use `fill=False` for KPI rows so value boxes keep natural height. +- Use `ui.layout_column_wrap()` when you want a clean, even dashboard rhythm. + +### `ui.layout_columns()` + +Use `ui.layout_columns()` when you need explicit control over width proportions or breakpoints. + +```python +ui.layout_columns( + ui.card(ui.card_header("Sidebar summary"), ui.output_text_verbatim("summary")), + ui.card(ui.card_header("Main chart"), ui.output_plot("plot"), full_screen=True), + col_widths=[4, 8], +) +``` + +Responsive layouts use breakpoint dictionaries: + +```python +ui.layout_columns( + chart_card, + table_card, + col_widths={"sm": 12, "md": [6, 6], "lg": [7, 5]}, +) +``` + +Negative values create gaps when needed: + +```python +ui.layout_columns(card_a, card_b, col_widths=[5, -2, 5]) +``` + +Use `ui.layout_columns()` when the proportions matter more than uniformity. + +## Navigation Containers + +Use `ui.nav_panel()` to group related content into tabs or pages. + +### `ui.navset_card_underline()` + +This is the most useful section-level navigation container for dashboards: it creates a tabbed card with a header and shared footer controls. + +**Core API:** + +```python +analysis = ui.navset_card_underline( + ui.nav_panel("Plot", ui.output_plot("plot")), + ui.nav_panel("Table", ui.output_data_frame("table")), + title="Analysis", + footer=ui.input_select("metric", "Metric", choices=metrics), +) +``` + +**Express API:** + +```python +footer = ui.input_select("metric", "Metric", choices=metrics) + +with ui.navset_card_underline(title="Analysis", footer=footer): + with ui.nav_panel("Plot"): + @render.plot + def plot(): + ... + + with ui.nav_panel("Table"): + @render.data_frame + def table(): + ... +``` + +Do not wrap `ui.nav_panel()` content in another `ui.card()` when the container is already `ui.navset_card_*()`. + +## Sidebars + +### Page-level sidebars + +Use page-level sidebars when the controls affect the whole page or the whole app section. + +```python +ui.page_sidebar( + ui.sidebar( + ui.input_checkbox_group("region", "Region", regions), + ui.input_action_button("reset", "Reset"), + open="desktop", + ), + ..., +) +``` + +For navbar apps, only use a single shared sidebar if every page really uses the same controls. + +### Component-level sidebars + +Use `ui.layout_sidebar()` inside a card when one chart or module needs local controls. + +```python +ui.card( + ui.card_header("Custom chart"), + ui.layout_sidebar( + ui.sidebar( + ui.input_select("color", "Color by", choices=color_vars), + position="right", + width="240px", + ), + ui.output_plot("custom_chart"), + fillable=True, + ), + full_screen=True, +) +``` + +This keeps advanced controls close to the output they affect and avoids overloading the global sidebar. + +## Fill Behavior + +The same rule from bslib still applies conceptually: fill behavior only works when the surrounding container has a meaningful height. + +- Use `fillable=True` on dashboard pages. +- Use `full_screen=True` when users may need more space. +- Use fixed `height`, `min_height`, or chart-specific dimensions when a card would otherwise collapse too far. +- Avoid putting non-filling KPI rows inside layouts that should donate space to charts. + +A common hybrid pattern is a non-filling KPI row followed by cards that can grow: + +```python +ui.page_sidebar( + ui.sidebar(...), + ui.layout_columns(kpi_a, kpi_b, kpi_c, fill=False), + ui.layout_columns(chart_a, chart_b, col_widths=[6, 6]), + fillable=True, +) +``` + +## Best Practices + +1. Start with `ui.page_sidebar()` unless the app clearly needs multiple pages. +2. Use `ui.page_navbar()` for separate workflows, not just to hide content overflow. +3. Prefer `ui.layout_column_wrap()` for evenly sized cards and `ui.layout_columns()` for asymmetric layouts. +4. Keep KPI rows near the top and mark them `fill=False`. +5. Put page-specific controls in page-specific sidebars rather than forcing one global sidebar everywhere. +6. Use `ui.navset_card_underline()` to organize plots, tables, and notes within a single card-sized region. diff --git a/shiny/shiny-python-dashboard/references/reactivity-and-rendering.md b/shiny/shiny-python-dashboard/references/reactivity-and-rendering.md new file mode 100644 index 0000000..bd9a8b7 --- /dev/null +++ b/shiny/shiny-python-dashboard/references/reactivity-and-rendering.md @@ -0,0 +1,195 @@ +# Reactivity and Rendering in Shiny for Python + +This reference covers the reactive graph and output rendering patterns that make a Shiny for Python dashboard predictable and maintainable. + +## Reactive Graph Design + +### `@reactive.calc` + +Use `@reactive.calc` for filtered data, derived metrics, and reusable intermediate objects. + +```python +@reactive.calc +def filtered_data(): + frame = df.copy() + if input.region(): + frame = frame[frame["region"].isin(input.region())] + return frame + + +@reactive.calc +def metric_series(): + return pd.to_numeric(filtered_data()[input.metric()], errors="coerce").dropna() +``` + +Chain reactive calcs instead of repeating the same filtering logic in every output. + +### `req()` + +Use `req()` to stop the reactive pipeline when an input or intermediate result is empty. + +```python +from shiny import req + + +@reactive.calc +def selected_players(): + players = req(input.players()) + return careers()[careers()["person_id"].isin(players)] +``` + +This keeps render functions simple and avoids error-prone empty-state code in every output. + +### `@reactive.effect` and `@reactive.event` + +Use `@reactive.effect` for imperative updates such as resetting inputs or synchronizing dependent controls. Add `@reactive.event(...)` when the effect should only run in response to a specific trigger. + +```python +@reactive.effect +@reactive.event(input.reset) +def _(): + ui.update_slider("amount", value=[10, 90]) + ui.update_checkbox_group("service", selected=["Lunch", "Dinner"]) +``` + +Without `@reactive.event`, the effect will rerun whenever one of its reactive dependencies changes. + +### No global mutation + +Do not mutate module-level globals inside reactive functions. Compute new values and return them through the reactive graph instead. + +## Rendering Patterns + +### Matplotlib and Seaborn with `@render.plot` + +Import `matplotlib.pyplot` inside the render function. + +```python +@render.plot +def histogram(): + import matplotlib.pyplot as plt + + series = req(metric_series()) + fig, ax = plt.subplots(figsize=(8, 4)) + ax.hist(series, bins=20, color="#0d6efd", edgecolor="white") + ax.set_xlabel(input.metric()) + ax.set_ylabel("Count") + fig.tight_layout() + return fig +``` + +Rules: + +- use `figsize=(8, 4)` as a strong default +- label axes +- call `fig.tight_layout()` before returning +- drop missing values before plotting + +### Plotly with `shinywidgets` + +Use `output_widget()` in Core apps and `@render_plotly` in the server or Express block. + +```python +from shinywidgets import output_widget, render_plotly + +app_ui = ui.card( + ui.card_header("Scatterplot"), + output_widget("scatterplot"), + full_screen=True, +) + + +@render_plotly +def scatterplot(): + return px.scatter(filtered_data(), x="total_bill", y="tip", color=input.color()) +``` + +Avoid duplicate keys when you merge Plotly layout dictionaries: + +```python +axis_style = {"gridcolor": "#d0d7de", "showline": False} +fig.update_layout(yaxis={**axis_style, "tickfont": {"size": 11}}) +``` + +Do not write `dict(**axis_style, tickfont=...)` if `axis_style` already contains `tickfont`. + +### Tables with `@render.data_frame` + +Use `render.DataGrid(...)` for sortable, filterable tables. + +```python +@render.data_frame +def summary_table(): + return render.DataGrid(filtered_data(), filters=True) +``` + +Treat the data table as the detailed layer beneath charts and KPIs. + +### Dynamic UI with `@render.ui` + +Use `@render.ui` for small dynamic fragments such as icons, badges, or conditional helper text. + +```python +@render.ui +def growth_note(): + if get_change() >= 0: + return ui.span("Up vs prior period", class_="text-success") + return ui.span("Down vs prior period", class_="text-danger") +``` + +Do not build major page sections with `@render.ui` if a regular static layout plus reactive outputs would be simpler. + +## Core and Express Equivalents + +Core API keeps outputs explicit in the UI tree: + +```python +ui.card(ui.card_header("Data"), ui.output_data_frame("table"), full_screen=True) + + +@render.data_frame +def table(): + return render.DataGrid(filtered_data()) +``` + +Express colocates the UI block and renderer: + +```python +with ui.card(full_screen=True): + ui.card_header("Data") + + @render.data_frame + def table(): + return render.DataGrid(filtered_data()) +``` + +Both are valid. Choose the style that fits the codebase and keep it consistent. + +## Common Patterns + +### Reset buttons + +Use `@reactive.effect` with `@reactive.event` and the appropriate `ui.update_*()` helpers. + +### Cascading inputs + +Use a plain `@reactive.effect` when one input's choices depend on the current filtered data. + +```python +@reactive.effect +def _(): + choices = dict(zip(filtered_data()["id"], filtered_data()["label"])) + ui.update_selectize("item", choices=choices) +``` + +### Derived KPI values + +Create one calc per reusable business metric instead of recalculating inside each value box renderer. + +## Common Errors + +1. Repeating filtering logic in every render function instead of centralizing it in `@reactive.calc`. +2. Forgetting `req()` before indexing into empty selections. +3. Importing `matplotlib.pyplot` at module scope. +4. Building large layouts with `@render.ui` instead of static UI plus small reactive outputs. +5. Duplicating Plotly layout keys during dict unpacking. diff --git a/shiny/shiny-python-dashboard/references/styling-and-data.md b/shiny/shiny-python-dashboard/references/styling-and-data.md new file mode 100644 index 0000000..6320dcd --- /dev/null +++ b/shiny/shiny-python-dashboard/references/styling-and-data.md @@ -0,0 +1,147 @@ +# Styling and Data Patterns for Shiny for Python + +This reference covers the non-widget parts of a polished dashboard: project structure, data loading, number formatting, CSS, and light or dark presentation choices. + +## Project Structure + +Use a small, predictable structure for dashboard apps. + +```text +my-dashboard/ +├── app-core.py +├── app-express.py +├── shared.py +├── plots.py +├── styles.css +├── data.csv +└── requirements.txt +``` + +Guidelines: + +- Keep one runnable app file per API style. +- Put data loading, reusable constants, and `app_dir` in `shared.py`. +- Move complex chart construction into `plots.py` or another helper module once a render function becomes hard to scan. +- Keep CSS overrides small and intentional. + +## Data Loading + +Load static data once at module scope or in `shared.py`. + +```python +from pathlib import Path + +import pandas as pd + +app_dir = Path(__file__).parent +df = pd.read_csv(app_dir / "data.csv") + +metric_columns = ["price", "rating", "reviews"] +neighborhood_choices = sorted(df["neighborhood"].dropna().unique()) +``` + +This keeps reactive code focused on filtering and rendering instead of repeated file I/O. + +### Clean data before it reaches plots + +For dashboard inputs and charts, normalize types early: + +```python +df["price"] = pd.to_numeric(df["price"], errors="coerce") +df["score_rating"] = pd.to_numeric(df["score_rating"], errors="coerce") +df["latitude"] = pd.to_numeric(df["latitude"], errors="coerce") +df["longitude"] = pd.to_numeric(df["longitude"], errors="coerce") +df = df.dropna(subset=["latitude", "longitude"]) +``` + +Guidelines: + +- Coerce mixed-type numeric columns with `errors="coerce"`. +- Drop invalid map coordinates up front. +- Fill or label missing categorical values before exposing them in inputs. +- Precompute choice lists and slider ranges once. + +## Number Formatting + +Dashboard text should be formatted before it reaches a value box, annotation, or table summary. + +```python +def fmt_currency(amount: float) -> str: + return f"${amount:,.0f}" + + +def fmt_percent(ratio: float) -> str: + return f"{ratio:.1%}" + + +def fmt_large(number: float) -> str: + if number >= 1_000_000: + return f"{number / 1_000_000:.1f}M" + if number >= 1_000: + return f"{number / 1_000:.1f}K" + return f"{number:,.0f}" +``` + +Avoid raw values like `12345.6789` or `0.873421` in user-facing UI. + +## CSS and Theming + +Use a small stylesheet for layout polish and component-specific tuning. + +```python +from pathlib import Path +from shiny import ui + +app_dir = Path(__file__).parent + +app_ui = ui.page_sidebar( + ui.include_css(app_dir / "styles.css"), + ..., +) +``` + +Use CSS for: + +- spacing and alignment helpers +- custom card or hero section styling +- subtle borders, backgrounds, and typography rules +- responsive tweaks that do not belong inside Python layout logic + +Avoid using CSS to rebuild the layout system from scratch when Shiny layout primitives already solve the problem. + +### Theme direction + +Shiny for Python does not use the same `bs_theme()` object as bslib in R, so keep the theme story practical: + +- use named Bootstrap colors consistently across value boxes and accents +- define a small set of CSS custom properties for brand colors if the app needs a distinct look +- avoid mixing many unrelated accent colors across cards and charts +- if the app needs color-mode switching, use `ui.input_dark_mode()` intentionally rather than adding large unrelated CSS overrides + +```python +ui.input_dark_mode(id="mode") +``` + +## Requirements + +Include the packages your dashboard actually uses. Common dashboard requirements are: + +```text +shiny +pandas +plotly +matplotlib +seaborn +faicons +shinywidgets +``` + +Add mapping or table packages only when the app needs them. + +## Best Practices + +1. Load data once and reuse it through reactive calcs. +2. Clean numeric and coordinate columns before they hit inputs or plots. +3. Keep formatting helpers near the data layer, not scattered across render functions. +4. Use `ui.include_css(...)` for small style layers instead of large inline `