|
<!DOCTYPE html> |
|
<meta charset='utf-8'> |
|
<style> |
|
svg { |
|
font: 11px sans-serif; |
|
} |
|
|
|
.heading { |
|
font: 30px sans-serif; |
|
} |
|
|
|
.case rect, .case path { |
|
fill: #bbb; |
|
} |
|
.male rect, .male path { |
|
fill: #3388EE; |
|
} |
|
.female rect, .female path { |
|
fill: #EEAACC; |
|
} |
|
.case .mean { |
|
fill: white; |
|
stroke: black; |
|
stroke-width: .5; |
|
} |
|
.case .deviation { |
|
fill: black; |
|
} |
|
|
|
.gums { |
|
fill: #fdd; |
|
} |
|
.tooth path { |
|
stroke: #555; |
|
stroke-width: 1.5; |
|
fill: white; |
|
} |
|
rect.focus, .focus path { |
|
fill: #eef; |
|
} |
|
.focus path { |
|
stroke: #000; |
|
stroke-width: 2; |
|
} |
|
|
|
.axis { |
|
font: 10px sans-serif; |
|
} |
|
.axis path, |
|
.axis line { |
|
fill: none; |
|
stroke: #ddd; |
|
shape-rendering: crispEdges; |
|
stroke-dasharray: 2, 2; |
|
} |
|
.frame, |
|
.axis .major line { |
|
fill: none; |
|
stroke: #ddd; |
|
stroke-dasharray: none; |
|
} |
|
.label { |
|
font: 12px sans-serif; |
|
} |
|
|
|
.legend rect { |
|
fill: white; |
|
fill-opacity: .75; |
|
} |
|
.legend line { |
|
stroke: #888; |
|
stroke-dasharray: 1,1; |
|
} |
|
.dev-range { |
|
fill: none; |
|
stroke: #888; |
|
stroke-dasharray: 1,1; |
|
} |
|
|
|
</style> |
|
<body> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script src="teeth.js"></script> |
|
<script src="d3.svg.devbox.js"></script> |
|
<script> |
|
|
|
var margin = { top: 10, right: 10, bottom: 40, left: 65 }, |
|
width = 960 - margin.left - margin.right, |
|
height = 500 - margin.top - margin.bottom, |
|
y = d3.scale.ordinal().rangeBands([ 0, height ]), |
|
x = d3.scale.linear().range([ 0, width ]); |
|
|
|
var svg = d3.select( 'body' ).append( 'svg' ) |
|
.attr( 'width', width + margin.left + margin.right ) |
|
.attr( 'height', height + margin.top + margin.bottom ) |
|
.append( 'g' ) |
|
.attr( 'transform', `translate(${margin.left},${margin.top})` ); |
|
|
|
var defs = svg.append( 'defs' ) |
|
.append( 'clipPath' ) |
|
.attr( 'id', 'plotclip' ) |
|
.append( 'rect' ) |
|
.attr( 'id', 'plotrect' ) |
|
.attr( 'x', 0 ) |
|
.attr( 'y', 0 ) |
|
.attr( 'width', width ) |
|
.attr( 'height', height ); |
|
|
|
d3.csv( 'eruptions.csv', function ( eruptions ) { |
|
|
|
var data = d3.nest() |
|
.key( d => d.sex + '/' + d.teeth ) |
|
.rollup( values => { |
|
return { |
|
'mean': d3.mean( values, d => d.months ), |
|
'std': d3.deviation( values, d => d.months ), |
|
'sex': values[0].sex, |
|
'teeth': values[0].teeth |
|
}; |
|
}) |
|
.entries( eruptions ); |
|
|
|
y.domain( data.map( d => d.key ) ); |
|
x.domain([ 0, 12 * 3 ]); // 3 years |
|
|
|
// hover focus |
|
var hover_line = svg.append( 'rect' ) |
|
.attr( 'class', 'mouseline focus' ) |
|
.attr( 'width', width ) |
|
.attr( 'height', y.rangeBand() ) |
|
.style( 'display', 'none' ); |
|
|
|
|
|
// X-Axis |
|
var xAxisBottom = d3.svg.axis() |
|
.scale( x ) |
|
.ticks( 30 ) |
|
.tickPadding( 6 ) |
|
.tickSize( -height ) |
|
.orient( "bottom" ); |
|
|
|
var axx = svg.append( "g" ) |
|
.attr( "class", "x axis" ) |
|
.attr( "transform", `translate(0,${height})` ) |
|
.call( xAxisBottom ); |
|
|
|
axx.selectAll( '.tick' ) |
|
.attr( 'class', d => !( d % 12 ) ? 'major tick' : 'tick' ) |
|
|
|
axx.append( 'text' ) |
|
.attr( 'class', 'label' ) |
|
.attr( 'x', width/2 ) |
|
.attr( 'y', 30 ) |
|
.text( 'Age in months' ) |
|
.style( 'text-anchor', 'middle' ); |
|
|
|
|
|
// Y-Axis |
|
var yAxisBottom = d3.svg.axis() |
|
.scale( y ) |
|
.tickPadding( 8 ) |
|
.tickSize( -width ) |
|
.orient( "left" ) |
|
.tickFormat( d => d.split( '/' )[1] ); |
|
|
|
var axy = svg.append( "g" ) |
|
.attr( "class", "y axis" ) |
|
.call( yAxisBottom ); |
|
|
|
axy.append( 'text' ) |
|
.attr( 'class', 'label' ) |
|
.attr( 'transform', `translate(-45,${height/2}) rotate(-90)` ) |
|
.text( 'Teeth ID pairs by ISO 3950 notation' ) |
|
.style( 'text-anchor', 'middle' ); |
|
|
|
|
|
// Plot frame |
|
svg.append( 'use' ) |
|
.attr( 'xlink:href', '#plotrect' ) |
|
.attr( 'class', 'frame' ); |
|
|
|
|
|
// Legend |
|
var legend_width = x(35) - x(24); |
|
|
|
var legend = svg.append( 'g' ) |
|
.attr( 'class', 'legend' ) |
|
.attr( 'transform', d => `translate(${x(24.5)},15)` ); |
|
|
|
legend.append( 'rect' ) |
|
.attr( 'width', legend_width ) |
|
.attr( 'height', 278 ) |
|
.attr( 'rx', 7 ); |
|
|
|
// description |
|
legend.append( 'text' ) |
|
.attr( 'class', 'heading' ) |
|
.attr( 'x', legend_width * .5 ) |
|
.attr( 'y', 40 ) |
|
.style( 'text-anchor', 'middle' ) |
|
.text( 'Primary Teeth' ) |
|
|
|
var population = d3.set( eruptions.map( d => d.id ) ).size(); |
|
legend.append( 'text' ) |
|
.attr( 'y', 53 ) |
|
.style( 'font', '12px sans-serif' ) |
|
.style( 'text-anchor', 'start' ) |
|
.selectAll( 'tspan' ) |
|
.data([ |
|
'Eruption of teeth and the dental maturity', |
|
'of Icelandic children. From a study of ' + population, |
|
'children between the ages of 0-83 months', |
|
'in the winter of 1978-1979' |
|
]).enter() |
|
.append( 'tspan' ) |
|
.attr( 'x', x(1) ) |
|
.attr( 'dy', '1.2em' ) |
|
.text( String ); |
|
|
|
var f_label = legend.append( 'g' ) |
|
.attr( 'class', 'female' ) |
|
.attr( 'transform', 'translate('+(legend_width*.6)+',130)' ); |
|
f_label.append( 'text' ) |
|
.attr( 'class', 'label' ) |
|
.attr( 'dy', '.32em' ) |
|
.attr( 'x', 10 ) |
|
.text( 'Girls' ); |
|
f_label.append( 'path' ) |
|
.attr( 'd', d3.svg.symbol().type( 'square' ).size( 60 ) ); |
|
|
|
var m_label = legend.append( 'g' ) |
|
.attr( 'class', 'male' ) |
|
.attr( 'transform', 'translate('+(legend_width*.3)+',130)' ); |
|
m_label.append( 'text' ) |
|
.attr( 'class', 'label' ) |
|
.attr( 'dy', '.32em' ) |
|
.attr( 'x', 10 ) |
|
.text( 'Boys' ); |
|
m_label.append( 'path' ) |
|
.attr( 'd', d3.svg.symbol().type( 'square' ).size( 60 ) ); |
|
|
|
var sdlegend = d3.svg.devbox() |
|
.scale( x ) |
|
.height( y.rangeBand() ) |
|
.tickFormat( d => (d > 0 ? '+' : '' ) + d + 'σ' ); |
|
|
|
var stdlabel = legend.append( 'g' ) |
|
.attr( 'transform', `translate(${legend_width/2},173)` ); |
|
|
|
stdlabel.append( 'g' ) |
|
.datum({ mean: 0, deviation: 1.5 }) |
|
.attr( 'class', 'case' ) |
|
.call( sdlegend ); |
|
|
|
stdlabel.append( 'line' ) |
|
.attr( 'y1', 0 - 10 ) |
|
.attr( 'y2', 0 ); |
|
|
|
stdlabel.append( 'text' ) |
|
.attr( 'y', 0 - y.rangeBand() + 5 ) |
|
.attr( 'dy', '.32em' ) |
|
.style( 'text-anchor', 'middle' ) |
|
.text( 'Average' ); |
|
|
|
// single stdev |
|
stdlabel.append( 'path' ) |
|
.attr( 'transform', 'translate(0,35)' ) |
|
.attr( 'd', `m${-x(1.5)},0s 0,5, 5,5 l${x(1.5*2)-10},0 s5,0 5,-5` ) |
|
.attr( 'class', 'dev-range' ); |
|
|
|
stdlabel.append( 'text' ) |
|
.attr( 'y', 50 ) |
|
.attr( 'dy', '.32em' ) |
|
.style( 'text-anchor', 'middle' ) |
|
.text( '68.27% of measures' ); |
|
|
|
// double stdev |
|
stdlabel.append( 'path' ) |
|
.attr( 'transform', 'translate(0,35)' ) |
|
.attr( 'd', `m${-x(1.5*2)},0 l0,30 s 0,5, 5,5 l${x(1.5*4)-10},0 s5,0 5,-5 l0,-30` ) |
|
.attr( 'class', 'dev-range' ); |
|
|
|
stdlabel.append( 'text' ) |
|
.attr( 'y', 80 ) |
|
.attr( 'dy', '.32em' ) |
|
.style( 'text-anchor', 'middle' ) |
|
.text( '95.45% of measures' ); |
|
|
|
|
|
// Teething cases |
|
var sdplot = d3.svg.devbox() |
|
.scale( x ) |
|
.height( y.rangeBand() ) |
|
.mean( d => d.values.mean ) |
|
.deviation( d => d.values.std ) |
|
; |
|
var cases = svg.append( 'g' ) |
|
.attr( 'class', 'group' ) |
|
.selectAll( '.case' ).data( data ).enter() |
|
.append( 'g' ) |
|
.attr( 'class', d => 'case ' + d.values.sex ) |
|
.attr( 'transform', d => `translate(0,${y(d.key)})` ) |
|
.call( sdplot ) |
|
.selectAll( 'path.box' ) |
|
.attr( 'clip-path', 'url(#plotclip)' ) |
|
; |
|
|
|
// Mouth illustration |
|
var mouth = svg.append( 'g' ) |
|
.attr( 'transform', 'translate(100,210) scale(.4,.4)' ); |
|
|
|
mouth.append( 'path' ) |
|
.attr( 'd', teeth.path.gum ) |
|
.attr( 'class', 'gums' ); |
|
|
|
mouth.append( 'path' ) |
|
.attr( 'transform', 'translate(0,510) scale(1,-1)' ) |
|
.attr( 'd', teeth.path.gum ) |
|
.attr( 'class', 'gums' ); |
|
|
|
mouth.selectAll('.label') |
|
.data([ 'Upper', 'teeth' ]).enter() |
|
.append( 'text' ) |
|
.text( String ) |
|
.attr( 'dy', (d,i) => i * 40 ) |
|
.attr( 'y', d => 140 ) |
|
.style( 'text-anchor', 'middle' ) |
|
.style( 'font-size', '32px' ) |
|
|
|
mouth.selectAll('.label') |
|
.data([ 'Lower', 'teeth' ]).enter() |
|
.append( 'text' ) |
|
.text( String ) |
|
.attr( 'dy', (d,i) => i * 40 ) |
|
.attr( 'y', d => 200 + 150 ) |
|
.style( 'text-anchor', 'middle' ) |
|
.style( 'font-size', '32px' ) |
|
|
|
var teethg = mouth.selectAll( '.tooth' ) |
|
.data( teeth.deciduous ) |
|
.enter() |
|
.append( 'g' ) |
|
.attr( 'class', 'tooth' ); |
|
|
|
teethg.append( 'path' ) |
|
.attr( 'd', d => teeth.path[ d.type ] ) |
|
.attr( 'id', d => 'tooth_' + d.id ) |
|
.attr( 'transform', d => { |
|
var s = [ 1, 1 ], |
|
p = d.pos.concat(); |
|
if ( d.pos[0] > 0 ) { s[0] = -1; } |
|
if ( d.pos[1] < 0 ) { s[1] = -1; p[1] += 510; } |
|
return `translate(${p}) scale(${s}) rotate(${d.r})`; |
|
}); |
|
|
|
// extend a line running from a centerpoint to a |
|
// destination point by a given length |
|
function expandBy ( cpos, dpos, l ) { |
|
var alpha = Math.atan2( dpos[0] - cpos[0], dpos[1] - cpos[1] ), |
|
dist = Math.sqrt( Math.pow( dpos[0] - cpos[0], 2 ) + |
|
Math.pow( dpos[1] - cpos[1], 2 ) ); |
|
return [ cpos[0] + ( dist + l ) * Math.sin( alpha ) |
|
, cpos[1] + ( dist + l ) * Math.cos( alpha ) ]; |
|
} |
|
|
|
teethg.append( 'text' ) |
|
.attr( 'dy', '.32em' ) |
|
.attr( 'x', d => { |
|
var cx = ( d.pos[1] < 0 ) ? -180 : 180; |
|
return expandBy( [ 0, cx ], d.pos, 40 )[ 0 ]; |
|
}) |
|
.attr( 'y', d => { |
|
var cx = ( d.pos[1] < 0 ) ? -180 : 180, |
|
yp = ( d.pos[1] < 0 ) ? 510 : 0; |
|
return expandBy( [ 0, cx ], d.pos, 40 )[ 1 ] + yp; |
|
}) |
|
.text( d => d.id ) |
|
.style( 'text-anchor', d => ( d.pos[0] > 0 ) ? 'start' : 'end' ) |
|
.style( 'font-size', '24px' ); |
|
|
|
|
|
// Mouse events |
|
svg.attr( 'pointer-events', 'all' ) |
|
.on( 'mouseout', function () { |
|
hover_line.style( 'display', 'none' ); |
|
mouth.selectAll( '.tooth' ).attr( 'class', 'tooth' ); |
|
}) |
|
.on( 'mousemove', function () { |
|
var pos = d3.mouse( this ), |
|
ypos = Math.max( pos[1], 0 ); |
|
if ( ypos > 0 && ypos < height ) { |
|
var ent = y.domain()[ d3.bisect( y.range(), ypos ) - 1 ], |
|
tgt = ent.split( '/' )[1].split( ' ' ); |
|
mouth.selectAll( '.tooth' ) |
|
.attr( 'class', d => tgt.indexOf( d.id ) > -1 ? 'tooth focus' : 'tooth'); |
|
hover_line |
|
.style( 'display', '' ) |
|
.attr( 'y', y( ent ) ); |
|
} |
|
}); |
|
|
|
}); |
|
</script> |