Public
Edited
Apr 25, 2023
1 fork
Importers
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function smallMultiplesOverMap(
data,
{
// **
// Required
// **
features,
drawChart = () => {},
drawLegend = () => {},
// ⚠️ Apply rotation to the projection if needed.
// d3.geoTransverseMercator() is good for showing a country or smalles area
// d3.geoWinkel3() or d3.geoEqualEarth() is good to show larger area
projection = d3.geoTransverseMercator(),

// **
// Optional
// **
drawOnlyPolygonFeatures = true,
width = 640,
height = 400,
chartSize = 60,
margin = 20,

featuresFill = main.grey["200"],
featuresStroke = main.grey["700"], // stroke color for features
featuresStrokeWidth = 0.75, // stroke width for features

backgroundFill = "white",

debug = false
} = {}
) {
const mapExtents = [
[margin, margin],
[width - margin, height - margin]
];

features = normalizeWinding(features);
// Remove point and lines
features = drawOnlyPolygonFeatures
? featureRemoveNonPolygonFeatures(features)
: features;
// overlay = overlay != null ? featurePolygonsOnly(overlay) : null;
projection = projection.scale === undefined ? projection() : projection;
// Adding margin to chart
// https://github.com/d3/d3-geo/blob/main/README.md#projection_fitSize
projection.fitExtent(mapExtents, features);

const C = features.features.map((f) => d3.geoCentroid(f)).map(projection);
const T = features.features.map((f) =>
getValueByPath(f, "properties.Province")
);

const path = d3.geoPath(projection);

const svg = DOM.svg(width, height);

d3.select(svg).style("background", backgroundFill);

// Draw features
const boundaries = d3
.select(svg)
.append("g")
.attr("class", "features")
.selectAll(".feature")
.data(features.features)
.join("path")
.attr("class", "feature")
.attr("d", path)
.attr("fill", featuresFill)
.attr("stroke", featuresStroke)
.attr("stroke-width", featuresStrokeWidth)
.attr("stroke-linejoin", "round");

boundaries.append("title").text((d, i) => T[i]);

// Draw charts
const charts = d3.select(svg).append("g").attr("class", "charts");

charts
.selectAll(".chart")
.data(C)
.join("g")
.attr("class", "chart")
.each(function (d, i) {
const [cx, cy] = d;
const g = d3.select(this);
g.attr("transform", `translate(${d})`);

if (typeof drawChart === "function") {
drawChart(g, features.features[i], data, {
cx,
cy,
size: chartSize,
svg
});
}
});

if (typeof drawLegend === "function") {
const legend = d3.select(svg).append("g").attr("class", "legend");
drawLegend(legend, { margin, width, height });
}

if (debug) {
const debugEls = d3.select(svg).append("g").attr("class", "debug");

debugEls
.selectAll(".d-centroid")
.data(C)
.join("circle")
.attr("class", "d-centroid")
.each(function (d) {
const [cx, cy] = d;
d3.select(this)
.attr("r", 4)
.attr("cx", cx)
.attr("cy", cy)
.attr("fill", "#f0f");
});

debugEls
.selectAll(".d-incirclce")
.data(C)
.join("circle")
.attr("class", "d-incirclce")
.each(function (d) {
const [cx, cy] = d;

d3.select(this)
.attr("r", chartSize / Math.SQRT2) // r = a / sq.root(2), formula for circumcircle of a square
.attr("cx", cx)
.attr("cy", cy)
.attr("stroke", "#f0f")
.attr("fill", "none");
});
debugEls
.selectAll(".d-chart-bounds")
.data(C)
.join("rect")
.attr("class", "d-chart-bounds")
.each(function (d) {
const [cx, cy] = d;

d3.select(this)
.attr("width", chartSize)
.attr("height", chartSize)
.attr("x", cx - chartSize / 2)
.attr("y", cy - chartSize / 2)
.attr("stroke", "#f0f")
.attr("fill", "none");
});
}
return svg;
}
Insert cell
function drawStripedCircle(selection, f, data, { size, svg }) {
const region = getValueByPath(f, "properties.STATE_CODE");
const d = data.find((d) => d["STATE_CODE"] === region);
if (d == null) return;

const stroke = "white";
const strokeWidth = 0;

const id = `pattern-${region}`;
const outerRadius = rScale(d.households);
const T = d3.sum([
d.households - d.householdsWithFacility,
d.householdsWithFacility
]);
const D = [d.households - d.householdsWithFacility, d.householdsWithFacility];
const P = D.map((d) => (100 * d) / T);
const C = colorScale.range();
const orientation = 0;

makePattern(d3.select(svg), id, outerRadius * 2, P, C);

selection
.append("circle")
.attr("r", outerRadius)
.attr("fill", `url(#${id})`)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth + 2);

