From 0c159186f9df367ba4f3062635ee00ef7566fb18 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Tue, 10 Nov 2026 13:29:25 -0800 Subject: [PATCH] fix(shipwrecks): small row and query fixes: era counts in one pass, pickers list only years with a wreck, no Portuguese default - The legend counted each era twice, scanning every record eight times per row refresh; it now counts once. - Each year picker listed every year from the oldest dated wreck (well over a thousand entries); it lists the years that have a wreck, plus the chosen ends. - Labels fell back to Portuguese and links to Portuguese Wikipedia while the app ships 11 locales: labels now fall back through the app's languages after English, and a wreck with no English article links its Wikidata item, which lists the article in every language. --- docs/CURRENT-STATE.md | 2 +- src/layers/shipwrecks/index.js | 34 +++++++++++++++++------- src/layers/shipwrecks/index.test.mjs | 36 ++++++++++++++++++++++++++ src/layers/shipwrecks/records.js | 11 ++++---- src/layers/shipwrecks/records.test.mjs | 5 ++++ 5 files changed, 71 insertions(+), 17 deletions(-) diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 6e3797168..996af1fcb 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -178,7 +178,7 @@ cull GhostMaps uses, sized by deaths (a bigger toll draws on top) and coloured by era (a legend under the row): before 1900 (ancient wrecks dated only by period count here), 1900–1945, after 1945, and date unknown. A period filter under the row picks a start year and an end year -side by side (two pickers over the years in the data; a reversed pair is +side by side (two pickers over the years that have a wreck; a reversed pair is stored swapped); undated wrecks show only when every year is selected. The filter is session-only (not in the share link). Clicking a marker opens a `stateless` card: deaths and survivors first, then the sinking date or year (or the archaeological dating) and diff --git a/src/layers/shipwrecks/index.js b/src/layers/shipwrecks/index.js index 285881743..f2451d7bd 100644 --- a/src/layers/shipwrecks/index.js +++ b/src/layers/shipwrecks/index.js @@ -277,11 +277,20 @@ export function createShipwrecksLayer({ picking?.unregisterPickOwner?.(SHIPWRECKS_LAYER_ID); } - function countOf(era) { - let count = 0; + /** Wrecks shown per era, in one pass over the records. */ + function eraCounts() { + const counts = Object.fromEntries(SHIPWRECK_ERAS.map((era) => [era, 0])); for (const record of _records.values()) - if (eraOf(record) === era && inPeriod(record, _range)) count++; - return count; + if (inPeriod(record, _range)) counts[eraOf(record)]++; + return counts; + } + + /** The years that have a dated wreck, plus `extra` ones, newest first. */ + function yearsInData(extra = []) { + const years = new Set(extra); + for (const record of _records.values()) + if (Number.isInteger(record.year)) years.add(record.year); + return [...years].sort((a, b) => b - a); } /** The start and end year pickers, side by side. */ @@ -293,9 +302,13 @@ export function createShipwrecksLayer({ : range.from === range.to ? String(range.from) : `${range.from}–${range.to}`; - const options = []; - for (let year = _extent.max; year >= _extent.min; year--) - options.push({ value: year, label: String(year) }); + // Only years with a wreck: a picker of every year since the oldest + // dated wreck would run to well over a thousand entries. + // The chosen ends stay listed even when no wreck sank that year. + const options = yearsInData([range.from, range.to]).map((year) => ({ + value: year, + label: String(year), + })); return { range: { id: 'period', @@ -425,16 +438,17 @@ export function createShipwrecksLayer({ }, getRowControls() { + const counts = eraCounts(); return { chips: [], ...periodControls(), // The colours are a legend: when the ship sank. legend: SHIPWRECK_ERAS.map((era) => ({ label: t(ERA_LABELS[era]), - count: countOf(era), + count: counts[era], color: ERA_COLORS[era], - blurb: tn(countOf(era), '{count} wreck', '{count} wrecks', { - count: formatNumber(countOf(era)), + blurb: tn(counts[era], '{count} wreck', '{count} wrecks', { + count: formatNumber(counts[era]), }), })), info: t('EMODnet (European waters) · Wikidata (worldwide)'), diff --git a/src/layers/shipwrecks/index.test.mjs b/src/layers/shipwrecks/index.test.mjs index 2e42dfd7f..d42aedaf1 100644 --- a/src/layers/shipwrecks/index.test.mjs +++ b/src/layers/shipwrecks/index.test.mjs @@ -208,3 +208,39 @@ test('a click opens the card; a period that excludes the wreck closes it', async ); assert.equal(h.layer.getStats().count, 1); }); + +test('the year pickers list only years with a wreck, and each era is counted', async () => { + const h = harness({ + snapshots: [ + snapshot([ + wreck('a', { year: 300 }), + wreck('b', { year: 1912 }), + wreck('c', { year: 1912 }), + wreck('d', { year: 1990 }), + wreck('e', { year: null }), + ]), + ], + }); + h.layer.enable(); + await h.layer.update(); + let controls = h.layer.getRowControls(); + assert.deepEqual( + controls.range.options.map((option) => option.value), + [1990, 1912, 300], + ); + assert.deepEqual( + controls.legend.map((era) => era.count), + [1, 2, 1, 1], + ); + h.layer.setParams({ from: 1900, to: 1950 }); + controls = h.layer.getRowControls(); + assert.deepEqual( + controls.range.options.map((option) => option.value), + [1990, 1950, 1912, 1900, 300], + 'the chosen ends are listed too', + ); + assert.deepEqual( + controls.legend.map((era) => era.count), + [0, 2, 0, 0], + ); +}); diff --git a/src/layers/shipwrecks/records.js b/src/layers/shipwrecks/records.js index e52e67702..8d362e7a1 100644 --- a/src/layers/shipwrecks/records.js +++ b/src/layers/shipwrecks/records.js @@ -20,7 +20,7 @@ export const SHIPWRECK_SOURCES = Object.freeze(['emodnet', 'wikidata']); * come with their precision so a month-precise date is not shown as the 1st. * Only ship and event facts are read: no items about people. */ -export const WIKIDATA_QUERY = `SELECT ?item ?itemLabel ?itemDescription ?coord ?sunk ?sunkPrecision ?end ?endPrecision ?deaths ?survivors ?typeLabel ?enwiki ?ptwiki ?event WHERE { +export const WIKIDATA_QUERY = `SELECT ?item ?itemLabel ?itemDescription ?coord ?sunk ?sunkPrecision ?end ?endPrecision ?deaths ?survivors ?typeLabel ?enwiki ?event WHERE { { ?item wdt:P31/wdt:P279* wd:Q852190 ; wdt:P625 ?coord ; wikibase:sitelinks ?links . FILTER(?links > 0) @@ -35,8 +35,7 @@ export const WIKIDATA_QUERY = `SELECT ?item ?itemLabel ?itemDescription ?coord ? } OPTIONAL { ?item wdt:P1120 ?deaths } OPTIONAL { ?enwiki schema:about ?item ; schema:isPartOf } - OPTIONAL { ?ptwiki schema:about ?item ; schema:isPartOf } - SERVICE wikibase:label { bd:serviceParam wikibase:language "en,pt,mul". } + SERVICE wikibase:label { bd:serviceParam wikibase:language "en,mul,fr,de,es,it,pt,ru,uk,ja,zh,ar". } }`; const MAX_TEXT = 160; @@ -300,9 +299,9 @@ export function normalizeWikidataResults(payload) { country: null, originator: null, url: - wikiUrl(value('enwiki')) || - wikiUrl(value('ptwiki')) || - `https://www.wikidata.org/wiki/${qid}`, + // English Wikipedia, else the Wikidata item, which links the + // article in every language that has one. + wikiUrl(value('enwiki')) || `https://www.wikidata.org/wiki/${qid}`, }); } return foldSinkingEvents([...byItem.values()]); diff --git a/src/layers/shipwrecks/records.test.mjs b/src/layers/shipwrecks/records.test.mjs index 2033e391f..4ce5514e2 100644 --- a/src/layers/shipwrecks/records.test.mjs +++ b/src/layers/shipwrecks/records.test.mjs @@ -307,3 +307,8 @@ test('the query reads the end of service with its precision', () => { ); assert.doesNotMatch(WIKIDATA_QUERY, /wdt:P576/); }); + +test('labels and links do not default to one other language', () => { + assert.doesNotMatch(WIKIDATA_QUERY, /ptwiki|pt\.wikipedia/); + assert.match(WIKIDATA_QUERY, /wikibase:language "en,mul,/); +});