From 06271d90e4b0556c2e2a260a38f321be1972c488 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Wed, 26 Aug 2026 11:17:50 +0200 Subject: [PATCH 1/7] include scaffolding for toggling full size plots --- CLAUDE.md | 38 ++++++++++++++++++++- src/assets/css/main.css | 71 ++++++++++++++++++++++++++++++++++++++++ src/assets/fullscreen.js | 61 ++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/assets/fullscreen.js diff --git a/CLAUDE.md b/CLAUDE.md index bf00de3..9cd51b3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,6 @@ python-dashboard-template/ ├── src/ │ ├── app.py # Main application file │ ├── theme.py # Colours, type scale and the Plotly template -│ ├── memory_log.py # Dev aid: prints RSS memory usage, see LOG_MEMORY │ ├── assets/ # Static files (CSS, images, sample data) │ └── pages/ # One module per page, each with dash.register_page │ ├── home.py @@ -54,6 +53,43 @@ python-dashboard-template/ - `columnSize="responsiveSizeToFit"` - `defaultColDef={"filter": True, "sortable": True}` +## Fullscreen Toggle Pattern +A reusable "expand to fullscreen" button for any chart inside a `.visual` box. No Dash callback is needed — it's pure CSS + one small JS file in `assets/`, so it automatically applies to any current or future chart that follows the markup pattern below. + +**Why it's built this way (read this before changing it):** the naive version — toggle a `position: fixed` class and call `Plotly.Plots.resize(gd)` — breaks in two ways that are easy to reintroduce by accident: +1. Outside fullscreen, a plot's container normally has no explicit height (it's sized *by* the plot, not the other way round). Asking Plotly to "resize to fit its container" on exit is therefore circular — the container has no size to resize to, and the chart doesn't shrink back. +2. `dcc.Loading` wraps the graph in one or two extra `
`s whose class names aren't part of the public Dash API. Trying to cascade a height down through them with CSS percentages (`height: 100%` chained through unknown wrapper divs) silently breaks and leaves the chart stuck at a stale pixel size — which, in a flex row with the default `align-items: stretch`, then drags the *other* column's box height along with it. + +The fix: give the chart's own wrapper (`.graph-wrap`, not the Plotly div itself) an explicit, always-defined height in both states (fixed px normally, flex-filled in fullscreen), then measure that wrapper directly in JS and set the chart's exact pixel size via `Plotly.relayout(gd, {width, height, autosize: false})`. This never depends on the unknown internal `dcc.Loading` DOM structure. + +**Markup** — wrap every chart to make fullscreen-able like this: +```python +html.Div( + [ + html.Button( + "⛶", + className="fullscreen-toggle-btn", + title="Toggle full screen", + **{"aria-label": "Toggle full screen"}, + ), + html.Div("Chart Title", className="visual-title"), + html.Div( + dcc.Loading(dcc.Graph(id="my-chart")), + className="graph-wrap", + ), + ], + className="visual", +) +``` +If several charts sit side by side in a flex row, add `"alignItems": "flex-start"` to that row's `style` dict — a safety net so one chart's sizing hiccup can never stretch its neighbor. + +The CSS lives in `assets/css/main.css` (the `.visual`, `.fullscreen-toggle-btn`, `.visual--fullscreen`, `.graph-wrap` and `body.fullscreen-active` rules) and the JS lives in `assets/fullscreen.js`. Both are already in the template and apply automatically — no per-page wiring needed beyond the markup above. + +**Rules when reusing this:** +- Always wrap the chart in `.graph-wrap` — never put `fullscreen-toggle-btn` next to a bare `dcc.Graph`/`dcc.Loading` without it; the JS measures `.graph-wrap`, not the Plotly div, so skipping it breaks the resize. +- Don't try to make the chart's height cascade through CSS percentages past `.graph-wrap` — that's the exact thing that broke before. Let the JS set the Plotly size explicitly. +- Only one visual can be fullscreen at a time by design (`enterFullscreen` clears any other `.visual--fullscreen` first); don't remove that if adding more charts. + ## Avoid Hallucinations - Never use `app.run_server`; only use `app.run` - Never use obsolete patterns like `app.validation_layout`. Modern Dash handles dynamic layouts smoothly; just use `suppress_callback_exceptions=True` on app initialization if building dynamic layouts. diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 7aedc2d..9cfc7aa 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -257,6 +257,10 @@ body, -------------------------------------------------------------------------- */ .visual { + position: relative; + /* so .fullscreen-toggle-btn can be pinned to a corner */ + overflow: hidden; + /* clips chart overflow during resize, see fullscreen.js */ border: 1px solid var(--border-grey-1); border-radius: var(--radius); padding: 12px; @@ -391,4 +395,71 @@ input[disabled] { border: 1px solid var(--border-grey-2); background-color: var(--border-grey-1); color: var(--grey); +} + +/* -------------------------------------------------------------------------- + Fullscreen toggle for chart visuals — see assets/fullscreen.js and the + "Fullscreen Toggle Pattern" section in CLAUDE.md for the markup and the + reasoning behind this approach. + -------------------------------------------------------------------------- */ + +.fullscreen-toggle-btn { + position: absolute; + top: 8px; + right: 8px; + z-index: 10; + width: 28px; + height: 28px; + min-height: 28px; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.visual--fullscreen { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100vw; + height: 100vh; + z-index: 2000; + margin: 0; + border-radius: 0; + padding: 24px; + box-sizing: border-box; + display: flex; + flex-direction: column; + overflow: auto; +} + +.visual--fullscreen .fullscreen-toggle-btn { + top: 16px; + right: 16px; +} + +.visual--fullscreen .visual-title { + flex: 0 0 auto; +} + +/* .graph-wrap's own box size is always deterministic - a fixed height + normally, or flex-filled the remaining fullscreen space. We do NOT rely + on that cascading further down into dcc.Loading's wrapper divs; the JS + in fullscreen.js measures this box directly instead. */ +.graph-wrap { + height: 460px; + overflow: hidden; +} + +.visual--fullscreen .graph-wrap { + flex: 1 1 auto; + min-height: 0; + height: auto; +} + +body.fullscreen-active { + overflow: hidden; } \ No newline at end of file diff --git a/src/assets/fullscreen.js b/src/assets/fullscreen.js new file mode 100644 index 0000000..131b000 --- /dev/null +++ b/src/assets/fullscreen.js @@ -0,0 +1,61 @@ +// Fullscreen toggle for chart visuals. See the "Fullscreen Toggle Pattern" +// section in CLAUDE.md for the required markup and the reasoning behind +// this approach. +// +// Event delegation (rather than per-chart wiring) so this survives +// dcc.Loading re-rendering the DOM, and applies automatically to any +// current or future chart that follows the markup pattern. + +function resizeGraphsIn(container) { + if (!window.Plotly) return; + container.querySelectorAll(".graph-wrap").forEach(function (wrap) { + const gd = wrap.querySelector(".js-plotly-plot"); + if (!gd) return; + const rect = wrap.getBoundingClientRect(); + const width = Math.round(rect.width); + const height = Math.round(rect.height); + if (width > 0 && height > 0) { + window.Plotly.relayout(gd, { width: width, height: height, autosize: false }); + } + }); +} + +function settleResize(visual) { + requestAnimationFrame(function () { + resizeGraphsIn(visual); + setTimeout(function () { resizeGraphsIn(visual); }, 100); + }); +} + +function exitFullscreen(visual) { + visual.classList.remove("visual--fullscreen"); + document.body.classList.remove("fullscreen-active"); + settleResize(visual); +} + +function enterFullscreen(visual) { + document.querySelectorAll(".visual--fullscreen").forEach(function (el) { + el.classList.remove("visual--fullscreen"); + }); + visual.classList.add("visual--fullscreen"); + document.body.classList.add("fullscreen-active"); + settleResize(visual); +} + +document.addEventListener("click", function (event) { + const btn = event.target.closest(".fullscreen-toggle-btn"); + if (!btn) return; + const visual = btn.closest(".visual"); + if (!visual) return; + if (visual.classList.contains("visual--fullscreen")) { + exitFullscreen(visual); + } else { + enterFullscreen(visual); + } +}); + +document.addEventListener("keydown", function (event) { + if (event.key !== "Escape") return; + const visual = document.querySelector(".visual--fullscreen"); + if (visual) exitFullscreen(visual); +}); From 8d3eaebd116b0150c7ed42481c308ab6b55a4dc9 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Wed, 26 Aug 2026 11:32:28 +0200 Subject: [PATCH 2/7] include revision log and text on landing page --- CLAUDE.md | 1 + README.md | 12 +++--- src/assets/css/main.css | 13 +++++++ src/pages/analytics.py | 2 +- src/pages/data_table.py | 64 +++++++++++++++++++++++++++++++ src/pages/home.py | 83 +++++++++++++++++++++++------------------ tests/test_app.py | 4 +- 7 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 src/pages/data_table.py diff --git a/CLAUDE.md b/CLAUDE.md index 9cd51b3..b7a2dbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,7 @@ python-dashboard-template/ │ ├── assets/ # Static files (CSS, images, sample data) │ └── pages/ # One module per page, each with dash.register_page │ ├── home.py +│ ├── data_table.py │ └── analytics.py ├── tests/ # pytest suite ├── notebooks/ # ad-hoc exploration, outside the running app diff --git a/README.md b/README.md index 2840f59..d9bb27a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Python Dashboard Template -A starting point for Plotly Dash dashboards: one theme, one working example -page and one filterable-chart page, a mock-up of 4insight's header for local -layout work, and the process scaffolding (CI, PR template, `CLAUDE.md` +A starting point for Plotly Dash dashboards: one theme, a home page with a +revision log and notes box, a data-table example page and a +filterable-chart page, a mock-up of 4insight's header for local layout +work, and the process scaffolding (CI, PR template, `CLAUDE.md` conventions) set up. To start a new project from this template: clone or copy it, replace -`src/assets/sample_data.csv` and the two pages in `src/pages/` with your own, +`src/assets/sample_data.csv` and the pages in `src/pages/` with your own, and update this README and the browser-tab title in `src/app.py`. ## Running it @@ -50,7 +51,8 @@ src/ ├── theme.py colours, type scale and the Plotly template ├── memory_log.py dev aid: prints RSS memory usage, see LOG_MEMORY ├── pages/ -│ ├── home.py example: an AgGrid over the sample data +│ ├── home.py landing page: revision log AgGrid and a free-text notes box +│ ├── data_table.py example: an AgGrid over the sample data │ └── analytics.py example: a slicer driving a filtered Plotly chart └── assets/ ├── css/main.css page styling, mirrors theme.py as CSS variables diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 9cfc7aa..3b82191 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -291,6 +291,19 @@ body, margin-bottom: var(--gap); } +.notes-textarea { + width: 100%; + min-height: 120px; + padding: 8px 12px; + border: 1px solid var(--border-grey-2); + border-radius: var(--radius); + font-family: var(--font-main); + font-size: var(--size-body); + color: var(--body-text); + box-sizing: border-box; + resize: vertical; +} + /* -------------------------------------------------------------------------- Tables (AG Grid) top border 2px dark grey · inner 1px grey · headers size 9 dark grey diff --git a/src/pages/analytics.py b/src/pages/analytics.py index 5e9b3b1..b4fd5ad 100644 --- a/src/pages/analytics.py +++ b/src/pages/analytics.py @@ -12,7 +12,7 @@ import plotly.express as px from dash import Input, Output, callback, dcc, html -dash.register_page(__name__, path="/analytics", name="Analytics", order=1) +dash.register_page(__name__, path="/analytics", name="Analytics", order=2) SAMPLE_DATA_PATH = pathlib.Path(__file__).resolve().parents[1] / "assets" / "sample_data.csv" diff --git a/src/pages/data_table.py b/src/pages/data_table.py new file mode 100644 index 0000000..d50119f --- /dev/null +++ b/src/pages/data_table.py @@ -0,0 +1,64 @@ +""" +Data table page +---------------- + +Example page showing a table of sample data fetched from the +"assets/sample_data.csv" file. The table is implemented using the Dash AG +Grid component, which allows for filtering, sorting, and pagination. + +""" + +import pathlib + +import dash +import dash_ag_grid as dag +import pandas as pd +from dash import dcc, html + +dash.register_page(__name__, path="/data-table", name="Data table", order=1) + +SAMPLE_DATA_PATH = pathlib.Path(__file__).resolve().parents[1] / "assets" / "sample_data.csv" + + +def load_sample_data(): + """Not shared with app.py: importing from app here would re-trigger Dash's + own page auto-discovery when the app is run as a script. See analytics.py + for the same function - duplicated rather than imported, on purpose.""" + return pd.read_csv(SAMPLE_DATA_PATH) + + +def layout(): + df = load_sample_data() + grid = dag.AgGrid( + id="data-table-sample-grid", + rowData=df.to_dict("records"), + columnDefs=[{"field": col, "headerName": col} for col in df.columns], + defaultColDef={"filter": True, "sortable": True}, + columnSize="responsiveSizeToFit", + dashGridOptions={ + "theme": "themeBalham", + "animateRows": True, + "pagination": True, + "paginationPageSize": 10, + }, + ) + return html.Div( + [ + html.Div( + [ + dcc.Markdown( + "Replace this page, `src/assets/sample_data.csv` and the " + "example on the Analytics page with your own." + ), + ], + className="visual", + ), + html.Div( + [ + html.Div("Sample data", className="visual-title"), + dcc.Loading(grid), + ], + className="visual", + ), + ] + ) diff --git a/src/pages/home.py b/src/pages/home.py index 6ed07c8..b1eb4d0 100644 --- a/src/pages/home.py +++ b/src/pages/home.py @@ -1,64 +1,75 @@ -""" -Landing page ------------- - -This is the "Home" page, which is the first page users see when they open the app. -It contains a table of sample data fetched from the "assets/sample_data.csv" file. -The table is implemented using the Dash AG Grid component, which allows for filtering, sorting, and pagination. +"""Home page: revision log and a free-text notes box. +Landing page. The revision log records who issued, checked and approved +each version of the app's content - add a row to issue a new revision +rather than editing the last one, since the point of the log is the +history. """ -import pathlib - import dash import dash_ag_grid as dag -import pandas as pd from dash import dcc, html dash.register_page(__name__, path="/", name="Home", order=0) -SAMPLE_DATA_PATH = pathlib.Path(__file__).resolve().parents[1] / "assets" / "sample_data.csv" +# Empty Checked, Approved, etc. mean exactly that: not yet checked, not yet +# approved. Fill them in when you fill in a real revision. - -def load_sample_data(): - """Not shared with app.py: importing from app here would re-trigger Dash's - own page auto-discovery when the app is run as a script. See analytics.py - for the same function - duplicated rather than imported, on purpose.""" - return pd.read_csv(SAMPLE_DATA_PATH) +REVISION_LOG = [ + { + "Revision No.": "1.0", + "Date": "2026-01-01", + "Author": "ABC", + "Checked": "DEF", + "Approved": "GHI", + "Reason for issue": "Issued for client review", + }, +] def layout(): - df = load_sample_data() grid = dag.AgGrid( - id="home-sample-grid", - rowData=df.to_dict("records"), - columnDefs=[{"field": col, "headerName": col} for col in df.columns], + id="home-revision-log-grid", + rowData=REVISION_LOG, + # headerName explicitly, or AG Grid title-cases the field: "Reason for + # issue" would render as "Reason For Issue". + columnDefs=[{"field": col, "headerName": col} for col in REVISION_LOG[0]], defaultColDef={"filter": True, "sortable": True}, columnSize="responsiveSizeToFit", dashGridOptions={ "theme": "themeBalham", "animateRows": True, - "pagination": True, - "paginationPageSize": 10, + # No pagination: the log only ever holds a handful of rows, and + # autoHeight sizes the grid to its content instead of drawing a + # tall empty box with a pager underneath it. + "pagination": False, + "domLayout": "autoHeight", + # A dot in a field name is a nested-property path to AG Grid, so + # "Revision No." would look up row["Revision No"][""] and render + # blank. The field names here are human labels, never paths. + "suppressFieldDotNotation": True, }, ) return html.Div( [ + html.Div( - [ - dcc.Markdown( - "Replace this page, `src/assets/sample_data.csv` and the " - "example on the Analytics page with your own." - ), - ], + dcc.Textarea( + id="home-notes-textarea", + value="Here you can write free-text notes about the app's content, or anything else you want to remember.", + className="notes-textarea", + ), className="visual", ), html.Div( - [ - html.Div("Sample data", className="visual-title"), - dcc.Loading(grid), - ], - className="visual", - ), - ] + [ + html.Div("Revision log", className="visual-title"), + dcc.Loading(grid), + ], + className="visual row-gap", + ) + ], + # A small table and a text box read as mostly whitespace spread + # across the full 1800px content width. + className="narrow-page", ) diff --git a/tests/test_app.py b/tests/test_app.py index 8dd30a8..9b09fff 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -11,6 +11,7 @@ # against, so add your new page's path/name here too. EXPECTED_PAGES = { "/": "Home", + "/data-table": "Data table", "/analytics": "Analytics", } @@ -62,7 +63,8 @@ def component_ids(node, found=None): # Same deal as EXPECTED_PAGES: add your new page's callback-bound component # ids here, or they simply aren't checked (not a failure, just a silent gap). CALLBACK_IDS = { - "/": {"home-sample-grid"}, + "/": {"home-revision-log-grid"}, + "/data-table": {"data-table-sample-grid"}, "/analytics": {"analytics-category-filter", "analytics-chart"}, } From 40e1408008e1b0b55ab6c811f09a8389bfb7b156 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Wed, 26 Aug 2026 11:54:41 +0200 Subject: [PATCH 3/7] include Introduction page --- src/assets/css/main.css | 5 ++--- src/pages/home.py | 47 +++++++++++++++++++++++++---------------- tests/test_app.py | 2 +- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 3b82191..524bacf 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -294,9 +294,8 @@ body, .notes-textarea { width: 100%; min-height: 120px; - padding: 8px 12px; - border: 1px solid var(--border-grey-2); - border-radius: var(--radius); + padding: 0; + border: none; font-family: var(--font-main); font-size: var(--size-body); color: var(--body-text); diff --git a/src/pages/home.py b/src/pages/home.py index b1eb4d0..c93687d 100644 --- a/src/pages/home.py +++ b/src/pages/home.py @@ -10,7 +10,7 @@ import dash_ag_grid as dag from dash import dcc, html -dash.register_page(__name__, path="/", name="Home", order=0) +dash.register_page(__name__, path="/", name="Introduction", order=0) # Empty Checked, Approved, etc. mean exactly that: not yet checked, not yet # approved. Fill them in when you fill in a real revision. @@ -22,7 +22,7 @@ "Author": "ABC", "Checked": "DEF", "Approved": "GHI", - "Reason for issue": "Issued for client review", + "Comment": " ", }, ] @@ -31,11 +31,20 @@ def layout(): grid = dag.AgGrid( id="home-revision-log-grid", rowData=REVISION_LOG, - # headerName explicitly, or AG Grid title-cases the field: "Reason for - # issue" would render as "Reason For Issue". - columnDefs=[{"field": col, "headerName": col} for col in REVISION_LOG[0]], + # headerName explicitly, or AG Grid title-cases the field: "Comment" + # would still be fine, but "Revision No." would render as "Revision No .". + # flex: the first 5 columns are 20% narrower than an even split, and + # Comment absorbs that freed width (0.8 * 5 = 4, so Comment's flex of + # 2 keeps the same total of 6 that six equal columns would have had). + columnDefs=[ + {"field": col, "headerName": col, "flex": 0.8} for col in list(REVISION_LOG[0])[:5] + ] + + [{"field": "Comment", "headerName": "Comment", "flex": 2}], defaultColDef={"filter": True, "sortable": True}, - columnSize="responsiveSizeToFit", + # No columnSize: AG Grid's sizeColumnsToFit (what "responsiveSizeToFit" + # calls) recalculates widths on its own and overrides colDef.flex in + # the process - the two are alternative sizing mechanisms, not + # composable. Flex-sized columns already resize responsively without it. dashGridOptions={ "theme": "themeBalham", "animateRows": True, @@ -52,22 +61,24 @@ def layout(): ) return html.Div( [ - html.Div( - dcc.Textarea( - id="home-notes-textarea", - value="Here you can write free-text notes about the app's content, or anything else you want to remember.", - className="notes-textarea", - ), + [ + html.Div("Project info", className="visual-title"), + dcc.Textarea( + id="home-notes-textarea", + value="Here you can write free text about the project, the dashboard or other relevant information.", + className="notes-textarea", + ), + ], className="visual", ), html.Div( - [ - html.Div("Revision log", className="visual-title"), - dcc.Loading(grid), - ], - className="visual row-gap", - ) + [ + html.Div("Revision log", className="visual-title"), + dcc.Loading(grid), + ], + className="visual row-gap", + ), ], # A small table and a text box read as mostly whitespace spread # across the full 1800px content width. diff --git a/tests/test_app.py b/tests/test_app.py index 9b09fff..a2999e6 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -10,7 +10,7 @@ # sharing an `order` - this dict is the spec these tests check reality # against, so add your new page's path/name here too. EXPECTED_PAGES = { - "/": "Home", + "/": "Introduction", "/data-table": "Data table", "/analytics": "Analytics", } From b0225fc54f42bb6227f96cc76b0591d774dcb478 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Wed, 26 Aug 2026 12:15:24 +0200 Subject: [PATCH 4/7] pr comments small fixes --- CLAUDE.md | 5 ++++- src/assets/css/main.css | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b7a2dbe..2fcf064 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,6 +3,7 @@ python-dashboard-template/ ├── src/ │ ├── app.py # Main application file │ ├── theme.py # Colours, type scale and the Plotly template +│ ├── memory_log.py # Opt-in dev aid: prints RSS memory to the terminal, see LOG_MEMORY │ ├── assets/ # Static files (CSS, images, sample data) │ └── pages/ # One module per page, each with dash.register_page │ ├── home.py @@ -50,9 +51,11 @@ python-dashboard-template/ - **Component Libraries**: Prioritize component libraries in this order: Dash Core Components combined with Dash HTML Components, then Dash Mantine Components, then Dash Bootstrap Components if required. Try to minimize the number of libraries required. - **Data Tables**: Do not use `dash.datatable`; use `dash.AgGrid` instead. - **AgGrid Configs**: When instantiating `dag.AgGrid`, always set the following properties: - - `dashGridOptions={"theme": "themeBalham", "animateRows": True, "pagination": True, "paginationPageSize": 10}` - `columnSize="responsiveSizeToFit"` - `defaultColDef={"filter": True, "sortable": True}` + - `dashGridOptions={"theme": "themeBalham", "animateRows": True, **pagination_options(row_count)}`, where `pagination_options` switches on row count rather than pagination being on for every table: + - 15 rows or fewer: `{"pagination": False, "domLayout": "autoHeight"}` — the grid sizes to its content instead of drawing a tall empty box with a pager underneath a handful of rows. + - more than 15 rows: `{"pagination": True, "paginationPageSize": 10}` ## Fullscreen Toggle Pattern A reusable "expand to fullscreen" button for any chart inside a `.visual` box. No Dash callback is needed — it's pure CSS + one small JS file in `assets/`, so it automatically applies to any current or future chart that follows the markup pattern below. diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 524bacf..cdbe314 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -258,9 +258,11 @@ body, .visual { position: relative; - /* so .fullscreen-toggle-btn can be pinned to a corner */ - overflow: hidden; - /* clips chart overflow during resize, see fullscreen.js */ + /* so .fullscreen-toggle-btn can be pinned to a corner. No overflow: hidden + here - .graph-wrap already clips chart overflow during resize (see + fullscreen.js), and hiding overflow on every .visual would also clip + unrelated content that legitimately grows past the box, such as a + resizable .notes-textarea. */ border: 1px solid var(--border-grey-1); border-radius: var(--radius); padding: 12px; From 3d1d32304e409f5a5d1b6b88ef05b2263449b380 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss <72202484+anna-follestad-4ss@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:22:49 +0200 Subject: [PATCH 5/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2fcf064..50a23b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,7 @@ python-dashboard-template/ - **AgGrid Configs**: When instantiating `dag.AgGrid`, always set the following properties: - `columnSize="responsiveSizeToFit"` - `defaultColDef={"filter": True, "sortable": True}` - - `dashGridOptions={"theme": "themeBalham", "animateRows": True, **pagination_options(row_count)}`, where `pagination_options` switches on row count rather than pagination being on for every table: + - `dashGridOptions={"theme": "themeBalham", "animateRows": True, ...}`, choosing pagination settings based on row count: - 15 rows or fewer: `{"pagination": False, "domLayout": "autoHeight"}` — the grid sizes to its content instead of drawing a tall empty box with a pager underneath a handful of rows. - more than 15 rows: `{"pagination": True, "paginationPageSize": 10}` From a874ad9b73fc555ee4b0a3f47be71e1acc52b99d Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss <72202484+anna-follestad-4ss@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:29:34 +0200 Subject: [PATCH 6/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/pages/home.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/home.py b/src/pages/home.py index c93687d..74d5133 100644 --- a/src/pages/home.py +++ b/src/pages/home.py @@ -36,10 +36,13 @@ def layout(): # flex: the first 5 columns are 20% narrower than an even split, and # Comment absorbs that freed width (0.8 * 5 = 4, so Comment's flex of # 2 keeps the same total of 6 that six equal columns would have had). - columnDefs=[ - {"field": col, "headerName": col, "flex": 0.8} for col in list(REVISION_LOG[0])[:5] - ] - + [{"field": "Comment", "headerName": "Comment", "flex": 2}], + columnDefs=( + [ + {"field": col, "headerName": col, "flex": 0.8} + for col in list(REVISION_LOG[0])[:5] + ] + + [{"field": "Comment", "headerName": "Comment", "flex": 2}] + ), defaultColDef={"filter": True, "sortable": True}, # No columnSize: AG Grid's sizeColumnsToFit (what "responsiveSizeToFit" # calls) recalculates widths on its own and overrides colDef.flex in From bba0d46ef6d40e1d1c828e7e866186ee0eca0320 Mon Sep 17 00:00:00 2001 From: anna-follestad-4ss Date: Wed, 26 Aug 2026 13:48:37 +0200 Subject: [PATCH 7/7] black --- CLAUDE.md | 2 +- README.md | 8 ++++---- src/pages/analytics.py | 4 ++-- src/pages/{home.py => introduction.py} | 11 ++++------- tests/test_app.py | 2 +- 5 files changed, 12 insertions(+), 15 deletions(-) rename src/pages/{home.py => introduction.py} (91%) diff --git a/CLAUDE.md b/CLAUDE.md index 50a23b8..499d3de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ python-dashboard-template/ │ ├── memory_log.py # Opt-in dev aid: prints RSS memory to the terminal, see LOG_MEMORY │ ├── assets/ # Static files (CSS, images, sample data) │ └── pages/ # One module per page, each with dash.register_page -│ ├── home.py +│ ├── introduction.py │ ├── data_table.py │ └── analytics.py ├── tests/ # pytest suite diff --git a/README.md b/README.md index d9bb27a..c2e2114 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Python Dashboard Template -A starting point for Plotly Dash dashboards: one theme, a home page with a +A starting point for Plotly Dash dashboards: one theme, a landing page with a revision log and notes box, a data-table example page and a filterable-chart page, a mock-up of 4insight's header for local layout work, and the process scaffolding (CI, PR template, `CLAUDE.md` @@ -51,9 +51,9 @@ src/ ├── theme.py colours, type scale and the Plotly template ├── memory_log.py dev aid: prints RSS memory usage, see LOG_MEMORY ├── pages/ -│ ├── home.py landing page: revision log AgGrid and a free-text notes box -│ ├── data_table.py example: an AgGrid over the sample data -│ └── analytics.py example: a slicer driving a filtered Plotly chart +│ ├── introduction.py landing page: revision log AgGrid and a free-text notes box +│ ├── data_table.py example: an AgGrid over the sample data +│ └── analytics.py example: a slicer driving a filtered Plotly chart └── assets/ ├── css/main.css page styling, mirrors theme.py as CSS variables ├── 4insight_logo.png diff --git a/src/pages/analytics.py b/src/pages/analytics.py index b4fd5ad..d1857fb 100644 --- a/src/pages/analytics.py +++ b/src/pages/analytics.py @@ -19,8 +19,8 @@ def load_sample_data(): """Not shared with app.py: importing from app here would re-trigger Dash's - own page auto-discovery when the app is run as a script. See home.py for - the same function - duplicated rather than imported, on purpose.""" + own page auto-discovery when the app is run as a script. See data_table.py + for the same function - duplicated rather than imported, on purpose.""" return pd.read_csv(SAMPLE_DATA_PATH) diff --git a/src/pages/home.py b/src/pages/introduction.py similarity index 91% rename from src/pages/home.py rename to src/pages/introduction.py index 74d5133..b37d668 100644 --- a/src/pages/home.py +++ b/src/pages/introduction.py @@ -1,4 +1,4 @@ -"""Home page: revision log and a free-text notes box. +"""Introduction page: revision log and a free-text notes box. Landing page. The revision log records who issued, checked and approved each version of the app's content - add a row to issue a new revision @@ -29,7 +29,7 @@ def layout(): grid = dag.AgGrid( - id="home-revision-log-grid", + id="introduction-revision-log-grid", rowData=REVISION_LOG, # headerName explicitly, or AG Grid title-cases the field: "Comment" # would still be fine, but "Revision No." would render as "Revision No .". @@ -37,10 +37,7 @@ def layout(): # Comment absorbs that freed width (0.8 * 5 = 4, so Comment's flex of # 2 keeps the same total of 6 that six equal columns would have had). columnDefs=( - [ - {"field": col, "headerName": col, "flex": 0.8} - for col in list(REVISION_LOG[0])[:5] - ] + [{"field": col, "headerName": col, "flex": 0.8} for col in list(REVISION_LOG[0])[:5]] + [{"field": "Comment", "headerName": "Comment", "flex": 2}] ), defaultColDef={"filter": True, "sortable": True}, @@ -68,7 +65,7 @@ def layout(): [ html.Div("Project info", className="visual-title"), dcc.Textarea( - id="home-notes-textarea", + id="introduction-notes-textarea", value="Here you can write free text about the project, the dashboard or other relevant information.", className="notes-textarea", ), diff --git a/tests/test_app.py b/tests/test_app.py index a2999e6..9e9d505 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -63,7 +63,7 @@ def component_ids(node, found=None): # Same deal as EXPECTED_PAGES: add your new page's callback-bound component # ids here, or they simply aren't checked (not a failure, just a silent gap). CALLBACK_IDS = { - "/": {"home-revision-log-grid"}, + "/": {"introduction-revision-log-grid"}, "/data-table": {"data-table-sample-grid"}, "/analytics": {"analytics-category-filter", "analytics-chart"}, }