Public
Edited
Apr 26, 2023
1 fork
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
sampleChart = radarChart(sampleData, {
group: "Province",
attribute: "Attribute",
value: "Penetration",

tickFormat: d3.format(".0%"),
radarCurve: curve
})
Insert cell
Insert cell
sampleData
Insert cell
Insert cell
Insert cell
Insert cell
radarChart(sampleSingleGroupData, {
attribute: "Attribute",
value: "Penetration",

tickFormat: d3.format(".0%"),
radarCurve: curve
})
Insert cell
sampleSingleGroupData = {
const group = sampleData[0].Province;

return sampleData.filter((p) => p.Province === group);
}
Insert cell
Insert cell
function radarChart(
data,
{
// Required
attribute = "attribute",
value = "value",
group,

// Optionals
maxValue = undefined, // will calculate it
scheme = schemeBuReGnGr,

width = 600,
height = width,
margin,
marginTop,
marginRight,
marginBottom,
marginLeft,

fontFamily = "var(--sans-serif, sans-serif)",

angleOffset = -Math.PI / 2,
ticks = 3,

radarStrokeWidth = 2.5,
radarDotRadius = radarStrokeWidth * 1.25,
radarFillOpacity = 0.1,
radarCurve = d3.curveLinearClosed,

sortGroups,
sortAttributes,

gridStroke = main.grey["300"],
gridStrokeWidth = 1.5,
tickFontSize = "0.75rem",
tickFill = main.grey["700"],
tickFormat = formatTick,

axisLabelFill = main.grey["1000"],
axisLabelFontSize = "0.8rem",
axisLabelFontWeight = "normal",
axisLabelMaxWidth = 120,

showLegend = true
} = {}
) {
// Access data
const getGroup = group === null ? null : generateAccessor(group);
const getValue = generateAccessor(value);
const getAttribute = generateAccessor(attribute);

// Compute values
let G = getGroup ? [...new Set(data.map(getGroup))] : null;
let A = [...new Set(data.map(getAttribute))];

G = typeof sortGroups === "function" ? G.slice().sort(sortGroups) : G;
A = typeof sortAttributes === "function" ? A.slice().sort(sortGroups) : A;

let V = [];
G.forEach((g, i) => {
V[i] = [];
A.forEach((a, j) => {
const obj = data.find((d) => g === getGroup(d) && a === getAttribute(d));
const v = getValue(obj);
V[i][j] = v == null ? NaN : +v;
});
});

// Construct scales
marginTop = margin ?? marginTop ?? 0;
marginRight = margin ?? marginRight ?? 0;
marginBottom = margin ?? marginBottom ?? 0;
marginLeft = margin ?? marginLeft ?? 0;

const w = width - marginLeft - marginRight;
const h = height - marginTop - marginBottom;
const maxR = (Math.min(w, h) * 0.5 * 2) / 3;
maxValue = maxValue || d3.max(data, getValue);
const radialGridStrokeWidth = Math.max(1.5, gridStrokeWidth / 2);

const radius = d3.scaleLinear().domain([0, maxValue]).range([0, maxR]);
const angle = d3
.scaleBand()
.domain(A)
.range([0 + angleOffset, Math.PI * 2 + angleOffset]);

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

// Construct generator
const radarLine = d3
.lineRadial()
.curve(radarCurve)
.radius((d) => radius(d))
.angle((_, i) => angle(A[i]) - angleOffset);

// Draw canvas
const svg = DOM.svg(width, height);
const canvas = d3
.select(svg)
.style("background", "white")
.append("g")
.attr("transform", `translate(${width / 2},${height / 2})`);

const peripherals = canvas.append("g").attr("class", "peripherals");

// Add axes
peripherals
.selectAll(".axis")
.data(A)
.join("line")
.attr("class", "axis")
.attr("stroke", gridStroke)
.attr("stroke-width", gridStrokeWidth)
.each(function (d, i) {
const theta = angle(d);
const [x, y] = getCoordinatesForAngle(
theta,
maxR + radialGridStrokeWidth / 2
);
d3.select(this).attr("x2", x).attr("y2", y);
});

// Add axis labels
setTimeout(() => {
// Running within timeout since wrap(), to wrap labels needs to be in DOM to measure width
peripherals
.selectAll(".axis-label")
.data(A)
.join("text")
.attr("class", "axis-label")
.text((d) => d)
.attr("dominant-baseline", "middle")
.attr("fill", axisLabelFill)
.attr("fill-opacity", 1)
.attr("font-size", axisLabelFontSize)
.attr("font-weight", axisLabelFontWeight)
.style("font-family", fontFamily)
.each(function (d, i) {
const theta = angle(d);
const [x, y] = getCoordinatesForAngle(theta, maxR * 1.125);
d3.select(this)
.attr("x", x)
.attr("y", y)
.style(
"text-anchor",
Math.abs(x) < 5 ? "middle" : x > 0 ? "start" : "end"
);
})
.attr("dy", "0em")
.call(wrap, axisLabelMaxWidth);
});

// Add radial ticks
const radialTicks = radius.ticks(ticks);
peripherals
.selectAll(".radial-grid")
.data(radialTicks)
.join("circle")
.attr("class", "radial-grid")
.attr("r", radius)
.attr("fill", "none")
.attr("stroke", gridStroke)
.attr("stroke-width", radialGridStrokeWidth)
.attr(
"stroke-dasharray",
`${radialGridStrokeWidth} ${radialGridStrokeWidth * 2}`
);

peripherals
.selectAll(".radial-tick")
.data(radialTicks.slice(1)) // Ignore the zero tick
.join("text")
.attr("class", "radial-tick")
.style("font-size", tickFontSize)
.style("fill", tickFill)
.style("font-family", fontFamily)
.attr("dy", "16px")
.attr("dx", "4px")
.attr("y", (d) => -radius(d))
.text((d) => tickFormat(d));

// Draw data
const plot = canvas.append("g").attr("class", "plot");
const groups = plot
.selectAll(".group")
.data(G)
.join("g")
.attr("class", "group");

groups.each(function (group, groupIndex) {
const groupData = V[groupIndex];
const positions = groupData.map((value, attrIndex) => {
const theta = angle(A[attrIndex]);
return getCoordinatesForAngle(theta, radius(value));
});

const g = d3.select(this);

const radarTitle = `${group ? `${group}\n\n` : ""}${groupData
.map((v, i) => `${A[i]}: ${tickFormat(v)}`)
.join("\n")}`;

// Add radarLine
g.selectAll(".radar")
.data([group])
.join("path")
.attr("class", "radar")
.attr("fill", () => color(group))
.attr("fill-opacity", radarFillOpacity)
.attr("stroke", () => color(group))
.attr("d", radarLine(groupData))
.append("title")
.text(radarTitle);

// Add dots
g.selectAll(".dot")
.data(positions)
.join("circle")
.attr("class", "dot")
.attr("r", radarDotRadius)
.attr("fill", () => color(group))
.attr("cx", (d) => d[0])
.attr("cy", (d) => d[1])
.append("title")
.text(
(d, i) =>
`${group ? `${group}\n` : ""}${A[i]}: ${tickFormat(groupData[i])}`
);
});

// Set up interactions
const radars = plot
.selectAll(".radar")
.on("mouseover", function (d, i) {
d3.selectAll(".radar")
.transition()
.duration(200)
.attr("stroke-opacity", 1 / 2)
.attr("fill-opacity", 1 / 50);

d3.select(this)
.transition()
.duration(200)
.attr("stroke-opacity", 1)
.attr("fill-opacity", 1 / 3);
})
.on("mouseout", function (d, u) {
d3.selectAll(".radar")
.transition()
.duration(200)
.attr("stroke-opacity", 1)
.attr("fill-opacity", radarFillOpacity);
});

if (showLegend && G.length > 1) {
const key = swatch(color);
d3.select(svg)
.append("g")
.attr("transform", `translate(${marginLeft},${marginTop})`)
.node()
.appendChild(key);
}

return svg;
}
Insert cell
formatTick = d3.format(".2s")
Insert cell
function generateAccessor(accessor) {
return function (obj) {
if (typeof accessor === "function") return accessor(obj);
return obj[accessor];
};
}
Insert cell
function getCoordinatesForAngle(angle, r = 1, offset = 1) {
return [Math.cos(angle) * r * offset, Math.sin(angle) * r * offset];
}
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