Source
152 chart lines · 125 data-selection lines · 3 files · 7.3 kBBenchmark harness excluded · shared/mount.ts
cases/110-projection-gallery/tanstack.ts50 lines · entry
cases/110-projection-gallery/tanstack.ts
import { defineChart } from '@tanstack/charts'
import { geoShape } from '@tanstack/charts/geo'
import { worldLand, worldSphere } from '../../shared/fixtures/country-atlas'
import {
fitGalleryProjection,
projectionGalleryData,
projectionPane,
} from './projection'
import { tanstackMount } from '../../shared/mount'
import type { ConformanceInput } from '../../types'
const projectionColors = [
['#2563eb', '#7c3aed', '#0891b2', '#ea580c'],
['#1d4ed8', '#6d28d9', '#0e7490', '#c2410c'],
]
const definition = (input: ConformanceInput) => {
const projections = projectionGalleryData()
return defineChart({
marks: projections.flatMap((entry, index) => [
geoShape([worldSphere], {
projection: ({ chart }) =>
fitGalleryProjection(entry.create(), projectionPane(chart, index)),
fill: 'none',
stroke: 'currentColor',
strokeOpacity: 0.5,
strokeWidth: 0.8,
}),
geoShape([worldLand], {
projection: ({ chart }) =>
fitGalleryProjection(entry.create(), projectionPane(chart, index)),
color: () => entry.id,
fillOpacity: 0.78,
stroke: 'currentColor',
strokeOpacity: 0.28,
strokeWidth: 0.45,
}),
]),
color: {
range: projectionColors[input.revision % 2] ?? projectionColors[0],
},
margin: 0,
})
}
export const mount = tanstackMount(
definition,
'Standard world projection gallery',
)cases/110-projection-gallery/projection.ts102 lines · support
cases/110-projection-gallery/projection.ts
import {
geoEqualEarth,
geoEquirectangular,
geoMercator,
geoNaturalEarth1,
} from 'd3-geo'
import { worldSphere } from '../../shared/fixtures/country-atlas'
import type { GeoProjection } from 'd3-geo'
export type ProjectionGalleryId =
'equal-earth' | 'natural-earth' | 'mercator' | 'equirectangular'
interface ProjectionGalleryDefinition {
id: ProjectionGalleryId
label: string
}
export interface ProjectionGalleryDatum {
id: ProjectionGalleryId
label: string
create: () => GeoProjection
}
export interface ProjectionPane {
x: number
y: number
width: number
height: number
}
const projectionGalleryDefinitions: readonly ProjectionGalleryDefinition[] = [
{
id: 'equal-earth',
label: 'Equal Earth',
},
{
id: 'natural-earth',
label: 'Natural Earth',
},
{
id: 'mercator',
label: 'Mercator',
},
{
id: 'equirectangular',
label: 'Equirectangular',
},
]
export function projectionGalleryData(): readonly ProjectionGalleryDatum[] {
return projectionGalleryDefinitions.map((definition) => ({
id: definition.id,
label: definition.label,
create: projectionFactory(definition.id),
}))
}
export function projectionPane(
bounds: ProjectionPane,
index: number,
): ProjectionPane {
const leftWidth = Math.floor(bounds.width / 2)
const topHeight = Math.floor(bounds.height / 2)
const column = index % 2
const row = Math.floor(index / 2)
const width = column === 0 ? leftWidth : bounds.width - leftWidth
const height = row === 0 ? topHeight : bounds.height - topHeight
return {
x: bounds.x + (column === 0 ? 0 : leftWidth),
y: bounds.y + (row === 0 ? 0 : topHeight),
width,
height,
}
}
export function fitGalleryProjection(
projection: GeoProjection,
{ x, y, width, height }: ProjectionPane,
inset = 8,
): GeoProjection {
return projection.fitExtent(
[
[x + inset, y + inset],
[x + width - inset, y + height - inset],
],
worldSphere,
)
}
function projectionFactory(id: ProjectionGalleryId): () => GeoProjection {
switch (id) {
case 'equal-earth':
return geoEqualEarth
case 'natural-earth':
return geoNaturalEarth1
case 'mercator':
return geoMercator
case 'equirectangular':
return geoEquirectangular
}
}shared/fixtures/country-atlas.ts125 lines · fixture
shared/fixtures/country-atlas.ts
import countriesAtlasJson from 'world-atlas/countries-110m.json'
import landAtlasJson from 'world-atlas/land-110m.json'
import detailedLandAtlasJson from 'world-atlas/land-50m.json'
import { geoGraticule10 } from 'd3-geo'
import { feature } from 'topojson-client'
import type {
ExtendedFeature,
ExtendedFeatureCollection,
GeoGeometryObjects,
GeoSphere,
} from 'd3-geo'
type AtlasTopology = Parameters<typeof feature>[0]
export type CountryGeometry = Extract<
GeoGeometryObjects,
{ type: 'Polygon' | 'MultiPolygon' }
>
export interface CountryProperties {
name: string
}
export type CountryFeature = ExtendedFeature<CountryGeometry, CountryProperties>
export type LandFeature = ExtendedFeature<CountryGeometry, Record<never, never>>
export const worldSphere: GeoSphere = { type: 'Sphere' }
export const worldGraticule = geoGraticule10()
const countriesTopology = atlasTopology(
countriesAtlasJson,
'world-atlas countries-110m',
)
const countriesObject = countriesTopology.objects.countries
if (!countriesObject) {
throw new TypeError('world-atlas countries-110m is missing countries')
}
const convertedCountries = feature(countriesTopology, countriesObject)
if (convertedCountries.type !== 'FeatureCollection') {
throw new TypeError('world-atlas countries did not produce a collection')
}
export const worldCountries: readonly CountryFeature[] =
convertedCountries.features.flatMap<CountryFeature>((entry) => {
if (
!isCountryGeometry(entry.geometry) ||
!isRecord(entry.properties) ||
typeof entry.properties.name !== 'string'
) {
return []
}
return [
{
type: 'Feature',
id: entry.id === undefined ? entry.properties.name : String(entry.id),
geometry: entry.geometry,
properties: {
name: entry.properties.name,
},
},
]
})
if (worldCountries.length !== 177) {
throw new TypeError(
`Expected 177 world-atlas countries, got ${worldCountries.length}`,
)
}
export const worldCountryCollection: ExtendedFeatureCollection<CountryFeature> =
{
type: 'FeatureCollection',
features: [...worldCountries],
}
export const worldLand = convertLand(landAtlasJson, 'world-atlas land-110m')
export const detailedWorldLand = convertLand(
detailedLandAtlasJson,
'world-atlas land-50m',
)
function atlasTopology(value: unknown, label: string): AtlasTopology {
if (
!isRecord(value) ||
value.type !== 'Topology' ||
!Array.isArray(value.arcs) ||
!isRecord(value.objects)
) {
throw new TypeError(`${label} is not valid TopoJSON`)
}
return value as unknown as AtlasTopology
}
function convertLand(value: unknown, label: string): LandFeature {
const topology = atlasTopology(value, label)
const landObject = topology.objects.land
if (!landObject) {
throw new TypeError(`${label} is missing land`)
}
const converted = feature(topology, 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`)
}
return {
type: 'Feature',
geometry: land.geometry,
properties: {},
}
}
function isCountryGeometry(
geometry: GeoGeometryObjects,
): geometry is CountryGeometry {
return geometry.type === 'Polygon' || geometry.type === 'MultiPolygon'
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}