Last active
May 21, 2016 21:17
-
-
Save biovisualize/c43740892e8f96769084bcf3e0bb7839 to your computer and use it in GitHub Desktop.
Soil composition ternary plot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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