makeCarto = (Plot) => {
Plot.carto = ({ projection = "equalEarth", rotate, ...options }) => {
const svg = d3.select(Plot.plot(options));
if (typeof projection === "string") {
projection = d3[
"geo" + projection[0].toUpperCase() + projection.slice(1)
]();
svg
.select("g.carto")
.each(function ({
width,
height,
marginTop,
marginRight,
marginBottom,
marginLeft
}) {
const features = d3.select(this).selectChildren("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 "Geometry":
return d;
}
})
};
projection.fitExtent(
[
[marginLeft, marginTop],
[width - marginRight, height - marginBottom]
],
geo
);
});
}
if (rotate && projection.rotate) projection.rotate(rotate);
const path = d3.geoPath(projection);
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);
return path(G[i]);
});
});
}
update();
return Object.assign(svg.node(), { projection, update });
};
class Features extends Plot.Mark {
constructor(
data,
{
z,
r,
title,
fill,
fillOpacity,
stroke,
strokeOpacity,
strokeWidth,
geometry = (d) => d,
...options
} = {}
) {
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: "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;
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,
fill: F,
fillOpacity: FO,
stroke: S,
strokeOpacity: SO,
strokeWidth: SW
},
dimensions
) {
let { data, r } = this;
let index = filter(I, G);
if (R) index = index.filter((i) => positive(R[i]));
return create("svg:g")
.datum(dimensions)
.classed("carto", true)
.call(applyIndirectStyles, this)
.call((g) =>
g
.append("g")
.datum({ r, R, G }) // attach the feature data to the root
.selectAll()
.data(index)
.join("path")
.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(data, options);
Plot.points = (data, { lonLat = (d) => d, ...options }) =>
new Features(data, {
...options,
geometry: (d) => ({ type: "Point", coordinates: lonLat(d) })
});
return Plot;
}