diff --git a/benchmarks/conformance/shared/fixtures/geography.test.ts b/benchmarks/conformance/shared/fixtures/geography.test.ts index 6e27fff1..868d382f 100644 --- a/benchmarks/conformance/shared/fixtures/geography.test.ts +++ b/benchmarks/conformance/shared/fixtures/geography.test.ts @@ -1,6 +1,7 @@ import { beagle } from '@tanstack/charts-data/beagle' import { learningPoverty } from '@tanstack/charts-data/learning-poverty' import { usCountyUnemployment } from '@tanstack/charts-data/us-county-unemployment' +import { geoContains } from 'd3-geo' import { describe, expect, it } from 'vitest' import { beagleRoute } from '../../cases/105-route-map/transform' import { @@ -18,15 +19,28 @@ import { } from '../transforms/learning-poverty' describe('geography demo data', () => { - it('converts the published world atlases without replacing their geometry', () => { - expect(worldCountries).toHaveLength(177) + it('converts the published world atlases without New Zealand', () => { + expect(worldCountries).toHaveLength(176) expect(worldCountries.every(({ id }) => typeof id === 'string')).toBe(true) + expect( + worldCountries.some( + ({ properties }) => properties.name === 'New Zealand', + ), + ).toBe(false) expect(worldLand.geometry.coordinates).not.toHaveLength(0) expect(detailedWorldLand.geometry.coordinates).not.toHaveLength(0) + expect(geoContains(worldLand, [174.78, -41.29])).toBe(false) + expect(geoContains(detailedWorldLand, [174.78, -41.29])).toBe(false) + expect(geoContains(worldLand, [151.21, -33.87])).toBe(true) }) it('joins learning-poverty rows without changing published values', () => { - expect(learningPovertyCountries).toHaveLength(95) + expect(learningPovertyCountries).toHaveLength(94) + expect( + learningPovertyCountries.some( + ({ properties }) => properties.name === 'New Zealand', + ), + ).toBe(false) const sourceByName = new Map( learningPoverty.map((row) => [row['Country Name'], row]), diff --git a/packages/charts-demo-data/src/country-atlas.ts b/packages/charts-demo-data/src/country-atlas.ts index e11ea9af..b91dd395 100644 --- a/packages/charts-demo-data/src/country-atlas.ts +++ b/packages/charts-demo-data/src/country-atlas.ts @@ -1,7 +1,13 @@ import countriesAtlasJson from 'world-atlas/countries-110m.json' +import detailedCountriesAtlasJson from 'world-atlas/countries-50m.json' import landAtlasJson from 'world-atlas/land-110m.json' import detailedLandAtlasJson from 'world-atlas/land-50m.json' -import { geoGraticule, geoGraticule10 } from 'd3-geo' +import { + geoCentroid, + geoContains, + geoGraticule, + geoGraticule10, +} from 'd3-geo' import { feature } from 'topojson-client' import { simplifyPolygonGeometry } from './simplify-geo' import type { @@ -13,6 +19,8 @@ import type { type AtlasTopology = Parameters[0] +const excludedCountryName = 'New Zealand' + export type CountryGeometry = Extract< GeoGeometryObjects, { type: 'Polygon' | 'MultiPolygon' } @@ -53,6 +61,8 @@ export const worldCountries: readonly CountryFeature[] = return [] } + if (entry.properties.name === excludedCountryName) return [] + return [ { type: 'Feature', @@ -65,9 +75,9 @@ export const worldCountries: readonly CountryFeature[] = ] }) -if (worldCountries.length !== 177) { +if (worldCountries.length !== 176) { throw new TypeError( - `Expected 177 world-atlas countries, got ${worldCountries.length}`, + `Expected 176 world-atlas countries after excluding ${excludedCountryName}, got ${worldCountries.length}`, ) } @@ -77,14 +87,19 @@ export const worldCountryCollection: ExtendedFeatureCollection = features: [...worldCountries], } -export const worldLand = convertLand(landAtlasJson, 'world-atlas land-110m') +export const worldLand = convertLandWithoutExcludedCountry( + landAtlasJson, + countriesAtlasJson, + 'world-atlas 110m', +) export const previewWorldLand: LandFeature = { ...worldLand, geometry: simplifyPolygonGeometry(worldLand.geometry, 2), } -export const detailedWorldLand = convertLand( +export const detailedWorldLand = convertLandWithoutExcludedCountry( detailedLandAtlasJson, - 'world-atlas land-50m', + detailedCountriesAtlasJson, + 'world-atlas 50m', ) function atlasTopology(value: unknown, label: string): AtlasTopology { @@ -94,23 +109,62 @@ function atlasTopology(value: unknown, label: string): AtlasTopology { return value } -function convertLand(value: unknown, label: string): LandFeature { - const topology = atlasTopology(value, label) - const landObject = topology.objects.land +function convertLandWithoutExcludedCountry( + landValue: unknown, + countriesValue: unknown, + label: string, +): LandFeature { + const countriesTopology = atlasTopology(countriesValue, `${label} countries`) + const countriesObject = countriesTopology.objects.countries + if (!countriesObject) { + throw new TypeError(`${label} is missing countries`) + } + + const convertedCountries = feature(countriesTopology, countriesObject) + if (convertedCountries.type !== 'FeatureCollection') { + throw new TypeError(`${label} countries did not produce a collection`) + } + const excludedCountry = convertedCountries.features.find( + (country) => + isRecord(country.properties) && + country.properties.name === excludedCountryName, + ) + if (!excludedCountry || !isCountryGeometry(excludedCountry.geometry)) { + throw new TypeError(`${label} did not contain ${excludedCountryName}`) + } + + const landTopology = atlasTopology(landValue, `${label} land`) + const landObject = landTopology.objects.land if (!landObject) { throw new TypeError(`${label} is missing land`) } - - const converted = feature(topology, landObject) + const convertedLand = feature(landTopology, landObject) const land = - converted.type === 'FeatureCollection' ? converted.features[0] : converted - if (!land || land.type !== 'Feature' || !isCountryGeometry(land.geometry)) { - throw new TypeError(`${label} did not produce polygon geometry`) + convertedLand.type === 'FeatureCollection' + ? convertedLand.features[0] + : convertedLand + if (!land || !isCountryGeometry(land.geometry)) { + throw new TypeError(`${label} did not produce polygon land geometry`) + } + + const polygons = + land.geometry.type === 'MultiPolygon' + ? land.geometry.coordinates + : [land.geometry.coordinates] + const includedPolygons = polygons.filter((coordinates) => { + const polygon: CountryGeometry = { type: 'Polygon', coordinates } + return !geoContains(excludedCountry, geoCentroid(polygon)) + }) + if (includedPolygons.length === polygons.length) { + throw new TypeError(`${label} land did not contain ${excludedCountryName}`) } return { type: 'Feature', - geometry: land.geometry, + geometry: { + type: 'MultiPolygon', + coordinates: includedPolygons, + }, properties: {}, } } diff --git a/packages/charts-demo-data/src/learning-poverty-geography.ts b/packages/charts-demo-data/src/learning-poverty-geography.ts index 54261752..8035baf0 100644 --- a/packages/charts-demo-data/src/learning-poverty-geography.ts +++ b/packages/charts-demo-data/src/learning-poverty-geography.ts @@ -57,9 +57,9 @@ export const previewLearningPovertyCountries: readonly LearningPovertyCountry[] geometry: simplifyPolygonGeometry(country.geometry, 2), })) -if (learningPovertyCountries.length !== 95) { +if (learningPovertyCountries.length !== 94) { throw new TypeError( - `Expected 95 learning-poverty countries in world-atlas, got ${learningPovertyCountries.length}`, + `Expected 94 learning-poverty countries in the filtered world-atlas, got ${learningPovertyCountries.length}`, ) }