Last active
March 2, 2018 00:15
Three-way interactive scatter, showing where points lie on three axes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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