Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<chart-id>.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 |
Expand Down
30 changes: 15 additions & 15 deletions posts/spatial-access-to-public-libraries-toronto/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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")
);

</script>
Expand All @@ -33,7 +33,7 @@

<p>
People who live closer to a library tend to visit it more often, borrow more resources,
and access its social services more easily.<Footnote id={fBhatt} /><Footnote id={fPark} /> 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.<Footnote id={fUsage} /> We mapped how long it takes to reach the nearest public library from any location in Toronto, on foot and by public transit.
</p>
<p>
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.
Expand All @@ -47,7 +47,7 @@

<div class="text">
<p>
According to research conducted in other urban areas, accessibility to libraries varies across travel modes and population groups.<Footnote id={fAllen} /><Footnote id={fCheng} /><Footnote id={fDonnelly} /> 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.<Footnote id={fAccess} /> 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.
</p>
<p>
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 <a href="https://schoolofcities.github.io/libraries-by-transit/toronto-proximity" target="_blank" rel="noopener noreferrer">interactive tool</a>.
Expand Down
6 changes: 3 additions & 3 deletions posts/spatial-access-to-public-libraries-toronto/charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
Expand All @@ -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'
},
Expand All @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion scripts/export-png.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
6 changes: 6 additions & 0 deletions scripts/validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
127 changes: 114 additions & 13 deletions src/lib/ChartFrame.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -237,6 +241,23 @@
</div>
</div>

<!-- The long description. meta.alt on the graphic above is read out as one
unbroken string and is invisible to everyone else, so it stays short;
the detail lives here as ordinary text, outside the role="img" wrapper
and straight after it so the two are read together,
where a screen reader can step through it and any reader can open it.
Native <details>, so it works with scripting off. -->
{#if descriptionParagraphs.length > 0}
<details class="chart-description">
<summary>Description of this graphic</summary>
<div class="chart-description-text">
{#each descriptionParagraphs as paragraph}
<p>{paragraph}</p>
{/each}
</div>
</details>
{/if}

<div class="chart-footer">
<div class="chart-source-group">
<p class="chart-source"><strong>Data source:</strong> {meta.source}</p>
Expand Down Expand Up @@ -292,6 +313,7 @@
</div>
</div>
</div>

{/snippet}

{#snippet dataIcon()}
Expand Down Expand Up @@ -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 {
Expand All @@ -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. */
Expand Down
18 changes: 17 additions & 1 deletion src/lib/footnotes/Footnote.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@
>
<span>[{id[0]}]</span>
</a>{#if isHovered}<div class="footnoteTooltip" on:mouseenter={show} on:mouseleave={scheduleHide}>
<span class="footnoteNumber">[{id[0]}]</span>
<p>
[{id[0]}] {@html footnoteText}
{@html footnoteText}
</p>
</div>{/if}
</span>
Expand Down Expand Up @@ -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;
Expand All @@ -83,6 +89,7 @@
-ms-user-select: text;
}

.footnoteNumber,
.footnoteTooltip p {
padding: 0px;
margin: 0px;
Expand All @@ -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;
Expand Down
Loading