Public
Edited
4 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function drawStripedCircle({
svg,
selection,
features,
centroids,
width,
height,
margin,
svgHeight,
svgWidth,
stroke = "#fff",
strokeWidth = 1.25,
fontFamily = "var(--sans-serif)",
debug
} = {}) {
const canvas = selection
.selectAll(".small-multiple-canvas")
.data(features)
.join("g")
.attr("class", "small-multiple-canvas")
.attr(
"transform",
(d, i) => `translate(${centroids[i][0]},${centroids[i][1]})`
);

const D = features.map((f) => {
const name = getFeatureName(f);
const d = getDataByDistrict(data, name);
const harvestedPercentage = Math.min(
(100 * d.harvestedArea) / d.sownArea,
100
);
return { ...d, name, harvestedPercentage };
});
const PRD = D.map((d) => d.production);
const PRDExtent = d3.extent(PRD);
const ID = D.map(
(d) => `stripped-pattern-${d.name.toLowerCase().replaceAll(" ", "-")}`
);

const rScale = d3.scaleSqrt().domain(PRDExtent).range([5, 30]);
const computeR = (d, i) => (PRD[i] === 0 ? 0 : rScale(PRD[i]));

D.forEach((d, i) => {
const { harvestedPercentage } = d;

makePattern(
d3.select(svg),
ID[i],
computeR(null, i) * 2,
[100 - harvestedPercentage, harvestedPercentage],
colorScale.range()
);
});

canvas
.append("circle")
.attr("r", computeR)
.attr("fill", (d, i) => `url(#${ID[i]})`)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth);

canvas
.append("circle")
.attr("fill", "transparent")
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("r", computeR)
.append("title")
.text(
(d, i) => `${D[i].name}

Production: ${formatNumber(D[i].production)} metric tons

Area sown: ${formatNumber(D[i].sownArea)} acres
Area harvested: ${formatNumber(D[i].harvestedArea)} acres
Loss: ${formatPercentage(1 - D[i].harvestedPercentage / 100)}`
);

canvas
.append("text")
.attr("class", "district-label")
.attr("stroke-width", 0)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "hanging")
.attr("paint-order", "stroke")
.attr("fill", "#222")
.attr("fill-opacity", (d, i) => (computeR(d, i) === 0 ? 0 : 1))
.attr("stroke", "#fff")
.attr("stroke-width", 3)
.attr("stroke-opacity", (d, i) => (computeR(d, i) === 0 ? 0 : 0.75))
.attr("style", "font-size: 0.7rem")
.attr("font-weight", "500")
.attr("dy", (d, i) => computeR(d, i) * 1.15)
.style("font-family", fontFamily)
.text((d) => getFeatureName(d));

if (debug) {
selection
.selectAll(".debug-small-multiple-canvas")
.data(features)
.join("g")
.attr("class", "debug-small-multiple-canvas")
.append("rect")
.attr("x", (d, i) => centroids[i][0] - width / 2)
.attr("y", (d, i) => centroids[i][1] - height / 2)
.attr("width", width)
.attr("height", height)
.attr("stroke", "#0ff")
.attr("fill", "none");
}

