D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
biovisualize
Full window
Github gist
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>
https://d3js.org/d3.v3.min.js