From fcdf8b8209751ce564df111962be1ace26a750ad Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Tue, 3 Mar 2026 14:44:26 -0800 Subject: [PATCH 1/2] Add Shiny Python dashboard guide and references Add a comprehensive style guide and reference docs for building polished Shiny (Python) dashboards. SKILL.md contains critical rules (icon usage, layout hierarchy, value box/card conventions, number formatting), Core and Express quick-start examples, and best practices for charts and responsive grids. Six reference files (components.md, core-vs-express.md, icons-and-maps.md, layout-and-navigation.md, reactivity-and-rendering.md, styling-and-data.md) provide detailed patterns for components, imports/usage differences, icons/maps, navigation, reactivity/rendering, and styling/data-loading to ensure consistent, maintainable dashboards. --- shiny/shiny-python-dashboard/SKILL.md | 329 ++++++++++++++++ .../references/components.md | 220 +++++++++++ .../references/core-vs-express.md | 192 ++++++++++ .../references/icons-and-maps.md | 351 ++++++++++++++++++ .../references/layout-and-navigation.md | 137 +++++++ .../references/reactivity-and-rendering.md | 225 +++++++++++ .../references/styling-and-data.md | 163 ++++++++ 7 files changed, 1617 insertions(+) create mode 100644 shiny/shiny-python-dashboard/SKILL.md create mode 100644 shiny/shiny-python-dashboard/references/components.md create mode 100644 shiny/shiny-python-dashboard/references/core-vs-express.md create mode 100644 shiny/shiny-python-dashboard/references/icons-and-maps.md create mode 100644 shiny/shiny-python-dashboard/references/layout-and-navigation.md create mode 100644 shiny/shiny-python-dashboard/references/reactivity-and-rendering.md create mode 100644 shiny/shiny-python-dashboard/references/styling-and-data.md diff --git a/shiny/shiny-python-dashboard/SKILL.md b/shiny/shiny-python-dashboard/SKILL.md new file mode 100644 index 0000000..2af8175 --- /dev/null +++ b/shiny/shiny-python-dashboard/SKILL.md @@ -0,0 +1,329 @@ +````skill +--- +name: shiny-python-dashboard +description: Best practices for building polished dashboards in Shiny for Python. Covers Core and Express APIs, proper icon usage (faicons/Bootstrap Icons — never emojis), value box layout, responsive grids, charts, and reactive patterns. +--- + +## Critical rules — ALWAYS follow these + +1. **NEVER use emoji characters as icons.** No emojis anywhere — not in value box showcase, not in nav panel titles, not in card headers. Use `faicons.icon_svg()` or Bootstrap Icons SVG instead. Emojis render inconsistently and look unprofessional. +2. **Limit value boxes to 3-4 per row.** More causes text overlap and cramped layouts. +3. **Always set `fill=False`** on the row wrapping value boxes (`layout_columns` or `layout_column_wrap`) to prevent vertical stretching. +4. **Always set `fillable=True`** on `page_sidebar` / `page_opts`. +5. **Always add `full_screen=True`** on cards containing charts or tables. +6. **Always add `ui.card_header("Title")`** to every card — never leave cards untitled. +7. **Use `ui.layout_columns(col_widths=[...])`** for grid layouts. +8. **Set explicit chart dimensions** — `fig.update_layout(height=400)` for Plotly, `plt.subplots(figsize=(8, 4))` for Matplotlib. +9. **Handle NaN/missing data gracefully** — some columns have blanks. Use `df.dropna(subset=[col])` before plotting, `pd.to_numeric(col, errors="coerce")` for mixed-type columns, and guard with `req()` for empty inputs. +10. **Use named Bootstrap theme colors** for value boxes: `"primary"`, `"success"`, `"info"`, `"warning"`, `"danger"`. Avoid `bg-gradient-*`. +11. **Place ALL imports at the top of the file.** Never import inside functions, reactive calcs, or render blocks — except `matplotlib.pyplot` which must be imported inside `@render.plot` to avoid backend conflicts. +12. **Format all numbers for readability.** Never display raw floats or unformatted integers. Use comma separators, currency symbols, and percentage formatting. +13. **Follow the standard dashboard layout hierarchy**: value boxes (KPIs) at top → charts in middle → data table at bottom. +14. **Use responsive `col_widths` with breakpoints** for layouts that work on mobile: `col_widths={"sm": 12, "md": [6, 6]}`. + +## Number formatting + +Always format numbers for professional display: + +```python +# Integers — comma separator +f"{count:,}" # "12,345" + +# Currency +f"${amount:,.2f}" # "$1,234.56" +f"${amount:,.0f}" # "$1,235" + +# Percentages +f"{ratio:.1%}" # "78.3%" +f"{ratio:.0%}" # "78%" + +# Decimals — control precision +f"{value:,.1f}" # "1,234.6" +f"{value:.2f}" # "1234.57" + +# Large numbers — abbreviate +def fmt_large(n): + if n >= 1_000_000: return f"{n/1_000_000:.1f}M" + if n >= 1_000: return f"{n/1_000:.1f}K" + return f"{n:,.0f}" +``` + +Never display raw floats like `0.7834523123` or unformatted integers like `12345`. + +## Icons — ONLY faicons or Bootstrap Icons + +```python +# CORRECT: faicons (Font Awesome SVGs) — preferred +from faicons import icon_svg +icon = icon_svg("chart-line") # solid style (default) +icon = icon_svg("user", "regular") # outlined style +``` + +```python +# WRONG — NEVER do this: +showcase="\U0001F4CA" # emoji is NOT an icon +showcase="\U0001F3E5" # emoji is NOT an icon +``` + +### Common faicons for dashboards + +| Icon name | Use case | +|---------------------|-----------------------------| +| `"chart-line"` | Trends, time series | +| `"chart-bar"` | Bar charts, distributions | +| `"users"` | People count | +| `"user"` | Individual person | +| `"dollar-sign"` | Currency, revenue | +| `"wallet"` | Money, tips | +| `"clipboard-check"` | Completed tasks | +| `"calendar"` | Dates, scheduling | +| `"flask"` | Science, experiments | +| `"stethoscope"` | Healthcare | +| `"heart-pulse"` | Health metrics | +| `"arrow-up"` | Positive change | +| `"arrow-down"` | Negative change | +| `"percent"` | Percentages | +| `"database"` | Data, records | +| `"filter"` | Filtering | +| `"table"` | Tabular data | +| `"magnifying-glass"`| Search | +| `"globe"` | Geographic data | +| `"building"` | Organizations | +| `"pills"` | Medications, pharma | +| `"vial"` | Lab samples | +| `"truck"` | Shipping, logistics | +| `"circle-check"` | Success, completion | +| `"clock"` | Time, duration | +| `"hospital"` | Healthcare facility | + +## Quick start — Core dashboard + +```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("var", "Variable", choices=list(df.columns)), + open="desktop", + ), + # Value boxes — fill=False prevents vertical stretching + ui.layout_columns( + ui.value_box("Total Records", ui.output_text("total"), + showcase=icon_svg("database"), theme="primary"), + ui.value_box("Average", ui.output_text("avg"), + showcase=icon_svg("chart-line"), theme="info"), + ui.value_box("Maximum", ui.output_text("max_val"), + showcase=icon_svg("arrow-up"), theme="success"), + fill=False, + ), + # Charts — col_widths controls the grid + ui.layout_columns( + ui.card(ui.card_header("Distribution"), + ui.output_plot("hist"), full_screen=True), + ui.card(ui.card_header("Trend"), + ui.output_plot("trend"), full_screen=True), + col_widths=[6, 6], + ), + # Data table + ui.card(ui.card_header("Data"), ui.output_data_frame("table"), + full_screen=True), + title="Dashboard", + fillable=True, +) + +def server(input, output, session): + @reactive.calc + def filtered(): + return df + + @render.text + def total(): + return f"{len(filtered()):,}" + + @render.text + def avg(): + return f"{filtered()[input.var()].mean():,.1f}" + + @render.text + def max_val(): + return f"{filtered()[input.var()].max():,.0f}" + + @render.plot + def hist(): + import matplotlib.pyplot as plt + fig, ax = plt.subplots(figsize=(8, 4)) + ax.hist(filtered()[input.var()].dropna(), bins=20, + color="#0d6efd", edgecolor="white") + ax.set_xlabel(input.var()) + ax.set_ylabel("Count") + return fig + + @render.plot + def trend(): + import matplotlib.pyplot as plt + fig, ax = plt.subplots(figsize=(8, 4)) + ax.plot(filtered()[input.var()].dropna().values, + color="#198754", linewidth=1.5) + ax.set_ylabel(input.var()) + return fig + + @render.data_frame + def table(): + return render.DataGrid(filtered(), filters=True) + +app = App(app_ui, server) +``` + +## Quick start — Express dashboard + +```python +from pathlib import Path +import pandas as pd +from faicons import icon_svg +from shiny import reactive +from shiny.express import input, render, ui + +app_dir = Path(__file__).parent +df = pd.read_csv(app_dir / "data.csv") + +ui.page_opts(title="Dashboard", fillable=True) + +with ui.sidebar(open="desktop"): + ui.input_select("var", "Variable", choices=list(df.columns)) + +@reactive.calc +def filtered(): + return df + +# Value boxes — fill=False prevents vertical stretching +with ui.layout_columns(fill=False): + with ui.value_box(showcase=icon_svg("database"), theme="primary"): + "Total Records" + @render.express + def total(): + f"{len(filtered()):,}" + + with ui.value_box(showcase=icon_svg("chart-line"), theme="info"): + "Average" + @render.express + def avg(): + f"{filtered()[input.var()].mean():,.1f}" + + with ui.value_box(showcase=icon_svg("arrow-up"), theme="success"): + "Maximum" + @render.express + def max_val(): + f"{filtered()[input.var()].max():,.0f}" + +# Charts +with ui.layout_columns(col_widths=[6, 6]): + with ui.card(full_screen=True): + ui.card_header("Distribution") + @render.plot + def hist(): + import matplotlib.pyplot as plt + fig, ax = plt.subplots(figsize=(8, 4)) + ax.hist(filtered()[input.var()].dropna(), bins=20, + color="#0d6efd", edgecolor="white") + return fig + + with ui.card(full_screen=True): + ui.card_header("Trend") + @render.plot + def trend(): + import matplotlib.pyplot as plt + fig, ax = plt.subplots(figsize=(8, 4)) + ax.plot(filtered()[input.var()].dropna().values, color="#198754") + return fig + +# Data table +with ui.card(full_screen=True): + ui.card_header("Data") + @render.data_frame + def table(): + return render.DataGrid(filtered(), filters=True) +``` + +## Dashboard layout hierarchy + +Always follow this top-to-bottom structure: + +``` +1. Value boxes (KPIs) — fill=False row at the top +2. Charts — in cards with full_screen=True +3. Data table — full-width card at the bottom +``` + +For multi-page apps with `page_navbar`, apply this hierarchy within each `nav_panel`. + +## Responsive design + +Use `col_widths` with breakpoint dictionaries so dashboards look good on all screen sizes: + +```python +# Stacks on mobile, side-by-side on desktop +ui.layout_columns( + chart_card_1, chart_card_2, + col_widths={"sm": 12, "md": [6, 6]}, +) + +# Three columns on desktop, stacked on mobile +ui.layout_columns( + card_a, card_b, card_c, + col_widths={"sm": 12, "md": [6, 6, 12], "lg": [4, 4, 4]}, +) + +# Use negative values for spacing +ui.layout_columns( + card_a, card_b, + col_widths=[5, -2, 5], # 5-wide cards with 2-unit gap +) +``` + +For uniform grids (all items same size), use `ui.layout_column_wrap(width=1/3)` instead. + +## Chart best practices + +- **Plotly**: Set `height=400`, hide modebar, use `margin=dict(l=40, r=20, t=40, b=40)` +- **Matplotlib**: Use `figsize=(8, 4)`, call `fig.tight_layout()` before returning +- **Seaborn**: Build on matplotlib — same `figsize` rules apply +- **Color palette**: Use Bootstrap-aligned colors for consistency: + - Primary blue: `"#0d6efd"` — main charts + - Success green: `"#198754"` — positive trends + - Danger red: `"#dc3545"` — negative trends, alerts + - Info cyan: `"#0dcaf0"` — secondary charts + - Warning amber: `"#ffc107"` — caution indicators +- **Always label axes** with `ax.set_xlabel()` / `ax.set_ylabel()` +- **Remove chartjunk**: no unnecessary gridlines, borders, or legends for single-series charts + +## Project structure + +``` +my-dashboard/ ++-- app.py # Main app (Core or Express) ++-- shared.py # Data loading, constants, app_dir ++-- styles.css # Minimal CSS overrides ++-- data.csv # Static data files ++-- requirements.txt # Must include faicons +``` + +`shared.py` always exports `app_dir = Path(__file__).parent` and pre-loaded data. +`requirements.txt` must always include `faicons`. + +## Reference files + +Read these for detailed patterns on specific topics: + +**Layout & navigation**: See [references/layout-and-navigation.md](references/layout-and-navigation.md) +**Value boxes, cards & grids**: See [references/components.md](references/components.md) +**Reactivity & rendering**: See [references/reactivity-and-rendering.md](references/reactivity-and-rendering.md) +**Styling & data loading**: See [references/styling-and-data.md](references/styling-and-data.md) +**Icons & maps**: See [references/icons-and-maps.md](references/icons-and-maps.md) +**Core vs Express**: See [references/core-vs-express.md](references/core-vs-express.md) +```` diff --git a/shiny/shiny-python-dashboard/references/components.md b/shiny/shiny-python-dashboard/references/components.md new file mode 100644 index 0000000..b4bba59 --- /dev/null +++ b/shiny/shiny-python-dashboard/references/components.md @@ -0,0 +1,220 @@ +# Value Boxes, Cards, and Grid Layout + +## Contents + +- Value boxes +- Dynamic showcase icons +- Standard card pattern +- Grid layout with col_widths +- Card headers with inline controls + +--- + +## Value boxes + +### Critical rules + +- **NEVER use emoji characters** as the `showcase` parameter. Always use `icon_svg()` from faicons. +- **Always wrap value boxes** in `ui.layout_columns(fill=False)` to prevent vertical stretching. +- **Limit to 3-4 value boxes per row** to avoid text overlap. +- **Always set a `theme`** for visual distinction: `"primary"`, `"success"`, `"info"`, `"warning"`, `"danger"`. + +### Correct pattern + +```python +# Core +ui.layout_columns( + ui.value_box("Total tippers", ui.output_ui("total_tippers"), + showcase=icon_svg("user", "regular"), theme="primary"), + ui.value_box("Average tip", ui.output_ui("average_tip"), + showcase=icon_svg("wallet"), theme="info"), + ui.value_box("Average bill", ui.output_ui("average_bill"), + showcase=icon_svg("dollar-sign"), theme="success"), + fill=False, +) +``` + +```python +# Express +with ui.layout_columns(fill=False): + with ui.value_box(showcase=icon_svg("user", "regular"), theme="primary"): + "Total tippers" + @render.express + def total_tippers(): + tips_data().height + + with ui.value_box(showcase=icon_svg("wallet"), theme="info"): + "Average tip" + @render.express + def average_tip(): + d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() + f"{d:.1%}" if d else "N/A" +``` + +### Value box themes + +Use named Bootstrap theme values for the `theme` parameter: + +| Theme | Color | Use for | +|-------------|---------|--------------------------------| +| `"primary"` | Blue | Main KPI, total counts | +| `"success"` | Green | Positive metrics, completions | +| `"info"` | Cyan | Averages, informational | +| `"warning"` | Yellow | Alerts, pending items | +| `"danger"` | Red | Errors, critical metrics | + +Avoid `bg-gradient-*` combinations — they often clash with text colors. + +### Anti-patterns — DO NOT do these + +```python +# WRONG: emoji as showcase +ui.value_box("Total", "100", showcase="\U0001F4CA") # NO + +# WRONG: no theme +ui.value_box("Total", "100", showcase=icon_svg("database")) # missing theme + +# WRONG: too many value boxes (5+) in one row — causes text overlap +ui.layout_columns(vb1, vb2, vb3, vb4, vb5, vb6, fill=False) # TOO MANY + +# WRONG: missing fill=False — value boxes stretch vertically +ui.layout_columns(vb1, vb2, vb3) # missing fill=False +``` + +`ui.layout_column_wrap(fill=False)` is an alternative to `ui.layout_columns(fill=False)` — +both work for value box rows. + +--- + +## Dynamic showcase icons + +Render a conditional icon (e.g., up/down arrow) via `@render.ui` and pass +`ui.output_ui("change_icon")` as the `showcase` parameter: + +```python +# Core — dynamic showcase +ui.value_box("Change", ui.output_ui("change"), showcase=ui.output_ui("change_icon")) + +@render.ui +def change_icon(): + icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") + icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") + return icon +``` + +In Express, use `with ui.hold():` when an output is referenced **before** it is defined: + +```python +# Express — forward-referenced output +with ui.value_box(showcase=output_ui("change_icon")): + "Change" + @render.ui + def change(): return f"${get_change():.2f}" + +# Define the forward-referenced output after the widget that uses it +with ui.hold(): + @render.ui + def change_icon(): + icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") + icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") + return icon +``` + +--- + +## Standard card pattern + +Every card MUST have a `ui.card_header()` and use `full_screen=True` for charts/tables: + +```python +# Core +ui.card( + ui.card_header("Price history"), + output_widget("price_history"), + full_screen=True, +) +``` + +```python +# Express +with ui.card(full_screen=True): + ui.card_header("Price history") + @render_plotly + def price_history(): ... +``` + +Use `ui.card_footer()` for supplementary text below the card content: + +```python +ui.card_footer("Percentiles are based on career per game averages.") +``` + +### Anti-patterns for cards + +```python +# WRONG: no card_header — card has no title +ui.card(ui.output_plot("plot"), full_screen=True) + +# WRONG: no full_screen — user cannot expand chart +ui.card(ui.card_header("Plot"), ui.output_plot("plot")) + +# WRONG: bare output without card wrapping +ui.output_plot("plot") # should be inside a card +``` + +--- + +## Grid layout with col_widths + +Use `ui.layout_columns(col_widths=[...])` for asymmetric card arrangements: + +```python +ui.layout_columns( + card_a, card_b, card_c, + col_widths=[6, 6, 12], # two cards on top row, one full-width below +) +``` + +For responsive breakpoints, pass a dict: + +```python +col_widths={"sm": 12, "md": 12, "lg": [4, 8]} +``` + +This stacks cards vertically on small/medium screens and splits 4:8 on large screens. + +--- + +## Card headers with inline controls + +Use flexbox classes for alignment. + +### Popover with secondary inputs + +```python +ui.card_header( + "Total bill vs tip", + ui.popover( + icon_svg("ellipsis"), + ui.input_radio_buttons("color_var", None, ["none", "sex", "day"], inline=True), + title="Add a color variable", + placement="top", + ), + class_="d-flex justify-content-between align-items-center", +) +``` + +### Inline select dropdown + +```python +ui.card_header( + "Player career ", + ui.input_select("stat", None, choices=stats, selected="PTS", width="auto"), + " vs the rest of the league", + class_="d-flex align-items-center gap-1", +) +``` + +Use `"d-flex justify-content-between align-items-center"` when the control should be +pushed to the far right. Use `"d-flex align-items-center gap-1"` when the control +is inline within the title text. 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..8a8819c --- /dev/null +++ b/shiny/shiny-python-dashboard/references/core-vs-express.md @@ -0,0 +1,192 @@ +# Core vs Express — Full Comparison + +## Contents + +- Side-by-side quick reference +- Import differences +- Page setup +- UI construction +- Server function vs module-level +- Output placement +- Value box rendering +- Forward references + +--- + +## Side-by-side quick reference + +| Aspect | Core | Express | +| --- | --- | --- | +| **Imports** | `from shiny import App, reactive, render, ui` | `from shiny.express import input, render, ui` | +| **Page setup** | `app_ui = ui.page_sidebar(title=...)` | `ui.page_opts(title=..., fillable=True)` | +| **Sidebar** | `ui.sidebar(...)` as arg | `with ui.sidebar():` | +| **Layout** | Nested function calls | `with` context managers | +| **Server** | `def server(input, output, session):` | Module-level decorators | +| **Output placement** | `output_widget("id")` / `ui.output_*("id")` | Decorator inline in `with` block | +| **Value boxes** | `@render.ui` + `return` | `@render.express` (no `return`) | +| **Forward refs** | N/A | `with ui.hold():` | +| **App creation** | `app = App(app_ui, server)` | Not needed | +| **Nav panels** | `ui.nav_panel("Name", content)` as arg | `with ui.nav_panel("Name"):` | + +--- + +## Import differences + +```python +# Core +from shiny import App, reactive, render, req, ui +from shinywidgets import output_widget, render_plotly + +# Express +from shiny import reactive, req +from shiny.express import input, render, ui +from shinywidgets import render_plotly +``` + +In Express, `input` is imported from `shiny.express`, not `shiny`. +`output_widget` is not needed in Express — `@render_plotly` is placed inline. + +--- + +## Page setup + +```python +# Core — declarative, assigned to a variable +app_ui = ui.page_sidebar( + ui.sidebar(...), + # ... layout content ... + title="Dashboard", + fillable=True, +) +``` + +```python +# Express — imperative, called at module level +ui.page_opts(title="Dashboard", fillable=True) +with ui.sidebar(): + ... +``` + +--- + +## UI construction + +Core builds the entire UI tree as nested function calls: + +```python +# Core +ui.layout_columns( + ui.card(ui.card_header("Plot"), output_widget("plot"), full_screen=True), + ui.card(ui.card_header("Table"), ui.output_data_frame("table")), + col_widths=[8, 4], +) +``` + +Express uses `with` context managers: + +```python +# Express +with ui.layout_columns(col_widths=[8, 4]): + with ui.card(full_screen=True): + ui.card_header("Plot") + @render_plotly + def plot(): ... + with ui.card(): + ui.card_header("Table") + @render.data_frame + def table(): ... +``` + +--- + +## Server function vs module-level + +Core wraps all reactive logic in an explicit server function: + +```python +# Core +def server(input, output, session): + @reactive.calc + def filtered(): ... + + @render.plot + def plot(): ... + +app = App(app_ui, server) +``` + +Express places reactive logic at module level — no `server` function, no `App()`: + +```python +# Express +@reactive.calc +def filtered(): ... + +# Render decorators placed inline within with-blocks (see UI construction above) +``` + +--- + +## Output placement + +In Core, outputs must be placed in the UI tree using explicit placeholder functions: + +- `output_widget("id")` for Plotly charts (from `shinywidgets`) +- `ui.output_ui("id")` for dynamic UI / value box content +- `ui.output_plot("id")` for matplotlib/seaborn +- `ui.output_data_frame("id")` for data tables + +In Express, render decorators are placed inline where the output should appear: + +```python +with ui.card(): + @render_plotly + def chart(): ... # output appears here in the card +``` + +--- + +## Value box rendering + +```python +# Core — @render.ui with explicit return +@render.ui +def average_tip(): + d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() + return f"{d:.1%}" if d else "N/A" +``` + +```python +# Express — @render.express, value printed implicitly (no return) +with ui.value_box(showcase=icon_svg("wallet")): + "Average tip" + @render.express + def average_tip(): + d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() + f"{d:.1%}" if d else "N/A" +``` + +--- + +## Forward references + +Core has no forward-reference issue — the UI tree and server are separate. + +In Express, if an output is used as a parameter (e.g., `showcase=output_ui("icon")`) +before the render function is defined, wrap the definition in `with ui.hold()`: + +```python +# The value_box references "change_icon" before it exists +with ui.value_box(showcase=output_ui("change_icon")): + "Change" + @render.ui + def change(): return f"${get_change():.2f}" + +# Define the forward-referenced output later +with ui.hold(): + @render.ui + def change_icon(): + icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") + icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") + return icon +``` 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..90f6d24 --- /dev/null +++ b/shiny/shiny-python-dashboard/references/icons-and-maps.md @@ -0,0 +1,351 @@ +# Icons with faicons and Interactive Maps + +## Contents + +- Using faicons for icons +- Icon patterns: static, dictionary, dynamic +- Icons as popover triggers +- Interactive maps with ipyleaflet +- Map markers, layers, and basemaps +- Draggable markers with reactive updates + +--- + +## Using faicons + +The `faicons` package provides Font Awesome SVG icons for Shiny for Python. +Add `faicons` to `requirements.txt`. + +### CRITICAL: Never use emoji characters as icons + +Emojis are NOT icons. They render differently across platforms, cannot be styled +with CSS, and look unprofessional in dashboards. ALWAYS use `icon_svg()` from +faicons or Bootstrap Icons via `ui.HTML()`. + +```python +# CORRECT +from faicons import icon_svg +showcase = icon_svg("chart-line") +showcase = icon_svg("hospital") +showcase = icon_svg("users") + +# WRONG — never do this +showcase = "\U0001F4CA" # chart emoji — NO +showcase = "\U0001F3E5" # hospital emoji — NO +showcase = "\U0001F465" # people emoji — NO +``` + +### Import styles + +```python +# Option A: namespace import (dashboard-tips pattern) +import faicons as fa +icon = fa.icon_svg("user", "regular") + +# Option B: direct import (stock-app, map-distance pattern) +from faicons import icon_svg +icon = icon_svg("dollar-sign") +``` + +### Static icons in value boxes + +Pass `icon_svg()` directly to the `showcase` parameter: + +```python +# Core +ui.value_box("Current Price", ui.output_ui("price"), + showcase=icon_svg("dollar-sign")) + +# Express +with ui.value_box(showcase=icon_svg("dollar-sign")): + "Current Price" + @render.ui + def price(): return f"{close.iloc[-1]:.2f}" +``` + +### Icon dictionary pattern + +Pre-build icons in a dict when reusing across multiple components (value boxes + popovers). +This avoids repeated `icon_svg()` calls: + +```python +ICONS = { + "user": fa.icon_svg("user", "regular"), + "wallet": fa.icon_svg("wallet"), + "currency-dollar": fa.icon_svg("dollar-sign"), + "ellipsis": fa.icon_svg("ellipsis"), +} +``` + +Then reference by key: + +```python +ui.value_box("Total tippers", ui.output_ui("total_tippers"), + showcase=ICONS["user"]) +``` + +### Dynamic / conditional icons + +Render icons conditionally via `@render.ui`. Use `.add_class()` to style with +Bootstrap text-color utilities: + +```python +# Core +ui.value_box("Change", ui.output_ui("change"), + showcase=ui.output_ui("change_icon")) + +@render.ui +def change_icon(): + icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") + icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") + return icon +``` + +In Express, use `ui.hold()` for the forward-referenced output: + +```python +from shiny.ui import output_ui + +with ui.value_box(showcase=output_ui("change_icon")): + "Change" + @render.ui + def change(): return f"${get_change():.2f}" + +with ui.hold(): + @render.ui + def change_icon(): + icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") + icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") + return icon +``` + +### Icons as popover triggers + +Use an icon (typically `"ellipsis"`) as the trigger for a popover containing +secondary inputs: + +```python +ui.card_header( + "Total bill vs tip", + ui.popover( + ICONS["ellipsis"], # trigger icon + ui.input_radio_buttons("color", None, ["none", "sex", "day"], inline=True), + title="Add a color variable", + placement="top", + ), + class_="d-flex justify-content-between align-items-center", +) +``` + +### Common icon names + +| Icon name | Typical use | +|----------------------|-----------------------------| +| `"user"` | Count / people metrics | +| `"users"` | Groups / team size | +| `"wallet"` | Money / tip metrics | +| `"dollar-sign"` | Currency / price | +| `"percent"` | Percentage values | +| `"ellipsis"` | Popover / settings trigger | +| `"arrow-up"` | Positive change indicator | +| `"arrow-down"` | Negative change indicator | +| `"chart-line"` | Trends / time series | +| `"chart-bar"` | Bar charts / distributions | +| `"globe"` | Geographic / distance | +| `"ruler"` | Measurement | +| `"mountain"` | Altitude / elevation | +| `"database"` | Data / records | +| `"clipboard-check"` | Completed tasks | +| `"calendar"` | Dates / scheduling | +| `"flask"` | Science / experiments | +| `"stethoscope"` | Healthcare | +| `"heart-pulse"` | Health metrics | +| `"hospital"` | Healthcare facility | +| `"pills"` | Medications / pharma | +| `"vial"` | Lab samples | +| `"building"` | Organizations | +| `"truck"` | Shipping / logistics | +| `"filter"` | Filtering | +| `"table"` | Tabular data | +| `"magnifying-glass"` | Search | +| `"circle-check"` | Success / completion | +| `"clock"` | Time / duration | + +Pass `"regular"` as second arg for outlined style: `icon_svg("user", "regular")`. +Default style is `"solid"`. + +--- + +## Interactive maps with ipyleaflet + +The `map-distance` app demonstrates interactive maps using `ipyleaflet` rendered +through `shinywidgets`. Add to `requirements.txt`: + +```txt +ipyleaflet +shinywidgets +geopy # for distance calculations +requests # for elevation API lookups +``` + +### Basic map rendering + +Use `@render_widget` from `shinywidgets` to render an ipyleaflet `Map`: + +```python +# Core +from shinywidgets import output_widget, render_widget +import ipyleaflet as L + +# In UI +output_widget("map") + +# In server +@render_widget +def map(): + m = L.Map(zoom=4, center=(0, 0)) + m.add_layer(L.basemap_to_tiles(BASEMAPS["WorldImagery"])) + return m +``` + +```python +# Express +from shinywidgets import render_widget + +@render_widget +def map(): + m = L.Map(zoom=4, center=(0, 0)) + m.add_layer(L.basemap_to_tiles(BASEMAPS[input.basemap()])) + return m +``` + +### Basemap configuration + +Define available basemaps in `shared.py`: + +```python +from ipyleaflet import basemaps + +BASEMAPS = { + "WorldImagery": basemaps.Esri.WorldImagery, + "Mapnik": basemaps.OpenStreetMap.Mapnik, + "Positron": basemaps.CartoDB.Positron, + "DarkMatter": basemaps.CartoDB.DarkMatter, +} +``` + +Let users switch via `ui.input_selectize("basemap", "Choose a basemap", choices=list(BASEMAPS.keys()))`. + +### Partial map updates with reactive effects + +Render the map **once**, then update layers incrementally via `@reactive.effect` +and helper functions. Access the underlying widget via `map.widget`: + +```python +@reactive.effect +def _(): + update_marker(map.widget, loc1xy(), on_move1, "loc1") + +@reactive.effect +def _(): + update_marker(map.widget, loc2xy(), on_move2, "loc2") + +@reactive.effect +def _(): + update_line(map.widget, loc1xy(), loc2xy()) +``` + +### Layer management helpers + +Name layers so they can be found and replaced: + +```python +def update_marker(map: L.Map, loc: tuple, on_move: object, name: str): + remove_layer(map, name) + m = L.Marker(location=loc, draggable=True, name=name) + m.on_move(on_move) + map.add_layer(m) + +def update_line(map: L.Map, loc1: tuple, loc2: tuple): + remove_layer(map, "line") + map.add_layer( + L.Polyline(locations=[loc1, loc2], color="blue", weight=2, name="line") + ) + +def remove_layer(map: L.Map, name: str): + for layer in map.layers: + if layer.name == name: + map.remove_layer(layer) + +def update_basemap(map: L.Map, basemap: str): + for layer in map.layers: + if isinstance(layer, L.TileLayer): + map.remove_layer(layer) + map.add_layer(L.basemap_to_tiles(BASEMAPS[basemap])) +``` + +### Draggable markers with input sync + +When a marker is dragged, update a `selectize` input so the new coordinates flow +back through the reactive graph: + +```python +def on_move1(**kwargs): + return on_move("loc1", **kwargs) + +def on_move(id, **kwargs): + loc = kwargs["location"] + loc_str = f"{loc[0]}, {loc[1]}" + choices = city_names + [loc_str] + ui.update_selectize(id, selected=loc_str, choices=choices) +``` + +### Fit bounds reactively + +Auto-zoom to show both markers when they move outside the current viewport: + +```python +@reactive.effect +def _(): + l1, l2 = loc1xy(), loc2xy() + lat_rng = [min(l1[0], l2[0]), max(l1[0], l2[0])] + lon_rng = [min(l1[1], l2[1]), max(l1[1], l2[1])] + new_bounds = [[lat_rng[0], lon_rng[0]], [lat_rng[1], lon_rng[1]]] + + b = map.widget.bounds + if len(b) == 0 or ( + lat_rng[0] < b[0][0] or lat_rng[1] > b[1][0] or + lon_rng[0] < b[0][1] or lon_rng[1] > b[1][1] + ): + map.widget.fit_bounds(new_bounds) +``` + +### Distance calculations with geopy + +```python +from geopy.distance import geodesic, great_circle + +@render.text +def great_circle_dist(): + circle = great_circle(loc1xy(), loc2xy()) + return f"{circle.kilometers.__round__(1)} km" + +@render.text +def geo_dist(): + dist = geodesic(loc1xy(), loc2xy()) + return f"{dist.kilometers.__round__(1)} km" +``` + +### Value box theming for maps + +Use named Bootstrap themes for value boxes — avoid gradient themes: + +```python +ui.value_box("Great Circle Distance", ui.output_text("great_circle_dist"), + theme="primary", showcase=icon_svg("globe")) + +ui.value_box("Geodesic Distance", ui.output_text("geo_dist"), + theme="info", showcase=icon_svg("ruler")) +``` + +Available themes: `"primary"`, `"success"`, `"info"`, `"warning"`, `"danger"`. 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..2ab5253 --- /dev/null +++ b/shiny/shiny-python-dashboard/references/layout-and-navigation.md @@ -0,0 +1,137 @@ +# Layout, Sidebar, and Navigation + +## Contents +- Choosing the right page layout +- Sidebar patterns +- Navigation between panels +- Two-level navigation hierarchy + +--- + +## Choosing the right page layout + +### `ui.page_sidebar` — Global sidebar controlling all content + +Use when a single sidebar of filters drives all outputs on one page. +**Always set `fillable=True`** so content fills the viewport. + +```python +# Core +app_ui = ui.page_sidebar(ui.sidebar(...), title="My Dashboard", fillable=True) +# Express +ui.page_opts(title="My Dashboard", fillable=True) +with ui.sidebar(): ... +``` + +### `ui.page_navbar` — Multi-page navigation via top navbar + +Use when the app has distinct pages/sections the user switches between. +**Never use emojis in nav panel titles** — use plain text only. + +```python +# Core +app_ui = ui.page_navbar( + ui.nav_spacer(), + ui.nav_panel("Overview", page1), + ui.nav_panel("Details", page2), + title="My App", + fillable=True, +) +``` + +```python +# Express +ui.page_opts(title="My App", fillable=True) +ui.nav_spacer() +with ui.nav_panel("Overview"): ... +with ui.nav_panel("Details"): ... +``` + +`ui.nav_spacer()` pushes subsequent nav items to the right in the navbar. + +--- + +## Sidebar patterns + +Place all primary filter inputs inside `ui.sidebar()`. + +```python +# Core +ui.sidebar( + ui.input_selectize("ticker", "Stock", choices=stocks, selected="AAPL"), + ui.input_slider("bill", "Amount", min=0, max=100, value=[10, 90], pre="$"), + ui.input_checkbox_group("time", "Service", ["Lunch", "Dinner"], + selected=["Lunch", "Dinner"], inline=True), + ui.input_date_range("dates", "Dates", start=start, end=end), + ui.input_switch("group", "Group by species", value=True), + ui.input_action_button("reset", "Reset filter"), + open="desktop", # auto-collapse on mobile +) +``` + +```python +# Express +with ui.sidebar(open="desktop"): + ui.input_selectize("ticker", "Stock", choices=stocks, selected="AAPL") + ui.input_slider("bill", "Amount", min=0, max=100, value=[10, 90], pre="$") + ui.input_action_button("reset", "Reset filter") +``` + +### Key rules + +- Use `open="desktop"` to auto-collapse the sidebar on mobile viewports. +- Group filters logically — filters first, reset button last. +- Sidebar inputs drive `@reactive.calc` functions that filter/transform data. +- Common input types in sidebars: `input_select`, `input_selectize`, `input_slider`, + `input_checkbox_group`, `input_date_range`, `input_switch`, `input_action_button`. + +--- + +## Navigation between panels + +### Two-level navigation hierarchy + +Combine `page_navbar` (top-level pages) with `navset_card_underline` (sub-tabs within a page): + +```python +# Core — nested navigation +page1 = ui.navset_card_underline( + ui.nav_panel("Plot", ui.output_plot("hist")), + ui.nav_panel("Table", ui.output_data_frame("data")), + footer=ui.input_select("var", "Variable", choices=["col_a", "col_b"]), + title="Data explorer", +) + +app_ui = ui.page_navbar( + ui.nav_panel("Page 1", page1), + ui.nav_panel("Page 2", "Second page content."), + title="My App", +) +``` + +```python +# Express — nested navigation +with ui.nav_panel("Page 1"): + footer = ui.input_select("var", "Variable", choices=["col_a", "col_b"]) + with ui.navset_card_underline(title="Data explorer", footer=footer): + with ui.nav_panel("Plot"): + @render.plot + def hist(): ... + with ui.nav_panel("Table"): + @render.data_frame + def data(): ... + +with ui.nav_panel("Page 2"): + "Second page content." +``` + +### Key rules + +- The `footer` kwarg on `navset_card_underline` places shared inputs **below all + sub-tabs**, keeping them visible regardless of which tab is active. +- In Core, define navset content as a variable (e.g., `page1`), then pass into + `ui.nav_panel()`. +- In Express, define the `footer` input widget **before** the + `with ui.navset_card_underline(...)` block so it can be passed as a kwarg. +- `ui.nav_spacer()` pushes navbar items to the right for visual alignment. +- No sidebar is used in basic-navigation apps — inputs live in the navset card's footer. 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..2f64280 --- /dev/null +++ b/shiny/shiny-python-dashboard/references/reactivity-and-rendering.md @@ -0,0 +1,225 @@ +# Reactivity and Rendering + +## Contents + +- `@reactive.calc` — the primary primitive +- `@reactive.effect` and `@reactive.event` +- `req()` — guarding against empty inputs +- Rendering Plotly charts +- Rendering Matplotlib/Seaborn plots +- Rendering data tables +- Rendering value box content +- Interactive Plotly click events + +--- + +## `@reactive.calc` — The primary primitive + +Use for all filtered/derived data. Chain calcs for multi-step transformations: + +```python +@reactive.calc +def get_ticker(): + return yf.Ticker(input.ticker()) + +@reactive.calc +def get_data(): + return get_ticker().history(start=input.dates()[0], end=input.dates()[1]) + +@reactive.calc +def get_change(): + close = get_data()["Close"] + if len(close) < 2: + return 0.0 + return close.iloc[-1] - close.iloc[-2] +``` + +Filtering pattern with Polars: + +```python +@reactive.calc +def tips_data(): + bill = input.total_bill() + return tips.filter( + pl.col("total_bill").is_between(bill[0], bill[1]), + pl.col("time").is_in(input.time()), + ) +``` + +Filtering pattern with Pandas: + +```python +@reactive.calc +def careers(): + games = input.games() + idx = (careers_df["GP"] >= games[0]) & (careers_df["GP"] <= games[1]) + return careers_df[idx] +``` + +### Core rule: No global state mutation + +All reactivity flows one direction through `@reactive.calc` chains. Never mutate +module-level variables inside reactive functions. + +--- + +## `@reactive.effect` and `@reactive.event` + +### Reset buttons + +Use `@reactive.effect` + `@reactive.event(input.reset)` to reset inputs to defaults: + +```python +@reactive.effect +@reactive.event(input.reset) +def _(): + ui.update_slider("total_bill", value=bill_rng) + ui.update_checkbox_group("time", selected=["Lunch", "Dinner"]) +``` + +### Cascading UI updates + +Use `@reactive.effect` (without `@reactive.event`) to update dependent inputs +when upstream data changes: + +```python +@reactive.effect +def _(): + players = dict(zip(careers()["person_id"], careers()["player_name"])) + ui.update_selectize("players", choices=players, selected=input.players()) +``` + +This re-runs whenever `careers()` changes, keeping the selectize choices in sync. + +--- + +## `req()` — Guarding against empty inputs + +```python +from shiny import req + +@reactive.calc +def player_stats(): + players = req(input.players()) # stops execution if None/empty + return careers()[careers()["person_id"].isin(players)] +``` + +`req()` silently stops reactive execution when the value is falsy, preventing +downstream errors from empty selections. + +--- + +## Rendering Plotly charts + +Use `@render_plotly` from `shinywidgets`. Always set explicit `height` on the figure: + +```python +from shinywidgets import output_widget, render_plotly + +# Core — requires output_widget("scatterplot") in UI tree +@render_plotly +def scatterplot(): + color = input.scatter_color() + fig = px.scatter(tips_data(), x="total_bill", y="tip", + color=None if color == "none" else color, trendline="lowess") + fig.update_layout(height=400, margin=dict(l=40, r=20, t=40, b=40)) + return fig +``` + +```python +# Express — place inline inside card +with ui.card(full_screen=True): + ui.card_header("Scatter Plot") + @render_plotly + def scatterplot(): ... +``` + +In Core, always pair `@render_plotly` with `output_widget("id")` in the UI tree. +In Express, place the decorator inline inside `with ui.card():`. + +--- + +## Rendering Matplotlib/Seaborn plots + +Use `@render.plot` — returns a matplotlib figure or axes. +Always set `figsize` explicitly to prevent tiny charts: + +```python +@render.plot +def hist(): + import matplotlib.pyplot as plt + fig, ax = plt.subplots(figsize=(8, 4)) + ax.hist(df[input.var()].dropna(), bins=20, + color="#0d6efd", edgecolor="white") + ax.set_xlabel(input.var()) + ax.set_ylabel("Count") + return fig +``` + +Combine `sns.kdeplot` + `sns.rugplot` for density with rug marks: + +```python +@render.plot +def density(): + hue = "species" if input.species() else None + sns.kdeplot(df, x=input.var(), hue=hue) + if input.show_rug(): + sns.rugplot(df, x=input.var(), hue=hue, color="black", alpha=0.25) +``` + +In Core, use `ui.output_plot("id")` in the UI tree. + +--- + +## Rendering data tables + +```python +@render.data_frame +def table(): + return render.DataGrid(tips_data()) +``` + +Add `filters=True` for column-level filtering: `render.DataGrid(df, filters=True)`. + +In Core, use `ui.output_data_frame("table")` in the UI tree. + +--- + +## Rendering value box content + +```python +# Core — @render.ui returning a formatted string +@render.ui +def average_tip(): + d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() + return f"{d:.1%}" if d else "N/A" +``` + +```python +# Express — @render.express (prints value, no return needed) +@render.express +def average_tip(): + d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() + f"{d:.1%}" if d else "N/A" +``` + +--- + +## Interactive Plotly click events + +Convert a plotly Figure to `go.FigureWidget` and attach `.on_click()`: + +```python +import plotly.graph_objects as go + +fig = go.FigureWidget(fig.data, fig.layout) +fig.data[1].on_click(on_rug_click) +return fig + +def on_rug_click(trace, points, state): + player_id = trace.customdata[points.point_inds[0]] + selected = list(input.players()) + [player_id] + ui.update_selectize("players", selected=selected) +``` + +This enables click-to-select interactions on plotly rug plots or scatter traces. 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..e651203 --- /dev/null +++ b/shiny/shiny-python-dashboard/references/styling-and-data.md @@ -0,0 +1,163 @@ +# Styling and Data Loading + +## Contents + +- CSS inclusion +- Recommended CSS patterns +- Static data loading +- Live API data loading +- Pandas vs Polars + +--- + +## CSS inclusion + +Include CSS at the end of the page layout: + +```python +# Core — as last arg inside ui.page_sidebar(...) +ui.include_css(app_dir / "styles.css") + +# Express — at module level (after layout blocks) +ui.include_css(Path(__file__).parent / "styles.css") +``` + +--- + +## Recommended CSS patterns + +### Minimal styles.css + +```css +:root { + --bslib-sidebar-main-bg: #f8f8f8; +} +``` + +Every template uses this sidebar background override. + +### Hide Plotly toolbar + +```css +.plotly .modebar-container { + display: none !important; +} +``` + +Used in `nba-dashboard` and `stock-app` for a cleaner look. + +--- + +## Chart sizing best practices + +Always set explicit chart dimensions to prevent charts from rendering too small: + +### Plotly + +```python +fig.update_layout( + height=400, # explicit pixel height + margin=dict(l=40, r=20, t=40, b=40), +) +``` + +### Matplotlib / Seaborn + +```python +fig, ax = plt.subplots(figsize=(8, 4)) # width=8, height=4 inches +``` + +### Rules + +- Always set `height` on Plotly figures (default can be too small in cards) +- Always use `figsize=(8, 4)` or similar for Matplotlib — never use the default +- Wrap charts in `ui.card(full_screen=True)` so users can expand them +- Handle missing data before plotting: `df.dropna(subset=[col])` + +### Dark popover headers + +```css +.popover { + --bs-popover-header-bg: #222; + --bs-popover-header-color: #fff; +} +.popover .btn-close { + filter: var(--bs-btn-close-white-filter); +} +``` + +Used in `dashboard-tips` when `ui.popover()` is used for secondary inputs. + +### General approach + +No custom theme objects needed — rely on default bslib/Bootstrap theme with +CSS variable overrides. Keep `styles.css` minimal. + +--- + +## Static data loading + +Load CSVs in `shared.py` at module level — never inside the app file: + +```python +# shared.py +from pathlib import Path +import pandas as pd # or: import polars as pl + +app_dir = Path(__file__).parent +df = pd.read_csv(app_dir / "data.csv") +``` + +Export computed constants that UI inputs need: + +```python +bill_rng = (df["total_bill"].min(), df["total_bill"].max()) +gp_max = df["GP"].max() +players_dict = dict(zip(df["person_id"], df["player_name"])) +``` + +--- + +## Live API data loading + +Fetch live data inside `@reactive.calc` so it re-runs on input changes: + +```python +@reactive.calc +def get_ticker(): + return yf.Ticker(input.ticker()) + +@reactive.calc +def get_data(): + dates = input.dates() + return get_ticker().history(start=dates[0], end=dates[1]) +``` + +Never fetch API data at module level — it would only run once at startup. + +--- + +## Pandas vs Polars + +Both are supported across templates. Choose based on ecosystem needs. + +### Polars filtering (method chaining) + +```python +tips.filter( + pl.col("total_bill").is_between(bill[0], bill[1]), + pl.col("time").is_in(input.time()), +) +``` + +### Pandas filtering (boolean indexing) + +```python +idx = (df["GP"] >= games[0]) & (df["GP"] <= games[1]) +return df[idx] +``` + +| Library | Used in | +|---|---| +| Polars | `dashboard-tips` | +| Pandas | `nba-dashboard`, `stock-app`, `basic-sidebar`, `basic-navigation` | From c4a0a1b55e934445d972742665214c99d906c913 Mon Sep 17 00:00:00 2001 From: Karan Gathani Date: Wed, 15 Apr 2026 18:59:44 -0700 Subject: [PATCH 2/2] Revamp Shiny for Python dashboard docs Major rewrite and reorganization of the Shiny for Python dashboard skill and reference pages. SKILL.md was modernized and consolidated: guidance is reframed around page/layout primitives (ui.page_sidebar/ui.page_navbar, ui.card, ui.layout_columns/wrap), critical rules were clarified, examples updated (renamed inputs/outputs, improved plotting/code patterns, explicit number formatting, import placement for matplotlib, use of DataGrid, full_screen usage), and many anti-patterns/emoji usage were removed and replaced with faicons/bootstrap-icon recommendations. Reference files were refactored: components.md focuses on cards, value boxes, headers, popovers, accordions, tooltips, and best practices; core-vs-express.md was condensed to a clear decision guide and side-by-side skeletons for choosing Core vs Express; icons-and-maps.md was trimmed to practical icon rules and map guidance (clean coordinates, full-screen cards, explicit heights). Cross-references to related docs (layout, reactivity, styling) were added or emphasized. Overall this commit standardizes examples and guidance to produce more consistent, mobile-friendly, and maintainable dashboard patterns. --- shiny/shiny-python-dashboard/SKILL.md | 449 ++++++++---------- .../references/components.md | 286 +++++------ .../references/core-vs-express.md | 220 +++------ .../references/icons-and-maps.md | 374 +++------------ .../references/layout-and-navigation.md | 294 ++++++++---- .../references/reactivity-and-rendering.md | 254 +++++----- .../references/styling-and-data.md | 206 ++++---- 7 files changed, 873 insertions(+), 1210 deletions(-) diff --git a/shiny/shiny-python-dashboard/SKILL.md b/shiny/shiny-python-dashboard/SKILL.md index 2af8175..f5fecf7 100644 --- a/shiny/shiny-python-dashboard/SKILL.md +++ b/shiny/shiny-python-dashboard/SKILL.md @@ -1,105 +1,32 @@ -````skill --- name: shiny-python-dashboard -description: Best practices for building polished dashboards in Shiny for Python. Covers Core and Express APIs, proper icon usage (faicons/Bootstrap Icons — never emojis), value box layout, responsive grids, charts, and reactive patterns. +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. --- -## Critical rules — ALWAYS follow these - -1. **NEVER use emoji characters as icons.** No emojis anywhere — not in value box showcase, not in nav panel titles, not in card headers. Use `faicons.icon_svg()` or Bootstrap Icons SVG instead. Emojis render inconsistently and look unprofessional. -2. **Limit value boxes to 3-4 per row.** More causes text overlap and cramped layouts. -3. **Always set `fill=False`** on the row wrapping value boxes (`layout_columns` or `layout_column_wrap`) to prevent vertical stretching. -4. **Always set `fillable=True`** on `page_sidebar` / `page_opts`. -5. **Always add `full_screen=True`** on cards containing charts or tables. -6. **Always add `ui.card_header("Title")`** to every card — never leave cards untitled. -7. **Use `ui.layout_columns(col_widths=[...])`** for grid layouts. -8. **Set explicit chart dimensions** — `fig.update_layout(height=400)` for Plotly, `plt.subplots(figsize=(8, 4))` for Matplotlib. -9. **Handle NaN/missing data gracefully** — some columns have blanks. Use `df.dropna(subset=[col])` before plotting, `pd.to_numeric(col, errors="coerce")` for mixed-type columns, and guard with `req()` for empty inputs. -10. **Use named Bootstrap theme colors** for value boxes: `"primary"`, `"success"`, `"info"`, `"warning"`, `"danger"`. Avoid `bg-gradient-*`. -11. **Place ALL imports at the top of the file.** Never import inside functions, reactive calcs, or render blocks — except `matplotlib.pyplot` which must be imported inside `@render.plot` to avoid backend conflicts. -12. **Format all numbers for readability.** Never display raw floats or unformatted integers. Use comma separators, currency symbols, and percentage formatting. -13. **Follow the standard dashboard layout hierarchy**: value boxes (KPIs) at top → charts in middle → data table at bottom. -14. **Use responsive `col_widths` with breakpoints** for layouts that work on mobile: `col_widths={"sm": 12, "md": [6, 6]}`. +# Modern Shiny for Python Dashboards -## Number formatting - -Always format numbers for professional display: - -```python -# Integers — comma separator -f"{count:,}" # "12,345" - -# Currency -f"${amount:,.2f}" # "$1,234.56" -f"${amount:,.0f}" # "$1,235" - -# Percentages -f"{ratio:.1%}" # "78.3%" -f"{ratio:.0%}" # "78%" - -# Decimals — control precision -f"{value:,.1f}" # "1,234.6" -f"{value:.2f}" # "1234.57" - -# Large numbers — abbreviate -def fmt_large(n): - if n >= 1_000_000: return f"{n/1_000_000:.1f}M" - if n >= 1_000: return f"{n/1_000:.1f}K" - return f"{n:,.0f}" -``` +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. -Never display raw floats like `0.7834523123` or unformatted integers like `12345`. +## Critical rules -## Icons — ONLY faicons or Bootstrap Icons - -```python -# CORRECT: faicons (Font Awesome SVGs) — preferred -from faicons import icon_svg -icon = icon_svg("chart-line") # solid style (default) -icon = icon_svg("user", "regular") # outlined style -``` +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. -```python -# WRONG — NEVER do this: -showcase="\U0001F4CA" # emoji is NOT an icon -showcase="\U0001F3E5" # emoji is NOT an icon -``` +## Quick Start -### Common faicons for dashboards - -| Icon name | Use case | -|---------------------|-----------------------------| -| `"chart-line"` | Trends, time series | -| `"chart-bar"` | Bar charts, distributions | -| `"users"` | People count | -| `"user"` | Individual person | -| `"dollar-sign"` | Currency, revenue | -| `"wallet"` | Money, tips | -| `"clipboard-check"` | Completed tasks | -| `"calendar"` | Dates, scheduling | -| `"flask"` | Science, experiments | -| `"stethoscope"` | Healthcare | -| `"heart-pulse"` | Health metrics | -| `"arrow-up"` | Positive change | -| `"arrow-down"` | Negative change | -| `"percent"` | Percentages | -| `"database"` | Data, records | -| `"filter"` | Filtering | -| `"table"` | Tabular data | -| `"magnifying-glass"`| Search | -| `"globe"` | Geographic data | -| `"building"` | Organizations | -| `"pills"` | Medications, pharma | -| `"vial"` | Lab samples | -| `"truck"` | Shipping, logistics | -| `"circle-check"` | Success, completion | -| `"clock"` | Time, duration | -| `"hospital"` | Healthcare facility | - -## Quick start — Core dashboard +**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 @@ -109,221 +36,233 @@ df = pd.read_csv(app_dir / "data.csv") app_ui = ui.page_sidebar( ui.sidebar( - ui.input_select("var", "Variable", choices=list(df.columns)), + ui.input_select("metric", "Metric", choices=list(df.columns)), open="desktop", ), - # Value boxes — fill=False prevents vertical stretching ui.layout_columns( - ui.value_box("Total Records", ui.output_text("total"), - showcase=icon_svg("database"), theme="primary"), - ui.value_box("Average", ui.output_text("avg"), - showcase=icon_svg("chart-line"), theme="info"), - ui.value_box("Maximum", ui.output_text("max_val"), - showcase=icon_svg("arrow-up"), theme="success"), + 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, ), - # Charts — col_widths controls the grid ui.layout_columns( - ui.card(ui.card_header("Distribution"), - ui.output_plot("hist"), full_screen=True), - ui.card(ui.card_header("Trend"), - ui.output_plot("trend"), full_screen=True), + 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], ), - # Data table - ui.card(ui.card_header("Data"), ui.output_data_frame("table"), - full_screen=True), title="Dashboard", fillable=True, ) + def server(input, output, session): @reactive.calc - def filtered(): - return df + def series(): + return pd.to_numeric(df[input.metric()], errors="coerce").dropna() @render.text - def total(): - return f"{len(filtered()):,}" + def row_count(): + return f"{len(df):,}" @render.text - def avg(): - return f"{filtered()[input.var()].mean():,.1f}" - - @render.text - def max_val(): - return f"{filtered()[input.var()].max():,.0f}" + def avg_value(): + return f"{series().mean():,.1f}" @render.plot - def hist(): + def distribution(): import matplotlib.pyplot as plt - fig, ax = plt.subplots(figsize=(8, 4)) - ax.hist(filtered()[input.var()].dropna(), bins=20, - color="#0d6efd", edgecolor="white") - ax.set_xlabel(input.var()) - ax.set_ylabel("Count") - return fig - @render.plot - def trend(): - import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(8, 4)) - ax.plot(filtered()[input.var()].dropna().values, - color="#198754", linewidth=1.5) - ax.set_ylabel(input.var()) + 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 table(): - return render.DataGrid(filtered(), filters=True) + def preview(): + return render.DataGrid(df.head(20), filters=True) + app = App(app_ui, server) ``` -## Quick start — Express dashboard +**Multi-page dashboard with a navbar:** ```python -from pathlib import Path -import pandas as pd -from faicons import icon_svg -from shiny import reactive -from shiny.express import input, render, ui +from shiny import App, ui -app_dir = Path(__file__).parent -df = pd.read_csv(app_dir / "data.csv") +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], +) -ui.page_opts(title="Dashboard", fillable=True) - -with ui.sidebar(open="desktop"): - ui.input_select("var", "Variable", choices=list(df.columns)) - -@reactive.calc -def filtered(): - return df - -# Value boxes — fill=False prevents vertical stretching -with ui.layout_columns(fill=False): - with ui.value_box(showcase=icon_svg("database"), theme="primary"): - "Total Records" - @render.express - def total(): - f"{len(filtered()):,}" - - with ui.value_box(showcase=icon_svg("chart-line"), theme="info"): - "Average" - @render.express - def avg(): - f"{filtered()[input.var()].mean():,.1f}" - - with ui.value_box(showcase=icon_svg("arrow-up"), theme="success"): - "Maximum" - @render.express - def max_val(): - f"{filtered()[input.var()].max():,.0f}" - -# Charts -with ui.layout_columns(col_widths=[6, 6]): - with ui.card(full_screen=True): - ui.card_header("Distribution") - @render.plot - def hist(): - import matplotlib.pyplot as plt - fig, ax = plt.subplots(figsize=(8, 4)) - ax.hist(filtered()[input.var()].dropna(), bins=20, - color="#0d6efd", edgecolor="white") - return fig - - with ui.card(full_screen=True): - ui.card_header("Trend") - @render.plot - def trend(): - import matplotlib.pyplot as plt - fig, ax = plt.subplots(figsize=(8, 4)) - ax.plot(filtered()[input.var()].dropna().values, color="#198754") - return fig - -# Data table -with ui.card(full_screen=True): - ui.card_header("Data") - @render.data_frame - def table(): - return render.DataGrid(filtered(), filters=True) +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, +) ``` -## Dashboard layout hierarchy +## Core Concepts -Always follow this top-to-bottom structure: +### Page layouts -``` -1. Value boxes (KPIs) — fill=False row at the top -2. Charts — in cards with full_screen=True -3. Data table — full-width card at the bottom -``` +- `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. -For multi-page apps with `page_navbar`, apply this hierarchy within each `nav_panel`. +See [references/layout-and-navigation.md](references/layout-and-navigation.md) for layout, sidebar, navigation, and responsive grid patterns. -## Responsive design +### Grids -Use `col_widths` with breakpoint dictionaries so dashboards look good on all screen sizes: +- `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. -```python -# Stacks on mobile, side-by-side on desktop -ui.layout_columns( - chart_card_1, chart_card_2, - col_widths={"sm": 12, "md": [6, 6]}, -) +See [references/layout-and-navigation.md](references/layout-and-navigation.md) and [references/components.md](references/components.md) for detailed layout guidance. -# Three columns on desktop, stacked on mobile -ui.layout_columns( - card_a, card_b, card_c, - col_widths={"sm": 12, "md": [6, 6, 12], "lg": [4, 4, 4]}, -) +### Cards -# Use negative values for spacing -ui.layout_columns( - card_a, card_b, - col_widths=[5, -2, 5], # 5-wide cards with 2-unit gap -) -``` +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. -For uniform grids (all items same size), use `ui.layout_column_wrap(width=1/3)` instead. +See [references/components.md](references/components.md) for card composition, tabbed card patterns, and inline controls. -## Chart best practices +### Value boxes -- **Plotly**: Set `height=400`, hide modebar, use `margin=dict(l=40, r=20, t=40, b=40)` -- **Matplotlib**: Use `figsize=(8, 4)`, call `fig.tight_layout()` before returning -- **Seaborn**: Build on matplotlib — same `figsize` rules apply -- **Color palette**: Use Bootstrap-aligned colors for consistency: - - Primary blue: `"#0d6efd"` — main charts - - Success green: `"#198754"` — positive trends - - Danger red: `"#dc3545"` — negative trends, alerts - - Info cyan: `"#0dcaf0"` — secondary charts - - Warning amber: `"#ffc107"` — caution indicators -- **Always label axes** with `ax.set_xlabel()` / `ax.set_ylabel()` -- **Remove chartjunk**: no unnecessary gridlines, borders, or legends for single-series charts +Use `ui.value_box()` for KPIs, summaries, and status indicators. Place them in a non-filling row so they stay compact and scannable. -## Project structure +See [references/components.md](references/components.md) for showcase icons, layouts, and dynamic output patterns. -``` -my-dashboard/ -+-- app.py # Main app (Core or Express) -+-- shared.py # Data loading, constants, app_dir -+-- styles.css # Minimal CSS overrides -+-- data.csv # Static data files -+-- requirements.txt # Must include faicons -``` +### 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. -`shared.py` always exports `app_dir = Path(__file__).parent` and pre-loaded data. -`requirements.txt` must always include `faicons`. +### Core and Express APIs -## Reference files +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}" +``` -Read these for detailed patterns on specific topics: +## Reference Files -**Layout & navigation**: See [references/layout-and-navigation.md](references/layout-and-navigation.md) -**Value boxes, cards & grids**: See [references/components.md](references/components.md) -**Reactivity & rendering**: See [references/reactivity-and-rendering.md](references/reactivity-and-rendering.md) -**Styling & data loading**: See [references/styling-and-data.md](references/styling-and-data.md) -**Icons & maps**: See [references/icons-and-maps.md](references/icons-and-maps.md) -**Core vs Express**: See [references/core-vs-express.md](references/core-vs-express.md) -```` +- [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 index b4bba59..876c5d4 100644 --- a/shiny/shiny-python-dashboard/references/components.md +++ b/shiny/shiny-python-dashboard/references/components.md @@ -1,220 +1,184 @@ -# Value Boxes, Cards, and Grid Layout +# Components for Shiny for Python Dashboards -## Contents +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. -- Value boxes -- Dynamic showcase icons -- Standard card pattern -- Grid layout with col_widths -- Card headers with inline controls +## Cards ---- - -## Value boxes - -### Critical rules - -- **NEVER use emoji characters** as the `showcase` parameter. Always use `icon_svg()` from faicons. -- **Always wrap value boxes** in `ui.layout_columns(fill=False)` to prevent vertical stretching. -- **Limit to 3-4 value boxes per row** to avoid text overlap. -- **Always set a `theme`** for visual distinction: `"primary"`, `"success"`, `"info"`, `"warning"`, `"danger"`. - -### Correct pattern +Cards are the default container for dashboard content. ```python -# Core -ui.layout_columns( - ui.value_box("Total tippers", ui.output_ui("total_tippers"), - showcase=icon_svg("user", "regular"), theme="primary"), - ui.value_box("Average tip", ui.output_ui("average_tip"), - showcase=icon_svg("wallet"), theme="info"), - ui.value_box("Average bill", ui.output_ui("average_bill"), - showcase=icon_svg("dollar-sign"), theme="success"), - fill=False, +ui.card( + ui.card_header("Revenue by month"), + ui.output_plot("revenue_plot"), + ui.card_footer("Updated daily at 6am"), + full_screen=True, ) ``` -```python -# Express -with ui.layout_columns(fill=False): - with ui.value_box(showcase=icon_svg("user", "regular"), theme="primary"): - "Total tippers" - @render.express - def total_tippers(): - tips_data().height - - with ui.value_box(showcase=icon_svg("wallet"), theme="info"): - "Average tip" - @render.express - def average_tip(): - d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() - f"{d:.1%}" if d else "N/A" -``` +Use cards for: -### Value box themes +- charts +- tables +- maps +- summaries that need supporting text +- module-like dashboard sections -Use named Bootstrap theme values for the `theme` parameter: +Guidelines: -| Theme | Color | Use for | -|-------------|---------|--------------------------------| -| `"primary"` | Blue | Main KPI, total counts | -| `"success"` | Green | Positive metrics, completions | -| `"info"` | Cyan | Averages, informational | -| `"warning"` | Yellow | Alerts, pending items | -| `"danger"` | Red | Errors, critical metrics | +- 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. -Avoid `bg-gradient-*` combinations — they often clash with text colors. +### Card headers with inline controls -### Anti-patterns — DO NOT do these +Card headers can hold small secondary controls. ```python -# WRONG: emoji as showcase -ui.value_box("Total", "100", showcase="\U0001F4CA") # NO - -# WRONG: no theme -ui.value_box("Total", "100", showcase=icon_svg("database")) # missing theme - -# WRONG: too many value boxes (5+) in one row — causes text overlap -ui.layout_columns(vb1, vb2, vb3, vb4, vb5, vb6, fill=False) # TOO MANY - -# WRONG: missing fill=False — value boxes stretch vertically -ui.layout_columns(vb1, vb2, vb3) # missing fill=False +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", +) ``` -`ui.layout_column_wrap(fill=False)` is an alternative to `ui.layout_columns(fill=False)` — -both work for value box rows. +Use this for secondary options that do not deserve a full sidebar section. ---- +## Value Boxes -## Dynamic showcase icons +Use `ui.value_box()` for KPIs and headline metrics. -Render a conditional icon (e.g., up/down arrow) via `@render.ui` and pass -`ui.output_ui("change_icon")` as the `showcase` parameter: +**Core API:** ```python -# Core — dynamic showcase -ui.value_box("Change", ui.output_ui("change"), showcase=ui.output_ui("change_icon")) - -@render.ui -def change_icon(): - icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") - icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") - return icon +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, +) ``` -In Express, use `with ui.hold():` when an output is referenced **before** it is defined: +**Express API:** ```python -# Express — forward-referenced output -with ui.value_box(showcase=output_ui("change_icon")): - "Change" - @render.ui - def change(): return f"${get_change():.2f}" - -# Define the forward-referenced output after the widget that uses it -with ui.hold(): - @render.ui - def change_icon(): - icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") - icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") - return icon +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: -## Standard card pattern +- 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. -Every card MUST have a `ui.card_header()` and use `full_screen=True` for charts/tables: +### Dynamic showcase icons -```python -# Core -ui.card( - ui.card_header("Price history"), - output_widget("price_history"), - full_screen=True, -) -``` +Use `@render.ui` when the icon depends on the data. ```python -# Express -with ui.card(full_screen=True): - ui.card_header("Price history") - @render_plotly - def price_history(): ... +@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 ``` -Use `ui.card_footer()` for supplementary text below the card content: +In Express, `with ui.hold():` is useful when an output is referenced before it is defined. -```python -ui.card_footer("Percentiles are based on career per game averages.") -``` +## Accordions -### Anti-patterns for cards +Accordions are most useful in sidebars with many controls. ```python -# WRONG: no card_header — card has no title -ui.card(ui.output_plot("plot"), full_screen=True) +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", +) +``` -# WRONG: no full_screen — user cannot expand chart -ui.card(ui.card_header("Plot"), ui.output_plot("plot")) +Use accordions when: -# WRONG: bare output without card wrapping -ui.output_plot("plot") # should be inside a card -``` +- 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. -## Grid layout with col_widths +## Tooltips and Popovers -Use `ui.layout_columns(col_widths=[...])` for asymmetric card arrangements: +Tooltips provide quick read-only help. Popovers hold small interactive controls. ```python -ui.layout_columns( - card_a, card_b, card_c, - col_widths=[6, 6, 12], # two cards on top row, one full-width below +ui.tooltip( + icon_svg("circle-info", title="More information", a11y="sem"), + "Shows how the metric is calculated", ) ``` -For responsive breakpoints, pass a dict: - ```python -col_widths={"sm": 12, "md": 12, "lg": [4, 8]} +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", +) ``` -This stacks cards vertically on small/medium screens and splits 4:8 on large screens. +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. -## Card headers with inline controls +## Toast-like Feedback -Use flexbox classes for alignment. +Use the framework's notification helpers for lightweight success, warning, or error feedback instead of permanently allocating screen space to ephemeral status messages. -### Popover with secondary inputs +Guidelines: -```python -ui.card_header( - "Total bill vs tip", - ui.popover( - icon_svg("ellipsis"), - ui.input_radio_buttons("color_var", None, ["none", "sex", "day"], inline=True), - title="Add a color variable", - placement="top", - ), - class_="d-flex justify-content-between align-items-center", -) -``` - -### Inline select dropdown +- 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. -```python -ui.card_header( - "Player career ", - ui.input_select("stat", None, choices=stats, selected="PTS", width="auto"), - " vs the rest of the league", - class_="d-flex align-items-center gap-1", -) -``` +## Best Practices -Use `"d-flex justify-content-between align-items-center"` when the control should be -pushed to the far right. Use `"d-flex align-items-center gap-1"` when the control -is inline within the title text. +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 index 8a8819c..d096109 100644 --- a/shiny/shiny-python-dashboard/references/core-vs-express.md +++ b/shiny/shiny-python-dashboard/references/core-vs-express.md @@ -1,192 +1,126 @@ -# Core vs Express — Full Comparison +# Core vs Express in Shiny for Python -## Contents +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. -- Side-by-side quick reference -- Import differences -- Page setup -- UI construction -- Server function vs module-level -- Output placement -- Value box rendering -- Forward references +## High-Level Difference ---- +- Core separates UI declaration from server outputs. +- Express colocates layout blocks and render functions. -## Side-by-side quick reference +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. -| Aspect | Core | Express | -| --- | --- | --- | -| **Imports** | `from shiny import App, reactive, render, ui` | `from shiny.express import input, render, ui` | -| **Page setup** | `app_ui = ui.page_sidebar(title=...)` | `ui.page_opts(title=..., fillable=True)` | -| **Sidebar** | `ui.sidebar(...)` as arg | `with ui.sidebar():` | -| **Layout** | Nested function calls | `with` context managers | -| **Server** | `def server(input, output, session):` | Module-level decorators | -| **Output placement** | `output_widget("id")` / `ui.output_*("id")` | Decorator inline in `with` block | -| **Value boxes** | `@render.ui` + `return` | `@render.express` (no `return`) | -| **Forward refs** | N/A | `with ui.hold():` | -| **App creation** | `app = App(app_ui, server)` | Not needed | -| **Nav panels** | `ui.nav_panel("Name", content)` as arg | `with ui.nav_panel("Name"):` | +## When to Choose Core ---- +Choose Core when: -## Import differences +- 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 -```python -# Core -from shiny import App, reactive, render, req, ui -from shinywidgets import output_widget, render_plotly +Core is often the safer default for production dashboards with several sections. -# Express -from shiny import reactive, req -from shiny.express import input, render, ui -from shinywidgets import render_plotly -``` +## When to Choose Express -In Express, `input` is imported from `shiny.express`, not `shiny`. -`output_widget` is not needed in Express — `@render_plotly` is placed inline. +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 -## Page setup +Express works well for focused dashboards where the layout is easy to read top to bottom. -```python -# Core — declarative, assigned to a variable -app_ui = ui.page_sidebar( - ui.sidebar(...), - # ... layout content ... - title="Dashboard", - fillable=True, -) -``` +## Equivalent Patterns -```python -# Express — imperative, called at module level -ui.page_opts(title="Dashboard", fillable=True) -with ui.sidebar(): - ... -``` +| 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 -## UI construction - -Core builds the entire UI tree as nested function calls: +### Core skeleton ```python -# Core -ui.layout_columns( - ui.card(ui.card_header("Plot"), output_widget("plot"), full_screen=True), - ui.card(ui.card_header("Table"), ui.output_data_frame("table")), - col_widths=[8, 4], -) -``` +from shiny import App, reactive, render, ui -Express uses `with` context managers: - -```python -# Express -with ui.layout_columns(col_widths=[8, 4]): - with ui.card(full_screen=True): - ui.card_header("Plot") - @render_plotly - def plot(): ... - with ui.card(): - ui.card_header("Table") - @render.data_frame - def table(): ... -``` - ---- - -## Server function vs module-level +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, +) -Core wraps all reactive logic in an explicit server function: -```python -# Core def server(input, output, session): @reactive.calc - def filtered(): ... + def filtered(): + return df @render.plot - def plot(): ... + def plot(): + ... + app = App(app_ui, server) ``` -Express places reactive logic at module level — no `server` function, no `App()`: +### Express skeleton ```python -# Express -@reactive.calc -def filtered(): ... +from shiny import reactive +from shiny.express import input, render, ui -# Render decorators placed inline within with-blocks (see UI construction above) -``` +ui.page_opts(title="Dashboard", fillable=True) ---- +with ui.sidebar(open="desktop"): + ui.input_select("metric", "Metric", choices=metrics) -## Output placement -In Core, outputs must be placed in the UI tree using explicit placeholder functions: +@reactive.calc +def filtered(): + return df -- `output_widget("id")` for Plotly charts (from `shinywidgets`) -- `ui.output_ui("id")` for dynamic UI / value box content -- `ui.output_plot("id")` for matplotlib/seaborn -- `ui.output_data_frame("id")` for data tables -In Express, render decorators are placed inline where the output should appear: +with ui.card(full_screen=True): + ui.card_header("Plot") -```python -with ui.card(): - @render_plotly - def chart(): ... # output appears here in the card + @render.plot + def plot(): + ... ``` ---- - -## Value box rendering +## Important Express Detail -```python -# Core — @render.ui with explicit return -@render.ui -def average_tip(): - d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() - return f"{d:.1%}" if d else "N/A" -``` +`ui.hold()` exists in the Express namespace and is useful when an output is referenced before it is defined. ```python -# Express — @render.express, value printed implicitly (no return) -with ui.value_box(showcase=icon_svg("wallet")): - "Average tip" - @render.express - def average_tip(): - d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() - f"{d:.1%}" if d else "N/A" -``` +from shiny.express import render, ui ---- +with ui.value_box(showcase=ui.output_ui("trend_icon")): + "Trend" -## Forward references +with ui.hold(): + @render.ui + def trend_icon(): + return icon_svg("arrow-up") +``` -Core has no forward-reference issue — the UI tree and server are separate. +## Rules for Mixing Styles -In Express, if an output is used as a parameter (e.g., `showcase=output_ui("icon")`) -before the render function is defined, wrap the definition in `with ui.hold()`: +- 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. -```python -# The value_box references "change_icon" before it exists -with ui.value_box(showcase=output_ui("change_icon")): - "Change" - @render.ui - def change(): return f"${get_change():.2f}" +## Best Practices -# Define the forward-referenced output later -with ui.hold(): - @render.ui - def change_icon(): - icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") - icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") - return icon -``` +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 index 90f6d24..1e8ac78 100644 --- a/shiny/shiny-python-dashboard/references/icons-and-maps.md +++ b/shiny/shiny-python-dashboard/references/icons-and-maps.md @@ -1,351 +1,105 @@ -# Icons with faicons and Interactive Maps +# Icons and Maps in Shiny for Python Dashboards -## Contents +This reference covers two dashboard details that strongly affect quality: icon usage and geographic views. Both should feel intentional, readable, and easy to expand. -- Using faicons for icons -- Icon patterns: static, dictionary, dynamic -- Icons as popover triggers -- Interactive maps with ipyleaflet -- Map markers, layers, and basemaps -- Draggable markers with reactive updates +## Icons ---- - -## Using faicons - -The `faicons` package provides Font Awesome SVG icons for Shiny for Python. -Add `faicons` to `requirements.txt`. - -### CRITICAL: Never use emoji characters as icons - -Emojis are NOT icons. They render differently across platforms, cannot be styled -with CSS, and look unprofessional in dashboards. ALWAYS use `icon_svg()` from -faicons or Bootstrap Icons via `ui.HTML()`. - -```python -# CORRECT -from faicons import icon_svg -showcase = icon_svg("chart-line") -showcase = icon_svg("hospital") -showcase = icon_svg("users") - -# WRONG — never do this -showcase = "\U0001F4CA" # chart emoji — NO -showcase = "\U0001F3E5" # hospital emoji — NO -showcase = "\U0001F465" # people emoji — NO -``` - -### Import styles +Use `faicons.icon_svg()` for dashboard icons. ```python -# Option A: namespace import (dashboard-tips pattern) -import faicons as fa -icon = fa.icon_svg("user", "regular") - -# Option B: direct import (stock-app, map-distance pattern) from faicons import icon_svg -icon = icon_svg("dollar-sign") -``` - -### Static icons in value boxes - -Pass `icon_svg()` directly to the `showcase` parameter: - -```python -# Core -ui.value_box("Current Price", ui.output_ui("price"), - showcase=icon_svg("dollar-sign")) -# Express -with ui.value_box(showcase=icon_svg("dollar-sign")): - "Current Price" - @render.ui - def price(): return f"{close.iloc[-1]:.2f}" +icon_svg("chart-line") +icon_svg("users") +icon_svg("dollar-sign") +icon_svg("globe") ``` -### Icon dictionary pattern +Guidelines: -Pre-build icons in a dict when reusing across multiple components (value boxes + popovers). -This avoids repeated `icon_svg()` calls: +- 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. -```python -ICONS = { - "user": fa.icon_svg("user", "regular"), - "wallet": fa.icon_svg("wallet"), - "currency-dollar": fa.icon_svg("dollar-sign"), - "ellipsis": fa.icon_svg("ellipsis"), -} -``` +### Accessible icon-only triggers -Then reference by key: +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.value_box("Total tippers", ui.output_ui("total_tippers"), - showcase=ICONS["user"]) -``` - -### Dynamic / conditional icons - -Render icons conditionally via `@render.ui`. Use `.add_class()` to style with -Bootstrap text-color utilities: - -```python -# Core -ui.value_box("Change", ui.output_ui("change"), - showcase=ui.output_ui("change_icon")) - -@render.ui -def change_icon(): - icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") - icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") - return icon -``` - -In Express, use `ui.hold()` for the forward-referenced output: - -```python -from shiny.ui import output_ui - -with ui.value_box(showcase=output_ui("change_icon")): - "Change" - @render.ui - def change(): return f"${get_change():.2f}" - -with ui.hold(): - @render.ui - def change_icon(): - icon = icon_svg("arrow-up" if get_change() >= 0 else "arrow-down") - icon.add_class(f"text-{'success' if get_change() >= 0 else 'danger'}") - return icon -``` - -### Icons as popover triggers - -Use an icon (typically `"ellipsis"`) as the trigger for a popover containing -secondary inputs: - -```python -ui.card_header( - "Total bill vs tip", - ui.popover( - ICONS["ellipsis"], # trigger icon - ui.input_radio_buttons("color", None, ["none", "sex", "day"], inline=True), - title="Add a color variable", - placement="top", - ), - class_="d-flex justify-content-between align-items-center", +ui.tooltip( + icon_svg("circle-info", title="More information", a11y="sem"), + "Explains how this value is calculated", ) ``` -### Common icon names +The title should describe the purpose of the trigger, not the icon itself. -| Icon name | Typical use | -|----------------------|-----------------------------| -| `"user"` | Count / people metrics | -| `"users"` | Groups / team size | -| `"wallet"` | Money / tip metrics | -| `"dollar-sign"` | Currency / price | -| `"percent"` | Percentage values | -| `"ellipsis"` | Popover / settings trigger | -| `"arrow-up"` | Positive change indicator | -| `"arrow-down"` | Negative change indicator | -| `"chart-line"` | Trends / time series | -| `"chart-bar"` | Bar charts / distributions | -| `"globe"` | Geographic / distance | -| `"ruler"` | Measurement | -| `"mountain"` | Altitude / elevation | -| `"database"` | Data / records | -| `"clipboard-check"` | Completed tasks | -| `"calendar"` | Dates / scheduling | -| `"flask"` | Science / experiments | -| `"stethoscope"` | Healthcare | -| `"heart-pulse"` | Health metrics | -| `"hospital"` | Healthcare facility | -| `"pills"` | Medications / pharma | -| `"vial"` | Lab samples | -| `"building"` | Organizations | -| `"truck"` | Shipping / logistics | -| `"filter"` | Filtering | -| `"table"` | Tabular data | -| `"magnifying-glass"` | Search | -| `"circle-check"` | Success / completion | -| `"clock"` | Time / duration | +### Common dashboard icons -Pass `"regular"` as second arg for outlined style: `icon_svg("user", "regular")`. -Default style is `"solid"`. +- `"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 ---- - -## Interactive maps with ipyleaflet - -The `map-distance` app demonstrates interactive maps using `ipyleaflet` rendered -through `shinywidgets`. Add to `requirements.txt`: - -```txt -ipyleaflet -shinywidgets -geopy # for distance calculations -requests # for elevation API lookups -``` +## Maps -### Basic map rendering +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. -Use `@render_widget` from `shinywidgets` to render an ipyleaflet `Map`: +### Clean coordinates first ```python -# Core -from shinywidgets import output_widget, render_widget -import ipyleaflet as L - -# In UI -output_widget("map") - -# In server -@render_widget -def map(): - m = L.Map(zoom=4, center=(0, 0)) - m.add_layer(L.basemap_to_tiles(BASEMAPS["WorldImagery"])) - return m -``` - -```python -# Express -from shinywidgets import render_widget - -@render_widget -def map(): - m = L.Map(zoom=4, center=(0, 0)) - m.add_layer(L.basemap_to_tiles(BASEMAPS[input.basemap()])) - return m -``` - -### Basemap configuration - -Define available basemaps in `shared.py`: - -```python -from ipyleaflet import basemaps - -BASEMAPS = { - "WorldImagery": basemaps.Esri.WorldImagery, - "Mapnik": basemaps.OpenStreetMap.Mapnik, - "Positron": basemaps.CartoDB.Positron, - "DarkMatter": basemaps.CartoDB.DarkMatter, -} -``` - -Let users switch via `ui.input_selectize("basemap", "Choose a basemap", choices=list(BASEMAPS.keys()))`. - -### Partial map updates with reactive effects - -Render the map **once**, then update layers incrementally via `@reactive.effect` -and helper functions. Access the underlying widget via `map.widget`: - -```python -@reactive.effect -def _(): - update_marker(map.widget, loc1xy(), on_move1, "loc1") - -@reactive.effect -def _(): - update_marker(map.widget, loc2xy(), on_move2, "loc2") - -@reactive.effect -def _(): - update_line(map.widget, loc1xy(), loc2xy()) -``` - -### Layer management helpers - -Name layers so they can be found and replaced: - -```python -def update_marker(map: L.Map, loc: tuple, on_move: object, name: str): - remove_layer(map, name) - m = L.Marker(location=loc, draggable=True, name=name) - m.on_move(on_move) - map.add_layer(m) - -def update_line(map: L.Map, loc1: tuple, loc2: tuple): - remove_layer(map, "line") - map.add_layer( - L.Polyline(locations=[loc1, loc2], color="blue", weight=2, name="line") - ) - -def remove_layer(map: L.Map, name: str): - for layer in map.layers: - if layer.name == name: - map.remove_layer(layer) - -def update_basemap(map: L.Map, basemap: str): - for layer in map.layers: - if isinstance(layer, L.TileLayer): - map.remove_layer(layer) - map.add_layer(L.basemap_to_tiles(BASEMAPS[basemap])) +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"]) ``` -### Draggable markers with input sync +Do not defer coordinate cleanup to the rendering function if the whole dashboard depends on the map. -When a marker is dragged, update a `selectize` input so the new coordinates flow -back through the reactive graph: +### Put maps in full-screen cards ```python -def on_move1(**kwargs): - return on_move("loc1", **kwargs) - -def on_move(id, **kwargs): - loc = kwargs["location"] - loc_str = f"{loc[0]}, {loc[1]}" - choices = city_names + [loc_str] - ui.update_selectize(id, selected=loc_str, choices=choices) +ui.card( + ui.card_header("Market map"), + output_widget("market_map", height="540px"), + full_screen=True, +) ``` -### Fit bounds reactively +Guidelines: -Auto-zoom to show both markers when they move outside the current viewport: +- 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. -```python -@reactive.effect -def _(): - l1, l2 = loc1xy(), loc2xy() - lat_rng = [min(l1[0], l2[0]), max(l1[0], l2[0])] - lon_rng = [min(l1[1], l2[1]), max(l1[1], l2[1])] - new_bounds = [[lat_rng[0], lon_rng[0]], [lat_rng[1], lon_rng[1]]] +### Match the map to the question - b = map.widget.bounds - if len(b) == 0 or ( - lat_rng[0] < b[0][0] or lat_rng[1] > b[1][0] or - lon_rng[0] < b[0][1] or lon_rng[1] > b[1][1] - ): - map.widget.fit_bounds(new_bounds) -``` +Use the map when users need to answer geographic questions such as: -### Distance calculations with geopy +- where listings cluster +- which neighborhoods command higher prices +- how a filtered subset changes by location +- which points deserve drill-down inspection -```python -from geopy.distance import geodesic, great_circle +If the geography is incidental, use a ranked table or bar chart instead. -@render.text -def great_circle_dist(): - circle = great_circle(loc1xy(), loc2xy()) - return f"{circle.kilometers.__round__(1)} km" +### Widget choices -@render.text -def geo_dist(): - dist = geodesic(loc1xy(), loc2xy()) - return f"{dist.kilometers.__round__(1)} km" -``` +Shiny for Python dashboards in this repo use widget-style geographic outputs, so keep the guidance library-agnostic: -### Value box theming for maps +- 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 -Use named Bootstrap themes for value boxes — avoid gradient themes: - -```python -ui.value_box("Great Circle Distance", ui.output_text("great_circle_dist"), - theme="primary", showcase=icon_svg("globe")) - -ui.value_box("Geodesic Distance", ui.output_text("geo_dist"), - theme="info", showcase=icon_svg("ruler")) -``` +## Best Practices -Available themes: `"primary"`, `"success"`, `"info"`, `"warning"`, `"danger"`. +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 index 2ab5253..fbfb00e 100644 --- a/shiny/shiny-python-dashboard/references/layout-and-navigation.md +++ b/shiny/shiny-python-dashboard/references/layout-and-navigation.md @@ -1,137 +1,255 @@ -# Layout, Sidebar, and Navigation +# Layout and Navigation in Shiny for Python -## Contents -- Choosing the right page layout -- Sidebar patterns -- Navigation between panels -- Two-level navigation hierarchy +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 -## Choosing the right page layout +### `ui.page_sidebar()` -### `ui.page_sidebar` — Global sidebar controlling all content +Use `ui.page_sidebar()` for single-page dashboards where one sidebar controls the whole page. -Use when a single sidebar of filters drives all outputs on one page. -**Always set `fillable=True`** so content fills the viewport. +**Core API:** ```python -# Core -app_ui = ui.page_sidebar(ui.sidebar(...), title="My Dashboard", fillable=True) -# Express -ui.page_opts(title="My Dashboard", fillable=True) -with ui.sidebar(): ... +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) ``` -### `ui.page_navbar` — Multi-page navigation via top navbar +Best practices: -Use when the app has distinct pages/sections the user switches between. -**Never use emojis in nav panel titles** — use plain text only. +- 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 -# Core +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_spacer(), - ui.nav_panel("Overview", page1), - ui.nav_panel("Details", page2), - title="My App", + ui.nav_panel("Overview", overview), + ui.nav_panel("Details", ui.output_data_frame("details")), + title="Analytics Platform", fillable=True, ) ``` +**Express API:** + ```python -# Express -ui.page_opts(title="My App", fillable=True) +ui.page_opts(title="Analytics Platform", fillable=True) ui.nav_spacer() -with ui.nav_panel("Overview"): ... -with ui.nav_panel("Details"): ... + +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(): + ... ``` -`ui.nav_spacer()` pushes subsequent nav items to the right in the navbar. +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. -## Sidebar patterns +## Grid Layouts -Place all primary filter inputs inside `ui.sidebar()`. +### `ui.layout_column_wrap()` + +Use `ui.layout_column_wrap()` for uniform cards or KPI boxes. ```python -# Core -ui.sidebar( - ui.input_selectize("ticker", "Stock", choices=stocks, selected="AAPL"), - ui.input_slider("bill", "Amount", min=0, max=100, value=[10, 90], pre="$"), - ui.input_checkbox_group("time", "Service", ["Lunch", "Dinner"], - selected=["Lunch", "Dinner"], inline=True), - ui.input_date_range("dates", "Dates", start=start, end=end), - ui.input_switch("group", "Group by species", value=True), - ui.input_action_button("reset", "Reset filter"), - open="desktop", # auto-collapse on mobile +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 -# Express -with ui.sidebar(open="desktop"): - ui.input_selectize("ticker", "Stock", choices=stocks, selected="AAPL") - ui.input_slider("bill", "Amount", min=0, max=100, value=[10, 90], pre="$") - ui.input_action_button("reset", "Reset filter") +ui.layout_column_wrap(card_a, card_b, card_c, width="280px") ``` -### Key rules +Guidelines: -- Use `open="desktop"` to auto-collapse the sidebar on mobile viewports. -- Group filters logically — filters first, reset button last. -- Sidebar inputs drive `@reactive.calc` functions that filter/transform data. -- Common input types in sidebars: `input_select`, `input_selectize`, `input_slider`, - `input_checkbox_group`, `input_date_range`, `input_switch`, `input_action_button`. +- 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()` -## Navigation between panels +Use `ui.layout_columns()` when you need explicit control over width proportions or breakpoints. -### Two-level navigation hierarchy +```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], +) +``` -Combine `page_navbar` (top-level pages) with `navset_card_underline` (sub-tabs within a page): +Responsive layouts use breakpoint dictionaries: ```python -# Core — nested navigation -page1 = ui.navset_card_underline( - ui.nav_panel("Plot", ui.output_plot("hist")), - ui.nav_panel("Table", ui.output_data_frame("data")), - footer=ui.input_select("var", "Variable", choices=["col_a", "col_b"]), - title="Data explorer", +ui.layout_columns( + chart_card, + table_card, + col_widths={"sm": 12, "md": [6, 6], "lg": [7, 5]}, ) +``` -app_ui = ui.page_navbar( - ui.nav_panel("Page 1", page1), - ui.nav_panel("Page 2", "Second page content."), - title="My App", +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 -# Express — nested navigation -with ui.nav_panel("Page 1"): - footer = ui.input_select("var", "Variable", choices=["col_a", "col_b"]) - with ui.navset_card_underline(title="Data explorer", footer=footer): - with ui.nav_panel("Plot"): - @render.plot - def hist(): ... - with ui.nav_panel("Table"): - @render.data_frame - def data(): ... +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", + ), + ..., +) +``` -with ui.nav_panel("Page 2"): - "Second page content." +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, +) ``` -### Key rules +## Best Practices -- The `footer` kwarg on `navset_card_underline` places shared inputs **below all - sub-tabs**, keeping them visible regardless of which tab is active. -- In Core, define navset content as a variable (e.g., `page1`), then pass into - `ui.nav_panel()`. -- In Express, define the `footer` input widget **before** the - `with ui.navset_card_underline(...)` block so it can be passed as a kwarg. -- `ui.nav_spacer()` pushes navbar items to the right for visual alignment. -- No sidebar is used in basic-navigation apps — inputs live in the navset card's footer. +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 index 2f64280..bd9a8b7 100644 --- a/shiny/shiny-python-dashboard/references/reactivity-and-rendering.md +++ b/shiny/shiny-python-dashboard/references/reactivity-and-rendering.md @@ -1,225 +1,195 @@ -# Reactivity and Rendering +# Reactivity and Rendering in Shiny for Python -## Contents +This reference covers the reactive graph and output rendering patterns that make a Shiny for Python dashboard predictable and maintainable. -- `@reactive.calc` — the primary primitive -- `@reactive.effect` and `@reactive.event` -- `req()` — guarding against empty inputs -- Rendering Plotly charts -- Rendering Matplotlib/Seaborn plots -- Rendering data tables -- Rendering value box content -- Interactive Plotly click events +## Reactive Graph Design ---- +### `@reactive.calc` -## `@reactive.calc` — The primary primitive - -Use for all filtered/derived data. Chain calcs for multi-step transformations: +Use `@reactive.calc` for filtered data, derived metrics, and reusable intermediate objects. ```python @reactive.calc -def get_ticker(): - return yf.Ticker(input.ticker()) +def filtered_data(): + frame = df.copy() + if input.region(): + frame = frame[frame["region"].isin(input.region())] + return frame -@reactive.calc -def get_data(): - return get_ticker().history(start=input.dates()[0], end=input.dates()[1]) @reactive.calc -def get_change(): - close = get_data()["Close"] - if len(close) < 2: - return 0.0 - return close.iloc[-1] - close.iloc[-2] +def metric_series(): + return pd.to_numeric(filtered_data()[input.metric()], errors="coerce").dropna() ``` -Filtering pattern with Polars: +Chain reactive calcs instead of repeating the same filtering logic in every output. -```python -@reactive.calc -def tips_data(): - bill = input.total_bill() - return tips.filter( - pl.col("total_bill").is_between(bill[0], bill[1]), - pl.col("time").is_in(input.time()), - ) -``` +### `req()` -Filtering pattern with Pandas: +Use `req()` to stop the reactive pipeline when an input or intermediate result is empty. ```python -@reactive.calc -def careers(): - games = input.games() - idx = (careers_df["GP"] >= games[0]) & (careers_df["GP"] <= games[1]) - return careers_df[idx] -``` - -### Core rule: No global state mutation +from shiny import req -All reactivity flows one direction through `@reactive.calc` chains. Never mutate -module-level variables inside reactive functions. ---- +@reactive.calc +def selected_players(): + players = req(input.players()) + return careers()[careers()["person_id"].isin(players)] +``` -## `@reactive.effect` and `@reactive.event` +This keeps render functions simple and avoids error-prone empty-state code in every output. -### Reset buttons +### `@reactive.effect` and `@reactive.event` -Use `@reactive.effect` + `@reactive.event(input.reset)` to reset inputs to defaults: +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("total_bill", value=bill_rng) - ui.update_checkbox_group("time", selected=["Lunch", "Dinner"]) + ui.update_slider("amount", value=[10, 90]) + ui.update_checkbox_group("service", selected=["Lunch", "Dinner"]) ``` -### Cascading UI updates +Without `@reactive.event`, the effect will rerun whenever one of its reactive dependencies changes. -Use `@reactive.effect` (without `@reactive.event`) to update dependent inputs -when upstream data changes: +### No global mutation -```python -@reactive.effect -def _(): - players = dict(zip(careers()["person_id"], careers()["player_name"])) - ui.update_selectize("players", choices=players, selected=input.players()) -``` +Do not mutate module-level globals inside reactive functions. Compute new values and return them through the reactive graph instead. -This re-runs whenever `careers()` changes, keeping the selectize choices in sync. +## Rendering Patterns ---- +### Matplotlib and Seaborn with `@render.plot` -## `req()` — Guarding against empty inputs +Import `matplotlib.pyplot` inside the render function. ```python -from shiny import req +@render.plot +def histogram(): + import matplotlib.pyplot as plt -@reactive.calc -def player_stats(): - players = req(input.players()) # stops execution if None/empty - return careers()[careers()["person_id"].isin(players)] + 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 ``` -`req()` silently stops reactive execution when the value is falsy, preventing -downstream errors from empty selections. +Rules: ---- +- use `figsize=(8, 4)` as a strong default +- label axes +- call `fig.tight_layout()` before returning +- drop missing values before plotting -## Rendering Plotly charts +### Plotly with `shinywidgets` -Use `@render_plotly` from `shinywidgets`. Always set explicit `height` on the figure: +Use `output_widget()` in Core apps and `@render_plotly` in the server or Express block. ```python from shinywidgets import output_widget, render_plotly -# Core — requires output_widget("scatterplot") in UI tree +app_ui = ui.card( + ui.card_header("Scatterplot"), + output_widget("scatterplot"), + full_screen=True, +) + + @render_plotly def scatterplot(): - color = input.scatter_color() - fig = px.scatter(tips_data(), x="total_bill", y="tip", - color=None if color == "none" else color, trendline="lowess") - fig.update_layout(height=400, margin=dict(l=40, r=20, t=40, b=40)) - return fig + return px.scatter(filtered_data(), x="total_bill", y="tip", color=input.color()) ``` +Avoid duplicate keys when you merge Plotly layout dictionaries: + ```python -# Express — place inline inside card -with ui.card(full_screen=True): - ui.card_header("Scatter Plot") - @render_plotly - def scatterplot(): ... +axis_style = {"gridcolor": "#d0d7de", "showline": False} +fig.update_layout(yaxis={**axis_style, "tickfont": {"size": 11}}) ``` -In Core, always pair `@render_plotly` with `output_widget("id")` in the UI tree. -In Express, place the decorator inline inside `with ui.card():`. - ---- +Do not write `dict(**axis_style, tickfont=...)` if `axis_style` already contains `tickfont`. -## Rendering Matplotlib/Seaborn plots +### Tables with `@render.data_frame` -Use `@render.plot` — returns a matplotlib figure or axes. -Always set `figsize` explicitly to prevent tiny charts: +Use `render.DataGrid(...)` for sortable, filterable tables. ```python -@render.plot -def hist(): - import matplotlib.pyplot as plt - fig, ax = plt.subplots(figsize=(8, 4)) - ax.hist(df[input.var()].dropna(), bins=20, - color="#0d6efd", edgecolor="white") - ax.set_xlabel(input.var()) - ax.set_ylabel("Count") - return fig +@render.data_frame +def summary_table(): + return render.DataGrid(filtered_data(), filters=True) ``` -Combine `sns.kdeplot` + `sns.rugplot` for density with rug marks: +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.plot -def density(): - hue = "species" if input.species() else None - sns.kdeplot(df, x=input.var(), hue=hue) - if input.show_rug(): - sns.rugplot(df, x=input.var(), hue=hue, color="black", alpha=0.25) +@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") ``` -In Core, use `ui.output_plot("id")` in the UI tree. +Do not build major page sections with `@render.ui` if a regular static layout plus reactive outputs would be simpler. ---- +## Core and Express Equivalents -## Rendering data tables +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(tips_data()) + return render.DataGrid(filtered_data()) ``` -Add `filters=True` for column-level filtering: `render.DataGrid(df, filters=True)`. +Express colocates the UI block and renderer: -In Core, use `ui.output_data_frame("table")` in the UI tree. +```python +with ui.card(full_screen=True): + ui.card_header("Data") ---- + @render.data_frame + def table(): + return render.DataGrid(filtered_data()) +``` -## Rendering value box content +Both are valid. Choose the style that fits the codebase and keep it consistent. -```python -# Core — @render.ui returning a formatted string -@render.ui -def average_tip(): - d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() - return f"{d:.1%}" if d else "N/A" -``` +## Common Patterns -```python -# Express — @render.express (prints value, no return needed) -@render.express -def average_tip(): - d = tips_data().select((pl.col("tip") / pl.col("total_bill")).mean()).item() - f"{d:.1%}" if d else "N/A" -``` +### Reset buttons ---- +Use `@reactive.effect` with `@reactive.event` and the appropriate `ui.update_*()` helpers. -## Interactive Plotly click events +### Cascading inputs -Convert a plotly Figure to `go.FigureWidget` and attach `.on_click()`: +Use a plain `@reactive.effect` when one input's choices depend on the current filtered data. ```python -import plotly.graph_objects as go +@reactive.effect +def _(): + choices = dict(zip(filtered_data()["id"], filtered_data()["label"])) + ui.update_selectize("item", choices=choices) +``` -fig = go.FigureWidget(fig.data, fig.layout) -fig.data[1].on_click(on_rug_click) -return fig +### Derived KPI values -def on_rug_click(trace, points, state): - player_id = trace.customdata[points.point_inds[0]] - selected = list(input.players()) + [player_id] - ui.update_selectize("players", selected=selected) -``` +Create one calc per reusable business metric instead of recalculating inside each value box renderer. + +## Common Errors -This enables click-to-select interactions on plotly rug plots or scatter traces. +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 index e651203..6320dcd 100644 --- a/shiny/shiny-python-dashboard/references/styling-and-data.md +++ b/shiny/shiny-python-dashboard/references/styling-and-data.md @@ -1,163 +1,147 @@ -# Styling and Data Loading +# Styling and Data Patterns for Shiny for Python -## Contents +This reference covers the non-widget parts of a polished dashboard: project structure, data loading, number formatting, CSS, and light or dark presentation choices. -- CSS inclusion -- Recommended CSS patterns -- Static data loading -- Live API data loading -- Pandas vs Polars +## Project Structure ---- +Use a small, predictable structure for dashboard apps. -## CSS inclusion - -Include CSS at the end of the page layout: - -```python -# Core — as last arg inside ui.page_sidebar(...) -ui.include_css(app_dir / "styles.css") - -# Express — at module level (after layout blocks) -ui.include_css(Path(__file__).parent / "styles.css") +```text +my-dashboard/ +├── app-core.py +├── app-express.py +├── shared.py +├── plots.py +├── styles.css +├── data.csv +└── requirements.txt ``` ---- +Guidelines: -## Recommended CSS patterns +- 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. -### Minimal styles.css +## Data Loading -```css -:root { - --bslib-sidebar-main-bg: #f8f8f8; -} -``` - -Every template uses this sidebar background override. +Load static data once at module scope or in `shared.py`. -### Hide Plotly toolbar +```python +from pathlib import Path -```css -.plotly .modebar-container { - display: none !important; -} -``` +import pandas as pd -Used in `nba-dashboard` and `stock-app` for a cleaner look. +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()) +``` -## Chart sizing best practices +This keeps reactive code focused on filtering and rendering instead of repeated file I/O. -Always set explicit chart dimensions to prevent charts from rendering too small: +### Clean data before it reaches plots -### Plotly +For dashboard inputs and charts, normalize types early: ```python -fig.update_layout( - height=400, # explicit pixel height - margin=dict(l=40, r=20, t=40, b=40), -) +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"]) ``` -### Matplotlib / Seaborn +Guidelines: -```python -fig, ax = plt.subplots(figsize=(8, 4)) # width=8, height=4 inches -``` +- 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. -### Rules +## Number Formatting -- Always set `height` on Plotly figures (default can be too small in cards) -- Always use `figsize=(8, 4)` or similar for Matplotlib — never use the default -- Wrap charts in `ui.card(full_screen=True)` so users can expand them -- Handle missing data before plotting: `df.dropna(subset=[col])` +Dashboard text should be formatted before it reaches a value box, annotation, or table summary. -### Dark popover headers +```python +def fmt_currency(amount: float) -> str: + return f"${amount:,.0f}" -```css -.popover { - --bs-popover-header-bg: #222; - --bs-popover-header-color: #fff; -} -.popover .btn-close { - filter: var(--bs-btn-close-white-filter); -} -``` -Used in `dashboard-tips` when `ui.popover()` is used for secondary inputs. +def fmt_percent(ratio: float) -> str: + return f"{ratio:.1%}" -### General approach -No custom theme objects needed — rely on default bslib/Bootstrap theme with -CSS variable overrides. Keep `styles.css` minimal. +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. -## Static data loading +## CSS and Theming -Load CSVs in `shared.py` at module level — never inside the app file: +Use a small stylesheet for layout polish and component-specific tuning. ```python -# shared.py from pathlib import Path -import pandas as pd # or: import polars as pl +from shiny import ui app_dir = Path(__file__).parent -df = pd.read_csv(app_dir / "data.csv") -``` -Export computed constants that UI inputs need: - -```python -bill_rng = (df["total_bill"].min(), df["total_bill"].max()) -gp_max = df["GP"].max() -players_dict = dict(zip(df["person_id"], df["player_name"])) +app_ui = ui.page_sidebar( + ui.include_css(app_dir / "styles.css"), + ..., +) ``` ---- +Use CSS for: -## Live API data loading +- 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 -Fetch live data inside `@reactive.calc` so it re-runs on input changes: +Avoid using CSS to rebuild the layout system from scratch when Shiny layout primitives already solve the problem. -```python -@reactive.calc -def get_ticker(): - return yf.Ticker(input.ticker()) - -@reactive.calc -def get_data(): - dates = input.dates() - return get_ticker().history(start=dates[0], end=dates[1]) -``` +### Theme direction -Never fetch API data at module level — it would only run once at startup. +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 -## Pandas vs Polars +```python +ui.input_dark_mode(id="mode") +``` -Both are supported across templates. Choose based on ecosystem needs. +## Requirements -### Polars filtering (method chaining) +Include the packages your dashboard actually uses. Common dashboard requirements are: -```python -tips.filter( - pl.col("total_bill").is_between(bill[0], bill[1]), - pl.col("time").is_in(input.time()), -) +```text +shiny +pandas +plotly +matplotlib +seaborn +faicons +shinywidgets ``` -### Pandas filtering (boolean indexing) +Add mapping or table packages only when the app needs them. -```python -idx = (df["GP"] >= games[0]) & (df["GP"] <= games[1]) -return df[idx] -``` +## Best Practices -| Library | Used in | -|---|---| -| Polars | `dashboard-tips` | -| Pandas | `nba-dashboard`, `stock-app`, `basic-sidebar`, `basic-navigation` | +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 `