Public
Edited
Apr 25, 2023
1 fork
Importers
7 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
samplePlot = unitChart(disability, {
group: d => d["Group"],
value: "Total",
mark,

width: 600,
height: 300
})
Insert cell
Insert cell
Insert cell
sampleTextPlot = unitChart(sampleData, {
group: "kind",
value: "total",
mark: "text",
text: (group) => livestockEmoji[group],
fontSize: 24 // Optional
})
Insert cell
livestockEmoji = ({
"Heifers/Goat": "🐐",
Buffalo: "🐃",
Pig: "🐖",
Rabbit: "🐇",
Sheep: "🐑",
Cow: "🐄"
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Icons from https://tablericons.com/
images = ({
Others: await FileAttachment("others.svg").url(),
"Water and snow": await FileAttachment("water.svg").url(),
Croplands: await FileAttachment("cropland.svg").url(),
"Forest and grasslands": await FileAttachment("forest.svg").url(),
"Built-up": await FileAttachment("building@2.svg").url()
})
Insert cell
Insert cell
function unitChart(
data,
{
// Required
group,
value,

// Optional
width = 640,
height = 400,
margin = 10,
marginTop,
marginRight,
marginLeft,
marginBottom,
scheme = schemeCategory10,
formatLabel = (d) => `${accessor(group)(d)} (${d.__ratio.toFixed(1)}%)`,

cellPadding = 0.0333,

mark = "square", // "circle" | "text" | "image"
showLegend = true,
legendCellSize = width / 32,

// Options for text mark
text = (d) => d,
fontSize, // (number in px) Can be used set font size of text marks

// Options for image marq

src = function () {
throw "src not not passed in the options";
}
} = {}
) {
const getValue = accessor(value);
const getGroup = accessor(group);
const w = width;
const h = height ?? (showLegend ? (w * 2) / 3 : w);

marginTop = marginTop ?? margin;
marginRight = marginRight ?? margin;
marginBottom = marginBottom ?? margin;
marginLeft = marginLeft ?? margin;

const svg = d3
.create("svg")
.attr("viewBox", [0, 0, w, h])
.attr("width", w)
.attr("height", h)
.style("display", "block")
.style("background", "white")
.style("height", "auto")
.style("height", "intrinsic")
.style("max-width", "100%");

if (height) {
svg.style("width", w).style("height", h);
}

const boundedWidth = w - marginLeft - marginRight;
const boundedHeight = h - marginTop - marginBottom;
const plotSize = boundedWidth < boundedHeight ? boundedWidth : boundedHeight;

const scale = d3
.scaleBand()
.domain(d3.range(10))
.range([0, plotSize])
.paddingInner(cellPadding);

const color = d3.scaleOrdinal(scheme).domain(d3.range(data.length));

let dataCopy = [...data].sort((a, b) => getValue(b) - getValue(a));
const total = computeTotal(dataCopy, getValue);
dataCopy = computeRatio(dataCopy, getValue, total);

const plotData = computePlotData(dataCopy, getGroup);
const groups = [...new Set(plotData.map((d) => d.group))];

const cellSize = scale.bandwidth();
const halfCellSize = cellSize / 2;
const plot = svg
.append("g")
.attr("transform", `translate(${marginLeft},${marginTop})`);

const cells = plot
.selectAll(".cell")
.data(plotData)
.join("g")
.attr("fill", (d) => (d.index === -1 ? "#ddd" : color(d.index)));

if (mark === "circle") {
cells
.append("circle")
.attr("cx", (d) => scale(d.x) + halfCellSize)
.attr("cy", (d) => scale(d.y) + halfCellSize)
.attr("r", halfCellSize);
} else if (mark === "text") {
cells
.append("text")
.text((c) => text(c.group))
.attr("x", (d) => scale(d.x) + halfCellSize)
.attr("y", (d) => scale(d.y) + halfCellSize)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("font-size", `${fontSize || cellSize * 0.5}px`);
} else if (mark === "image") {
cells
.append("image")
.attr("x", (d) => scale(d.x))
.attr("y", (d) => scale(d.y))
.attr("width", cellSize)
.attr("height", cellSize)
.attr("preserveAspectRatio", "xMinYMin")
.attr("href", (d) => src(d.group));
} else {
cells
.append("rect")
.attr("x", (d) => scale(d.x))
.attr("y", (d) => scale(d.y))
.attr("width", cellSize)
.attr("height", cellSize);
}

if (showLegend) {
const legend = svg
.append("g")
.attr(
"transform",
(d, i) => `translate(${marginLeft + cellSize + plotSize},${marginTop})`
)
.selectAll(".legend")
.data(dataCopy.filter((d) => groups.includes(getGroup(d))))
.join("g")
.attr("class", "legend")
.attr("transform", (d, i) => `translate(0,${i * legendCellSize * 1.5})`);

if (mark === "text") {
legend
.append("text")
.text((d) => text(getGroup(d)))
.attr("x", legendCellSize / 2) // 2
.attr("y", legendCellSize / 2) // 2
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("font-size", `${legendCellSize * 1.1}px`); // this controls the icon size; let's make it larger - and make corresponding text larger
} else if (mark === "image") {
legend
.append("image")
.attr("width", legendCellSize)
.attr("height", legendCellSize)
.attr("preserveAspectRatio", "xMinYMin")
.attr("href", (d) => src(getGroup(d)));
} else {
legend
.append("rect")
.attr("width", legendCellSize)
.attr("height", legendCellSize)
.attr("rx", (d) => (mark === "circle" ? legendCellSize : 0))
.attr("fill", (d, i) => color(i));
}
legend
.append("text")
.attr("x", legendCellSize * 1.5) //1.25
.attr("y", legendCellSize * 0.5) //0.5
.attr("dominant-baseline", "middle")
.text(formatLabel)
.style("font-family", fontFamily)
.style("font-size", `${legendCellSize * 0.8}px`);
}
return svg.node();
}
Insert cell
fontFamily = `-apple-system, BlinkMacSystemFont, "avenir next", avenir, helvetica, "helvetica neue", ubuntu, roboto, noto, "segoe ui", arial, sans-serif`
Insert cell
function computePlotData(chartData, getGroup) {
const data = chartData.map((d) => ({ ...d, cells: Math.round(d.__ratio) }));

const ratioTotal = computeTotal(data, accessor("__ratio"));

const waffle = [];
let dataIndex = 0;
let cellIndex = 1;

// One empty cell will be extra
// So add it to the to a candidate group
let hasExtraCell = ratioTotal < 100;
let candidateGroup;
let extraCellAdded = false;
if (hasExtraCell) {
candidateGroup = data
.map((d) => ({ ...d, diff: Math.abs(d.__ratio - d.cells) }))
.sort((a, b) => b.diff - a.diff);

candidateGroup = getGroup(candidateGroup[0]);
}
for (let y = 0; y < 10; y++)
for (let x = 0; x < 10; x++) {
// if (hasExtraCell && x == 0 && y == 0) continue;

if (!data[dataIndex]) {
waffle.push({ x, y, group: "Undefined", index: -1 });
break;
}

const group = getGroup(data[dataIndex]);

waffle.push({ x, y, group, index: dataIndex });
cellIndex++;
if (hasExtraCell && !extraCellAdded && candidateGroup === group) {
cellIndex--; // Do one extra pass, which will add one extra cell in this group
extraCellAdded = true;
continue;
}
if (cellIndex > data[dataIndex].cells) {
dataIndex++;
cellIndex = 1;
}
}

return waffle;
}
Insert cell
computeRatio = (arr, accessor, total) =>
arr.map((d) => ({
...d,
__ratio: (accessor(d) / total) * 100
}))
Insert cell
computeTotal = (arr, fn) => arr.reduce((sum, d) => sum + fn(d), 0)
Insert cell
// Data from district profile of Palpa, Nepal
// https://observablehq.com/@adb/nepal-cbs-2017-2018-district-profile-palpa#livestock
sampleData = [
{
total: 72973,
kind: "Cow"
},
{
total: 86696,
kind: "Buffalo"
},
{
total: 233716,
kind: "Heifers/Goat"
},
{
total: 2674,
kind: "Sheep"
},
{
total: 70896,
kind: "Pig"
},
{
total: 888,
kind: "Rabbit"
}
]
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