From c4ba403ff97a36ba8c4e53cafa0a78eec6d7f9b8 Mon Sep 17 00:00:00 2001 From: Jeff Allen Date: Fri, 25 Sep 2026 08:39:50 -0400 Subject: [PATCH] updating chartframe for mobile scrolling --- .../charts/travel-time-boxplot.svelte | 12 +- src/lib/ChartFrame.svelte | 156 +++++++++++++++--- 2 files changed, 138 insertions(+), 30 deletions(-) diff --git a/posts/spatial-access-to-public-libraries-toronto/charts/travel-time-boxplot.svelte b/posts/spatial-access-to-public-libraries-toronto/charts/travel-time-boxplot.svelte index 15cf5be..b2691e9 100644 --- a/posts/spatial-access-to-public-libraries-toronto/charts/travel-time-boxplot.svelte +++ b/posts/spatial-access-to-public-libraries-toronto/charts/travel-time-boxplot.svelte @@ -390,12 +390,12 @@ walking and transit share PX_PER_MIN, so a single shared scale per layout is what keeps their gridlines aligned when stacked — each panel picking its own independent scale (this component's first attempt at the stacked layout) - breaks that alignment. ChartFrame snaps .chart-frame to an exact 360px, 720px, - or 1080px via its own viewport media query (see widthSnapCss in - ChartFrame.svelte); this query then reads that resulting width as this - element's container content-box (.chart-frame sets container-type), which is - what actually distinguishes the three cases, not the browser viewport - directly. */ + breaks that alignment. ChartFrame draws the graphic at an exact 360px, 720px, + or 1080px (less the frame's inset) via its own viewport media query (see + widthSnapCss in ChartFrame.svelte); this query then reads that width as this + element's container content-box (.chart-body sets container-type), which is + what actually distinguishes the three cases, not the browser viewport or the + frame's own width directly. */ .panels-stacked, .panels-narrow { display: none; diff --git a/src/lib/ChartFrame.svelte b/src/lib/ChartFrame.svelte index 3818aca..fbe3e97 100644 --- a/src/lib/ChartFrame.svelte +++ b/src/lib/ChartFrame.svelte @@ -47,12 +47,20 @@ const downloadId = $derived(`download-${uid}`); const shareId = $derived(`share-${uid}`); + // Frame padding (2 x 20px) plus border (2 x 1px): what separates a snapped + // width, which is the frame's outer width, from the graphic's own width. + const FRAME_INSET = 42; + + // Sets --graphic-w, the exact width the graphic is drawn at, per viewport + // step. The frame itself stays fluid (see .chart-frame.snapped) so its text + // and buttons can reflow; only the graphic holds this width, and scrolls when + // the frame is narrower than it. function widthSnapCss(id, widths) { if (!widths || widths.length === 0) return ''; const [smallest, ...rest] = widths; - let css = `#chart-frame-${id} { width: ${smallest}px; }`; + let css = `#chart-frame-${id} { --graphic-w: ${smallest - FRAME_INSET}px; }`; for (const w of rest) { - css += `@media (min-width: ${w}px) { #chart-frame-${id} { width: ${w}px; } }`; + css += `@media (min-width: ${w}px) { #chart-frame-${id} { --graphic-w: ${w - FRAME_INSET}px; } }`; } return css; } @@ -127,6 +135,31 @@ if (!shareDialog.open) shareDialog.showModal(); } + // Sets --shade-start / --shade-end on the scroller's wrapper: 1 on a side that + // has more graphic to scroll to, 0 otherwise. The edge shadows in the styles + // read them. Done here rather than with scroll-driven CSS animations, which + // not every browser supports yet; with scripting off there is simply no + // shadow and the scrollbar remains. + function trackScroll(node) { + const wrap = node.parentElement; + const update = () => { + const max = node.scrollWidth - node.clientWidth; + wrap.style.setProperty('--shade-start', node.scrollLeft > 2 ? '1' : '0'); + wrap.style.setProperty('--shade-end', max > 1 && node.scrollLeft < max - 2 ? '1' : '0'); + }; + update(); + node.addEventListener('scroll', update, { passive: true }); + const observer = new ResizeObserver(update); + observer.observe(node); + if (node.firstElementChild) observer.observe(node.firstElementChild); + return { + destroy() { + node.removeEventListener('scroll', update); + observer.disconnect(); + } + }; + } + // Download dialog let downloadDialog = $state(); @@ -155,24 +188,22 @@ - -
-
- {@render frameContent()} -
+
+ {@render frameContent()}
{#if measureMounted} -
+
{@render frameContent()}
{/if} @@ -189,12 +220,21 @@

{meta.subtitle}

{/if} -