Skip to content

Instantly share code, notes, and snippets.

@biovisualize
Last active May 21, 2016 21:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save biovisualize/c43740892e8f96769084bcf3e0bb7839 to your computer and use it in GitHub Desktop.
Save biovisualize/c43740892e8f96769084bcf3e0bb7839 to your computer and use it in GitHub Desktop.
Soil composition ternary plot
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v3.min.js"></script>
<style>
svg {
border: 1px solid silver;
}
.container {
width: 500px;
}
.bg-triangle,
.tick path {
fill: white;
stroke: black;
stroke-opacity: 0.3;
}
.marker {
fill: skyblue;
stroke: royalblue;
fill-opacity: 0.8;
}
.axis-title path {
stroke: black;
fill: none;
}
.sector path {
stroke: maroon;
stroke-width: 1;
}
.sector text {
font-size: 10px;
font-family: Helvetica;
}
.clay path {
fill: #f0be99;
}
.silty-clay path {
fill: #c8ad98;
}
.silty-clay-loam path {
fill: #bbaea8;
}
.clay-loam path {
fill: #c0a18c;
}
.sandy-clay path {
fill: #e3e1cc;
}
.sandy-clay-loam path {
fill: #d3c19d;
}
.loam path {
fill: #cac3a9;
}
.sandy-loam path {
fill: #d1cca2;
}
.loamy-sand path {
fill: #c3bca0;
}
.sand path {
fill: #eeebcc;
}
.silt-loam path {
fill: #e8e2d6;
}
.silt path {
fill: #a6a8a7;
}
</style>
</head>
<body>
<div class="container"></div>
<script>
var w = 500;
var h = 600;
var margin = {
top: 20,
right: 40,
bottom: 20,
left: 40
};
var triangleSideLength = w - margin.left - margin.right;
var triangleH = ~~((triangleSideLength / 2) * Math.sqrt(3));
var data = [
[10, 70, 20], // [Clay, Silt, Sand]
[50, 0, 50],
[0, 0, 100]
];
function getPoint(d) {
var x = sandScale(d[2]);
var y = clayScale(d[0]);
var offset = Math.tan(30 * Math.PI / 180) * (triangleH - y);
return [x - offset, y];
}
var clayScale = d3.scale.linear().domain([0, 100]).range([triangleH, 0]);
var sandScale = d3.scale.linear().domain([0, 100]).range([triangleSideLength, 0]);
// base
var svg = d3.select('.container')
.append('svg')
.attr({
width: w,
height: h
});
var triangle = svg.append('g').attr({
transform: 'translate(' + [margin.left, margin.top] + ')'
});
// arrow
svg.append('defs').append('marker')
.attr({
id: 'markerArrow',
markerWidth: 13,
markerHeight: 13,
refX: 2,
refY: 6,
orient: 'auto'
})
.append('path')
.attr({
d: 'M2,2 L2,11 L10,6 L2,2'
});
// frame
triangle.append('path')
.attr({
'class': 'bg-triangle',
d: 'M' + [
[triangleSideLength / 2, 0],
[0, triangleH],
[triangleSideLength, triangleH]
].join('L') + 'Z'
})
.on('mousemove', function(d) {
console.log(d3.mouse(this));
});
// sectors
var sectorData = [
{
name: 'Clay',
limits: [
[100, 0, 0],
[60, 40, 0],
[40, 40, 20],
[40, 15, 45],
[55, 0, 45],
]
},
{
name: 'Silty Clay',
limits: [
[60, 40, 0],
[40, 60, 0],
[40, 40, 20]
]
},
{
name: 'Clay Loam',
limits: [
[40, 40, 20],
[27, 53, 20],
[27, 28, 45],
[40, 15, 45]
]
},
{
name: 'Silty Clay Loam',
limits: [
[40, 60, 0],
[27, 73, 0],
[27, 53, 20],
[40, 40, 20]
]
},
{
name: 'Sandy Clay',
limits: [
[55, 0, 45],
[35, 20, 45],
[35, 0, 65]
]
},
{
name: 'Sandy Clay Loam',
limits: [
[35, 20, 45],
[27, 28, 45],
[20, 28, 52],
[20, 0, 80],
[35, 0, 65]
]
},
{
name: 'Loam',
limits: [
[27, 28, 45],
[27, 50, 23],
[5, 50, 45],
[5, 43, 52],
[20, 28, 52]
]
},
{
name: 'Sandy Loam',
limits: [
[20, 28, 52],
[5, 43, 52],
[5, 50, 45],
[0, 50, 50],
[0, 30, 70],
[15, 0, 85],
[20, 0, 80]
]
},
{
name: 'Sand',
limits: [
[10, 0, 90],
[0, 10, 90],
[0, 0, 100]
]
},
{
name: 'Loamy Sand',
limits: [
[15, 0, 85],
[0, 30, 70],
[0, 10, 90],
[10, 0, 90]
]
},
{
name: 'Silt Loam',
limits: [
[27, 50, 23],
[27, 73, 0],
[13, 87, 0],
[13, 80, 7],
[0, 80, 20],
[0, 50, 50],
]
},
{
name: 'Silt',
limits: [
[13, 80, 7],
[13, 87, 0],
[0, 100, 0],
[0, 80, 20],
]
}
];
sectorData.forEach(function(d) {
d.points = d.limits.map(function(dB, iB) {
return getPoint(dB);
});
d.center = d3.geom.polygon(d.points).centroid();
});
var sector = triangle.selectAll('g.sector')
.data(sectorData);
var sectorEnter = sector.enter().append('g')
.attr({
'class': function(d){
var className = d.name.toLowerCase().replace(/ /g, '-')
return 'sector ' + className;
}
});
sectorEnter.append('path');
sectorEnter.append('text');
sector.exit().remove();
sector.select('path').attr({
d: function(d) {
return 'M' + [d.points].join('L') + 'Z';
}
});
sector.select('text').attr({
x: function(d) {
return d.center[0];
},
y: function(d) {
return d.center[1];
}
})
.text(function(d) {
return d.name;
});
sector.select('text').attr({
dx: function(d) {
return -this.getBBox().width / 2;
}
})
// axis 1
var axis1 = triangle.append('g')
.attr({
'class': 'axis1'
});
var tick = axis1.selectAll('g.tick')
.data(d3.range(10, 110, 10));
var tickEnter = tick.enter().append('g')
.attr({
'class': 'tick'
});
tickEnter.append('path');
tickEnter.append('text');
tick.exit().remove();
tick.select('path')
.attr({
d: function(d) {
var p1 = getPoint([d, 0, 100 - d]);
var p2 = getPoint([0, d, 100 - d]);
return 'M' + [p1, p2].join('L');
}
});
tick.select('text')
.attr({
x: function(d) {
var p1 = getPoint([d, 0, 100 - d]);
return p1[0] - 26;
},
y: function(d) {
var p1 = getPoint([d, 0, 100 - d]);
return p1[1] + 6;
}
})
.text(function(d) {
return d;
});
var axisTitle = axis1.append('g').classed('axis-title', true)
.attr({
transform: 'translate(' + [20, triangleSideLength / 2] + ') rotate(-60)'
});
axisTitle.append('text').text('Clay');
axisTitle.append('path').attr({
'marker-end': 'url(#markerArrow)',
d: 'M-10, 5L60, 5L70 22'
});
// axis 2
var axis2 = triangle.append('g')
.attr({
'class': 'axis2'
});
var tick = axis2.selectAll('g.tick')
.data(d3.range(10, 110, 10));
var tickEnter = tick.enter().append('g')
.attr({
'class': 'tick'
});
tickEnter.append('path');
tickEnter.append('text');
tick.exit().remove();
tick.select('path')
.attr({
d: function(d) {
var p1 = getPoint([d, 100 - d, 0]);
var p2 = getPoint([0, 100 - d, d]);
return 'M' + [p1, p2].join('L');
}
});
tick.select('text')
.attr({
x: function(d) {
var p1 = getPoint([100 - d, d, 0]);
return p1[0] + 6;
},
y: function(d) {
var p1 = getPoint([100 - d, d, 0]);
return p1[1] + 6;
}
})
.text(function(d) {
return d;
});
var axisTitle = axis2.append('g').classed('axis-title', true)
.attr({
transform: 'translate(' + [triangleSideLength - 60, triangleSideLength / 2 - 50] + ') rotate(60)'
});
axisTitle.append('text').text('Silt');
axisTitle.append('path').attr({
'marker-end': 'url(#markerArrow)',
d: 'M-10, 5L60, 5L70 22'
});
// axis 3
var axis3 = triangle.append('g')
.attr({
'class': 'axis3'
})
var tick = axis3.selectAll('g.tick')
.data(d3.range(10, 110, 10));
var tickEnter = tick.enter().append('g')
.attr({
'class': 'tick'
});
tickEnter.append('path');
tickEnter.append('text');
tick.exit().remove();
tick.select('path')
.attr({
d: function(d) {
var p1 = getPoint([100 - d, d, 0]);
var p2 = getPoint([100 - d, 0, d]);
return 'M' + [p1, p2].join('L');
}
});
tick.select('text')
.attr({
x: function(d) {
var p1 = getPoint([0, 100 - d, d]);
return p1[0] - 6;
},
y: function(d) {
var p1 = getPoint([0, 100 - d, d]);
return p1[1] + 16;
}
})
.text(function(d) {
return d;
});
var axisTitle = axis3.append('g').classed('axis-title', true)
.attr({
transform: 'translate(' + [triangleSideLength / 2, triangleH + 60] + ')'
});
axisTitle.append('text').text('Sand');
axisTitle.append('path').attr({
'marker-end': 'url(#markerArrow)',
d: 'M60, -20L-10, -20L-20, -36'
});
// markers
var marker = triangle.selectAll('g.marker')
.data(data);
marker.enter().append('g')
.attr({
transform: function(d) {
var p = getPoint(d);
return 'translate(' + p + ')';
}
})
.append('path')
.attr({
'class': 'marker',
});
marker.exit().remove();
marker.select('path')
.attr({
d: function(d) {
var markerSideLength = 12;
var markerH = ~~((markerSideLength / 2) * Math.sqrt(3));
return 'M' + [
[0, -markerH / 2],
[markerSideLength / 2, markerH / 2],
[-markerSideLength / 2, markerH / 2],
].join('L') + 'Z'
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment