TanStack
Catalog

World population-density choropleth

geography

563 lines · 5 files · 34.4 kB

cases/108-country-choropleth/example.tsx98 lines · entry
cases/108-country-choropleth/example.tsx
import { Chart } from '@tanstack/charts/react/tooltip'
import { tooltip as exampleTooltip } from '@tanstack/charts/tooltip'

import { defineChart } from '@tanstack/charts'
import { geoShape } from '@tanstack/charts/geo'
import { geoEqualEarth } from 'd3-geo'
import { scaleQuantize } from 'd3-scale'
import {
  previewWorldLand,
  worldLand,
  worldSphere,
} from '@tanstack/charts-data/country-atlas'
import {
  learningPovertyCountries,
  previewLearningPovertyCountries,
} from '@tanstack/charts-data/learning-poverty-geography'

const colorRanges = [
  ['#ecfeff', '#a5f3fc', '#67e8f9', '#06b6d4', '#0e7490', '#164e63'],
  ['#f0fdf4', '#bbf7d0', '#86efac', '#22c55e', '#15803d', '#14532d'],
]
const projection = {
  type: geoEqualEarth,
  fit: 'sphere' as const,
}
const previewProjection = {
  type: () => geoEqualEarth().precision(2),
  fit: 'sphere' as const,
}

export const createExampleChart = (input: ChartOptions) =>
  defineChart(
    {
      marks: [
        geoShape([input.preview ? previewWorldLand : worldLand], {
          projection: input.preview ? previewProjection : projection,
          fill: '#e2e8f0',
          stroke: '#ffffff',
          strokeWidth: 0.55,
        }),
        geoShape(
          input.preview
            ? previewLearningPovertyCountries
            : learningPovertyCountries,
          {
            projection: input.preview ? previewProjection : projection,
            color: (country) => country.properties.density,
            stroke: 'currentColor',
            strokeOpacity: 0.34,
            strokeWidth: 0.55,
          },
        ),
        geoShape([worldSphere], {
          projection: input.preview ? previewProjection : projection,
          fill: 'none',
          stroke: 'currentColor',
          strokeOpacity: 0.35,
          strokeWidth: 0.75,
        }),
      ],
      scales: {
        x: null,
        y: null,
      },
      color: {
        scale: scaleQuantize<string>,
        range: colorRanges[input.revision % 2] ?? colorRanges[0],
      },
      margin: 12,
    },
    {
      keyboard: true,
      tooltip: {
        use: exampleTooltip,
        ...{
          format: ({ datum }) =>
            'properties' in datum && 'density' in datum.properties
              ? `${datum.properties['Country Name']} · ${datum.properties.density} people/km²`
              : 'World land',
        },
      },
    },
  )
export interface ChartOptions {
  revision: number
  preview?: boolean
}

export const exampleAriaLabel = 'World population-density choropleth'

export const chart = createExampleChart({
  revision: 0,
  preview: false,
})

export default function Example() {
  return <Chart ariaLabel={exampleAriaLabel} definition={chart} height={480} />
}
packages/charts-demo-data/src/country-atlas.ts135 lines · dependency
packages/charts-demo-data/src/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 { geoGraticule, geoGraticule10 } from 'd3-geo'
import { feature } from 'topojson-client'
import { simplifyPolygonGeometry } from './simplify-geo'
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()
export const previewWorldGraticule = geoGraticule().step([30, 30])()

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 previewWorldLand: LandFeature = {
  ...worldLand,
  geometry: simplifyPolygonGeometry(worldLand.geometry, 2),
}
export const detailedWorldLand = convertLand(
  detailedLandAtlasJson,
  'world-atlas land-50m',
)

function atlasTopology(value: unknown, label: string): AtlasTopology {
  if (!isAtlasTopology(value)) {
    throw new TypeError(`${label} is not valid TopoJSON`)
  }
  return value
}

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 isAtlasTopology(value: unknown): value is AtlasTopology {
  return (
    isRecord(value) &&
    value.type === 'Topology' &&
    Array.isArray(value.arcs) &&
    isRecord(value.objects)
  )
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null
}
packages/charts-demo-data/src/learning-poverty-geography.ts96 lines · dependency
packages/charts-demo-data/src/learning-poverty-geography.ts
import { learningPoverty } from '@tanstack/charts-data/learning-poverty'
import { geoCentroid } from 'd3-geo'
import { worldCountries } from './country-atlas'
import { simplifyPolygonGeometry } from './simplify-geo'
import type { LearningPovertyRow } from '@tanstack/charts-data/learning-poverty'
import type { ExtendedFeature, GeoGeometryObjects } from 'd3-geo'
import type { CountryFeature, CountryGeometry } from './country-atlas'

