Unlisted
Edited
Dec 19, 2022
1 fork
Importers
22 stars
Insert cell
Insert cell
Insert cell
CHOROPLETH = Plot.carto({
projection: "equalEarth",
rotate: [-10, 0],
color: { scheme: "rdylgn", legend: true },
marks: [
Plot.feature({ type: "Sphere" }, { fill: "lightblue" }),
Plot.feature(d3.geoGraticule10(), { stroke: "#fff", strokeWidth: 0.25 }),
Plot.feature(countries, { fill: "#333", dx: 1.5, dy: 1.5 }),
Plot.features(countries, {
fill: (d) => d.properties.income_grp,
strokeWidth: 0.25,
stroke: "#000",
title: (d) => d.properties.name_fr
}),
Plot.feature({ type: "Sphere" }, { strokeWidth: 2 })
],
width: width,
height: width / 2,
margin: 1
})
Insert cell
Plot.carto({
projection: "armadillo",
clipSphere: true, // 🌶 temporary: ideally the projection should tell us whether to clip or not
facet: {
data: countries.features,
y: (_, i) => i % 2
},
marks: [
Plot.feature({ type: "Sphere" }),
Plot.feature(land, { fill: "#999" }),
Plot.features(countries.features, {
fill: (d) => d.properties.name,
stroke: "white"
}),
Plot.features(data, { fill: "red" })
],
height: 500
})
Insert cell
data = [{type:"Point", coordinates: [0,0]}, {type:"Point", coordinates: [0,1]}]
Insert cell
LATLON = Plot.carto({
projection: "mercator",
marks: [
// 🚀 the very first mark defines the region of interest (TODO: design this)
Plot.feature({ type: "Sphere" }, { fill: "none" }),
Plot.feature(land, { fill: "black" }),

// transform 1: dataset to a single MultiPoint
Plot.features(
[
{ lat: 15, lon: 20 },
{ lat: -10, lon: 24.4 },
{ lon: -100, lat: 2 }
],
{
transform: (data, facets) => ({
facets,
data: [
{
type: "MultiPoint",
coordinates: data.map(({ lon, lat }) => [lon, lat])
}
]
}),
fill: "red"
}
),

// transform 2: dataset to as many Points
Plot.features(
[
{ lat: -15, lon: -20, name: "Cape Town" },
{ lat: 10, lon: -24.4, name: "Moscow" }
],
{
transform: (data, facets) => ({
facets,
data: data.map(({ lon, lat, name }) => ({
type: "Point",
name,
coordinates: [lon, lat]
}))
}),
fill: "blue",
title: "name"
}
),

// transform 3: dataset to polygon
Plot.features(
[
{ lat: -5, lon: -10 },
{ lat: 8, lon: -12.4 },
{ lat: -7, lon: 80 }
],
{
transform: (data, facets) => ({
facets,
data: [
{
type: "Polygon",
// the GeoJSON polygon needs to be closed, so we push point 0 at the end of the ring
coordinates: [
data
.slice()
.concat(data[0])
.map(({ lon, lat }) => [lon, lat])
],
properties: { hello: "Hello I'm a polygon" }
}
]
}),
fill: "rgba(100, 255, 100, 0.8)",
stroke: "lime",
title: (d) => d.properties.hello
}
),

Plot.feature({ type: "Sphere" }, { strokeWidth: 2 })
],
width: 600,
height: 600,
margin: 1
})
Insert cell
Insert cell
ORTHO = {
const svg = Plot.carto({
projection: "orthographic",
marks: [
// 🚀 the very first mark defines the region of interest (TODO: design this)
Plot.feature({ type: "Sphere" }, { fill: "none" }),
Plot.feature(land, { fill: "black" }),
Plot.feature({ type: "Sphere" }, { strokeWidth: 2 })
],
width: 600,
height: 600,
margin: 1
});

do {
svg.projection.rotate([Date.now() / 100, -35]);
svg.update();
yield svg;
} while (animate);
}
Insert cell
Plot.carto({
projection: "bertin1953",
r: { range: [0, 15] }, // 🌶 rescaling; the default r is a bit too large for this dataset
marks: [
Plot.feature({ type: "Sphere" }, { fill: "none" }),
Plot.feature(land, { fill: "black" }),
Plot.points(largeCities, {
lonLat: (d) => [d.lng, d.lat],
fill: "orange",
fillOpacity: 0.7,
r: "population"
}),
Plot.feature({ type: "Sphere" }, { strokeWidth: 2 })
],
width,
height: width * 0.7,
margin: 1
})
Insert cell
us = d3.json("https://unpkg.com/us-atlas@3/counties-10m.json")
Insert cell
topojson = require("topojson-client@3")
Insert cell
Plot.carto({
projection: "albersUsa",
r: { domain: [30000, 1e5], type: "log" },
color: { type: "log", scheme: "rdbu" },
marks: [
Plot.feature(topojson.feature(us, us.objects.nation)),
Plot.feature(topojson.feature(us, us.objects.counties), {
strokeWidth: 0.15
}),
Plot.feature(topojson.feature(us, us.objects.states), { strokeWidth: 0.5 }),
Plot.points(largeCities, {
filter: (d) => d.country === "United States",
lonLat: (d) => [d.lng, d.lat],
fill: "population",
fillOpacity: 0.7,
title: "city",
r: "population"
})
],
width,
height: width * 0.7,
margin: 0
})
Insert cell
{
const us = await FileAttachment("states-albers-10m.json").json();
const states = topojson.mesh(us, us.objects.states, (a, b) => a !== b);
const nation = topojson.feature(us, us.objects.nation);

const projection = d3.geoAlbersUsa().scale(1300).translate([487.5, 305]);
const parse = d3.utcParse("%m/%d/%Y");
const walmart = await d3
.tsv(await FileAttachment("walmart.tsv").url(), (d) => ({
pos: projection(d),
year: parse(d.date).getFullYear(),
decade: Math.floor(parse(d.date).getFullYear() / 10) * 10
}))
.then((data) => data.map((d) => ({ ...d.pos, ...d })));

const decades = d3.groupSort(
walmart,
(v) => v[0].decade,
(d) => d.decade
);

return Plot.carto({
facet: {
data: decades,
y: decades
},
fy: {
tickFormat: (d) => `${d}’s`
},
projection: "identity",
marks: [
Plot.feature(nation, { fill: "#f4f4f4", stroke: "black" }),
Plot.feature(states, { strokeWidth: 0.5 }),
Plot.features(decades, {
geometry: (decade) => ({
type: "MultiPoint",
coordinates: walmart
.filter((d) => d.decade < decade)
.map((d) => d.pos)
}),
fill: "black",
stroke: "#ccc",
strokeWidth: 0.5,
r: 2.5
}),
Plot.features(decades, {
geometry: (decade) => ({
type: "MultiPoint",
coordinates: walmart
.filter((d) => d.decade == decade)
.map((d) => d.pos)
}),
fill: "red",
stroke: "#fff",
strokeWidth: 0.5,
r: 2.5
})
],
width: width,
height: width * 3,
margin: 1,
marginLeft: 70
});
}
Insert cell
states = {
const us = await FileAttachment("states-albers-10m.json").json();
return topojson.feature(us, us.objects.states);
}
Insert cell
states.features.map((d) => d.geometry)
Insert cell
Plot.carto({
projection: "identity",
marks: [Plot.features(states.features.map((d) => d.geometry))]
})
Insert cell
Plot.carto({
marks: [
Plot.points(largeCities, {
lonLat: (d) => [d.lng, d.lat],
symbol: Math.random // 💡 TODO symbols
})
]
})
Insert cell
land = d3.json("https://unpkg.com/visionscarto-world-atlas@0.0.6/world/110m_land.geojson")
Insert cell
largeCities
Insert cell
import {largeCities} from "@fil/world-cities-urquhart"
Insert cell
countries = d3.json("https://unpkg.com/visionscarto-world-atlas@0.0.6/world/110m_countries.geojson")
Insert cell
makeCarto = (Plot) => {
Plot.carto = ({
projection,
rotate,
clipSphere, // 🌶 temporary, should be part of the projection description?
x,
y,
reflectY,
reflectX,
...options
}) => {
const svg = d3.select(
Plot.plot({
...options,
x: { ...x, axis: null, type: "identity" },
y: { ...y, axis: null, type: "identity" }
})
);

// the projection is then set according to the first carto mark
// 🌶 todo: pass the projection description as {projection, rotate, reflectX, etc}
if (typeof projection !== "function") {
svg
.select("g.carto")
.each(function ({
width,
height,
marginTop,
marginRight,
marginBottom,
marginLeft,
inset = 0,
insetTop = inset,
insetRight = inset,
insetBottom = inset,
insetLeft = inset
}) {
const g = d3.select(this).selectChildren("g");
const features = g.datum().G;
const geo = features.find((d) => d.type === "Sphere")
? { type: "Sphere" }
: {
type: "FeatureCollection",
features: features.flatMap((d) => {
switch (d.type) {
case "FeatureCollection":
return d.features;
case "Feature":
return d;
case "Geometry":
case "MultiPolygon":
case "Polygon":
case "MultiLineString":
case "LineString":
case "Point":
return { type: "Feature", geometry: d };
}
})
};

// 🌶 mercator being conformal and north-up, it works at all scales
// todo: auto-detect between identity (for alread-projected data),
// mercator, and equalEarth (when the extent is the Sphere)
if (projection === undefined) {
projection = geo.type === "Sphere" ? "equalEarth" : "mercator";
}
projection = d3[
"geo" + projection[0].toUpperCase() + projection.slice(1)
]();
if (reflectX) projection.reflectX(true);
if (reflectY) projection.reflectY(true); // useful for metric projections

projection.fitExtent(
[
[marginLeft + insetLeft, marginTop + insetTop],
[
width - marginRight - insetRight,
height - marginBottom - insetBottom
]
],
geo
);
});
}

if (rotate && projection.rotate) projection.rotate(rotate);
const path = d3.geoPath(projection);

if (clipSphere) {
svg
.selectChildren("g")
//.selectChildren("g")
.each(function (facetKey) {
const facet = d3.select(this);
const g = facet.selectAll("g.carto>g");
if (g.size()) {
const uid = DOM.uid("carto");
facet
.append("clipPath")
.attr("id", uid.id)
.datum({ type: "Sphere" })
.append("path")
.attr("d", path);
for (const m of g) {
if (m.__data__?.G?.[0]?.type !== "Sphere")
// 😱
d3.select(m).attr("clip-path", `${uid}`);
}
}
});
}

function update() {
svg.selectAll("g.carto > g").each(function ({ r, R, G }) {
d3.select(this)
.selectAll("path")
.attr("d", (i) => {
path.pointRadius(R ? R[i] : r || 0);
return path(G[i]);
});

d3.select(this)
.selectAll("text")
.attr("transform", (i) => {
const p = path.pointRadius(0)({
type: "Point",
coordinates: G[i].coordinates
});
return `translate(${
p ? p.replace(/m.*$/, "").slice(1) : [-1000, -1000]
})`;
});
});
}

update();

return Object.assign(svg.node(), { projection, update });
};

class Features extends Plot.Mark {
constructor(
data,
{
z,
r,
dx,
dy,
title,
text, // for labels
fill,
fillOpacity,
stroke,
strokeOpacity,
strokeWidth,
// TODO: add textAnchor, fontSize, etc for labels
textAnchor,
fontSize,
geometry = (d) => d,
...options
} = {}
) {
mutable debug = { stroke, strokeWidth };
const [vr, cr] = maybeNumber(r, 3);
const [vfill, cfill] = maybeColor(fill, "none");
const [vstroke, cstroke] = maybeColor(
stroke,
cfill === "none" ? "currentColor" : "none"
);
const [vstrokewidth, cstrokewidth] = maybeNumber(strokeWidth, 1);
const [vfillopacity, cfillopacity] = maybeNumber(fillOpacity, 1);
const [vstrokeopacity, cstrokeopacity] = maybeNumber(strokeOpacity, 1);

super(
data,
[
{ name: "geometry", value: geometry, optional: false },
{ name: "z", value: z, optional: true },
{ name: "r", value: vr, scale: "r", optional: true },
{ name: "title", value: title, optional: true },
{ name: "text", value: text, optional: true },
{ name: "fill", value: vfill, scale: "color", optional: true },
{ name: "stroke", value: vstroke, scale: "color", optional: true },
{
name: "strokeWidth",
value: vstrokewidth,
// scale: "strokewidth", // 🌶 channel works, but not the scale
optional: true
},
{
name: "fillOpacity",
value: vfillopacity,
// scale: "fillopacity",
optional: true
},
{
name: "strokeOpacity",
value: vstrokeopacity,
// scale: "fillopacity",
optional: true
}
],
options
);
this.r = cr;
this.dx = dx;
this.dy = dy;
this.fontSize = fontSize != null ? +fontSize : undefined;
this.textAnchor = textAnchor; // todo: maybeKeyword…
Style(this, {
fill: cfill,
fillOpacity: cfillopacity,
strokeOpacity: cstrokeopacity,
stroke: cstroke,
strokeWidth:
cstroke === "none"
? undefined
: cstrokewidth === undefined
? 1.5
: cstrokewidth,
...options
});
}
render(
I,
scales,
{
r: R,
geometry: G,
z: Z,
title: L,
text: T,
fill: F,
fillOpacity: FO,
stroke: S,
strokeOpacity: SO,
strokeWidth: SW
},
dimensions
) {
let { data, r, dx, dy, halo, fontSize, textAnchor } = this;
let index = filter(I, G);
if (R) index = index.filter((i) => positive(R[i]));

const elem = G[index[0]]?.type === "Text" ? "text" : "path";

return create("svg:g")
.datum(dimensions)
.classed("carto", true)
.call(applyIndirectStyles, this)
.attr("paint-order", elem === "text" ? "stroke" : null)
.attr(
"font-size",
elem === "text" && fontSize != null ? fontSize : null
)
.attr(
"text-anchor",
elem === "text" && textAnchor != null ? textAnchor : null
)
.call((g) =>
g
.append("g")
.attr(
"transform",
dx || dy ? `translate(${dx || 0},${dy || 0})` : null
)
.datum({ r, R, G }) // attach the feature data to the root
.selectAll()
.data(index)
.join(elem)
.text(elem === "text" && T ? (i) => T[i] : null)
.call(applyDirectStyles, this)
.attr("fill", F && ((i) => F[i]))
.attr("fill-opacity", FO && ((i) => clamp(FO[i], 0, 1)))
.attr("stroke", S && ((i) => S[i]))
.attr("stroke-opacity", SO && ((i) => clamp(SO[i], 0, 1)))
.attr("stroke-width", SW && ((i) => Math.max(0, SW[i])))
.call(title(L))
)
.node();
}
}

Plot.feature = (data, options) => new Features([data], options);
Plot.features = (data, options) => new Features(getFeatures(data), options);

Plot.points = (data, { lonLat = (d) => d, ...options }) =>
new Features((data = getFeatures(data)), {
...options,
geometry: Plot.valueof(data, lonLat).map((coordinates) => ({
type: "Point",
coordinates
}))
});

Plot.centroid = (data, { lonLat = d3.geoCentroid, ...options } = {}) =>
new Features((data = getFeatures(data)), {
...options,
geometry: Plot.valueof(data, lonLat).map((coordinates) => ({
type: "Point",
coordinates
}))
});

Plot.label = (
data,
{
lonLat = d3.geoCentroid, // TODO: centroid of largest polygon, or better yet: pole of inaccessibility
fill = "black",
halo,
stroke = halo ? "white" : undefined,
strokeWidth = halo ? 4 : undefined,
...options
} = {}
) =>
new Features((data = getFeatures(data)), {
geometry: Plot.valueof(data, lonLat).map((coordinates) => ({
type: "Text",
coordinates
})),
fill,
stroke,
strokeWidth,
...options
});

return Plot;
}
Insert cell
mutable debug = null
Insert cell
Plot.carto({
rotate: [-10, 0],
color: { scheme: "rdylgn" },
marks: [
Plot.feature({ type: "Sphere" }, { fill: "white" }),
Plot.feature(oceans, {
fill: "lightblue",
fillOpacity: 0.7,
stroke: "white"
}),
Plot.feature(land, { fill: "#333" }),
Plot.feature({ type: "Sphere" }, { stroke: "black", strokeWidth: 2 }),
Plot.label(oceans, {
lonLat: (d) => d3.geoInterpolate([-25, 30], d3.geoCentroid(d))(0.85),
fill: "black",
// fontSize: 29,
// textAnchor: "start",
// dx: 4,
stroke: "white",
strokeWidth: 4,
// halo: true, // deprecated shorthand
text: (d) => d.properties.name.replace(/ Ocean/, "")
})
],
width,
height: 500
})
Insert cell
import { oceans } from "@visionscarto/five-oceans-topojson"
Insert cell
import {
applyDirectStyles,
applyIndirectStyles,
applyTransform,
ascending,
clamp,
create,
filter,
maybeNumber,
maybeColor,
positive,
title,
Style,
string
} from "@data-workflows/plot-symbols" // 💡 TODO: update
Insert cell
// Since this is GeoJSON, we accept multiple features or geometries
// as an array and as a Collection
function getFeatures(data) {
if (data && !Array.isArray(data)) {
switch (data.type) {
case "FeatureCollection":
return data.features;
case "GeometryCollection":
return data.geometries;
}
}
return data;
}
Insert cell
Plot = require("@observablehq/plot@0.5.0").then(makeCarto)
Insert cell
d3 = require("d3@7", "d3-geo-projection@4")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more