A ternary plot forked from this one by Marielle Lange
forked from tomgp's block: Ternary plot
license: mit |
A ternary plot forked from this one by Marielle Lange
forked from tomgp's block: Ternary plot
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>D3 Ternary Plot</title> | |
<style> | |
a{ | |
font-family: sans-serif; | |
color: #DB7365; | |
padding: 0.3rem; | |
} | |
a:hover{ | |
background-color: #DB7365; | |
color: #fff1e0; | |
} | |
body{ | |
background: #fff1e0; | |
} | |
line.tick { | |
stroke-width: 0.5; | |
} | |
line.minor-tick { | |
stroke-width: 1; | |
stroke-opacity:0.1; | |
} | |
.a-axis{ | |
stroke: #333; | |
} | |
.b-axis{ | |
stroke: #333; | |
} | |
.c-axis{ | |
stroke: #333; | |
} | |
.axis-title{ | |
font-family: sans-serif; | |
font-size: 1.5rem; | |
} | |
text.tick-text { | |
font-family: sans-serif; | |
font-weight: lighter; | |
font-size: 1rem; | |
fill: #333; | |
stroke:none; | |
} | |
circle { | |
fill: #fff1e0; | |
stroke: #DB7365; | |
stroke-width: 2px; | |
} | |
</style> | |
</head> | |
<a id="nextbutton" href="#">Data change</a> | |
<div id="plot"> | |
</div> | |
<script charset="UTF-8" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.1.6/d3.min.js"></script> | |
<script> | |
function ternaryPlot(selector, userOpt ) { | |
var plot = { | |
dataset:[] | |
}; | |
var opt = { | |
width:900, | |
height:900, | |
side: 700, | |
margin: {top:50,left:50,bottom:50,right:50}, | |
axis_labels:['A','B','C'], | |
axis_ticks:[0,20,40,60,80,100], | |
tickLabelMargin:10, | |
axisLabelMargin:40 } | |
for(var o in userOpt){ | |
opt[o] = userOpt[o]; | |
} | |
var svg = d3.select(selector).append('svg') | |
.attr("width", opt.width) | |
.attr("height", opt.height); | |
var axes = svg.append('g').attr('class','axes'); | |
var w = opt.side; | |
var h = Math.sqrt( opt.side*opt.side - (opt.side/2)*(opt.side/2)); | |
var corners = [ | |
[opt.margin.left, h + opt.margin.top], // a | |
[ w + opt.margin.left, h + opt.margin.top], //b | |
[(w/2) + opt.margin.left, opt.margin.top] ] //c | |
//axis names | |
axes.selectAll('.axis-title') | |
.data(opt.axis_labels) | |
.enter() | |
.append('g') | |
.attr('class', 'axis-title') | |
.attr('transform',function(d,i){ | |
return 'translate('+corners[i][0]+','+corners[i][1]+')'; | |
}) | |
.append('text') | |
.text(function(d){ return d; }) | |
.attr('text-anchor', function(d,i){ | |
if(i===0) return 'end'; | |
if(i===2) return 'middle'; | |
return 'start'; | |
}) | |
.attr('transform', function(d,i){ | |
var theta = 0; | |
if(i===0) theta = 120; | |
if(i===1) theta = 60; | |
if(i===2) theta = -90; | |
var x = opt.axisLabelMargin * Math.cos(theta * 0.0174532925), | |
y = opt.axisLabelMargin * Math.sin(theta * 0.0174532925); | |
return 'translate('+x+','+y+')' | |
}); | |
//ticks | |
//(TODO: this seems a bit verbose/ repetitive!); | |
var n = opt.axis_ticks.length; | |
if(opt.minor_axis_ticks){ | |
opt.minor_axis_ticks.forEach(function(v) { | |
var coord1 = coord( [v, 0, 100-v] ); | |
var coord2 = coord( [v, 100-v, 0] ); | |
var coord3 = coord( [0, 100-v, v] ); | |
var coord4 = coord( [100-v, 0, v] ); | |
axes.append("line") | |
.attr( lineAttributes(coord1, coord2) ) | |
.classed('a-axis minor-tick', true); | |
axes.append("line") | |
.attr( lineAttributes(coord2, coord3) ) | |
.classed('b-axis minor-tick', true); | |
axes.append("line") | |
.attr( lineAttributes(coord3, coord4) ) | |
.classed('c-axis minor-tick', true); | |
}); | |
} | |
opt.axis_ticks.forEach(function(v) { | |
var coord1 = coord( [v, 0, 100-v] ); | |
var coord2 = coord( [v, 100-v, 0] ); | |
var coord3 = coord( [0, 100-v, v] ); | |
var coord4 = coord( [100-v, 0, v] ); | |
axes.append("line") | |
.attr( lineAttributes(coord1, coord2) ) | |
.classed('a-axis tick', true); | |
axes.append("line") | |
.attr( lineAttributes(coord2, coord3) ) | |
.classed('b-axis tick', true); | |
axes.append("line") | |
.attr( lineAttributes(coord3, coord4) ) | |
.classed('c-axis tick', true); | |
//tick labels | |
axes.append('g') | |
.attr('transform',function(d){ | |
return 'translate(' + coord1[0] + ',' + coord1[1] + ')' | |
}) | |
.append("text") | |
.attr('transform','rotate(60)') | |
.attr('text-anchor','end') | |
.attr('x',-opt.tickLabelMargin) | |
.text( function (d) { return v; } ) | |
.classed('a-axis tick-text', true ); | |
axes.append('g') | |
.attr('transform',function(d){ | |
return 'translate(' + coord2[0] + ',' + coord2[1] + ')' | |
}) | |
.append("text") | |
.attr('transform','rotate(-60)') | |
.attr('text-anchor','end') | |
.attr('x',-opt.tickLabelMargin) | |
.text( function (d) { return (100- v); } ) | |
.classed('b-axis tick-text', true); | |
axes.append('g') | |
.attr('transform',function(d){ | |
return 'translate(' + coord3[0] + ',' + coord3[1] + ')' | |
}) | |
.append("text") | |
.text( function (d) { return v; } ) | |
.attr('x',opt.tickLabelMargin) | |
.classed('c-axis tick-text', true); | |
}) | |
function lineAttributes(p1, p2){ | |
return { x1:p1[0], y1:p1[1], | |
x2:p2[0], y2:p2[1] }; | |
} | |
function coord(arr){ | |
var a = arr[0], b=arr[1], c=arr[2]; | |
var sum, pos = [0,0]; | |
sum = a + b + c; | |
if(sum !== 0) { | |
a /= sum; | |
b /= sum; | |
c /= sum; | |
pos[0] = corners[0][0] * a + corners[1][0] * b + corners[2][0] * c; | |
pos[1] = corners[0][1] * a + corners[1][1] * b + corners[2][1] * c; | |
} | |
return pos; | |
} | |
function scale(p, factor) { | |
return [p[0] * factor, p[1] * factor]; | |
} | |
plot.data = function(data, accessor, bindBy){ //bind by is the dataset property used as an id for the join | |
plot.dataset = data; | |
var circles = svg.selectAll("circle") | |
.data( data.map( function(d){ return coord(accessor(d)); }), function(d,i){ | |
if(bindBy){ | |
return plot.dataset[i][bindBy]; | |
} | |
return i; | |
} ); | |
circles.enter().append("circle"); | |
circles.transition().attr("cx", function (d) { return d[0]; }) | |
.attr("cy", function (d) { return d[1]; }) | |
.attr("r", 6); | |
return this; | |
} | |
plot.getPosition = coord; | |
plot.getTripple = function(x, y){ | |
//TODO, get percentages for a give x, y | |
} | |
return plot; | |
} | |
//ACTIVATE! | |
var plot_opts = { | |
side: 400, | |
margin: {top:70,left:150,bottom:150,right:150}, | |
axis_labels:['Journalist','Developer','Designer'], | |
axis_ticks:d3.range(0, 101, 20), | |
minor_axis_ticks:d3.range(0, 101, 5) | |
} | |
var tp = ternaryPlot( '#plot', plot_opts ); | |
/*.data([ | |
{journalist:75,developer:25,designer:0,label:'point 1'}, | |
{journalist:70,developer:10,designer:20,label:'point 2'}, | |
{journalist:75,developer:20,designer:5,label:'point 3'}, | |
{journalist:5,developer:60,designer:35,label:'point 4'}, | |
{journalist:10,developer:80,designer:10,label:'point 5'}, | |
{journalist:10,developer:90,designer:0,label:'point 6'}, | |
{journalist:20,developer:70,designer:10,label:'point 7'}, | |
{journalist:10,developer:20,designer:70,label:'point 8'}, | |
{journalist:15,developer:5,designer:80,label:'point 9'}, | |
{journalist:10,developer:10,designer:80,label:'point 10'}, | |
{journalist:20,developer:10,designer:70,label:'point 11'}, | |
], function(d){ return [d.journalist, d.developer, d.designer]}, 'label');*/ | |
function next(){ | |
var d = [] | |
for(var i = 0; i < 100; i++){ | |
d.push({ | |
journalist:Math.random(), | |
developer:Math.random(), | |
designer:Math.random(), | |
label:'point'+i | |
}) | |
} | |
tp.data(d, function(d){ return [d.journalist, d.developer, d.designer]}, 'label'); | |
} | |
next(); | |
d3.select('#nextbutton').on('click', function(e){ | |
next(); d3.event.preventDefault(); }); | |
</script> | |
</body> | |
</html> |