tomgp
12/3/2013 - 9:08 AM

Ternary Plot refactoring.

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