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

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