Public
Edited
Feb 20
4 forks
9 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// this function assumes the gridlines arrays have been converted to a single path command
grid = (g, gridLines) =>
g
.selectAll("path")
.data(gridLines)
.join(
enter =>
enter
.append("path")
.attr("d", d => d)
.attr("stroke", "#e3e3e3")
.attr("stroke-width", (d, i) => (i & 1 ? 1 : 2)),
update => update.attr('d', d => d) // update path command
// theres no exit, lines are only drawn upto 'initial' triangle bounds
)
Insert cell
ticks = (g, ticks) =>
g
.selectAll("g")
.data(d => d, d => d.tick)
.join(
enter => {
const tickGroups = enter
.append("g")
.attr("class", "tick")
.attr("transform", d => `translate(${d.position})`);

tickGroups
.append("text")
.attr("text-anchor", d => d.textAnchor)
.attr("transform", d => `rotate(${d.angle})`)
.attr("dx", d => (-d.size - 5) * (d.textAnchor === "start" ? -1 : 1))
.attr("dy", ".5em")
.text(d => d.tick);

tickGroups
.append("line")
.attr("transform", d => `rotate(${d.angle + 90})`)
.attr("y2", d => d.size * (d.textAnchor === "start" ? -1 : 1))
.attr("stroke", "grey");

return tickGroups;
},
update => update.attr("transform", d => `translate(${d.position})`),
exit => exit.remove()
)
Insert cell
axisLabels = (g, labels) =>
g
.selectAll("text")
.data(labels, (d) => d.label)
.join(
(enter) =>
enter
.append("text")
.attr("dominant-baseline", "middle")
.attr(
"transform",
(d, i) => `translate(${d.position})rotate(${d.angle})`
)
.attr("text-anchor", "middle")
.text((d) => d.label),
(update) =>
update.attr(
"transform",
(d, i) => `translate(${d.position})rotate(${d.angle})`
)
)
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
basicTernaryPlot = {
const normalTernary = barycentric()
.a((d) => d.agriculture)
.b((d) => d.industry)
.c((d) => d.service);

return ternaryPlot(normalTernary)
.tickFormat("%")
.radius(radius)
.labels(labels);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
domainsLengths = partialDomains.map(domain => Math.abs(domain[1] - domain[0]))
Insert cell
partialTernaryPlot = {
const partialTernary = barycentric()
.domains(partialDomains)
.a((d) => d.agriculture)
.b((d) => d.industry)
.c((d) => d.service);

return ternaryPlot(partialTernary).radius(radius).labels(labels);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
zoomBarycentric = barycentric()
.a(d => d.agriculture)
.b(d => d.industry)
.c(d => d.service);
Insert cell
Insert cell
Insert cell
viewof drawZoomableTernaryPlot = {
const node = DOM.svg(width, height);
const svg = d3.select(node);

const radius = zoomableTernaryPlot.radius();

const chart = svg
.append("g")
.attr("transform", `translate(${width / 2} ${height / 2 + yOffset})`)
.attr("font-family", "sans-serif")
.attr("id", "chart")
.on("mousemove", handleMouseMove);

const defs = chart.append("defs");

const clipPath = defs
.append("clipPath")
.attr("id", "trianglePath")
.append("path")
.attr("d", zoomableTernaryPlot.triangle());

const transformedTrianglePath = chart
.append("path")
.attr("d", zoomableTernaryPlot.triangle())
.attr("fill", "none")
.attr("stroke", "coral")
.attr("title", " Scaled & translated triangle")
.attr("stroke-width", 2);

const axisLabelsGroup = chart
.append("g")
.attr("class", "axis-labels")
.call(axisLabels, zoomableTernaryPlot.axisLabels());

const gridLinesPaths = zoomableTernaryPlot
.gridLines()
.map((axisGrid) => axisGrid.map(d3.line()).join(" "));

const gridGroup = chart
.append("g")
.attr("class", "grid")
.call(grid, gridLinesPaths);

const axisTicksGroups = chart
.append("g")
.attr("class", "ternary-ticks")
.attr("font-size", 10)
.selectAll("g")
.data(zoomableTernaryPlot.ticks())
.join("g")
.attr("class", "axis-ticks");

axisTicksGroups.call(ticks);

// initial triangle
const trianglePath = chart
.append("path")
.attr("d", zoomableTernaryPlot.triangle())
.attr("fill", "transparent")
.attr("stroke", "black")
.attr("title", "Initial untransformed triangle")
.attr("stroke-width", 2);

const ternaryData = data.map((d) => {
const [x, y] = zoomableTernaryPlot(d);
return { x, y, ...d };
});

// data
const dots = chart
.append("g")
.attr("class", "data")
.attr("clip-path", "url(#trianglePath)")
.selectAll("circle")
.data(ternaryData)
.join("circle")
.attr("r", 4)
.attr("cx", (d) => d.x)
.attr("cy", (d) => d.y)
.attr("fill", "#444")
.attr("stroke", "#ddd");

dots.append("title").text(
(d) => `${d.country}

Agriculture: ${d.agriculture}
Industry: ${d.industry}
Service: ${d.service}`
);

const zoom = d3.zoom().scaleExtent([1, 100]).on("zoom", zoomed);

// get the initial transform, in case of partial domains
const { x, y, k } = transformFromDomains(zoomBarycentric.domains());

// We need to sync d3-zoom with the tranform of the partial domains
const initialTransform = d3.zoomIdentity
.translate(x * radius, y * radius)
.scale(k);

chart.call(zoom).call(zoom.transform, initialTransform);

function zoomed({ transform }) {
const { x, y, k } = transform; // get the translation offsets and scale factor using d3-zoom

// note that translation offsets have to be unscaled by plot radius!
const tx = x / radius,
ty = y / radius;

// get the domains that match this tranform
const domains = domainsFromTransform({ k, x: tx, y: ty });

zoomBarycentric.domains(domains);

// update data
dots
.attr("cx", (d) => zoomableTernaryPlot(d)[0])
.attr("cy", (d) => zoomableTernaryPlot(d)[1]);

// update gridlines and ticks
const gridLinesPaths = zoomableTernaryPlot
.gridLines()
.map((axisGrid) => axisGrid.map(d3.line()).join(" "));

gridGroup.call(grid, gridLinesPaths);

axisTicksGroups.data(zoomableTernaryPlot.ticks()).call(ticks, (d) => d);
}

function handleMouseMove(d) {
const xy = d3.pointer(d);
const inverse = zoomableTernaryPlot.invert(xy);

node.dispatchEvent(new CustomEvent("input"), { bubbles: true });
node.value = inverse;
}

return svg.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// to keep the triangle centered at [0,0] of the svg
yOffset = radius / 4
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3Ternary = import("d3-ternary")
Insert cell
transformFromDomains = d3Ternary.transformFromDomains
Insert cell
domainsFromTransform = d3Ternary.domainsFromTransform
Insert cell
ternaryPlot = d3Ternary.ternaryPlot
Insert cell
barycentric = d3Ternary.barycentric
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