From 6bfad5082b85ecab30135e8ffee5fb605bec0ef2 Mon Sep 17 00:00:00 2001 From: Akib Date: Sun, 20 Sep 2026 15:56:09 +0000 Subject: [PATCH] fix(demo): pack the dashboard board, which was mostly empty space MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two separate kinds of gap, both measured rather than eyeballed. Between tiles. A tile spanning two columns cannot start until both are free, so one arriving while the columns are uneven strands the shorter column. The board opened with a 296px void under the second KPI for exactly this reason. Worse, it had eleven wide tiles to nine narrow ones, so the third column could never keep up and finished 900px short of its neighbours. Inside tiles. `.widget ul` is `flex: 1`, so a list stretches to whatever height the author picked. Traffic sources reserved 296px for 168px of rows and left the rest blank. List heights now come from the row count — 48 + 28n, measured against the rendered tile rather than guessed. The fix is eight wide tiles to twelve narrow, ordered so the wide ones land while the columns are level. Found by writing a simulator of the library's own shortest-column placement and searching orderings against it, then confirming the result in a browser — the simulator and the rendered board agree on height to the pixel. 3 columns height 2416 -> 1688, interior holes 676px -> 4px 2 columns height 3580 -> 2552, trailing gap 20px 4 columns height 2000 -> 1264, every column finishes level 185 tests pass, no console errors, no failed requests. --- .../demo/src/app/examples/dashboard-data.ts | 98 ++++++++++++------- 1 file changed, 62 insertions(+), 36 deletions(-) diff --git a/projects/demo/src/app/examples/dashboard-data.ts b/projects/demo/src/app/examples/dashboard-data.ts index 4ab63c8..8d3e71c 100644 --- a/projects/demo/src/app/examples/dashboard-data.ts +++ b/projects/demo/src/app/examples/dashboard-data.ts @@ -32,37 +32,66 @@ function bars( return { id, title, span, height, kind: 'bars', hue, series }; } +/** + * A list tile's height follows from its rows rather than being chosen. + * + * `.widget ul` uses `flex: 1`, so a list stretches to whatever height the + * author set — pick a number too large and the rows bunch at the top with dead + * space under them, which is what used to happen here. Measured against the + * rendered tile: 14px padding top and bottom, a 20px header, an 8px gap below + * it, and rows on a 28px pitch whose last gap is not drawn. + */ +function listHeight(rows: number): number { + return 48 + 28 * rows; +} + function list( id: number, title: string, hue: number, span: number, - height: number, rows: readonly (readonly [string, string])[], ): Widget { - return { id, title, span, height, kind: 'list', hue, rows }; + return { id, title, span, height: listHeight(rows.length), kind: 'list', hue, rows }; } /** - * Twenty tiles, every one a fixed size. Deliberately more than fits on a screen, - * so the deferred ones below the fold stay unrendered until you scroll. + * Twenty tiles, every one a fixed size. Deliberately more than fits on a + * screen, so the deferred ones below the fold stay unrendered until you scroll. + * + * Both the order and the mix of widths are deliberate. A tile spanning two + * columns cannot start until both are free, so one arriving while the columns + * are uneven leaves a hole under the shorter one — this board used to open with + * a 296px void under the second KPI. And with more wide tiles than narrow ones, + * the third column can never keep up, so it finished hundreds of pixels short + * of its neighbours. + * + * Eight wide tiles to twelve narrow ones, emitted so the wide ones land while + * the columns are level, gives a board with no interior holes and a bottom edge + * that finishes together. Checked by simulating the packing at one to five + * columns, then measuring the rendered board in a browser. */ export const WIDGETS: readonly Widget[] = [ + // The headline numbers first, as a dashboard would open. kpi(1, 'Revenue', 262, '$48.2k', 12.4), - bars(2, 'Sessions', 199, 2, 248, [42, 58, 51, 74, 66, 88, 79, 95, 71, 84, 92, 68]), - kpi(3, 'Conversion', 152, '3.84%', 0.6), - list(4, 'Traffic sources', 24, 1, 296, [ - ['Organic search', '41%'], - ['Direct', '23%'], - ['Referral', '16%'], - ['Social', '11%'], - ['Email', '6%'], - ['Paid', '3%'], + kpi(2, 'Conversion', 152, '3.84%', 0.6), + kpi(3, 'Error rate', 8, '0.42%', -1.8), + kpi(4, 'Churn', 38, '1.9%', -0.4), + + bars(5, 'Sessions', 199, 2, 248, [42, 58, 51, 74, 66, 88, 79, 95, 71, 84, 92, 68]), + kpi(6, 'Open alerts', 348, '3', -2), + list(7, 'Recent deploys', 250, 1, [ + ['api · v2.14.0', '12m ago'], + ['web · v3.2.1', '48m ago'], + ['worker · v1.8.7', '3h ago'], + ['api · v2.13.9', '9h ago'], ]), - bars(5, 'Latency p95', 291, 2, 176, [120, 138, 129, 154, 141, 133, 148, 162, 151, 139]), - kpi(6, 'Error rate', 8, '0.42%', -1.8), - bars(7, 'Deploys this week', 176, 3, 152, [3, 5, 2, 8, 6, 1, 4]), - list(8, 'Top pages', 46, 2, 312, [ + bars(8, 'Cache hit rate', 142, 1, 184, [88, 91, 86, 94, 92, 89, 95]), + kpi(9, 'Active users', 208, '18.6k', 4.2), + bars(10, 'Signups', 168, 2, 200, [14, 22, 18, 31, 27, 35, 29, 41, 38, 46]), + kpi(11, 'Support backlog', 300, '27', 8.5), + kpi(12, 'Uptime', 118, '99.98%', 0.01), + list(13, 'Top pages', 46, 2, [ ['/pricing', '12.4k'], ['/docs/quick-start', '9.8k'], ['/blog/masonry', '7.1k'], @@ -71,31 +100,28 @@ export const WIDGETS: readonly Widget[] = [ ['/support', '2.2k'], ['/about', '1.7k'], ]), - kpi(9, 'Uptime', 118, '99.98%', 0.01), - bars(10, 'Queue depth', 218, 1, 208, [12, 19, 8, 26, 31, 17, 22, 14]), - list(11, 'Storage', 330, 2, 192, [ - ['Postgres', '412 GB'], - ['Object store', '2.8 TB'], - ['Backups', '1.1 TB'], + bars(14, 'Latency p95', 291, 2, 176, [120, 138, 129, 154, 141, 133, 148, 162, 151, 139]), + bars(15, 'Build minutes', 84, 2, 224, [180, 156, 204, 168, 232, 196, 148, 176, 212]), + bars(16, 'Queue depth', 218, 1, 208, [12, 19, 8, 26, 31, 17, 22, 14]), + list(17, 'Traffic sources', 24, 1, [ + ['Organic search', '41%'], + ['Direct', '23%'], + ['Referral', '16%'], + ['Social', '11%'], + ['Email', '6%'], + ['Paid', '3%'], ]), - kpi(12, 'Open alerts', 348, '3', -2), - bars(13, 'Build minutes', 84, 2, 224, [180, 156, 204, 168, 232, 196, 148, 176, 212]), - kpi(14, 'Active users', 208, '18.6k', 4.2), - list(15, 'Slowest endpoints', 12, 2, 264, [ + list(18, 'Slowest endpoints', 12, 2, [ ['POST /import', '1.9s'], ['GET /reports', '1.2s'], ['GET /search', '840ms'], ['POST /export', '760ms'], ['GET /feed', '410ms'], ]), - kpi(16, 'Churn', 38, '1.9%', -0.4), - bars(17, 'Cache hit rate', 142, 1, 184, [88, 91, 86, 94, 92, 89, 95]), - list(18, 'Recent deploys', 250, 3, 208, [ - ['api · v2.14.0', '12m ago'], - ['web · v3.2.1', '48m ago'], - ['worker · v1.8.7', '3h ago'], - ['api · v2.13.9', '9h ago'], + bars(19, 'Deploys this week', 176, 1, 152, [3, 5, 2, 8, 6, 1, 4]), + list(20, 'Storage', 330, 1, [ + ['Postgres', '412 GB'], + ['Object store', '2.8 TB'], + ['Backups', '1.1 TB'], ]), - kpi(19, 'Support backlog', 300, '27', 8.5), - bars(20, 'Signups', 168, 2, 200, [14, 22, 18, 31, 27, 35, 29, 41, 38, 46]), ];