Public
Edited
Apr 9
2 forks
Importers
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
demoTernaryPlot = {
const normalBarycentric = barycentric()
.a(d => d.agriculture)
.b(d => d.industry)
.c(d => d.service);

return ternaryPlot(normalBarycentric)
.radius(radius)
.labels(labels);
}
Insert cell
Insert cell
Insert cell
drawTernaryPlot = (someTernaryPlot, data) => {
const node = DOM.svg(width, height);
const svg = d3.select(node);

const radius = someTernaryPlot.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", someTernaryPlot.triangle());

const axisLabelsGroups = chart
.append("g")
.attr("class", "axis-labels")
.append("g")
.attr("class", "labels")
.attr("font-size", 16);

axisLabelsGroups.call(axisLabels, someTernaryPlot.axisLabels());

const gridLinesPaths = someTernaryPlot
.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(someTernaryPlot.ticks())
.join("g")
.attr("class", "axis-ticks")
.call(ticks);

const ternaryData = data.map((d) => {
const [x, y] = someTernaryPlot(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}`
);

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

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

// const [tx, ty] = someTernaryPlot.translate();
// const k = someTernaryPlot.scale();

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

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

// function zoomed({ transform }) {
// const { x, y, k } = transform;

// const tx = x / radius,
// ty = y / radius;

// // apply transform
// someTernaryPlot.translate([tx, ty]);
// someTernaryPlot.scale(k);

// const zoomedDomains = someTernaryPlot.domainsFromVertices();

// someTernaryPlot.setDomains(zoomedDomains);

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

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

// gridGroup.call(grid, gridLinesPaths);

// axisTicksGroups.data(someTernaryPlot.ticks()).call(ticks, (d) => d);

// }

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

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

node.value = 0;

return svg.node();
}
Insert cell
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)
// theres no exit, lines are only drawn upto 'initial' triangle bounds
)
Insert cell
ticks = g =>
g
.selectAll("g")
.data(d => d, d => d.tick)
.join(
enter => {
const tickGroups = enter
.append("g")
.attr("class", "tick")
.attr("transform", tick => `translate(${tick.position})`);

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

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

return tickGroups;
},
update => update.attr("transform", tick => `translate(${tick.position})`),
exit => exit.attr("opacity", epsilon).remove()
)
Insert cell
labels = ["Agriculture (A)", "Industry (B)", "Service (C)"]
Insert cell
axisLabels = (g, labels) =>
g
.selectAll("text")
.data(labels, (d) => d.label)
.join(
(enter) =>
enter
.append("text")
.attr("dominant-baselines", "middle")
.attr(
"transform",
(label, i) => `translate(${label.position})rotate(${label.angle})`
)
.attr("alignment-baseline", "middle")
.attr("text-anchor", "middle")
.text((label) => label.label),
(update) =>
update.attr(
"transform",
(label, i) => `translate(${label.position})rotate(${label.angle})`
)
)
Insert cell
data = [
...demoData,
{ country: "Serviceland", agriculture: "0", industry: "1", service: "99" },
{ country: "Industryland", agriculture: "1", industry: "99", service: "0" },
{ country: "Agricultureland", agriculture: "99", industry: "1", service: "0" }
]
Insert cell
Insert cell
Insert cell
valuesTable = (labels, plot) => html`
<table style="height:50px">
<thead>
<tr style="width:33%">${labels.map((label) => `<th>${label}</th>`)}</tr>
</thead>
<tbody>
<tr>${
plot === 0
? `<td colspan="3">
Hover your mouse over the plot to view the ternary values at that point
</td>`
: plot.map((value) => `<td>${formatPercentage(value)}</td>`)
} </tr>
</tbody>
</table>
`
Insert cell
Insert cell
getDomainLengths = domains =>
new Set(
domains.map(domain => {
// round differences
// https://stackoverflow.com/questions/11832914/round-to-at-most-2-decimal-places-only-if-necessary
const d0 = Math.round((domain[0] + Number.EPSILON) * 100) / 100;
const d1 = Math.round((domain[1] + Number.EPSILON) * 100) / 100;

return Math.abs(d1 - d0);
})
)
Insert cell
insideDomain = n => (n > 0.999999 ? 1 : n < 0.000001 ? 0 : n)
Insert cell
formatPercentage = d3.format(".2%")
Insert cell
Insert cell
d3Ternary = import("d3-ternary")
Insert cell
ternaryPlot = d3Ternary.ternaryPlot
Insert cell
barycentric = d3Ternary.barycentric
Insert cell
radius = Math.min(width, height) / 2
Insert cell
// to keep the triangle centered
yOffset = radius/4
Insert cell
height = 640
Insert cell
import { freelanceBanner } from "@julesblm/freelance-banner"
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