Public
Edited
Apr 9
1 fork
5 stars
Insert cell
Insert cell
Insert cell
A small demo of the [\`d3-ternary\`](https://observablehq.com/@julesblm/introducing-d3-ternary?collection=@julesblm/ternary-plots) module: a recreation of the USDA soil textural triangle, a ternary plot used to classify soils based on their physical texture; the ratios of sand, silt, and clay in the soil.

## Make Your Own

Make your own USDA soil triangle on [TernaryPlot.com](https://www.ternaryplot.com), click the "Templates" button above the table and select the USDA soil triangle.

## Sources

* [The Historical Development of the USDA textural triangle](https://umnsoilsteam.blogspot.com/2014/09/the-historical-development-of-usda.html) by University of Minnesota Soil Judging Team
* [Wikipedia: Soil texture](https://en.wikipedia.org/wiki/Soil_texture)
* [USDA Soil Texture Calculator](https://www.nrcs.usda.gov/wps/portal/nrcs/detail/soils/survey/?cid=nrcs142p2_054167)

Insert cell
Insert cell
viewof texturalTrianglePlot = {
const node = DOM.svg(width, height);
const svg = d3.select(node);
const chart = svg
.attr("id", "chart")
.append("g")
.attr("font-family", "sans-serif")
.attr("transform", `translate(${width / 2} ${yOffset})`);

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

defs
.append("clipPath")
.attr("id", "trianglePath")
.append("path")
.attr("d", textureTernaryPlot.triangle());

defs
.append("marker")
.attr("id", "arrow")
.attr("markerWidth", "10")
.attr("markerHeight", "10")
.attr("refX", "0")
.attr("refY", "3")
.attr("orient", "auto")
.attr("markerUnits", "strokeWidth")
.append("path")
.attr("d", "M0,0 L0,6 L9,3 z");

chart
.append("g")
.attr("class", "grid")
.selectAll("g")
.data(gridLines)
.join("path")
.attr("d", (d) => d)
.attr("stroke", "black")
.attr("stroke-width", 0.5);

const tickGroup = chart
.append("g")
.attr("class", "ternary-ticks")
.attr("font-size", 10)
.selectAll("g")
.data(textureTernaryPlot.ticks())
.join("g")
.attr("class", "axis-ticks")
.selectAll("g")
.data((d) => d)
.join("g")
.attr("class", "tick")
.attr("transform", (d) => `translate(${d.position})`)
.call((g) =>
g
.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)
)
.call((g) =>
g
.append("line")
.attr("transform", (d) => `rotate(${d.angle + 90})`)
.attr("y2", (d) => d.size * (d.textAnchor === "start" ? -1 : 1))
.attr("stroke", "grey")
);

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

const axisLabelsGroups = axisLabelsGroup
.selectAll("text")
.data(textureTernaryPlot.axisLabels({ center: true }), (d) => d.label)
.join(
(enter) => {
const labelGroup = enter
.append("g")
.attr(
"transform",
(d, i) => `translate(${d.position})rotate(${d.angle})`
);

const axisArrow = labelGroup
.append("line")
.attr("stroke", "black")
.attr("x1", (d, i) => (i === 2 ? 100 : -100))
.attr("x2", (d, i) => (i === 2 ? -100 : 100))
.attr("marker-end", "url(#arrow)");

const axisLabelText = labelGroup
.append("text")
.attr("text-anchor", "middle")
.attr("stroke", "ghostwhite")
.attr("stroke-width", 7)
.attr("paint-order", "stroke")
.attr("alignment-baseline", "middle")
.text((d) => d.label);
},
(update) =>
update.attr(
"transform",
(d, i) => `translate(${d.position})rotate(${d.angle})`
)
);

chart
.append("path")
.attr("d", textureTernaryPlot.triangle())
.attr("fill", "none")
.attr("pointer-events", "all")
.attr("stroke", "black")
.attr("stroke-width", "1.2px")
.call(
d3
.drag()
.on("drag", ({ x, y }) => dragged(x, y))
.on("start", ({ x, y }) => dragged(x, y))
);

chart
.append("g")
.attr("class", "data")
.attr("clip-path", "url(#trianglePath)")
.selectAll("path")
.data(ternaryDivisions.map((d) => d.coords))
.join("path")
.attr("d", closedLine)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", 2);

chart
.append("g")
.selectAll("text")
.data(ternaryDivisions)
.join("text")
.attr("x", (d) => d.centroid[0])
.attr("y", (d) => d.centroid[1])
.attr("text-anchor", "middle")
.attr("stroke", "ghostwhite")
.attr("stroke-width", 5)
.attr("paint-order", "stroke")
.attr("alignment-baseline", "middle")
.attr("font-size", 11)
.text((d) => d.texture);

const handle = chart
.append("circle")
.attr("id", "drag-handle")
.attr("fill", "cornflowerblue")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 4);

handle.append("title").text((d) => d);

function dragged(x, y) {
handle.attr("cx", x).attr("cy", y);

node.dispatchEvent(new CustomEvent("input"), { bubbles: true });
node.value = textureTernaryPlot.invert([x, y]);
}

node.value = textureTernaryPlot.invert([0, 0]);

return node;
}
Insert cell
formatPercentage = d3.format(".0%")
Insert cell
textureTernaryPlot = {
const ternarySoil = barycentric()
.a((d) => d.silt)
.b((d) => d.clay)
.c((d) => d.sand)
.rotation(240);

return ternaryPlot(ternarySoil)
.radius(radius)
.labels(["Silt Separate, %", "Clay Separate, %", "Sand Separate, %"])
.labelAngles([60, -60, 0])
.labelOffsets([100, 100, 100])
.tickAngles([-60, 0, 60])
.tickFormat((t) => formatPercentage(1 - t))
.tickTextAnchors(["end", "start", "end"]);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require('d3@v6')
Insert cell
Insert cell
radius = height / 2
Insert cell
yOffset = height / 2 + textureTernaryPlot.radius() / 4
Insert cell
barycentric = d3Ternary.barycentric
Insert cell
ternaryPlot = d3Ternary.ternaryPlot
Insert cell
d3Ternary = import("d3-ternary")
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