type PointGeometry = Extract<GeoGeometryObjects, { type: 'Point' }>

export interface LearningPovertyProperties extends LearningPovertyRow {
  name: string
}

export type LearningPovertyCountry = ExtendedFeature<
  CountryGeometry,
  LearningPovertyProperties
>
export type LearningPovertyPoint = ExtendedFeature<
  PointGeometry,
  LearningPovertyProperties
>

// The source uses World Bank names while world-atlas uses Natural Earth names.
// Tiny states absent from the 110m atlas remain unmatched.
const naturalEarthNameBySourceName: Readonly<Record<string, string>> = {
  'Congo, Dem Rep': 'Dem. Rep. Congo',
  'Congo, Rep': 'Congo',
  'Cote d’Ivoire': "Côte d'Ivoire",
  'Czech Republic': 'Czechia',
  'Dominican Republic': 'Dominican Rep.',
  'Egypt, Arab Rep': 'Egypt',
  'Iran, Islamic Rep': 'Iran',
  'Korea, Rep': 'South Korea',
  'Kyrgyz Republic': 'Kyrgyzstan',
  'Russian Federation': 'Russia',
  'Slovak Republic': 'Slovakia',
  'United States': 'United States of America',
  'Yemen, Rep': 'Yemen',
}

const countryByName = new Map(
  worldCountries.map((country) => [country.properties.name, country]),
)

export const learningPovertyCountries: readonly LearningPovertyCountry[] =
  learningPoverty.flatMap((row) => {
    const sourceName = row['Country Name']
    const atlasName = naturalEarthNameBySourceName[sourceName] ?? sourceName
    const country = countryByName.get(atlasName)
    return country ? [joinCountry(country, row)] : []
  })

export const previewLearningPovertyCountries: readonly LearningPovertyCountry[] =
  learningPovertyCountries.map((country) => ({
    ...country,
    geometry: simplifyPolygonGeometry(country.geometry, 2),
  }))

if (learningPovertyCountries.length !== 95) {
  throw new TypeError(
    `Expected 95 learning-poverty countries in world-atlas, got ${learningPovertyCountries.length}`,
  )
}

export const learningPovertyPoints: readonly LearningPovertyPoint[] =
  learningPovertyCountries.map((country) => ({
    type: 'Feature',
    id: country.id,
    geometry: {
      type: 'Point',
      coordinates: geoCentroid(country),
    },
    properties: country.properties,
  }))

// Largest symbols render first so smaller countries remain selectable.
export const learningPovertyPointsByPopulation: readonly LearningPovertyPoint[] =
  [...learningPovertyPoints].sort(
    (left, right) => right.properties.population - left.properties.population,
  )

