diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4e733d..9e1cedc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ A post folder contains: | --- | --- | --- | | `meta.json` | Required | Title, type, summary, authors, published date, license, `protected` flag, related documents | | `+page.svelte` | Required | Body content | -| `charts.js` | Required | Chart metadata (ids, titles, sources, alt text, notes) | +| `charts.js` | Required | Chart metadata (ids, titles, sources, alt text, long descriptions, notes) | | `charts/` | Required | One `.svelte` per chart listed in `charts.js` | | `data/` | Optional | CSVs used by charts or offered for download | | `images/` | Optional | Static images, e.g. pre-rendered map PNGs | diff --git a/posts/spatial-access-to-public-libraries-toronto/+page.svelte b/posts/spatial-access-to-public-libraries-toronto/+page.svelte index 854a1dc..c8f5b13 100644 --- a/posts/spatial-access-to-public-libraries-toronto/+page.svelte +++ b/posts/spatial-access-to-public-libraries-toronto/+page.svelte @@ -11,20 +11,20 @@ const footnoteStore = createFootnoteStore(); - const fBhatt = footnoteStore.addFootnote( - "Bhatt, R. (2010). The impact of public library use on reading, television, and academic outcomes. *Journal of Urban Economics*, 68(2), 148–166. [DOI](https://doi.org/10.1016/j.jue.2010.03.008)." + // Adjacent citations share one footnote; a blank line between references + // stacks them in both the tooltip and the reference list. + const fUsage = footnoteStore.addFootnote( + [ + "Bhatt, R. (2010). The impact of public library use on reading, television, and academic outcomes. *Journal of Urban Economics*, 68(2), 148–166. [DOI](https://doi.org/10.1016/j.jue.2010.03.008).", + "Park, S. J. (2012). Measuring public library accessibility: A case study using GIS. *Library & Information Science Research*, 34(1), 13–21. [DOI](https://doi.org/10.1016/j.lisr.2011.07.007)." + ].join("\n\n") ); - const fPark = footnoteStore.addFootnote( - "Park, S. J. (2012). Measuring public library accessibility: A case study using GIS. *Library & Information Science Research*, 34(1), 13–21. [DOI](https://doi.org/10.1016/j.lisr.2011.07.007)." - ); - const fAllen = footnoteStore.addFootnote( - "Allen, J. (2019). Mapping differences in access to public libraries by travel mode and time of day. *Library & Information Science Research*, 41(1), 11–18. [DOI](https://doi.org/10.1016/j.lisr.2019.02.001)." - ); - const fCheng = footnoteStore.addFootnote( - "Cheng, W., Wu, J., Moen, W., & Hong, L. (2021). Assessing the spatial accessibility and spatial equity of public libraries' physical locations. *Library & Information Science Research*, 43(2), 101089. [DOI](https://doi.org/10.1016/j.lisr.2021.101089)." - ); - const fDonnelly = footnoteStore.addFootnote( - "Donnelly, F. P. (2014). The geographic distribution of United States public libraries: An analysis of locations and service areas. *Journal of Librarianship and Information Science*, 46(2), 110–129. [DOI](https://doi.org/10.1177/0961000612470276)." + const fAccess = footnoteStore.addFootnote( + [ + "Allen, J. (2019). Mapping differences in access to public libraries by travel mode and time of day. *Library & Information Science Research*, 41(1), 11–18. [DOI](https://doi.org/10.1016/j.lisr.2019.02.001).", + "Cheng, W., Wu, J., Moen, W., & Hong, L. (2021). Assessing the spatial accessibility and spatial equity of public libraries' physical locations. *Library & Information Science Research*, 43(2), 101089. [DOI](https://doi.org/10.1016/j.lisr.2021.101089).", + "Donnelly, F. P. (2014). The geographic distribution of United States public libraries: An analysis of locations and service areas. *Journal of Librarianship and Information Science*, 46(2), 110–129. [DOI](https://doi.org/10.1177/0961000612470276)." + ].join("\n\n") ); @@ -33,7 +33,7 @@

People who live closer to a library tend to visit it more often, borrow more resources, - and access its social services more easily. We mapped how long it takes to reach the nearest public library from any location in Toronto, on foot and by public transit. + and access its social services more easily. We mapped how long it takes to reach the nearest public library from any location in Toronto, on foot and by public transit.

On foot, 35% of Torontonians can reach the nearest library within 15 minutes and 79% within 30 minutes; by transit, those shares rise to about 40% and 95%. The areas least connected to libraries are inner-suburban neighbourhoods, which have fewer libraries and lower population density. @@ -47,7 +47,7 @@

- According to research conducted in other urban areas, accessibility to libraries varies across travel modes and population groups. With public libraries offering programming for such target populations as recent immigrants, youth, and seniors, it is crucial to account for differences in library access across demographic groups. Using census data, we mapped where these groups are concentrated across Toronto in relation to the locations of library branches. + According to research conducted in other urban areas, accessibility to libraries varies across travel modes and population groups. With public libraries offering programming for such target populations as recent immigrants, youth, and seniors, it is crucial to account for differences in library access across demographic groups. Using census data, we mapped where these groups are concentrated across Toronto in relation to the locations of library branches.

If you want to explore demographic data against the transit travel data, including at a more zoomed-in neighbourhood level, you can refer to our interactive tool. diff --git a/posts/spatial-access-to-public-libraries-toronto/charts.js b/posts/spatial-access-to-public-libraries-toronto/charts.js index 4a31f62..9f57cce 100644 --- a/posts/spatial-access-to-public-libraries-toronto/charts.js +++ b/posts/spatial-access-to-public-libraries-toronto/charts.js @@ -19,7 +19,7 @@ const charts = { source: 'Toronto Public Library, City of Toronto, Toronto Transit Commission, OpenStreetMap (2026)', alt: - "Two stacked maps of Toronto covering the same area, the upper one for walking and the lower one for public transit. Both shade a fine hexagon grid into three travel-time bands: dark blue under 15 minutes, mid blue 15 to 30 minutes, and pale blue 30 minutes or more. Yellow dots mark the 101 library locations, thin red lines the major transit routes, and dark outlines the 25 municipal wards, each labelled by number; Etobicoke, North York, East York, Scarborough and Downtown are named. On the walking map dark blue forms a small cluster around each branch, separated by broad pale-blue areas that are largest in western and southern Etobicoke, north-central North York, and northern and eastern Scarborough. On the transit map almost the whole city is mid or dark blue, with pale blue reduced to scattered pockets around the city's outer edges.", + "Two maps of Toronto showing travel time to the nearest public library, one for walking and one for public transit. On foot, short travel times form small clusters around each branch, with long times across much of the inner suburbs; by transit, almost the whole city is within 30 minutes.", widths: [1080], graphicVerb: 'created' }, @@ -29,7 +29,7 @@ const charts = { "Share of each census tract's population, with public library branches marked on every map", source: 'Census of Population, Statistics Canada (2021); Toronto Public Library', alt: - "Six small maps of Toronto, each shading the city's census tracts into four classes by one population measure, with its own colour ramp and class breaks: visible minority in purple (breaks at 25, 50 and 75 per cent), low income in blue (30, 35, 40), recent immigrants in red (3, 6, 9), first-generation immigrants in teal (35, 50, 65), seniors aged 65 and over in navy (15, 17.5, 20), and children aged 0 to 14 in green (12, 14, 16). Yellow dots mark the public library branches on every panel. Every group is least present in the central corridor running from downtown out to the east end, but concentrations beyond it differ by group: the northwest is highest for low income and recent immigrants, Scarborough for visible minority and first-generation immigrants, and western Etobicoke for seniors.", + "Six small maps of Toronto, each showing one population group's share of every census tract, with library branches marked: visible minority, low income, recent immigrants, first-generation immigrants, seniors, and children. Where each group is concentrated differs, but all are least present in the central corridor from downtown to the east end.", widths: [360, 720, 1080], graphicVerb: 'created' }, @@ -38,7 +38,7 @@ const charts = { subtitle: "Every group's median is at or just slightly above the citywide median", source: 'Census of Population, Statistics Canada (2021); Toronto Public Library, City of Toronto, Toronto Transit Commission, OpenStreetMap (2026)', alt: - "Two panels of horizontal box-and-whisker rows comparing travel time to the nearest library, one panel for walking on an axis running from 0 to about 40 minutes and one for public transit on a 0 to 30 minute axis. Each panel has seven rows: the total population in grey, then visible minority, low income, recent immigrants, first-generation immigrants, seniors aged 65 and over, and children aged 0 to 14, each in that group's own colour from the maps above. Every row shows the 10th and 90th percentiles as whisker ends, the 25th to 75th percentile as a box, and the median as a line inside the box. Within each panel the rows are near-identical: walking medians sit between 19 and 21 minutes and transit medians between 17 and 18, with every group's median within about a minute of the total population's. The walking distributions are much wider than the transit ones, spanning roughly 10 to 38 minutes against 10 to 27.", + "Box plots of travel time to the nearest library for the total population and six demographic groups, in one panel for walking and one for public transit. Within each panel the rows are nearly identical, with every group's median within about a minute of the total population's.", data: 'travel_time_percentiles.csv', widths: [360, 720], graphicVerb: 'created' diff --git a/scripts/export-png.js b/scripts/export-png.js index 4e81ebb..ba10945 100644 --- a/scripts/export-png.js +++ b/scripts/export-png.js @@ -182,7 +182,7 @@ async function main() { // The figure route renders Read more/Download/Share buttons for the // live embed page, but a flat PNG export has nothing for them to do. - await page.addStyleTag({ content: '.chart-buttons { display: none !important; }' }); + await page.addStyleTag({ content: '.chart-buttons, .chart-description { display: none !important; }' }); await page.locator('.chart-frame').screenshot({ path: outPath }); await context.close(); diff --git a/scripts/validate.js b/scripts/validate.js index bc6cc42..f81798c 100644 --- a/scripts/validate.js +++ b/scripts/validate.js @@ -141,6 +141,12 @@ function checkCharts(slug, charts) { if (!chart[field]) error(slug, `chart "${key}" is missing "${field}"`); } + // Optional, but without it the short alt is all a screen-reader user + // gets: ChartFrame hides everything inside the graphic from them. + if ([chart.description ?? []].flat().filter(Boolean).length === 0) { + warn(slug, `chart "${key}" has no "description" — the long description shown under the graphic`); + } + for (const file of [chart.data ?? []].flat().filter(Boolean)) { referencedData.add(file); if (!dataFiles.includes(file)) { diff --git a/src/lib/ChartFrame.svelte b/src/lib/ChartFrame.svelte index fbe3e97..5a6b2dd 100644 --- a/src/lib/ChartFrame.svelte +++ b/src/lib/ChartFrame.svelte @@ -18,6 +18,10 @@ // list, for posts where not every listed author worked on every chart. const chartAuthors = $derived(meta.authors ?? authors); + // meta.description (optional, set per-chart in charts.js) is the long + // description: one string, or an array of them for several paragraphs. + const descriptionParagraphs = $derived([meta.description ?? []].flat()); + // Derived from the current route's slug so a folder rename (which changes the // slug) updates these links everywhere automatically, with nothing hardcoded. @@ -237,6 +241,23 @@

+ + {#if descriptionParagraphs.length > 0} +
+ Description of this graphic +
+ {#each descriptionParagraphs as paragraph} +

{paragraph}

+ {/each} +
+
+ {/if} + + {/snippet} {#snippet dataIcon()} @@ -613,39 +635,71 @@ /* 678px, not 720px: container queries measure .chart-frame's content-box, which is its 720px outer width (the smallest core width above 540 — see - scripts/validate.js) minus its own 40px padding + 2px border. Using the - adjusted value means every chart at 720px or wider gets the same - source-text-left / buttons-right split, not just the 1080px+ ones. */ + scripts/validate.js) minus its own 40px padding + 2px border. Source text + on the left, buttons on the right — but at this width there isn't room + for both a readable text column and all four controls in one row, so the + buttons go back to the narrow view's 2-column grid (license + Read more + over Download + Share), sized to its content, and the text takes + everything that's left. */ @container (min-width: 678px) { .chart-footer { flex-direction: row; align-items: center; justify-content: space-between; - gap: 10px; + gap: 24px; } .chart-source-group { - flex: 1 1 360px; - max-width: 360px; + flex: 1 1 auto; + width: auto; min-width: 0; } .chart-buttons { - display: flex; - flex: 0 0 360px; - width: 360px; - justify-content: flex-end; + display: grid; + flex: 0 0 auto; + width: auto; + } + + .chart-buttons button, + .chart-buttons a, + .chart-license-inline { + white-space: nowrap; + } + + /* Alone on its row (no Read more), the license sits at the frame's + right edge, over the buttons. */ + .chart-license-inline { + text-align: right; + } + + /* With Read more, the four controls read in the same order as the + single row does at wider sizes: license, Read more / Download, Share. */ + .chart-buttons:has(.chart-read-more) .chart-license-inline { + grid-column: 1; + align-self: center; + text-align: center; + } + + .chart-read-more { + grid-column: 2; } } /* 1038px, not 1080px: same content-box adjustment as above, for the next - core width up. The text column gets more room to breathe once there's - this much extra space next to the fixed 360px button column. */ + core width up. Enough room now for all the controls in a single row + beside a capped text column. */ @container (min-width: 1038px) { .chart-source-group { - flex-basis: 540px; + flex: 1 1 540px; max-width: 540px; } + + .chart-buttons { + display: flex; + flex: 0 0 360px; + width: 360px; + } } .chart-source-group { @@ -665,6 +719,53 @@ padding: 0; } + .chart-description { + margin-bottom: 12px; + padding: 8px 12px; + /* Half a step lighter than --brandGray05, the lightest grey token. */ + background-color: #f9f9f9; + /* Hugs its contents: just the summary's words when closed, the text + column (capped at 680px below) when open. */ + box-sizing: border-box; + width: fit-content; + max-width: 100%; + } + + .chart-description summary { + font-family: OpenSansItalic; + font-weight: normal; + font-size: 12px; + line-height: 16px; + color: var(--brandGray55); + cursor: pointer; + /* Shrink-wrapped, so only the words are clickable, not the whole row. */ + width: fit-content; + text-decoration: underline; + } + + .chart-description summary:hover { + color: var(--brandGray90); + } + + .chart-description p { + font-family: OpenSans; + font-weight: normal; + font-size: 13px; + line-height: 19px; + color: var(--brandGray70); + max-width: 680px; + margin: 0; + padding: 0; + } + + .chart-description-text { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 8px; + padding-bottom: 4px; + } + /* 318px, not 360px: container queries measure .chart-frame's content-box, which is its 360px outer width (the smallest snap width configured for this chart) minus its own 40px padding + 2px border. */ diff --git a/src/lib/footnotes/Footnote.svelte b/src/lib/footnotes/Footnote.svelte index f360006..223e4cc 100644 --- a/src/lib/footnotes/Footnote.svelte +++ b/src/lib/footnotes/Footnote.svelte @@ -34,8 +34,9 @@ > [{id[0]}] {#if isHovered}
+ [{id[0]}]

- [{id[0]}] {@html footnoteText} + {@html footnoteText}

{/if} @@ -75,6 +76,11 @@ width: 340px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.38); border: 1px solid var(--brandGray10); + /* Hanging number: the marker sits in its own column so stacked + references all align to the same left edge. */ + display: flex; + align-items: flex-start; + gap: 6px; z-index: 1000; pointer-events: auto; user-select: text; @@ -83,6 +89,7 @@ -ms-user-select: text; } + .footnoteNumber, .footnoteTooltip p { padding: 0px; margin: 0px; @@ -91,6 +98,15 @@ font-weight: normal; font-size: 15px; line-height: 20px; + } + + .footnoteNumber { + flex: none; + } + + .footnoteTooltip p { + flex: 1; + min-width: 0; white-space: normal; word-wrap: break-word; overflow-wrap: break-word;