Skip to content

Instantly share code, notes, and snippets.

@johnburnmurdoch
Last active March 2, 2018 00:15
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 johnburnmurdoch/81486d2097cbc5b8ea4c8fba3cf9ae60 to your computer and use it in GitHub Desktop.
Save johnburnmurdoch/81486d2097cbc5b8ea4c8fba3cf9ae60 to your computer and use it in GitHub Desktop.
Three-way interactive scatter, showing where points lie on three axes
<!doctype html>
<html lang="">
<head>
<meta charset="utf-8">
<title>Three-way scatter</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type='text/javascript' src='https://unpkg.com/d3'></script>
<script type='text/javascript' src='https://unpkg.com/d3-selection-multi'></script>
<script type='text/javascript' src='https://unpkg.com/d3-scale-chromatic'></script>
<link href="https://fonts.googleapis.com/earlyaccess/mplus1p.css" rel="stylesheet" />
<style>
svg {
/*border: 1px solid gold;*/
}
text {
font-family: 'Mplus 1p',Arial,sans-serif;
}
.domain {
display: none;
}
.spine {
stroke: rgba(0, 0, 0, 0.75);
}
.tick line {
opacity: 0.2
}
line.xy {
stroke: red;
}
.point circle {
stroke: rgba(0, 0, 0, 0.15);
}
.highlighted circle {
stroke: black;
stroke-width: 2;
}
.refLine{
stroke: black;
stroke-dasharray: 2px 2px;
fill: none;
}
.refText{
font-weight: 500;
}
.refTextBack{
stroke: white;
stroke-width: 4px;
}
</style>
</head>
<body>
<svg></svg>
<script type='text/javascript'>
let width = 500,
height = 500,
extent = d3.min([width, height]) * 0.4,
svg = d3.select('svg')
.attrs({
width: width,
height: height
});
let seriesNames = [];
const axes = ['x', 'y', 'z'];
const scale = d3.scaleLinear().range([0, extent]).domain([0, 100]);
d3.csv('mtcars.csv', (e,d) => {
seriesNames = Object.keys(d[0]).slice(0,3);
const data = d.map((d, i) => {
return {
x: +d[seriesNames[0]],
y: +d[seriesNames[1]],
z: +d[seriesNames[2]],
id: `_${i}`,
name: d.name
}
});
const Xextent = d3.extent(data, d => d.x);
const Yextent = d3.extent(data, d => d.y);
const Zextent = d3.extent(data, d => d.z);
const scales = {
x: scale.copy().domain(Xextent),
y: scale.copy().domain(Yextent),
z: scale.copy().domain(Zextent)
};
console.log(data);
const scoreExtent = d3.extent(data, d => d3.mean(Object.values(d).slice(0, 3))).reverse();
// const colourScale = d3.scaleSequential(d3.interpolateRdBu).domain([100, 0]);
const colourScale = d3.scaleSequential(d3.interpolateRdBu).domain(scoreExtent);
axes.forEach((a, i) => {
const titleG = svg.append('g')
.attrs({
class: 'title',
transform: `translate(${width*0.5},${height*0.5}) rotate(${i*120 + 30})`
});
const titleText = titleG.append('text')
.attrs({
x: scales[a].range()[1],
transform: `rotate(${-30 - 120 * i},${scales[a].range()[1]},${0})`,
'text-anchor': ['start', 'end', 'middle'][i]
})
.html(seriesNames[i]);
const spine = svg.append('line')
.attrs({
class: 'spine',
x1: scales[a].range()[0],
x2: scales[a].range()[1],
y1: 0,
y2: 0,
transform: `translate(${width*0.5},${height*0.5}) rotate(${i*120 + 30})`
});
const axis = d3.axisBottom().ticks(5).tickSize(-extent).scale(scales[a]);
const axisG = svg.append('g').call(axis)
.attrs({
transform: `translate(${width*0.5},${height*0.5}) rotate(${i*120 + 30})`
});
const ticks2 = d3.axisBottom().ticks(5).tickSize(extent).scale(scales[a]);
const ticks2G = svg.append('g').call(ticks2)
.attrs({
transform: `translate(${width*0.5},${height*0.5}) rotate(${i*120 + 30})`
});
axisG.selectAll('.tick line')
.attrs({
transform: `rotate(-30)`
});
axisG.selectAll('.tick text')
.attrs({
transform: `rotate(${-30 - 120 * i})`
});
ticks2G.selectAll('.tick line')
.attrs({
transform: `rotate(30)`
});
ticks2G.selectAll('text').remove();
});
svg.selectAll('g.point.xy')
.data(data)
.enter()
.append('g')
.attrs({
class: d => `point xy ${d.id}`,
transform: d => `translate(${(width*0.5) + scales.x(d.x) * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y(d.y) + scales.x(d.x) * Math.sin(Math.PI*1/6))})`
})
.append('circle')
.attrs({
r: 3
})
.styles({
fill: d => colourScale(d3.mean(Object.values(d).slice(0, 3)))
});
svg.selectAll('g.point.xz')
.data(data)
.enter()
.append('g')
.attrs({
class: d => `point xz ${d.id}`,
transform: d => `translate(${(width*0.5) + scales.x(d.x) * Math.cos(Math.PI*1/6) - scales.z(d.z) * Math.cos(Math.PI*1/6)},${(height*0.5) + (scales.x(d.x) * Math.sin(Math.PI*1/6)) + (scales.z(d.z) * Math.sin(Math.PI*1/6))})`
})
.append('circle')
.attrs({
r: 3
})
.styles({
fill: d => colourScale(d3.mean(Object.values(d).slice(0, 3)))
});
svg.selectAll('g.point.yz')
.data(data)
.enter()
.append('g')
.attrs({
class: d => `point yz ${d.id}`,
transform: d => `translate(${(width*0.5) - scales.z(d.z) * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y(d.y) + scales.z(d.z) * Math.sin(Math.PI*1/6))})`
})
.append('circle')
.attrs({
r: 3
})
.styles({
fill: d => colourScale(d3.mean(Object.values(d).slice(0, 3)))
});
svg.selectAll('g.point').on('mouseover', e => {
svg.selectAll(`g.point`)
.classed('highlighted', p => p.id == e.id)
.selectAll('circle')
.attrs({
r: p => p.id == e.id ? 5 : 3
})
svg.insert('path', '.point')
.attrs({
class: 'refLine',
d: `M${(width*0.5) + scales.x(e.x) * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y(e.y) + scales.x(e.x) * Math.sin(Math.PI*1/6))} L${(width*0.5) + scales.x(e.x) * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y.range()[0] + scales.x(e.x) * Math.sin(Math.PI*1/6))} L${(width*0.5) + scales.x(e.x) * Math.cos(Math.PI*1/6) - scales.z(e.z) * Math.cos(Math.PI*1/6)},${(height*0.5) + (scales.x(e.x) * Math.sin(Math.PI*1/6)) + (scales.z(e.z) * Math.sin(Math.PI*1/6))} L${(width*0.5) + scales.x.range()[0] * Math.cos(Math.PI*1/6) - scales.z(e.z) * Math.cos(Math.PI*1/6)},${(height*0.5) + (scales.x.range()[0] * Math.sin(Math.PI*1/6)) + (scales.z(e.z) * Math.sin(Math.PI*1/6))} L${(width*0.5) - scales.z(e.z) * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y(e.y) + scales.z(e.z) * Math.sin(Math.PI*1/6))} L${(width*0.5) - scales.z.range()[0] * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y(e.y) + scales.z.range()[0] * Math.sin(Math.PI*1/6))}Z`
});
svg.selectAll('text.refTextBack')
.data(Object.values(e).slice(0, 3))
.enter()
.append('text')
.attrs({
class: 'refTextBack',
transform: (t,i) => {
let trans;
switch (i) {
case 0:
trans = `translate(${(width*0.5) + scales.x(e.x) * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y.range()[0] + scales.x(e.x) * Math.sin(Math.PI*1/6))})`;
break;
case 1:
trans = `translate(${(width*0.5) + scales.x.range()[0] * Math.cos(Math.PI*1/6) - scales.z(e.z) * Math.cos(Math.PI*1/6)},${(height*0.5) + (scales.x.range()[0] * Math.sin(Math.PI*1/6)) + (scales.z(e.z) * Math.sin(Math.PI*1/6))})`
break;
case 2:
trans = `translate(${(width*0.5) - scales.z.range()[0] * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y(e.y) + scales.z.range()[0] * Math.sin(Math.PI*1/6))})`
}
return trans;
},
'text-anchor': (t,i) => ['start', 'end', 'middle'][i]
})
.html(t => (d3.format(',.1f')(t)).replace(/.0$/g,''));
svg.selectAll('text.refText')
.data(Object.values(e).slice(0, 3))
.enter()
.append('text')
.attrs({
class: 'refText',
transform: (t,i) => {
let trans;
switch (i) {
case 0:
trans = `translate(${(width*0.5) + scales.x(e.x) * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y.range()[0] + scales.x(e.x) * Math.sin(Math.PI*1/6))})`;
break;
case 1:
trans = `translate(${(width*0.5) + scales.x.range()[0] * Math.cos(Math.PI*1/6) - scales.z(e.z) * Math.cos(Math.PI*1/6)},${(height*0.5) + (scales.x.range()[0] * Math.sin(Math.PI*1/6)) + (scales.z(e.z) * Math.sin(Math.PI*1/6))})`
break;
case 2:
trans = `translate(${(width*0.5) - scales.z.range()[0] * Math.cos(Math.PI*1/6)},${(height*0.5) + (-scales.y(e.y) + scales.z.range()[0] * Math.sin(Math.PI*1/6))})`
}
return trans;
},
'text-anchor': (t,i) => ['start', 'end', 'middle'][i]
})
.html(t => (d3.format(',.1f')(t)).replace(/.0$/g,''));
svg.selectAll('text.name')
.data([e])
.enter()
.append('text')
.attrs({
class: 'name',
x: 10,
y: 20
})
.html(e.name)
}).on('mouseout', e => {
svg.selectAll(`g.point`)
.classed('highlighted', 0)
.selectAll('circle')
.attrs({
r: 3
})
svg.selectAll('.refLine, .refText, .refTextBack, .name').remove();
});
})
</script>
</body>
</html>
disp wt hp name
160 2.62 110 Mazda RX4
160 2.875 110 Mazda RX4 Wag
108 2.32 93 Datsun 710
258 3.215 110 Hornet 4 Drive
360 3.44 175 Hornet Sportabout
225 3.46 105 Valiant
360 3.57 245 Duster 360
146.7 3.19 62 Merc 240D
140.8 3.15 95 Merc 230
167.6 3.44 123 Merc 280
167.6 3.44 123 Merc 280C
275.8 4.07 180 Merc 450SE
275.8 3.73 180 Merc 450SL
275.8 3.78 180 Merc 450SLC
472 5.25 205 Cadillac Fleetwood
460 5.424 215 Lincoln Continental
440 5.345 230 Chrysler Imperial
78.7 2.2 66 Fiat 128
75.7 1.615 52 Honda Civic
71.1 1.835 65 Toyota Corolla
120.1 2.465 97 Toyota Corona
318 3.52 150 Dodge Challenger
304 3.435 150 AMC Javelin
350 3.84 245 Camaro Z28
400 3.845 175 Pontiac Firebird
79 1.935 66 Fiat X1-9
120.3 2.14 91 Porsche 914-2
95.1 1.513 113 Lotus Europa
351 3.17 264 Ford Pantera L
145 2.77 175 Ferrari Dino
301 3.57 335 Maserati Bora
121 2.78 109 Volvo 142E
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment