Ternary Plot refactoring.
Simplifying calculations, adding inverse of plot, working sensibly with D3 scales etc.
data reverse engineered from here... http://thegrowingseason.wordpress.com/2013/05/10/soils-management-the-old-jar-test/
<html>
<head>
<title>ternary 2</title>
<script charset="UTF-8" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.1.6/d3.min.js"></script>
<style type="text/css">
.ternary-circle{
stroke:#c00;
fill:#fff;
}
.ternary-line{
fill:none;
stroke:#c00;
}
.ternary-tick{
fill:none;
stroke:#aaa;
}
.minor{
stroke:#fefefe;
}
</style>
</head>
<body>
</body>
<script type="text/javascript">
var test_data = [
{a:100,b:0,c:0,color:'#F00'},
{a:0,b:100,c:0,color:'#0F0'},
{a:0,b:0,c:100,color:'#00F'},
{a:33,b:33,c:33,color:'#999'}
];
function ternaryPlot(){
var ternary = {};
var height = Math.sqrt( 1*1 - (1/2)*(1/2));
var path;
function rescale(range){
if(!range.length) range = [0, 1];
ternary.scale = d3.scale.linear().domain([0, 1]).range(range);
}
function line(interpolator){
if(!interpolator) interpolator = 'linear'
path = d3.svg.line()
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; })
.interpolate(interpolator);
}
rescale([0, 400]);
line();
ternary.range = function(range){
rescale(range);
return ternary;
};
//map teranry coordinate [a, b, c] to an [x, y] position
ternary.point = function(coords){
var pos = [0,0];
var sum = d3.sum(coords);
if(sum !== 0) {
var normalized = coords.map( function(d){ return d/sum; } );
pos[0] = ternary.scale ( normalized[1] + normalized[2] / 2 );
pos[1] = ternary.scale ( height * normalized[0] + height * normalized[1] );
}
return pos;
};
//create an SVG path from a set of points
ternary.line = function(coordsList, accessor, interpolator){ //path generator wrapper
if(interpolator) line(interpolator)
if(!accessor) accessor = function(d){ return d; }
var positions = coordsList.map( function(d){
return ternary.point( accessor(d) );
});
return path(positions);
};
ternary.rule = function(value, axis){
console.log(value, axis);
var ends = [];
if(axis == 0){
ends = [
[value, 0, 100-value],
[value, 100-value, 0]
];
}else if(axis == 1){
ends = [
[0, value, 100-value],
[100-value, value, 0]
];
}else if(axis == 2){
ends = [
[0, 100-value, value],
[100-value, 0, value]
];
}
return ternary.line(ends);
}
// this inverse of point i.e. take an x,y positon and get the ternary coordinate
ternary.getValues = function(pos){ //NOTE! haven't checked if this works yet
pos = pos.map(ternary.scale.inverse);
var c = 1 - pos[1];
var b = pos[0] - c/2;
var a = y - b;
return [a, b, c];
};
return ternary;
}
////
function ternaryAxes(plot){
var axes = {};
var parent = d3.select('svg');
var defaultTicks = d3.range(0,101,25);
var ticks = [defaultTicks, defaultTicks, defaultTicks];
var minorTicks = [[],[],[]];
axes.draw = function(parentSelector){
if(parentSelector) parent = d3.select(parentSelector);
var minor = parent.append('g').attr('id','minor-ticks');
var major = parent.append('g').attr('id','major-ticks')
//minor ticks
for (i = 0; i<minorTicks.length; i++){
for (j = 0; j<minorTicks[i].length; j++){
minor.append('path').attr({
'class':'ternary-tick minor',
'd':plot.rule(minorTicks[i][j], i)
})
}
}
//major ticks
for (var i=0; i<ticks.length; i++){
for (var j=0; j<ticks[i].length; j++){
major.append('path').attr({
'class':'ternary-tick',
'd':plot.rule(ticks[i][j], i)
})
}
}
}
axes.ticks = function(tickArrays){ // an array containing 1 - 3 three arrays the first array will be copied over empty spaces at the end
if(!tickArrays) tickArrays = [defaultTicks,defaultTicks,defaultTicks];
if(!tickArrays[1]) tickArrays[1] = tickArrays[0];
if(!tickArrays[2]) tickArrays[2] = tickArrays[0];
ticks = tickArrays;
return axes;
}
axes.minorTicks = function(tickArrays){
if(!tickArrays) tickArrays = [[],[],[]];
if(!tickArrays[1]) tickArrays[1] = tickArrays[0];
if(!tickArrays[2]) tickArrays[2] = tickArrays[0];
minorTicks = tickArrays;
return axes;
}
return axes;
}
////
var svg = d3.select('body').append('svg').attr({ width:500, height:500 });
var axes = svg.append('g').attr('id','axes');
var plot = svg.append('g').attr('id','plot');
var myTernary = ternaryPlot().range([0,400]);
/*plot.append('path').attr(
{
d:function(){
return myTernary.line(test_data, function(d){ return [d.a, d.b, d.c]; }, 'basis')
},
'class':'ternary-line'
});
plot.selectAll('circle')
.data(test_data)
.enter()
.append('circle')
.attr({
class:'ternary-circle',
r:4,
transform:function(d){
var point = myTernary.point([d.a, d.b, d.c]);
return ("translate(" + point[0] + ", " + point[1] + ")");
},
fill:function(d){ return d.color }
});
*/
var myAxes = ternaryAxes(myTernary);
myAxes.ticks().minorTicks([d3.range(0,101,5)]).draw('#axes');
d3.json('data.json', gotData);
function gotData(d){
for (var type in d){
var f = function (){
return type
}
plot.append('path').attr(
{
d:function(){
return myTernary.line(d[type], function(d){ return [d.sand, d.silt, d.clay]; }) + "Z";
},
'class':'ternary-line',
'id':type.replace(' ','-')
})
.on('click', function(d){
console.log(this.id);
});
}
}
</script>
</html>
{
"sand":[
{
"clay":0,
"sand":100,
"silt":0
},
{
"clay":10,
"sand":90,
"silt":0
},
{
"clay":0,
"sand":90,
"silt":10
}
],
"loamy sand":[
{
"clay":0,
"sand":90,
"silt":10
},
{
"clay":10,
"sand":90,
"silt":0
},
{
"clay":15,
"sand":85,
"silt":0
},
{
"clay":0,
"sand":70,
"silt":30
}
],
"sandy loam":[
{
"clay":0,
"sand":70,
"silt":30
},
{
"clay":15,
"sand":85,
"silt":0
},
{
"clay":20,
"sand":80,
"silt":0
},
{
"clay":20,
"sand":53,
"silt":32
},
{
"clay":5,
"sand":53,
"silt":42
},
{
"clay":5,
"sand":45,
"silt":50
},
{
"clay":0,
"sand":50,
"silt":50
}
],
"sandy clay loam":[
{
"clay":20,
"sand":80,
"silt":0
},
{
"clay":35,
"sand":65,
"silt":0
},
{
"clay":35,
"sand":45,
"silt":20
},
{
"clay":28,
"sand":45,
"silt":27
},
{
"clay":20,
"sand":53,
"silt":32
}
],
"sandy clay":[
{
"clay":35,
"sand":65,
"silt":0
},
{
"clay":35,
"sand":45,
"silt":20
},
{
"clay":55,
"sand":45,
"silt":0
}],
"clay":[
{
"clay":55,
"sand":45,
"silt":0
},
{
"clay":100,
"sand":0,
"silt":0
},
{
"clay":60,
"sand":0,
"silt":40
},
{
"clay":40,
"sand":20,
"silt":40
},
{
"clay":40,
"sand":45,
"silt":15
}
],
"clay loam":[
{
"clay":40,
"sand":45,
"silt":15
},
{
"clay":40,
"sand":20,
"silt":40
},
{
"clay":28,
"sand":20,
"silt":52
},
{
"clay":28,
"sand":45,
"silt":27
}
],
"silty clay":[
{
"clay":60,
"sand":0,
"silt":40
},
{
"clay":40,
"sand":0,
"silt":60
},
{
"clay":40,
"sand":20,
"silt":40
}
],
"silty clay loam":[
{
"clay":28,
"sand":0,
"silt":72
},
{
"clay":28,
"sand":20,
"silt":52
},
{
"clay":40,
"sand":20,
"silt":40
},
{
"clay":40,
"sand":0,
"silt":60
}
],
"silty loam":[
{
"clay":0,
"sand":50,
"silt":50
},
{
"clay":28,
"sand":22,
"silt":50
},
{
"clay":28,
"sand":0,
"silt":72
},
{
"clay":12,
"sand":0,
"silt":88
},
{
"clay":12,
"sand":8,
"silt":80
},
{
"clay":0,
"sand":20,
"silt":80
}
],
"silt":[
{
"clay":0,
"sand":0,
"silt":100
},
{
"clay":0,
"sand":20,
"silt":80
},
{
"clay":12,
"sand":8,
"silt":80
},
{
"clay":12,
"sand":0,
"silt":88
}
],
"loam":[
{
"clay":28,
"sand":45,
"silt":27
},
{
"clay":28,
"sand":22,
"silt":50
},
{
"clay":5,
"sand":45,
"silt":50
},
{
"clay":5,
"sand":53,
"silt":42
},
{
"clay":20,
"sand":53,
"silt":32
}
]
}
notional2005result.csv
notional2005result.json