function joinCountry(
  country: CountryFeature,
  row: LearningPovertyRow,
): LearningPovertyCountry {
  return {
    type: 'Feature',
    id: country.id,
    geometry: country.geometry,
    properties: {
      name: country.properties.name,
      ...row,
    },
  }
}
packages/charts-demo-data/src/learning-poverty.js104 lines · dependency
packages/charts-demo-data/src/learning-poverty.js
// Generated by scripts/sync-demo-data.mjs from learning-poverty.csv.
// Do not edit this file directly.
export const learningPoverty = [
  {"Country Name": "Afghanistan", "Out-of-School (OoS)": 49.6, "Below Minimum Proficiency (in School)": 87, "Learning Poverty": 93.4, "Assessment Year": 2013, Assessment: "NLA", population: 38928346, density: 60},
  {"Country Name": "Argentina", "Out-of-School (OoS)": 0.6, "Below Minimum Proficiency (in School)": 53.6, "Learning Poverty": 53.9, "Assessment Year": 2013, Assessment: "LLECE", population: 45195774, density: 17},
  {"Country Name": "Armenia", "Out-of-School (OoS)": 7.2, "Below Minimum Proficiency (in School)": 30, "Learning Poverty": 35, "Assessment Year": 2015, Assessment: "TIMSS", population: 2963243, density: 104},
  {"Country Name": "Australia", "Out-of-School (OoS)": 3.2, "Below Minimum Proficiency (in School)": 5.5, "Learning Poverty": 8.6, "Assessment Year": 2016, Assessment: "PIRLS", population: 25499884, density: 3},
  {"Country Name": "Austria", "Out-of-School (OoS)": 0, "Below Minimum Proficiency (in School)": 2.4, "Learning Poverty": 2.4, "Assessment Year": 2016, Assessment: "PIRLS", population: 9006398, density: 109},
  {"Country Name": "Azerbaijan", "Out-of-School (OoS)": 5, "Below Minimum Proficiency (in School)": 19.2, "Learning Poverty": 23.3, "Assessment Year": 2016, Assessment: "PIRLS", population: 10139177, density: 123},
  {"Country Name": "Bahrain", "Out-of-School (OoS)": 2.1, "Below Minimum Proficiency (in School)": 30.6, "Learning Poverty": 32.1, "Assessment Year": 2016, Assessment: "PIRLS", population: 1701575, density: 2239},
  {"Country Name": "Bangladesh", "Out-of-School (OoS)": 4.9, "Below Minimum Proficiency (in School)": 56, "Learning Poverty": 58.1, "Assessment Year": 2017, Assessment: "NLA", population: 164689383, density: 1265},
  {"Country Name": "Belgium", "Out-of-School (OoS)": 1.3, "Below Minimum Proficiency (in School)": 5.1, "Learning Poverty": 6.4, "Assessment Year": 2016, Assessment: "PIRLS", population: 11589623, density: 383},
  {"Country Name": "Benin", "Out-of-School (OoS)": 3.6, "Below Minimum Proficiency (in School)": 77.3, "Learning Poverty": 78.2, "Assessment Year": 2014, Assessment: "PASEC", population: 12123200, density: 108},
  {"Country Name": "Botswana", "Out-of-School (OoS)": 7.2, "Below Minimum Proficiency (in School)": 44.3, "Learning Poverty": 48.3, "Assessment Year": 2011, Assessment: "PIRLS", population: 2351627, density: 4},
  {"Country Name": "Brazil", "Out-of-School (OoS)": 2.7, "Below Minimum Proficiency (in School)": 46.9, "Learning Poverty": 48.4, "Assessment Year": 2013, Assessment: "LLECE", population: 212559417, density: 25},
  {"Country Name": "Bulgaria", "Out-of-School (OoS)": 6.8, "Below Minimum Proficiency (in School)": 5.2, "Learning Poverty": 11.7, "Assessment Year": 2016, Assessment: "PIRLS", population: 6948445, density: 64},
  {"Country Name": "Burkina Faso", "Out-of-School (OoS)": 31.7, "Below Minimum Proficiency (in School)": 78.6, "Learning Poverty": 85.4, "Assessment Year": 2014, Assessment: "PASEC", population: 20903273, density: 76},
  {"Country Name": "Burundi", "Out-of-School (OoS)": 2.7, "Below Minimum Proficiency (in School)": 92.7, "Learning Poverty": 92.9, "Assessment Year": 2014, Assessment: "PASEC", population: 11890784, density: 463},
  {"Country Name": "Cambodia", "Out-of-School (OoS)": 2.6, "Below Minimum Proficiency (in School)": 49.8, "Learning Poverty": 51.1, "Assessment Year": 2013, Assessment: "NLA", population: 16718965, density: 95},
  {"Country Name": "Cameroon", "Out-of-School (OoS)": 5.2, "Below Minimum Proficiency (in School)": 75.9, "Learning Poverty": 77.2, "Assessment Year": 2014, Assessment: "PASEC", population: 26545863, density: 56},
  {"Country Name": "Canada", "Out-of-School (OoS)": 0, "Below Minimum Proficiency (in School)": 4.3, "Learning Poverty": 4.3, "Assessment Year": 2016, Assessment: "PIRLS", population: 37742154, density: 4},
  {"Country Name": "Chad", "Out-of-School (OoS)": 21.1, "Below Minimum Proficiency (in School)": 97, "Learning Poverty": 97.7, "Assessment Year": 2014, Assessment: "PASEC", population: 16425864, density: 13},
  {"Country Name": "Chile", "Out-of-School (OoS)": 9.3, "Below Minimum Proficiency (in School)": 30.3, "Learning Poverty": 36.8, "Assessment Year": 2013, Assessment: "LLECE", population: 19116201, density: 26},
  {"Country Name": "China", "Out-of-School (OoS)": 0, "Below Minimum Proficiency (in School)": 18.2, "Learning Poverty": 18.2, "Assessment Year": 2016, Assessment: "NLA", population: 1439323776, density: 153},
  {"Country Name": "Colombia", "Out-of-School (OoS)": 6.9, "Below Minimum Proficiency (in School)": 44.7, "Learning Poverty": 48.6, "Assessment Year": 2013, Assessment: "LLECE", population: 50882891, density: 46},
  {"Country Name": "Congo, Dem Rep", "Out-of-School (OoS)": 63.2, "Below Minimum Proficiency (in School)": 62, "Learning Poverty": 86, "Assessment Year": 2011, Assessment: "NLA", population: 89561403, density: 40},
  {"Country Name": "Congo, Rep", "Out-of-School (OoS)": 12.8, "Below Minimum Proficiency (in School)": 82.9, "Learning Poverty": 85.1, "Assessment Year": 2014, Assessment: "PASEC", population: 5518087, density: 16},
  {"Country Name": "Costa Rica", "Out-of-School (OoS)": 1.1, "Below Minimum Proficiency (in School)": 31.7, "Learning Poverty": 32.5, "Assessment Year": 2013, Assessment: "LLECE", population: 5094118, density: 100},
  {"Country Name": "Cote d’Ivoire", "Out-of-School (OoS)": 21.1, "Below Minimum Proficiency (in School)": 77.6, "Learning Poverty": 82.3, "Assessment Year": 2014, Assessment: "PASEC", population: 26378274, density: 83},
  {"Country Name": "Croatia", "Out-of-School (OoS)": 3, "Below Minimum Proficiency (in School)": 1, "Learning Poverty": 4, "Assessment Year": 2011, Assessment: "PIRLS", population: 4105267, density: 73},
  {"Country Name": "Cyprus", "Out-of-School (OoS)": 2.2, "Below Minimum Proficiency (in School)": 14.3, "Learning Poverty": 16.2, "Assessment Year": 2015, Assessment: "TIMSS", population: 1207359, density: 131},
  {"Country Name": "Czech Republic", "Out-of-School (OoS)": 0, "Below Minimum Proficiency (in School)": 3, "Learning Poverty": 3, "Assessment Year": 2016, Assessment: "PIRLS", population: 10708981, density: 139},
  {"Country Name": "Denmark", "Out-of-School (OoS)": 1, "Below Minimum Proficiency (in School)": 2.6, "Learning Poverty": 3.6, "Assessment Year": 2016, Assessment: "PIRLS", population: 5792202, density: 137},
  {"Country Name": "Dominican Republic", "Out-of-School (OoS)": 6.6, "Below Minimum Proficiency (in School)": 79.4, "Learning Poverty": 80.7, "Assessment Year": 2013, Assessment: "LLECE", population: 10847910, density: 225},
  {"Country Name": "Ecuador", "Out-of-School (OoS)": 1.9, "Below Minimum Proficiency (in School)": 62.1, "Learning Poverty": 62.8, "Assessment Year": 2013, Assessment: "LLECE", population: 17643054, density: 71},
  {"Country Name": "Egypt, Arab Rep", "Out-of-School (OoS)": 1.4, "Below Minimum Proficiency (in School)": 69.2, "Learning Poverty": 69.6, "Assessment Year": 2016, Assessment: "PIRLS", population: 102334404, density: 103},
  {"Country Name": "Ethiopia", "Out-of-School (OoS)": 14, "Below Minimum Proficiency (in School)": 88.7, "Learning Poverty": 90.3, "Assessment Year": 2015, Assessment: "NLA", population: 114963588, density: 115},
  {"Country Name": "Finland", "Out-of-School (OoS)": 0.9, "Below Minimum Proficiency (in School)": 1.7, "Learning Poverty": 2.6, "Assessment Year": 2016, Assessment: "PIRLS", population: 5540720, density: 18},
  {"Country Name": "France", "Out-of-School (OoS)": 0.9, "Below Minimum Proficiency (in School)": 6.3, "Learning Poverty": 7.1, "Assessment Year": 2016, Assessment: "PIRLS", population: 65273511, density: 119},
  {"Country Name": "Georgia", "Out-of-School (OoS)": 0.4, "Below Minimum Proficiency (in School)": 13.5, "Learning Poverty": 13.8, "Assessment Year": 2016, Assessment: "PIRLS", population: 3989167, density: 57},
  {"Country Name": "Germany", "Out-of-School (OoS)": 0.2, "Below Minimum Proficiency (in School)": 5.5, "Learning Poverty": 5.7, "Assessment Year": 2016, Assessment: "PIRLS", population: 83783942, density: 240},
  {"Country Name": "Guatemala", "Out-of-School (OoS)": 10.1, "Below Minimum Proficiency (in School)": 63.6, "Learning Poverty": 67.3, "Assessment Year": 2013, Assessment: "LLECE", population: 17915568, density: 167},
  {"Country Name": "Honduras", "Out-of-School (OoS)": 17.1, "Below Minimum Proficiency (in School)": 69.4, "Learning Poverty": 74.7, "Assessment Year": 2013, Assessment: "LLECE", population: 9904607, density: 89},
  {"Country Name": "Hong Kong SAR, China", "Out-of-School (OoS)": 1.9, "Below Minimum Proficiency (in School)": 1.4, "Learning Poverty": 3.2, "Assessment Year": 2016, Assessment: "PIRLS", population: 7496981, density: 7140},
  {"Country Name": "Hungary", "Out-of-School (OoS)": 3.1, "Below Minimum Proficiency (in School)": 2.9, "Learning Poverty": 5.9, "Assessment Year": 2016, Assessment: "PIRLS", population: 9660351, density: 107},
  {"Country Name": "India", "Out-of-School (OoS)": 2.3, "Below Minimum Proficiency (in School)": 53.7, "Learning Poverty": 54.8, "Assessment Year": 2017, Assessment: "NLA", population: 1380004385, density: 464},
  {"Country Name": "Indonesia", "Out-of-School (OoS)": 2.4, "Below Minimum Proficiency (in School)": 33.8, "Learning Poverty": 35.4, "Assessment Year": 2011, Assessment: "PIRLS", population: 273523615, density: 151},
  {"Country Name": "Iran, Islamic Rep", "Out-of-School (OoS)": 0.9, "Below Minimum Proficiency (in School)": 35.1, "Learning Poverty": 35.7, "Assessment Year": 2016, Assessment: "PIRLS", population: 83992949, density: 52},
  {"Country Name": "Ireland", "Out-of-School (OoS)": 0, "Below Minimum Proficiency (in School)": 2.3, "Learning Poverty": 2.3, "Assessment Year": 2016, Assessment: "PIRLS", population: 4937786, density: 72},
  {"Country Name": "Israel", "Out-of-School (OoS)": 2.9, "Below Minimum Proficiency (in School)": 9, "Learning Poverty": 11.7, "Assessment Year": 2016, Assessment: "PIRLS", population: 8655535, density: 400},
  {"Country Name": "Italy", "Out-of-School (OoS)": 1.4, "Below Minimum Proficiency (in School)": 2.1, "Learning Poverty": 3.5, "Assessment Year": 2016, Assessment: "PIRLS", population: 60461826, density: 206},
  {"Country Name": "Japan", "Out-of-School (OoS)": 1.2, "Below Minimum Proficiency (in School)": 1, "Learning Poverty": 2.2, "Assessment Year": 2015, Assessment: "TIMSS", population: 126476461, density: 347},
  {"Country Name": "Jordan", "Out-of-School (OoS)": 4, "Below Minimum Proficiency (in School)": 50, "Learning Poverty": 52, "Assessment Year": 2015, Assessment: "TIMSS", population: 10203134, density: 115},
  {"Country Name": "Kazakhstan", "Out-of-School (OoS)": 0.3, "Below Minimum Proficiency (in School)": 1.9, "Learning Poverty": 2.2, "Assessment Year": 2016, Assessment: "PIRLS", population: 18776707, density: 7},
  {"Country Name": "Korea, Rep", "Out-of-School (OoS)": 2.7, "Below Minimum Proficiency (in School)": 0.3, "Learning Poverty": 3, "Assessment Year": 2015, Assessment: "TIMSS", population: 51269185, density: 527},
  {"Country Name": "Kuwait", "Out-of-School (OoS)": 3.3, "Below Minimum Proficiency (in School)": 49.4, "Learning Poverty": 51, "Assessment Year": 2016, Assessment: "PIRLS", population: 4270571, density: 240},
  {"Country Name": "Kyrgyz Republic", "Out-of-School (OoS)": 1.9, "Below Minimum Proficiency (in School)": 63.8, "Learning Poverty": 64.5, "Assessment Year": 2014, Assessment: "NLA", population: 6524195, density: 34},
  {"Country Name": "Latvia", "Out-of-School (OoS)": 3.2, "Below Minimum Proficiency (in School)": 0.8, "Learning Poverty": 4, "Assessment Year": 2016, Assessment: "PIRLS", population: 1886198, density: 30},
  {"Country Name": "Lithuania", "Out-of-School (OoS)": 0.3, "Below Minimum Proficiency (in School)": 2.7, "Learning Poverty": 3, "Assessment Year": 2016, Assessment: "PIRLS", population: 2722289, density: 43},
  {"Country Name": "Macao SAR, China", "Out-of-School (OoS)": 1.3, "Below Minimum Proficiency (in School)": 2.4, "Learning Poverty": 3.7, "Assessment Year": 2016, Assessment: "PIRLS", population: 649335, density: 21645},
  {"Country Name": "Madagascar", "Out-of-School (OoS)": 21.9, "Below Minimum Proficiency (in School)": 95.8, "Learning Poverty": 96.7, "Assessment Year": 2015, Assessment: "NLA", population: 27691018, density: 48},
  {"Country Name": "Malaysia", "Out-of-School (OoS)": 1.4, "Below Minimum Proficiency (in School)": 11.7, "Learning Poverty": 12.9, "Assessment Year": 2017, Assessment: "NLA", population: 32365999, density: 99},
  {"Country Name": "Mali", "Out-of-School (OoS)": 33, "Below Minimum Proficiency (in School)": 86.6, "Learning Poverty": 91, "Assessment Year": 2012, Assessment: "NLA", population: 20250833, density: 17},
  {"Country Name": "Malta", "Out-of-School (OoS)": 2.4, "Below Minimum Proficiency (in School)": 26.8, "Learning Poverty": 28.6, "Assessment Year": 2016, Assessment: "PIRLS", population: 441543, density: 1380},
  {"Country Name": "Mexico", "Out-of-School (OoS)": 1.2, "Below Minimum Proficiency (in School)": 42.5, "Learning Poverty": 43.2, "Assessment Year": 2013, Assessment: "LLECE", population: 128932753, density: 66},
  {"Country Name": "Morocco", "Out-of-School (OoS)": 5.4, "Below Minimum Proficiency (in School)": 63.8, "Learning Poverty": 65.8, "Assessment Year": 2016, Assessment: "PIRLS", population: 36910560, density: 83},
  {"Country Name": "Netherlands", "Out-of-School (OoS)": 0.3, "Below Minimum Proficiency (in School)": 1.3, "Learning Poverty": 1.6, "Assessment Year": 2016, Assessment: "PIRLS", population: 17134872, density: 508},
  {"Country Name": "New Zealand", "Out-of-School (OoS)": 1.5, "Below Minimum Proficiency (in School)": 10, "Learning Poverty": 11.4, "Assessment Year": 2016, Assessment: "PIRLS", population: 4822233, density: 18},
  {"Country Name": "Nicaragua", "Out-of-School (OoS)": 1.6, "Below Minimum Proficiency (in School)": 69.3, "Learning Poverty": 69.8, "Assessment Year": 2013, Assessment: "LLECE", population: 6624554, density: 55},
  {"Country Name": "Niger", "Out-of-School (OoS)": 38.9, "Below Minimum Proficiency (in School)": 97.9, "Learning Poverty": 98.7, "Assessment Year": 2014, Assessment: "PASEC", population: 24206644, density: 19},
  {"Country Name": "Norway", "Out-of-School (OoS)": 0.2, "Below Minimum Proficiency (in School)": 5.8, "Learning Poverty": 6, "Assessment Year": 2016, Assessment: "PIRLS", population: 5421241, density: 15},
  {"Country Name": "Oman", "Out-of-School (OoS)": 1.5, "Below Minimum Proficiency (in School)": 40.9, "Learning Poverty": 41.8, "Assessment Year": 2016, Assessment: "PIRLS", population: 5106626, density: 16},
  {"Country Name": "Pakistan", "Out-of-School (OoS)": 27.3, "Below Minimum Proficiency (in School)": 65, "Learning Poverty": 74.5, "Assessment Year": 2014, Assessment: "NLA", population: 220892340, density: 287},
  {"Country Name": "Panama", "Out-of-School (OoS)": 7.1, "Below Minimum Proficiency (in School)": 64.1, "Learning Poverty": 66.6, "Assessment Year": 2013, Assessment: "LLECE", population: 4314767, density: 58},
  {"Country Name": "Paraguay", "Out-of-School (OoS)": 10.8, "Below Minimum Proficiency (in School)": 71.3, "Learning Poverty": 74.4, "Assessment Year": 2013, Assessment: "LLECE", population: 7132538, density: 18},
  {"Country Name": "Peru", "Out-of-School (OoS)": 4.2, "Below Minimum Proficiency (in School)": 53.7, "Learning Poverty": 55.7, "Assessment Year": 2013, Assessment: "LLECE", population: 32971854, density: 26},
  {"Country Name": "Poland", "Out-of-School (OoS)": 4.4, "Below Minimum Proficiency (in School)": 2, "Learning Poverty": 6.3, "Assessment Year": 2016, Assessment: "PIRLS", population: 37846611, density: 124},
  {"Country Name": "Portugal", "Out-of-School (OoS)": 3.6, "Below Minimum Proficiency (in School)": 3, "Learning Poverty": 6.5, "Assessment Year": 2016, Assessment: "PIRLS", population: 10196709, density: 111},
  {"Country Name": "Qatar", "Out-of-School (OoS)": 2.2, "Below Minimum Proficiency (in School)": 33.8, "Learning Poverty": 35.3, "Assessment Year": 2016, Assessment: "PIRLS", population: 2881053, density: 248},
  {"Country Name": "Romania", "Out-of-School (OoS)": 6.9, "Below Minimum Proficiency (in School)": 14.1, "Learning Poverty": 20, "Assessment Year": 2011, Assessment: "PIRLS", population: 19237691, density: 84},
  {"Country Name": "Russian Federation", "Out-of-School (OoS)": 2.4, "Below Minimum Proficiency (in School)": 0.9, "Learning Poverty": 3.3, "Assessment Year": 2016, Assessment: "PIRLS", population: 145934462, density: 9},
  {"Country Name": "Saudi Arabia", "Out-of-School (OoS)": 2.5, "Below Minimum Proficiency (in School)": 36.7, "Learning Poverty": 38.3, "Assessment Year": 2016, Assessment: "PIRLS", population: 34813871, density: 16},
  {"Country Name": "Senegal", "Out-of-School (OoS)": 25.7, "Below Minimum Proficiency (in School)": 65.2, "Learning Poverty": 74.1, "Assessment Year": 2014, Assessment: "PASEC", population: 16743927, density: 87},
  {"Country Name": "Serbia", "Out-of-School (OoS)": 0.8, "Below Minimum Proficiency (in School)": 7.4, "Learning Poverty": 8.1, "Assessment Year": 2015, Assessment: "TIMSS", population: 8737371, density: 100},
  {"Country Name": "Singapore", "Out-of-School (OoS)": 0.1, "Below Minimum Proficiency (in School)": 2.7, "Learning Poverty": 2.8, "Assessment Year": 2016, Assessment: "PIRLS", population: 5850342, density: 8358},
  {"Country Name": "Slovak Republic", "Out-of-School (OoS)": 2.1, "Below Minimum Proficiency (in School)": 6.6, "Learning Poverty": 8.5, "Assessment Year": 2016, Assessment: "PIRLS", population: 5459642, density: 114},
  {"Country Name": "Slovenia", "Out-of-School (OoS)": 2.2, "Below Minimum Proficiency (in School)": 3.7, "Learning Poverty": 5.8, "Assessment Year": 2016, Assessment: "PIRLS", population: 2078938, density: 103},
  {"Country Name": "South Africa", "Out-of-School (OoS)": 8.4, "Below Minimum Proficiency (in School)": 77.9, "Learning Poverty": 79.8, "Assessment Year": 2016, Assessment: "PIRLS", population: 59308690, density: 49},
  {"Country Name": "Spain", "Out-of-School (OoS)": 1.5, "Below Minimum Proficiency (in School)": 3.4, "Learning Poverty": 4.9, "Assessment Year": 2016, Assessment: "PIRLS", population: 46754778, density: 94},
  {"Country Name": "Sri Lanka", "Out-of-School (OoS)": 0.9, "Below Minimum Proficiency (in School)": 14, "Learning Poverty": 14.8, "Assessment Year": 2015, Assessment: "NLA", population: 21413249, density: 341},
  {"Country Name": "Sweden", "Out-of-School (OoS)": 0.4, "Below Minimum Proficiency (in School)": 1.9, "Learning Poverty": 2.3, "Assessment Year": 2016, Assessment: "PIRLS", population: 10099265, density: 25},
  {"Country Name": "Thailand", "Out-of-School (OoS)": 2, "Below Minimum Proficiency (in School)": 21.9, "Learning Poverty": 23.5, "Assessment Year": 2011, Assessment: "TIMSS", population: 69799978, density: 137},
  {"Country Name": "Togo", "Out-of-School (OoS)": 8.5, "Below Minimum Proficiency (in School)": 84.2, "Learning Poverty": 85.6, "Assessment Year": 2014, Assessment: "PASEC", population: 8278724, density: 152},
  {"Country Name": "Trinidad and Tobago", "Out-of-School (OoS)": 1.3, "Below Minimum Proficiency (in School)": 19.7, "Learning Poverty": 20.7, "Assessment Year": 2016, Assessment: "PIRLS", population: 1399488, density: 273},
  {"Country Name": "Tunisia", "Out-of-School (OoS)": 0.4, "Below Minimum Proficiency (in School)": 65.1, "Learning Poverty": 65.3, "Assessment Year": 2011, Assessment: "TIMSS", population: 11818619, density: 76},
  {"Country Name": "Turkey", "Out-of-School (OoS)": 5, "Below Minimum Proficiency (in School)": 17.6, "Learning Poverty": 21.7, "Assessment Year": 2015, Assessment: "TIMSS", population: 84339067, density: 110},
  {"Country Name": "Uganda", "Out-of-School (OoS)": 9, "Below Minimum Proficiency (in School)": 81.1, "Learning Poverty": 82.8, "Assessment Year": 2014, Assessment: "NLA", population: 45741007, density: 229},
  {"Country Name": "United Arab Emirates", "Out-of-School (OoS)": 2.8, "Below Minimum Proficiency (in School)": 32.4, "Learning Poverty": 34.3, "Assessment Year": 2016, Assessment: "PIRLS", population: 9890402, density: 118},
  {"Country Name": "United Kingdom", "Out-of-School (OoS)": 0.2, "Below Minimum Proficiency (in School)": 3.2, "Learning Poverty": 3.4, "Assessment Year": 2016, Assessment: "PIRLS", population: 67886011, density: 281},
  {"Country Name": "United States", "Out-of-School (OoS)": 4.1, "Below Minimum Proficiency (in School)": 3.9, "Learning Poverty": 7.9, "Assessment Year": 2016, Assessment: "PIRLS", population: 331002651, density: 36},
  {"Country Name": "Uruguay", "Out-of-School (OoS)": 0.5, "Below Minimum Proficiency (in School)": 41.4, "Learning Poverty": 41.7, "Assessment Year": 2013, Assessment: "LLECE", population: 3473730, density: 20},
  {"Country Name": "Vietnam", "Out-of-School (OoS)": 0.6, "Below Minimum Proficiency (in School)": 1.1, "Learning Poverty": 1.7, "Assessment Year": 2011, Assessment: "NLA", population: 97338579, density: 314},
  {"Country Name": "Yemen, Rep", "Out-of-School (OoS)": 18.9, "Below Minimum Proficiency (in School)": 93.5, "Learning Poverty": 94.7, "Assessment Year": 2011, Assessment: "TIMSS", population: 29825964, density: 56},
]
packages/charts-demo-data/src/simplify-geo.ts130 lines · dependency
packages/charts-demo-data/src/simplify-geo.ts
import type { GeoGeometryObjects } from 'd3-geo'

type PolygonGeometry = Extract<
  GeoGeometryObjects,
  { type: 'Polygon' | 'MultiPolygon' }
>
type Position = number[]

export function simplifyPolygonGeometry(
  geometry: PolygonGeometry,
  tolerance: number,
): PolygonGeometry {
  if (geometry.type === 'Polygon') {
    return {
      type: 'Polygon',
      coordinates: geometry.coordinates.map((ring) =>
        simplifyRing(ring, tolerance),
      ),
    }
  }

  return {
    type: 'MultiPolygon',
    coordinates: geometry.coordinates.map((polygon) =>
      polygon.map((ring) => simplifyRing(ring, tolerance)),
    ),
  }
}

function simplifyRing(
  ring: readonly Position[],
  tolerance: number,
): Position[] {
  if (ring.length <= 4) return [...ring]

  const openRing = ring.slice(0, -1)
  const anchor = openRing[0]
  if (!anchor) return [...ring]

  let splitIndex = 1
  let farthestDistance = 0
  for (let index = 1; index < openRing.length; index += 1) {
    const point = openRing[index]
    if (!point) continue
    const distance = squaredDistance(anchor, point)
    if (distance > farthestDistance) {
      farthestDistance = distance
      splitIndex = index
    }
  }

  const firstHalf = simplifyLine(
    openRing.slice(0, splitIndex + 1),
    tolerance * tolerance,
  )
  const secondHalf = simplifyLine(
    [...openRing.slice(splitIndex), anchor],
    tolerance * tolerance,
  )
  const simplified = [...firstHalf.slice(0, -1), ...secondHalf]

  return simplified.length >= 4 ? simplified : [...ring]
}

