Skip to content

Instantly share code, notes, and snippets.

@williaster
Last active November 23, 2016 19:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save williaster/3265cbcac3d18161952a07815142f61f to your computer and use it in GitHub Desktop.
Save williaster/3265cbcac3d18161952a07815142f61f to your computer and use it in GitHub Desktop.
Ternary plot
license: mit
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Data Viz as 3 Spectrums</title>
<style>
a{
font-family: sans-serif;
color: #DB7365;
padding: 0.3rem;
}
a:hover{
background-color: #DB7365;
color: #fff1e0;
}
body{
background: #fff;
}
line.tick {
stroke-width: 0.5;
stroke-opacity: 0.5
}
line.minor-tick {
stroke-width: 1;
stroke-opacity:0;
}
.a-axis,
.b-axis,
.c-axis {
stroke: #484848;
}
.axis-title{
font-family: sans-serif;
font-size: 1.5rem;
}
text.tick-text {
font-family: sans-serif;
font-weight: 200;
font-size: 1rem;
fill: #484848;
stroke:none;
}
circle {
fill:#00A699;
fill-opacity: 0.5;
stroke: none;
}
</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, 2, 4, 6, 8, 10],
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
]
var labels = [
[opt.margin.left + 20, (h/2) + opt.margin.top - 10, -60], // data
[(w/2) + opt.margin.left + 5, h + opt.margin.top + 50, 0], // eng
[opt.margin.left + w -20, (h/2) + opt.margin.top -10, +60], // designer
]
//axis names
axes.selectAll('.axis-title')
.data(opt.axis_labels)
.enter()
.append('g')
.attr('class', 'axis-title')
.append('text')
.text(function(d){ return d; })
.attr('transform',function(d,i){
return 'translate('+labels[i][0]+','+labels[i][1]+')rotate('+labels[i][2]+')';
//return 'translate('+corners[i][0]+','+corners[i][1]+')';
})
.attr('text-anchor', 'middle')
// .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;
var tickLabelLookup = {
0: 'low',
10: 'high',
};
opt.axis_ticks.forEach(function(v) {
var coord1 = coord( [v, 0, 10-v] );
var coord2 = coord( [v, 10-v, 0] );
var coord3 = coord( [0, 10-v, v] );
var coord4 = coord( [10-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(0)translate(-10,0)')
.attr('text-anchor','middle')
.attr('x',-opt.tickLabelMargin)
.text( function (d) { return tickLabelLookup[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(0)translate(10,20)')
.attr('text-anchor','middle')
.attr('x',-opt.tickLabelMargin)
.text( function (d) { return tickLabelLookup[10 - v]; } )
.classed('b-axis tick-text', true);
axes.append('g')
.attr('transform',function(d){
return 'translate(' + coord3[0] + ',' + coord3[1] + ')'
})
.append("text")
.attr('transform','rotate(0)translate(5,0)')
.text( function (d) { return tickLabelLookup[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;
}
//bind by is the dataset property used as an id for the join
plot.data = function(data, accessor, bindBy){
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: 300,
margin: { top: 70, left: 150, bottom: 150, right: 150 },
axis_labels:['Data Analysis','Engineer','Designer'],
axis_ticks: d3.range(0, 11, 2),
}
var tp = ternaryPlot( '#plot', plot_opts );
function next(){
var d = []
for (var i = 0; i < 100; i++) {
d.push({
data: Math.random(),
engineer: Math.random()/2,
designer: Math.random()*5,
label:'point' + i
})
}
tp.data(d, function(d){ return [d.data, d.engineer, d.designer]}, 'label');
}
next();
d3.select('#nextbutton').on('click', function(e){
next(); d3.event.preventDefault(); });
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment