Public
Edited
Mar 23, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
mutable topto2 = null
Insert cell
topto2.objects.countries.geometries[0]
Insert cell
topojson.mesh(
topo,
{
type: "GeometryCollection",
geometries: topo.objects.countries.geometries
},
(a, b) => a === b
)
Insert cell
Inputs.table(
features.map(({ id, properties }) => ({
id,
a3: properties.a3,
name: properties.name,
ori: ori.get(id),
DIFF: ori.get(id) !== properties.name
// pieces: properties.pieces
}))
)
Insert cell
original = d3.json(
"https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
)
Insert cell
original.objects.countries.geometries.find(
({ id, properties }) => properties.name === "Somaliland"
)
Insert cell
ori = new Map(
original.objects.countries.geometries.map(({ id, properties }) => [
id,
properties.name
])
)
Insert cell
features = topojson.feature(topo2, {
type: "GeometryCollection",
geometries: topo2.objects.countries.geometries
}).features
Insert cell
features.filter((d) => d.properties.a3 === "FRA")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
map({ projection_, visibility, center: false, displayObjects, sphere: false })
Insert cell
import { projectionInput2 } from "@fil/d3-projections"
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
map({
center: { type: "Point", coordinates: [20, 44] },
scale: 2500,
displayObjects,
visibility
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
o2017_countries110m = ({
name: "201707_countries110m",
zip: () => FileAttachment("201707-ne_110m_admin_0_countries.zip").zip(),
shpsource: "ne_110m_admin_0_countries"
})
Insert cell
o2018_countries110m = ({
name: "201808_countries110m",
zip: () => FileAttachment("201808-ne_110m_admin_0_countries.zip").zip(),
shpsource: "ne_110m_admin_0_countries"
})
Insert cell
o2019_countries110m = ({
name: "2019xx_countries110m",
zip: () => FileAttachment("2019xx-ne_110m_admin_0_countries.zip").zip(),
shpsource: "ne_110m_admin_0_countries"
})
Insert cell
o2021_countries110m = ({
name: "202112_countries110m",
zip: () => FileAttachment("202112-ne_110m_admin_0_countries.zip").zip(),
shpsource: "ne_110m_admin_0_countries"
})
Insert cell
countries110m = ({
name: "countries110m",
zipsource:
"https://naturalearth.s3.amazonaws.com/110m_cultural/ne_110m_admin_0_countries.zip",
shpsource: "ne_110m_admin_0_countries"
})
Insert cell
countries50m = ({
name: "countries50m",
zipsource:
"https://naturalearth.s3.amazonaws.com/50m_cultural/ne_50m_admin_0_countries.zip",
zip: () => FileAttachment("ne_50m_admin_0_countries.zip").zip(),
shpsource: "ne_50m_admin_0_countries"
})
Insert cell
import { viewof projection } with { projections } from "@fil/d3-projections"
Insert cell
projections = [
{ name: "Equal Earth", value: () => d3.geoEqualEarth().rotate([-10, 0]) },
{ name: "Airocean", value: d3.geoAirocean },
// { name: "Airy’s minimum error", value: d3.geoAiry },
// { name: "Aitoff", value: d3.geoAitoff },
// { name: "American polyconic", value: d3.geoPolyconic },
{
name: "Armadillo",
value: () => d3.geoArmadillo().rotate([-10, 0]),
options: { clip: { type: "Sphere" } }
},
{ name: "August", value: () => d3.geoAugust().rotate([-10, 0]) },
{ name: "azimuthal equal-area", value: d3.geoAzimuthalEqualArea },
{ name: "azimuthal equidistant", value: d3.geoAzimuthalEquidistant },
// { name: "Baker dinomic", value: d3.geoBaker },
// { name: "Berghaus’ star", value: d3.geoBerghaus, options: { clip: { type: "Sphere" } } },
{ name: "Bertin’s 1953", value: d3.geoBertin1953 },
// { name: "Boggs’ eumorphic", value: d3.geoBoggs },
// { name: "Boggs’ eumorphic (interrupted)", value: d3.geoInterruptedBoggs, options: { clip: { type: "Sphere" } } },
{ name: "Bonne", value: d3.geoBonne },
{ name: "Bottomley", value: d3.geoBottomley },
{ name: "Bromley", value: d3.geoBromley },
// { name: "Butterfly (gnomonic)", value: d3.geoPolyhedralButterfly },
// { name: "Butterfly (Collignon)", value: d3.geoPolyhedralCollignon },
// { name: "Butterfly (Waterman)", value: d3.geoPolyhedralWaterman },
{ name: "Cahill-Keyes", value: d3.geoCahillKeyes },
// { name: "Collignon", value: d3.geoCollignon },
// {name: "conic conformal", value: d3.geoConicConformal}, // Not suitable for world maps.
{ name: "conic equal-area", value: d3.geoConicEqualArea },
{ name: "conic equidistant", value: d3.geoConicEquidistant },
// { name: "Craig retroazimuthal", value: d3.geoCraig },
// { name: "Craster parabolic", value: d3.geoCraster },
{ name: "Cox", value: d3.geoCox },
// { name: "cubic", value: d3.geoCubic },
// { name: "cylindrical equal-area", value: d3.geoCylindricalEqualArea },
// { name: "cylindrical stereographic", value: d3.geoCylindricalStereographic },
{ name: "dodecahedral", value: d3.geoDodecahedral },
{ name: "Eckert I", value: d3.geoEckert1 },
{ name: "Eckert II", value: d3.geoEckert2 },
{ name: "Eckert III", value: d3.geoEckert3 },
{ name: "Eckert IV", value: d3.geoEckert4 },
{ name: "Eckert V", value: d3.geoEckert5 },
{ name: "Eckert VI", value: d3.geoEckert6 },
// { name: "Eisenlohr conformal", value: d3.geoEisenlohr },
{ name: "Equirectangular (plate carrée)", value: d3.geoEquirectangular },
// { name: "Fahey pseudocylindrical", value: d3.geoFahey },
// { name: "flat-polar parabolic", value: d3.geoMtFlatPolarParabolic },
// { name: "flat-polar quartic", value: d3.geoMtFlatPolarQuartic },
// { name: "flat-polar sinusoidal", value: d3.geoMtFlatPolarSinusoidal },
// { name: "Foucaut’s stereographic equivalent", value: d3.geoFoucaut },
// { name: "Foucaut’s sinusoidal", value: d3.geoFoucautSinusoidal },
// { name: "general perspective", value: d3.geoSatellite },
// {name: "Gilbert’s two-world", value: d3.geoGilbert}, // https://github.com/d3/d3-geo-projection/issues/165
// { name: "Gingery", value: d3.geoGingery, options: { clip: { type: "Sphere" } } },
// { name: "Ginzburg V", value: d3.geoGinzburg5 },
// { name: "Ginzburg VI", value: d3.geoGinzburg6 },
// { name: "Ginzburg VIII", value: d3.geoGinzburg8 },
// { name: "Ginzburg IX", value: d3.geoGinzburg9 },
{ name: "Goode’s homolosine", value: d3.geoHomolosine },
{
name: "Goode’s homolosine (interrupted)",
value: d3.geoInterruptedHomolosine,
options: { clip: { type: "Sphere" } }
},
// { name: "gnomonic", value: d3.geoGnomonic },
// { name: "Gringorten square", value: d3.geoGringorten },
// { name: "Gringorten quincuncial", value: d3.geoGringortenQuincuncial },
// { name: "Guyou square", value: d3.geoGuyou },
{ name: "Hammer", value: d3.geoHammer },
// { name: "Hammer retroazimuthal", value: d3.geoHammerRetroazimuthal, options: { clip: { type: "Sphere" } } },
// { name: "HEALPix", value: d3.geoHealpix, options: { clip: { type: "Sphere" } } },
{ name: "Hill eucyclic", value: d3.geoHill },
// { name: "Hufnagel pseudocylindrical", value: d3.geoHufnagel },
// { name: "icosahedral", value: d3.geoIcosahedral },
{ name: "Imago", value: d3.geoImago },
// { name: "Kavrayskiy VII", value: d3.geoKavrayskiy7 },
// { name: "Lagrange conformal", value: d3.geoLagrange },
{ name: "Larrivée", value: d3.geoLarrivee },
// { name: "Laskowski tri-optimal", value: d3.geoLaskowski },
// {name: "Littrow retroazimuthal", value: d3.geoLittrow}, // Not suitable for world maps.
// { name: "Loximuthal", value: d3.geoLoximuthal },
{ name: "Mercator", value: d3.geoMercator },
{ name: "Miller cylindrical", value: d3.geoMiller },
{ name: "Mollweide", value: d3.geoMollweide },
// {
// name: "Mollweide (Goode’s interrupted)",
// value: d3.geoInterruptedMollweide,
// options: { clip: { type: "Sphere" } }
// },
// {
// name: "Mollweide (interrupted hemispheres)",
// value: d3.geoInterruptedMollweideHemispheres,
// options: { clip: { type: "Sphere" } }
// },
// { name: "Natural Earth", value: d3.geoNaturalEarth1 },
// { name: "Natural Earth II", value: d3.geoNaturalEarth2 },
// { name: "Nell–Hammer", value: d3.geoNellHammer },
// { name: "Nicolosi globular", value: d3.geoNicolosi },
{ name: "orthographic", value: d3.geoOrthographic },
// { name: "Patterson cylindrical", value: d3.geoPatterson },
{ name: "Peirce quincuncial", value: d3.geoPeirceQuincuncial },
// { name: "rectangular polyconic", value: d3.geoRectangularPolyconic },
{ name: "Robinson", value: d3.geoRobinson },
// { name: "sinusoidal", value: d3.geoSinusoidal },
// { name: "sinusoidal (interrupted)", value: d3.geoInterruptedSinusoidal, options: { clip: { type: "Sphere" } } },
{ name: "sinu-Mollweide", value: d3.geoSinuMollweide },
// { name: "sinu-Mollweide (interrupted)", value: d3.geoInterruptedSinuMollweide, options: { clip: { type: "Sphere" } } },
{ name: "stereographic", value: d3.geoStereographic },
{ name: "Lee’s tetrahedal", value: d3.geoTetrahedralLee },
{ name: "Times", value: d3.geoTimes },
// { name: "Tobler hyperelliptical", value: d3.geoHyperelliptical },
// { name: "transverse Mercator", value: d3.geoTransverseMercator },
// { name: "Van der Grinten", value: d3.geoVanDerGrinten },
// { name: "Van der Grinten II", value: d3.geoVanDerGrinten2 },
// { name: "Van der Grinten III", value: d3.geoVanDerGrinten3 },
// { name: "Van der Grinten IV", value: d3.geoVanDerGrinten4 },
// { name: "Wagner IV", value: d3.geoWagner4 },
// { name: "Wagner VI", value: d3.geoWagner6 },
// { name: "Wagner VII", value: d3.geoWagner7 },
// { name: "Werner", value: d3.geoBonne ? () => d3.geoBonne().parallel(90) : null },
// { name: "Wiechel", value: d3.geoWiechel },
{ name: "Winkel tripel", value: d3.geoWinkel3 }
].filter((p) => p.value)
Insert cell
controls = ({
geostitch: () =>
Inputs.toggle({
label: "geostitch",
value: true
}),
guf: () =>
Inputs.select(
new Map([
["France (FRA)", "FRA"],
["Stand-alone (GUF)", "GUF"] // although useful, this creates a real issue with ids
]),
{ label: "Guyane" }
),
abyi: () =>
Inputs.select(
new Map([
["Sudan (SDN)", "SDN"],
["South Sudan (SSD)", "SSD"],
["Abyei (stand-alone, ABYI)", "ABYI"]
]),
{ label: "Abyei" }
),
crma: () =>
Inputs.select(
new Map([
["Ukraine (UKR)", "UKR"],
["Russia (RUS)", "RUS"],
["Crimea (stand-alone, CRMA)", "CRMA"]
]),
{ label: "Crimea" }
),
esh: () =>
Inputs.form({
esh: Inputs.select(
new Map([
["United Nations", "ESH"],
["Morocco", "MAR"],
["Disputed (stand-alone piece)", "UN"]
]),
{ label: "Western Sahara" }
),
cany: Inputs.select(
new Map([
["Spain", "ESP"],
["Stand-alone (CANY)", "CANY"]
]),
{ label: "Canary Islands - 50m" }
)
}),
kash: () =>
Inputs.form({
jamm: Inputs.select(
new Map([
["India (IND)", "IND"],
["Kashmir (KASH)", "KASH"],
["Jammu & Kashmir (stand-alone, JAMM)", "JAMM"]
]),
{ label: "Jammu & Kashmir" }
),
gilg: Inputs.select(
new Map([
["Pakistan (PAK)", "PAK"],
["Kashmir (KASH)", "KASH"],
["Gilgit and Azad (stand-alone, GILG)", "GILG"]
]),
{ label: "Gilgit and Azad" }
),
aksa: Inputs.select(
new Map([
["China (CHN)", "CHN"],
["Kashmir (KASH)", "KASH"],
["Aksai Chin (stand-alone, AKSA)", "AKSA"]
]),
{ label: "Aksai Chin" }
),
...(apply_sources === "50m" && {
kas: Inputs.select(
new Map([
["Siachen Glacier (stand-alone, KAS)", "KAS"],
["Kashmir (KASH)", "KASH"]
]),
{ label: "Siachen Glacier" }
)
})
}),
twn: () =>
Inputs.select(
new Map([
["Taïwan (TWN)", "TWN"],
["China (CHN)", "CHN"]
]),
{ label: "Taïwan" }
),
sol: () =>
Inputs.select(
new Map([
["Somaliland (SOL)", "SOL"]
// ["Somalia (SOM)", "SOM"]
]),
{ label: "Somaliland" }
)
})
Insert cell
localGeojson = () =>
o2021_countries110m
.zip()
.then((z) =>
Promise.all([
z.file(`${o2021_countries110m.shpsource}.shp`).arrayBuffer(),
z.file(`${o2021_countries110m.shpsource}.dbf`).arrayBuffer()
])
)
.then(([shp, dbf]) => shapefile.read(shp, dbf, { encoding }))
Insert cell
o2017Geojson = () =>
o2017_countries110m
.zip()
.then((z) =>
Promise.all([
z.file(`${o2017_countries110m.shpsource}.shp`).arrayBuffer(),
z.file(`${o2017_countries110m.shpsource}.dbf`).arrayBuffer()
])
)
.then(([shp, dbf]) => shapefile.read(shp, dbf, { encoding }))
Insert cell
o2018Geojson = () =>
o2018_countries110m
.zip()
.then((z) =>
Promise.all([
z.file(`${o2018_countries110m.shpsource}.shp`).arrayBuffer(),
z.file(`${o2018_countries110m.shpsource}.dbf`).arrayBuffer()
])
)
.then(([shp, dbf]) => shapefile.read(shp, dbf, { encoding }))
Insert cell
o2019Geojson = () =>
o2019_countries110m
.zip()
.then((z) =>
Promise.all([
z.file(`${o2019_countries110m.shpsource}.shp`).arrayBuffer(),
z.file(`${o2019_countries110m.shpsource}.dbf`).arrayBuffer()
])
)
.then(([shp, dbf]) => shapefile.read(shp, dbf, { encoding }))
Insert cell
tiny_both = async () => {
const geojsons = {
countries: await FileAttachment("tiny_countries@2.json").json(),
countries_lakes: await FileAttachment("tiny_countries_lakes@2.json").json()
};

return topojson.topology(geojsons);
}
Insert cell
localGeojson50m = () =>
Promise.all(
[
FileAttachment("ne_50m_admin_0_countries.zip"),
FileAttachment("ne_50m_admin_0_countries_lakes.zip"),
FileAttachment("ne_50m_admin_0_breakaway_disputed_areas.zip")
].map((d) =>
d
.zip()
.then((z) =>
Promise.all([
z.file(z.filenames.find((d) => d.endsWith(".shp"))).arrayBuffer(),
z.file(z.filenames.find((d) => d.endsWith(".dbf"))).arrayBuffer()
])
)
.then(([shp, dbf]) => shapefile.read(shp, dbf, { encoding }))
)
).then(([countries, countries_lakes, disputed]) =>
topojson.topology({
countries,
countries_lakes,
disputed
})
)
Insert cell
SOURCES = [
{
name: "110m",
description: "retrieves both 'tiny' countries with and without lakes",
process: tiny_both,
disabled: false
},
{
name: "50m",
description: "official zip from Natural Earth",
process: localGeojson50m,
disabled: true
}
]
Insert cell
Insert cell
Insert cell
mutable berm = null // will be used to add to the disputedMesh
Insert cell
mutable debug = null
Insert cell
function featureTransform(a, transform) {
return topojson.topology(
Object.fromEntries(
Object.entries(a.objects).map(([key, o]) => [
key,
transform(topojson.feature(a, o), key)
])
)
);
}
Insert cell
who_regions = FileAttachment("who_regions.csv")
.csv()
.then(
(data) =>
new Map([
...data.map((d) => [d.ISO_3_CODE, d.WHO_REGION]),
["WEST", "AFRO"], // like Western Sahara
["XKX", "EURO"], // Kosovo…
["SOL", "EMRO"], // Somaliland = Same WHO region as SOMALIA
["PSX", "EMRO"], // Palestine
["GUF", "AMRO"], // French Guyane is a member of PAHO
["ABYI", "EMRO"], // like Sudan
["CRMA", "EURO"], // Crimea
["AKSA", "WPRO"], // like China
["GILG", "EMRO"], // like Pakistan
["JAMM", "SEARO"], // like India
["TWN", "WPRO"] // Taiwan is either WPRO or UNKNOWN…
])
)
Insert cell
colors = [].concat(d3.schemeTableau10).slice(0, 9)
Insert cell
worlds = {
let a = {};
const steps = [];
for (const { name, process } of SOURCES) {
if (name === apply_sources) {
a = await process(a);
steps.push(a);
}
}

// geo to topo
if (!a.objects) {
a = topojson.topology({ countries: a });
}

// compute and transform the properties
// then index them and add the relevant IDs to the countries objects
let props = topojson.feature(a, a.objects.countries);
for (const { name, process } of PIPELINE_PROPERTIES) {
if (apply_properties.includes(name)) {
props = await process(props);
steps.push(props);
}
}
const properties = d3.index(
props.features.map((d) => d.properties),
(d) => d.adm0_a3
);
a.objects.countries.geometries.forEach((d) => {
d.id = d.properties.adm0_a3;
d.properties = {
adm0_a3: d.id,
iso_n3: d.properties.iso_n3, // https://en.wikipedia.org/wiki/ISO_3166-1_numeric
// note: we can't always use this as an ID since Guyane has the same as Metro France
name: d.properties.name
};
});

if (a.objects.countries_lakes) {
a.objects.countries_lakes.geometries.forEach((d) => {
if (d.properties.ADM0_A3 === "SDS") d.properties.ADM0_A3 = "SSD";
if (d.properties.ADM0_A3 === "SAH") d.properties.ADM0_A3 = "ESH";
d.id = d.properties.adm0_a3 || d.properties.ADM0_A3;
d.properties = { adm0_a3: d.id };
});
}

// geostitch
if (geostitch) {
a = featureTransform(a, d3.geoStitch);
}

for (const { name, process } of PIPELINE) {
if (apply_steps.includes(name)) {
a = await process(a, properties);
steps.push(a);
}
}

steps.push(a);

const piece = {
featurecla: "PIECE",
scalerank: 3,
labelrank: 6,
level: 3,
type: "Piece"
};
properties.set("WEST", {
...piece,
name: "Western part of Western Sahara",
continent: "Africa",
region_un: "Africa",
region_wb: "Middle East & North Africa"
});
properties.set("CRMA", {
...piece,
name: "Crimea",
continent: "Europe",
region_un: "Europe",
region_wb: "Europe & Central Asia"
});
properties.set("CANY", {
...piece,
name: "Canary Islands",
continent: "Africa",
region_un: "Europe", // ?
region_wb: "Europe & Central Asia" // ?
});
properties.set("GUF", {
...piece,
name: "Guyane",
continent: "South America",
region_un: "Americas",
region_wb: "Europe & Central Asia"
});
properties.set("ABYI", {
...piece,
name: "Abyei",
continent: "Africa",
region_un: "Africa",
region_wb: "Sub-Saharan Africa"
});
properties.set("AKSA", {
...piece,
name: "Aksai Chin",
continent: "Asia",
region_un: "Asia",
region_wb: "East Asia & Pacific" // like CHN
});
properties.set("GILG", {
...piece,
name: "Gilgit and Azad",
continent: "Asia",
region_un: "Asia",
region_wb: "South Asia" // like PAK
});
properties.set("JAMM", {
...piece,
name: "Jammu & Kashmir",
continent: "Asia",
region_un: "Asia",
region_wb: "South Asia" // like IND
});
properties.set("KASH", {
...piece,
name: "Kashmir Region",
continent: "Asia",
region_un: "Asia",
region_wb: "Asia"
});

properties.get("PSX").region_who = "EMRO";

steps.properties = properties;

return steps;
}
Insert cell
outTopo = worlds[worlds.length - 1]
Insert cell
properties = worlds.properties
Insert cell
countries = topojson.feature(outTopo, outTopo.objects.countries)
Insert cell
countries_lakes = outTopo.objects.countries_lakes &&
topojson.feature(outTopo, outTopo.objects.countries_lakes)
Insert cell
pieces = outTopo.objects.pieces && topojson.feature(outTopo, outTopo.objects.pieces)
Insert cell
disputed = outTopo.objects.disputed &&
topojson.feature(outTopo, outTopo.objects.disputed)
Insert cell
// Base topojson has all the countries and lakes and disputed areas
// Each zone is marked as a (multi)polygon
// We'll build countries up from multiple zones.
baseTopo = {
const topo = topojson.topology({
...(countries && { countries }),
...(countries_lakes && { countries_lakes }),
...(pieces && { pieces }),
...(disputed && { disputed }),
...(berm && { berm: berm.berm })
});

// merge optional pieces to alternatives
if (disputed) {
topo.objects.alternatives = {
type: "GeometryCollection",
geometries: d3
.rollups(
topo.objects.pieces.geometries,
(v) => [
Object.assign(
v.find((d) => d.id === d.properties.adm0_a3)?.properties || {
...v[0].properties,
name: v.map((d) => d.id).join(" + ")
},
{
pieces: v.map((d) => d.id).join(" + "),
...(v.find((d) => d.id === "FRA") && { name: "France" })
}
),
topojson.mergeArcs(topo, v)
],
({ id }) => {
if (id === "WEST") return ctr_esh.esh; // "MAR", "SAH"
if (id === "ABYI") return ctr_abyi; // "SDN", "ABYI", "SSD"
if (id === "AKSA") return ctr_kash.aksa; // "CHN", "KASH"
if (id === "GILG") return ctr_kash.gilg; // "PAK", "KASH"
if (id === "JAMM") return ctr_kash.jamm; // "IND", "KASH"
if (id === "KAS") return ctr_kash.kas; // Siachen glacier
if (id === "GUF") return ctr_guf; // "GUF", "FRA"
if (id === "CRMA") return ctr_crma; // "UKR", "RUS", "CRMA"
if (id === "TWN") return ctr_twn; // "TWN", "CHN"
return id;
}
)
.map(([id, [properties, shape]]) => ({
id,
...shape,
properties
}))
};
}
return topo;
}
Insert cell
land = ({
type: "GeometryCollection",
geometries: [
Object.assign(
topojson.mergeArcs(baseTopo, baseTopo.objects.pieces.geometries),
{ properties: { name: "land" }, id: "land" }
)
]
})
Insert cell
land_lakes = ({
type: "GeometryCollection",
geometries: [
Object.assign(
topojson.mergeArcs(baseTopo, baseTopo.objects.countries_lakes.geometries),
{ properties: { name: "land_lakes" }, id: "land_lakes" }
)
]
})
Insert cell
ocean = {
const ocean = JSON.parse(JSON.stringify(land.geometries[0]));
ocean.id = "ocean";
ocean.properties = { name: "ocean", fill: "#add8e6" };

ocean.arcs = [
ocean.arcs
.sort(([a], [b]) => b.length - a.length) // Eurasia, Americas, …
.map(([d]) =>
d
.slice()
.reverse()
.map((d) => -d - 1)
),
ocean.arcs.flatMap((
[, ...holes] // Caspian Sea
) =>
holes.map((d) =>
d
.slice()
.reverse()
.map((d) => -d - 1)
)
)
];
return { type: "GeometryCollection", geometries: [ocean] };
}
Insert cell
continents = ({
type: "GeometryCollection",
geometries: [
...d3.rollup(
baseTopo.objects.pieces.geometries,
(v) => topojson.mergeArcs(baseTopo, v),
(d) => properties.get(d.id)?.continent || `UNKNOWN: ${d.id}`
)
].map(([continent, o], i) =>
Object.assign(o, {
id: continent,
properties: { name: continent, fill: d3.schemeAccent[i % 10] }
})
)
})
Insert cell
region_un = ({
type: "GeometryCollection",
geometries: [
...d3.rollup(
baseTopo.objects.pieces.geometries,
(v) => topojson.mergeArcs(baseTopo, v),
(d) => properties.get(d.id)?.region_un
)
].map(([region_un, o], i) =>
Object.assign(o, {
id: region_un,
properties: { name: region_un, fill: d3.schemeAccent[i % 10] }
})
)
})
Insert cell
region_who = ({
type: "GeometryCollection",
geometries: [
...d3.rollup(
baseTopo.objects.pieces.geometries,
(v) => topojson.mergeArcs(baseTopo, v),
(d) => who_regions.get(d.id) || `UNKNOWN: ${d.id}`
)
].map(([region_who, o], i) =>
Object.assign(o, {
id: region_who,
properties: { name: region_who, fill: d3.schemeAccent[i % 10] }
})
)
})
Insert cell
region_wb = ({
type: "GeometryCollection",
geometries: [
...d3.rollup(
baseTopo.objects.pieces.geometries,
(v) => topojson.mergeArcs(baseTopo, v),
(d) => properties.get(d.id)?.region_wb || `UNKNOWN: ${d.id}`
)
].map(([region_wb, o], i) =>
Object.assign(o, {
id: region_wb,
properties: { name: region_wb, fill: d3.schemeAccent[i % 10] }
})
)
})
Insert cell
disputed_kashmir = baseTopo.objects.disputed && {
type: "GeometryCollection",
geometries: [
Object.assign(
topojson.mergeArcs(
baseTopo,
baseTopo.objects.disputed.geometries.filter((d) =>
["AKSA", "JAMM", "GILG", "KASH", "KAS"].includes(d.id)
)
),
{ id: "KASH", properties: { name: "KASH" } }
)
]
}
Insert cell
disputed_abyei = baseTopo.objects.disputed && {
type: "GeometryCollection",
geometries: [
Object.assign(
topojson.mergeArcs(
baseTopo,
baseTopo.objects.disputed.geometries.filter((d) => d.id === "ABYI")
),
{ id: "ABYI", properties: { name: "Abyei" } }
)
]
}
Insert cell
mesh = (topo, objects, name = "mesh") => ({
type: "GeometryCollection",
geometries: [
Object.assign(
topojson.meshArcs(topo, objects, (a, b) => a !== b),
{ id: name, properties: { name } }
)
]
})
Insert cell
taggedMesh = taggedBorders(baseTopo, baseTopo.objects.countries.geometries, (a, b) =>
a && b && a !== b ? [a.id, b.id].sort().join(".") : null
)
Insert cell
// TODO: for ESH.MAR, this could include the opposite shapes (?)
disputedMesh = {
if (!baseTopo.objects.pieces) return;

const borders = taggedBorders(
baseTopo,
baseTopo.objects.pieces.geometries,
(a, b) => (a && b && a !== b ? [a.id, b.id].sort().join(".") : null)
);
const disput = taggedBorders(
baseTopo,
baseTopo.objects.disputed.geometries,
(a, b) => (a && b && a !== b ? [a.id, b.id].sort().join(".") : null)
);
return {
type: "GeometryCollection",
geometries: [
...[baseTopo.objects.berm] /*"ESH.MAR"*/,
...borders.geometries
.filter((d) =>
["CRMA.UKR", /*"ESH.MAR",*/ "ABYI.SDN"].includes(d.properties.name)
)
.map((d) => ((d.arcs = [d.arcs[0]]), d)),
...disput.geometries.filter((d) => d.properties.name === "GILG.JAMM")
]
};
}
Insert cell
topo = {
const topo = {
...baseTopo,
objects: {
ocean,
land,
land_lakes,
...(continents && { continents }),
...(region_un && { region_un }),
...(region_who && { region_who }),
//income_grp,
...(region_wb && { region_wb }),
...(baseTopo.objects.pieces && { pieces: baseTopo.objects.pieces }),
countries: baseTopo.objects.countries,
...(baseTopo.objects.countries_lakes && {
countries_lakes: baseTopo.objects.countries_lakes
}),
...(baseTopo.objects.alternatives && {
alternatives: baseTopo.objects.alternatives
}),
...(baseTopo.objects.disputed && {
disputed: baseTopo.objects.disputed,
disputed_abyei,
disputed_kashmir
}),
taggedMesh,
...(disputedMesh && { disputedMesh }),
arcsMesh: {
type: "GeometryCollection",
geometries: d3.range(baseTopo.arcs.length).map((i) => ({
id: `arc${i}`,
properties: { name: `arc${i}` },
type: "LineString",
arcs: [i]
}))
}
}
};

for (let key in topo.objects) {
removeSelfNeighbors(topo, topo.objects[key].geometries);
}
return topo;
}
Insert cell
function removeSelfNeighbors(topo, geometries) {
for (const geom of geometries) {
switch (geom.type) {
case "Polygon":
geom.arcs = geom.arcs.map(arcClean).filter((d) => d.length > 0);
break;
case "MultiPolygon":
geom.arcs = geom.arcs
.map((arcs) => arcs.map(arcClean).filter((d) => d.length > 0))
.filter((d) => d.length > 0);
break;
}
}
return geometries;

function arcClean(a) {
let changed = false;

// [a, a]
for (let i = 1; i < a.length; ++i) {
if (a[i] === a[i - 1]) {
a = a.slice();
delete a[i - 1];
changed = true;
}
}

// [a, -1-a]
for (let i = 1; i < a.length; ++i) {
if (a[i] === -1 - a[i - 1]) {
a = a.slice();
delete a[i - 1];
delete a[i];
changed = true;
}
}

if (changed) {
a = a.filter((d) => d != null);
if (a.length === 1 && topo.arcs[a[0] < 0 ? -1 - a[0] : a[0]].length === 2)
a = [];
}
return a;
}
}
Insert cell
tests = {
let a = countries;
const tests = [];
for (const { name, test, disabled } of PIPELINE) {
if (test && !disabled) {
const result = await test(a);
tests.push({ name, result });
}
}
return tests;
}
Insert cell
// detach Guyane from France when it makes sense,
function detachGuyane(topo) {
return detachPolygon(
topo,
topo.objects.countries.geometries,
d => d.id === "FRA",
[-53.1258, 3.9339],
{ region_un: "Americas", continent: "South America" },
"GUF"
);
}
Insert cell
// option to detach Crimea from Russia
function detachCrma(topo, geometries, reason) {
return detachPolygon(
topo,
geometries,
(d) => d.id === "RUS",
[34.4997, 45.3453],
reason,
"CRMA"
);
}
Insert cell
function detachPolygons(geometries, test, indices, reason = {}, id) {
return geometries.flatMap((d) => {
if (test(d)) {
return [
{
id,
type: "MultiPolygon",
arcs: d.arcs.filter((d, i) => indices.includes(i)),
properties: { ...d.properties, ...reason }
},
{
id: d.id,
type: "MultiPolygon",
arcs: d.arcs.filter((d, i) => !indices.includes(i)),
properties: d.properties
}
].map((d) =>
d.arcs.length === 1 ? { ...d, type: "Polygon", arcs: d.arcs[0] } : d
);
} else return [d];
});
}
Insert cell
function detachPolygon(topo, geometries, test, coords, reason = {}, id) {
return geometries.flatMap((d) => {
if (test(d)) {
const centroids = topojson
.feature(topo, {
type: "GeometryCollection",
geometries: d.arcs.map((arc) => ({
type: "Polygon",
arcs: arc
}))
})
.features.map(d3.geoCentroid);
const closest = d3.leastIndex(centroids, (c) =>
d3.geoDistance(c, coords)
);

return [
{
id,
type: "MultiPolygon",
arcs: d.arcs.filter((d, i) => i === closest),
properties: { ...d.properties, ...reason }
},
{
id: d.id,
type: "MultiPolygon",
arcs: d.arcs.filter((d, i) => i !== closest),
properties: d.properties
}
].map((d) =>
d.arcs.length === 1 ? { ...d, type: "Polygon", arcs: d.arcs[0] } : d
);
} else return [d];
});
}
Insert cell
async function map({
center,
scale,
projection_ = d3.geoMercator(),
displayObjects,
t = topo,
sphere,
visibility
} = {}) {
const topo = t;
// projection_.rotate([-90, 0]);
visibility && (await visibility());
const height = (width * 0.58) | 0;
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width);

const hh = DOM.uid("hatched");
svg.append("defs")
.html(`<pattern id="${hh.id}" patternUnits="userSpaceOnUse" width="4" height="4">
<path d="M-1,1 l2,-2
M0,4 l4,-4
M3,5 l2,-2"
style="stroke: grey; stroke-width: .25" />
</pattern>
<style>
#disputedMesh {
stroke-dasharray: 4 6;
}
</style>`);
if (center) {
projection_.rotate(d3.geoCentroid(center).map((d) => -d));
} else {
projection_.fitExtent(
[
[3, 3],
[width - 3, height - 3]
],
{ type: "Sphere" }
);
}
if (scale) projection_.scale(scale);

const path = d3.geoPath(projection_);

const g = svg.append("g").attr("fill-opacity", 0.1);

if (sphere) {
g.append("g")
.attr("id", "sphere")
.selectAll()
.data([{ type: "Sphere" }])
.join("path")
.attr("d", path)
.attr("fill", "lightblue")
.attr("stroke", "black");
}

displayObjects.forEach((layer) => {
g.append("g")
.attr("id", layer)
.attr("fill-opacity", layer.startsWith("disputed") ? 1 : 0.1)
.selectAll()
.data(topojson.feature(topo, topo.objects[layer]).features)
.join("path")
.attr("d", path)
.attr(
"stroke",
layer === "land" || layer.endsWith("Mesh")
? d3.scaleOrdinal(["black", ...d3.schemeTableau10])
: "none"
)
.attr("fill", (d) =>
layer === "land" || layer.endsWith("Mesh")
? "none"
: layer.startsWith("disputed")
? `${hh}` //"url(#disputedHatch)"
: d.properties.fill
)
.append("title")
.text((d) => `${d.properties.name || d.id}`);
});

displayObjects.forEach((layer) => {
g.append("g")
.attr("id", `${layer}-mesh`)
.attr("fill", "none")
.attr("stroke-width", 0.5)
.selectAll()
.data(
topojson.feature(topo, mesh(topo, topo.objects[layer], `${layer}-mesh`))
.features
)
.join("path")
.attr("d", path)
.attr("stroke", d3.scaleOrdinal(["black", ...d3.schemeTableau10]));
});

return svg.node();
}
Insert cell
// https://bl.ocks.org/mbostock/8ba407a7a53d80be62a8d54774663c2f
// Usage:
// topojson.mesh(topo, topo.objects.countries, border("Ukraine", "Crimea")))
function border(id0, id1) {
return function (a, b) {
return (
(a.properties.name === id0 && b.properties.name === id1) ||
(a.properties.name === id1 && b.properties.name === id0)
);
};
}
Insert cell
// find the closest coordinates of point x in the target geojson
coordsMatcher = (geo, { precision = 0.0003, snap } = {}) => {
const c = geoVertices(geo);
const voronoi = d3.geoVoronoi(c);
return function (points) {
let snapped = 0;
for (let i = 0; i < points.length; i++) {
const point = points[i];
const candidate = c[voronoi.find(...point)];
const distance = d3.geoDistance(candidate, point);
const ok = distance < precision;
snapped += ok;
if (ok) points[i] = candidate;
}
if (false && snapped !== snap) {
if (snap != null) throw new Error(`points snapped: ${snapped}/${snap}`);
else console.info(`points snapped: ${snapped}`);
}
return points;
};
}
Insert cell
kashmir = ({
type: "FeatureCollection",
id: "GILG",
features: [
{
type: "Feature",
id: "GILG",
properties: {
name: "Gilgit and Azad"
},
geometry: {
type: "Polygon",
coordinates: [
[
[73.839111328125, 32.95336814579932],
[74.454345703125, 32.75494243654723],
[74.102783203125, 33.44060944370356],
[73.76220703125, 34.334364487026306],
[74.234619140625, 34.75966612466248],
[75.7177734375, 34.50655662164561],
[76.893310546875, 34.66935854524543],
[77.80517578125, 35.53222622770337],
[76.1956787109375, 35.902399875143615],
[75.8880615234375, 36.672824886786564],
[75.146484375, 37.142803443716836],
[74.57244873046875, 37.02229110771148],
[74.05609130859375, 36.83566824724438],
[72.92724609375, 36.721273880045004],
[72.50976562499999, 36.12900165569652],
[72.7734375, 35.746512259918504],
[73.17993164062499, 35.782170703266075],
[74.02587890625, 35.02999636902566],
[73.377685546875, 34.488447837809304],
[73.839111328125, 32.95336814579932]
].reverse()
]
}
},
{
type: "Feature",
id: "JAMM",
properties: {
stroke: "#555555",
"stroke-width": 2,
"stroke-opacity": 1,
fill: "#555555",
"fill-opacity": 0.5,
name: "Jammu Kashmir"
},
geometry: {
type: "Polygon",
coordinates: [
[
[75.772705078125, 32.879587173066305],
[76.79443359375, 33.128351191631566],
[78.46435546875, 32.62087018318113],
[79.178466796875, 32.491230287947594],
[79.2333984375, 32.96258644191747],
[78.804931640625, 33.52307880890422],
[78.90380859375, 34.32529192442733],
[77.838134765625, 35.523285179107816],
[76.88232421875, 34.65128519895413],
[75.706787109375, 34.49750272138159],
[74.24560546875, 34.75966612466248],
[73.751220703125, 34.334364487026306],
[74.10003662109374, 33.43831750748322],
[74.44335937499999, 32.75494243654723],
[75.26184082031249, 32.26855544621476],
[75.772705078125, 32.879587173066305]
].reverse()
]
}
},
{
type: "Feature",
id: "AKSA",
properties: {
stroke: "#555555",
"stroke-width": 2,
"stroke-opacity": 1,
fill: "#555555",
"fill-opacity": 0.5,
name: "Aksai-Chin"
},
geometry: {
type: "Polygon",
coordinates: [
[
[80.37597656249999, 35.505400093441324],
[79.332275390625, 36.00467348670187],
[77.838134765625, 35.523285179107816],
[78.914794921875, 34.34343606848294],
[78.826904296875, 33.52307880890422],
[79.22241210937499, 32.98102014898148],
[79.178466796875, 32.491230287947594],
[79.617919921875, 32.7872745269555],
[79.024658203125, 33.779147331286474],
[80.068359375, 34.66935854524543],
[80.37597656249999, 35.505400093441324]
].reverse()
]
}
}
]
})
Insert cell
abyei = ({
type: "Feature",
id: "ABYI",
properties: { name: "Abyei" },
geometry: {
type: "Polygon",
coordinates: [
[
[27.850341796875, 10.17437402751379],
[29.003906249999996, 10.163560279490476],
[29.0093994140625, 9.606166114941981],
[28.965454101562504, 9.400290685848121],
[27.9656982421875, 9.400290685848121],
[27.833862304687496, 9.606166114941981],
[27.850341796875, 10.17437402751379]
]
]
}
})
Insert cell
Insert cell
d3 = require("d3@7", "d3-geo-projection@3", "d3-geo-voronoi@1")
Insert cell
topojson = require("topojson-server@3", "topojson-simplify@3", "topojson-client@3")
Insert cell
shapefile = require("shapefile@0.6")
Insert cell
import { remote } from "@observablehq/remote"
Insert cell
import { diff } from "@jobleonard/diff"
Insert cell
// https://observablehq.com/@fil/geoformat
function geoformat(n, exclude = [], align = true) {
if (typeof n === "undefined") n = 14;
function q(o) {
if (
align &&
typeof o === "object" &&
typeof o[0] === "object" &&
typeof o[0][0] === "number"
)
return "°°°[" + o.map(q).join(",") + "]°°°";
if (align && typeof o === "object" && typeof o[0] === "number")
return "°°°[" + o.map(q).join(",") + "]°°°";
if (typeof o === "object") return o.map(q);
if (typeof o === "number") return +o.toFixed(n);
return o;
}
return function (x) {
let st = JSON.stringify(
x,
function (key, val) {
if (key === "coordinates") {
return q(val);
}
if (exclude.indexOf(key) > -1) return undefined;
return val;
},
2
);
return align
? st
.replace(/"°°°/g, "")
.replace(/°°°",\n */g, ", ")
.replace(/°°°"/g, "")
.replace(/°°°/g, "")
// no repeats
.replace(/(\[[\.\-0123456789]+,[\.\-0123456789]+\])(,\1)+,/g, "$1,")
: st;
};
}
Insert cell
geoVertices = function (feature, tag) {
const vertices = [];
const stream = {
point:
tag === undefined
? (lambda, phi) => vertices.push([lambda, phi])
: (lambda, phi) => vertices.push([lambda, phi, tag]),
lineStart: function () {},
lineEnd: function () {},
polygonStart: function () {},
polygonEnd: function () {}
};
d3.geoStream(feature, stream);
return vertices;
}
Insert cell
/*
* A tagged border is an arc that belongs to two polygons with a non-null tag
*/
function taggedBorders(topo, geometries, tag = (a, b) => a !== b) {
const x = [];
for (const d of geometries) {
for (const i of d.type === "MultiPolygon"
? d.arcs.flat(2)
: d.type === "Polygon"
? d.arcs.flat(1)
: [])
x[i] = d;
}
const borders = new Map();

for (let i = 0; i < x.length; i++) {
const t = x[-i - 1] ? tag(x[i], x[-i - 1]) : null;
if (t != null && t !== false) {
borders.set(t, borders.get(t) || []);
borders.get(t).push(i);
}
}
return {
type: "GeometryCollection",
geometries: Array.from(borders, ([name, arcs]) => {
const lines = [];
for (const i of arcs) {
lines.push([i]);
}
return lines.length === 1
? {
id: name,
type: "LineString",
arcs: lines[0],
properties: { name }
}
: {
id: name,
type: "MultiLineString",
arcs: lines,
properties: { name }
};
})
};
}
Insert cell
// See https://observablehq.com/@fil/hello-pclip
// https://github.com/substack/pclip
pclip = Promise.all([
import("https://cdn.skypack.dev/pclip"),
import("https://cdn.skypack.dev/pclip/geo").then((d) => d.default),
import("https://cdn.skypack.dev/pclip/xy").then((d) => d.default)
]).then(([pclip, geo, xy]) =>
Object.assign(pclip.default, { ...pclip, geo, xy })
)
Insert cell
function remove_duplicate_points(ring) {
const r = [ring[0]];
for (let i = 1; i < ring.length; i++) {
if (d3.geoDistance(ring[i], ring[i - 1]) > 0.000001) r.push(ring[i]);
}
r.push(ring[0].slice());
return r;
}
Insert cell
fflate = import("https://cdn.skypack.dev/fflate?min")
Insert cell
function evaluate_compressed_size(text) {
if (typeof text !== "string") text = JSON.stringify(text);
return fflate.zipSync({ test: fflate.strToU8(text) }).length;
}
Insert cell
islands = FileAttachment("island-nations@1.csv").csv({ typed: true })
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more