function simplifyLine(
  points: readonly Position[],
  squaredTolerance: number,
): Position[] {
  const first = points[0]
  const last = points.at(-1)
  if (!first || !last || points.length <= 2) return [...points]

  let farthestIndex = 0
  let farthestDistance = squaredTolerance
  for (let index = 1; index < points.length - 1; index += 1) {
    const point = points[index]
    if (!point) continue
    const distance = squaredSegmentDistance(point, first, last)
    if (distance > farthestDistance) {
      farthestDistance = distance
      farthestIndex = index
    }
  }

  if (farthestIndex === 0) return [first, last]

  const left = simplifyLine(
    points.slice(0, farthestIndex + 1),
    squaredTolerance,
  )
  const right = simplifyLine(points.slice(farthestIndex), squaredTolerance)
  return [...left.slice(0, -1), ...right]
}

function squaredSegmentDistance(
  point: Position,
  start: Position,
  end: Position,
): number {
  const [pointX = 0, pointY = 0] = point
  let [x = 0, y = 0] = start
  const [endX = 0, endY = 0] = end
  let dx = endX - x
  let dy = endY - y

  if (dx !== 0 || dy !== 0) {
    const progress =
      ((pointX - x) * dx + (pointY - y) * dy) / (dx * dx + dy * dy)
    if (progress > 1) {
      x = endX
      y = endY
    } else if (progress > 0) {
      x += dx * progress
      y += dy * progress
    }
    dx = pointX - x
    dy = pointY - y
  } else {
    dx = pointX - x
    dy = pointY - y
  }

  return dx * dx + dy * dy
}

function squaredDistance(left: Position, right: Position): number {
  const dx = (left[0] ?? 0) - (right[0] ?? 0)
  const dy = (left[1] ?? 0) - (right[1] ?? 0)
  return dx * dx + dy * dy
}