// Draw legend
const ticks = rScale.ticks(2);
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} metric tons produced` : str;
},
tickFont: `10px ${fontFamily}`,
tickStrokeWidth: 0.75,
tickStroke: "#666"
});

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

const sqSize = 15;
const sqGap = 6;
const colorLegendWidth = 120;
const colorLegendFontSize = 12;
const colorLegend = selection
.append("g")
.attr("class", "color-legend")
.attr(
"transform",
`translate(${svgWidth - 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
formatNumber = d3.format(",.2f")
Insert cell
formatPercentage = d3.format(".2%")
Insert cell
colorScale = d3.scaleOrdinal(
["Area lost", "Area harvested"],
["hsl(176, 11%, 75%)", "hsl(209, 32%, 41%)"]
)
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
function getDataByDistrict(data, district) {
return data.find((d) => d.dzongkhag === district);
}
Insert cell
getDataByDistrict(data, "Punakha")
Insert cell
data = {
const data = dataByCrop.reduce((obj, d, i) => {
const { dzongkhag, crop } = d;
if (obj[dzongkhag] === undefined) {
obj[dzongkhag] = { crop };
}

if (d.attribute.toLowerCase().includes("sown")) {
obj[dzongkhag].sownArea = d.value;
obj[dzongkhag].sownAreaUnit = d.unit;
} else if (d.attribute.toLowerCase().includes("harvested")) {
obj[dzongkhag].harvestedArea = d.value;
obj[dzongkhag].harvestedAreaUnit = d.unit;
} else if (d.attribute.toLowerCase().includes("production")) {
obj[dzongkhag].production = d.value;
obj[dzongkhag].productionUnit = d.unit;
}

return obj;
}, {});
return Object.entries(data).map(([dzongkhag, obj]) => {
let { sownArea, harvestedArea, harvestedAreaUnit } = obj;

sownArea = sownArea ?? harvestedArea;

const lostArea = sownArea - harvestedArea;
return {
dzongkhag,
sownArea,
lostArea,
lostAreaUnit: harvestedAreaUnit,
...obj
};
});
}
Insert cell
function mapWithFeatureWiseSmallMultiples({
// **
// Required
// **
features,
projection = d3.geoIdentity().reflectY(true),
getFeatureData = (d, i, arr) => {},
drawSmallMultiples = () => {},

// **
// Optional
// **
width = 640,
height = 400,
margin = 20,
smallMultipleWidth = 60,
smallMultipleHeight = 60,

featuresFill = "hsl(48, 65%, 90%)", //"hsl(48, 89%, 90%)",
featuresStroke = "#fff", // "hsl(294, 20%, 67%)", // stroke color for features
featuresStrokeWidth = 2, // stroke width for features

backgroundFill = "white",

debug
} = {}) {
// Adding margin to chart
const mapExtents = [
[margin, margin],
[width - margin, height - margin]
];
const smallMultipleInnerR =
Math.min(smallMultipleWidth, smallMultipleHeight) / 2;
const smallMultipleOuterR =
Math.min(smallMultipleWidth, smallMultipleHeight) * Math.SQRT2 * 0.5;

projection = projection.scale === undefined ? projection() : projection;

// https://github.com/d3/d3-geo/blob/main/README.md#projection_fitSize
projection.fitExtent(mapExtents, features);
projection.clipExtent(mapExtents);

const path = d3.geoPath(projection);

const F = Array.isArray(features)
? features.slice()
: features.features.slice();
const D = F.map(getFeatureData);

const C = F.map(computeLabelPole);
const CP = C.map(projection);
const CPNO = applyForce(CP, {
width,
height,
radius: smallMultipleOuterR,
padding: smallMultipleOuterR / 20
});

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(F)
.join("path")
.attr("class", "feature")
.attr("d", path)
.attr("fill", featuresFill)
.attr("stroke", featuresStroke)
.attr("stroke-width", featuresStrokeWidth)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round");

// Draw small multiples
const smallMultiples = d3
.select(svg)
.append("g")
.attr("class", "small-multiples");
drawSmallMultiples({
svg,
selection: smallMultiples,
features: F,
centroids: CPNO,
width: smallMultipleWidth,
height: smallMultipleHeight,
svgHeight: height,
svgWidth: width,
margin,
debug
});

// Draw debug elements
if (debug) {
const debugElements = d3.select(svg).append("g").attr("class", "debug");

const smMultiElements = debugElements
.selectAll(".debug-sm-multiples-og")
.data(CP)
.join("g")
.attr("class", "debug-sm-multiples-og");

smMultiElements
.append("circle")
.attr("r", 2)
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("fill", "#ff0");

const smMultiNonOverlappingElements = debugElements
.selectAll(".debug-sm-multiples")
.data(CPNO)
.join("g")
.attr("class", "debug-sm-multiples");
smMultiNonOverlappingElements
.append("circle")
.attr("r", 2)
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("fill", "#f0f");
smMultiNonOverlappingElements
.append("circle")
.attr("r", smallMultipleOuterR)
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.attr("stroke", "#f0f")
.attr("fill", "none");
// smMultiNonOverlappingElements
// .append("rect")
// .attr("x", (d) => d[0] - smallMultipleWidth / 2)
// .attr("y", (d) => d[1] - smallMultipleHeight / 2)
// .attr("width", smallMultipleWidth)
// .attr("height", smallMultipleHeight)
// .attr("stroke", "#f0f")
// .attr("fill", "none");
}

return svg;
}
Insert cell
Insert cell
dataStore = new Map([
["Cereals", tidyAgriData(cereals)],
["Oilseeds & Legumes", tidyAgriData(oilseeds_and_legumes)],
["Vegetables", tidyAgriData(vegetables)],
["Spices", tidyAgriData(spices)],
["Roots and tubers", tidyAgriData(roots_and_tubers)],
["Fruits", tidyAgriData(fruits)]
])
Insert cell
keyRegex = /(?<crop>.*) - (?<attribute>.*) \((?<unit>.*)\)/
Insert cell
function tidyAgriData(arr) {
return arr.map(tidyAgriDataObject).flat();
}
Insert cell
function tidyAgriDataObject(obj) {
const { Dzongkhag, ...rest } = obj;

const facts = Object.entries(rest)
.map(([k, v]) => {
const match = k.match(keyRegex);
if (match) {
const { crop, attribute, unit } = match.groups;
return { dzongkhag: Dzongkhag, crop, attribute, unit, value: v };
}
})
.filter(Boolean);
return facts;
}
Insert cell
Insert cell
Insert cell
Insert cell
function normalizeWinding(features) {
return rewind({ ...features }, true);
}
Insert cell
function computeLabelPole(feat, precision) {
if (feat.geometry.type === "Polygon") {
return polylabel(feat.geometry.coordinates, precision);
}

return d3.geoCentroid(feat);
}
Insert cell
getFeatureName(bhutan_dzongkhags_geo.features[0])
Insert cell
function getFeatureName(f) {
return f?.properties?.NAME_1;
}
Insert cell
Insert cell
import {
cereals,
oilseeds_and_legumes,
vegetables,
spices,
roots_and_tubers,
fruits
} from "@aaronkyle/bhutan-agriculture-statistics-2021"
Insert cell
Insert cell
bhutan_dzongkhags_geo = geotoolbox.map(gadm_BTN_admin1, (d) => {
let { NAME_1 } = d;

const namesSubstitutions = new Map([
["Chhukha", "Chukha"],
["Monggar", "Mongar"],
["Pemagatshel", "Pema Gatshel"],
["Samdrupjongkhar", "Samdrup Jongkhar"],
["Yangtse", "Trashi Yangtse"],
["Wangduephodrang", "Wangdue Phodrang"]
]);
return { ...d, NAME_1: namesSubstitutions.get(NAME_1) || NAME_1 };
})
Insert cell
import { rewind } from "@fil/rewind"
Insert cell
polylabel = (await import("https://cdn.skypack.dev/polylabel@1.1.0?min"))
.default
Insert cell
geotoolbox = require("geotoolbox@1.9.7/dist/index.min.js")
Insert cell
import { circleLegend } from "@bayre/circle-legend"
Insert cell
import { footer } from "@saneef/notebooks-footer"
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