selection
.append("circle")
.attr("fill", "transparent")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth + 2)
.attr("r", outerRadius)
.append("title")
.text(
`${d.province}
${formatNumber(d.households)} households.
${formatPercentage(d.rate)} households have ${facility.toLocaleLowerCase()}.`
);
}
Insert cell
function drawPie(selection, f, data, { size }) {
const region = getValueByPath(f, "properties.STATE_CODE");
const d = data.find((d) => d["STATE_CODE"] === region);
if (d == null) return;

const stroke = "white";
const strokeWidth = 0;

const outerRadius = rScale(d.households);
const D = [d.households - d.householdsWithFacility, d.householdsWithFacility];
const C = colorScale.domain();

const pie = d3.pie();
const pies = pie(D);

selection
.selectAll(".pie")
.data(pies)
.join("path")
.attr("class", "pie")
.attr("d", d3.arc().innerRadius(0).outerRadius(outerRadius))
.attr("fill", (d, i) => colorScale(C[i]))
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth);

selection
.append("circle")
.attr("fill", "transparent")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth + 2)
.attr("r", outerRadius)
.append("title")
.text(
`${d.province}
${formatNumber(d.households)} households.
${formatPercentage(d.rate)} households have ${facility.toLocaleLowerCase()}.`
);
}
Insert cell
function drawLegend(selection, { margin, width, height }) {
// Add radius legend
const ticks = rScale.ticks(3);
const r = rScale(d3.max(ticks));
const radiusLegend = circleLegend({
scale: rScale,

marginTop: 0,
marginBottom: 0,
stroke: "#666",
strokeWidth: 0.75,
tickFormat: (d, i, g) => {
const str = rScale.tickFormat(4, "s")(d);

return i === g.length - 1 ? `${str} households` : str;
},
tickFont: `10px ${fontFamily}`,
tickStrokeWidth: 0.75,
tickStroke: "#666"
});

selection
.append("g")
.attr("class", "radius-legend")
.attr("transform", `translate(${margin},${height - r * 2 - margin})`)
.node()
.appendChild(radiusLegend);

// Add color scale
const sqSize = 15;
const sqGap = 6;
const colorLegendWidth = 120;
const colorLegendFontSize = 12;
const colorLegend = selection
.append("g")
.attr("class", "color-legend")
.attr(
"transform",
`translate(${width - colorLegendWidth - margin},${margin})`
);

const swatches = colorLegend
.selectAll(".swatch")
.data(colorScale.domain())
.join("g")
.attr("class", "swatch");

swatches
.append("rect")
.attr("width", sqSize)
.attr("height", sqSize)
.attr("y", (d, i) => i * (sqGap + sqSize))
.attr("fill", colorScale);

swatches
.append("text")
.attr("x", sqGap + sqSize)
.attr("y", (d, i) => i * (sqGap + sqSize))
.attr("dy", sqSize * 0.66)
.attr("font-size", colorLegendFontSize)
.attr("font-family", fontFamily)
.attr("dominant-baseline", "middle")
.text((d) => d);
}
Insert cell
// import { makePattern } from "@jgaffuri/striped-circle"

// From: https://observablehq.com/@jgaffuri/striped-circle
makePattern = function (
svg,
id,
size,
percentages,
colors,
orientation = 0,
rotationPos = null,
withCorrection = true
) {
const frec = (v) => {
const t = Math.pow(6 * v, 1 / 3);
const t2 = t * t;
const t4 = t2 * t2;
const t6 = t4 * t2;
return (
(t * (1 - (1267 / 9428) * t2 + (31 / 6507) * t4 - (1 / 32473) * t6)) /
(1 - (2251 / 14902) * t2 + (63 / 9593) * t4 - (1 / 13890) * t6)
);
};

const d_ = (perc) => {
const theta = frec(perc * 2 * Math.PI);
return 0.5 * (1 - Math.cos(theta / 2));
};

const d = (perc) => {
if (perc > 0.5) return 1 - d_(1 - perc);
return d_(perc);
};

let defs = svg.select("defs");
if (defs.size() === 0) defs = svg.append("defs");

const pattern = defs
.append("pattern")
.attr("id", id)
.attr("patternUnits", "objectBoundingBox")
.attr("width", 1)
.attr("height", 1);

//set orientation
if (orientation) {
rotationPos = rotationPos || [0, 0];
pattern.attr(
"patternTransform",
"rotate(" +
orientation +
"," +
rotationPos[0] +
"," +
rotationPos[1] +
")"
);
}

//specify stripes
let cumPer = 0,
x0 = 0;
const corr = withCorrection ? d : (x) => x;
for (let i = 0; i < percentages.length; i++) {
cumPer = Math.min(1, cumPer + percentages[i] * 0.01);
const x1 = size * corr(cumPer);

pattern
.append("rect")
.attr("x", x0)
.attr("y", 0)
.attr("width", x1 - x0)
.attr("height", size)
.attr("fill", colors[i]);
x0 = x1;
}
}
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

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