|
<!DOCTYPE html> |
|
<meta charset='utf-8'> |
|
<style> |
|
body { |
|
background: rgb(240, 225, 210); |
|
text-align : center; |
|
} |
|
svg { |
|
font: italic 10.5px serif; |
|
} |
|
.positive { fill: rgba(174, 174, 184, .8); } |
|
.negative { fill: rgba(230, 130, 110, .8); } |
|
|
|
.penicillin { fill: rgb(10, 50, 100); } |
|
.streptomycin { fill: rgb(200, 70, 50); } |
|
.neomycin { fill: rgb(0, 0, 0); } |
|
|
|
.tick circle { |
|
fill: none; |
|
stroke: #eee; |
|
stroke-width: .75; |
|
} |
|
.tick text { |
|
fill: black; |
|
font: 10px sans-serif; |
|
} |
|
.separator { |
|
stroke-width : 1.1; |
|
stroke: black; |
|
} |
|
.label { |
|
fill: black; |
|
font-size: 11px; |
|
} |
|
</style> |
|
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script> |
|
<body> |
|
<script> |
|
var antibiotics = [ "penicillin", "streptomycin", "neomycin" ]; |
|
|
|
// Basic dimensions. |
|
var margin = { top: 0, right: 0, bottom: 50, left: 0 }, |
|
width = 700, |
|
height = 700, |
|
innerRadius = 90, |
|
outerRadius = 300 - 10; |
|
|
|
function parseRow ( d ) { |
|
antibiotics.forEach( a => d[ a ] = +d[ a ] ); |
|
return d; |
|
} |
|
|
|
d3.csv( 'bacteria.csv', parseRow, function ( data ) { |
|
|
|
// Burtin's radius encoding is, as far as I can tell, sqrt(log(mic)). |
|
var min = Math.sqrt( Math.log( .001 * 1E4 ) ), |
|
max = Math.sqrt( Math.log( 1000 * 1E4 ) ), |
|
a = ( outerRadius - innerRadius ) / ( min - max ), |
|
b = innerRadius - a * max, |
|
radius = function ( mic ) { |
|
return a * Math.sqrt( Math.log( mic * 1E4 ) ) + b |
|
}; |
|
|
|
// The pie is split into equal sections for each bacteria, with a blank |
|
// section at the top for the grid labels. Each wedge is further |
|
// subdivided to make room for the three antibiotics, equispaced. |
|
var bigAngle = Math.PI * 2 / ( data.length + 1 ), |
|
smallAngle = bigAngle / 7, |
|
bigDeg = 360 / ( data.length + 1 ); |
|
|
|
|
|
// The root panel. |
|
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+width/2},${margin.top+height/2})` ); |
|
|
|
|
|
var wedge = svg.selectAll( '.bacteria' ) |
|
.data( data ) // assumes Burtin's order |
|
.enter() |
|
.append( 'g' ) |
|
.attr( 'class', 'bacteria' ) |
|
.attr( 'transform', (d,i) => `rotate(${bigDeg * (i + .5)})` ) |
|
|
|
|
|
// Background wedges to indicate gram staining color. |
|
var bgWedge = d3.svg.arc() |
|
.innerRadius( innerRadius ) |
|
.outerRadius( outerRadius ) |
|
.startAngle( 0 ) |
|
.endAngle( bigAngle ); |
|
|
|
wedge.append( 'path' ) |
|
.attr( 'class', d => d.gram ) |
|
.attr( 'd', bgWedge ); |
|
|
|
|
|
// Antibiotics. |
|
var abWedge = d3.svg.arc() |
|
.innerRadius( innerRadius ) |
|
.outerRadius( d => radius( d ) ) |
|
.startAngle( (d,i) => smallAngle * ( i * 2 + 1 ) ) |
|
.endAngle( (d,i) => smallAngle * ( i * 2 + 2 ) ); |
|
|
|
wedge.selectAll( '.antibiotic' ) |
|
.data( antibiotics ).enter() |
|
.append( 'path' ) |
|
.attr( 'class', d => 'antibiotic ' + d ) |
|
.attr( 'd', (d,i,p) => abWedge( wedge.data()[ p ][ d ], i ) ); |
|
|
|
|
|
// Circular grid lines. |
|
var ticks = svg.append( 'g' ) |
|
.attr( 'class', 'axis' ) |
|
.selectAll( '.tick' ) |
|
.data( d3.range( -3, 4 ) ) |
|
.enter() |
|
.append( 'g' ) |
|
.attr( 'class', 'tick' ); |
|
|
|
ticks.append( 'circle' ) |
|
.attr( 'r', d => radius( Math.pow( 10, d ) ) ); |
|
|
|
ticks.append( 'text' ) |
|
.style( 'text-anchor', 'middle' ) |
|
.attr( 'dy', '.32em' ) |
|
.attr( 'y', d => -radius( Math.pow( 10, d ) ) ) |
|
.text( d => (d < 3) ? Math.pow( 10, d ).toFixed( (d > 0) ? 0 : -d ) : '' ); |
|
|
|
|
|
// Radial grid lines. |
|
svg.selectAll( '.separator' ) |
|
.data( d3.range( data.length + 1 ) ) |
|
.enter() |
|
.append( 'line' ) |
|
.attr( 'class', 'separator' ) |
|
.attr( 'transform', i => `rotate(${180 + bigDeg * (i + .5)})` ) |
|
.attr( 'y1', innerRadius * 0.84 ) |
|
.attr( 'y2', outerRadius * 1.1 ); |
|
|
|
svg.selectAll( '.label' ) |
|
.data( data ) |
|
.enter() |
|
.append( 'text' ) |
|
.attr( 'class', 'label' ) |
|
.attr( 'transform', (d,i) => `rotate(${((i + 1 > data.length / 2) ? 90 : -90) + bigDeg * (i + 1)})` ) |
|
.style( 'text-anchor', (d,i) => (i + 1 > data.length / 2) ? 'start' : 'end' ) |
|
.attr( 'dy', '.32em' ) |
|
.attr( 'x', (d,i) => ( i + 1 > data.length / 2 ) ? -outerRadius * 1.1 : outerRadius * 1.1 ) |
|
.text( d => d.name ); |
|
|
|
|
|
// Antibiotic legend. |
|
var ab_legend = svg.append( 'g' ) |
|
.attr( 'class', 'antibiotic legend' ) |
|
.selectAll( 'g' ) |
|
.data( antibiotics ) |
|
.enter() |
|
.append( 'g' ) |
|
.attr( 'class', String ) |
|
.attr( 'transform', (d,i) => `translate(-8,${-18 + i * 18})` ); |
|
|
|
ab_legend.append( 'text' ) |
|
.attr( 'class', 'label' ) |
|
.attr( 'dy', '.32em' ) |
|
.attr( 'x', 3 ) |
|
.text( d => d[0].toUpperCase() + d.slice(1) ); |
|
|
|
ab_legend.append( 'rect' ) |
|
.attr( 'x', -40 ) |
|
.attr( 'y', -4 ) |
|
.attr( 'width', 36 ) |
|
.attr( 'height', 8 ); |
|
|
|
// Gram-stain legend. |
|
var stain_legend = svg.append( 'g' ) |
|
.attr( 'class', 'gram legend' ) |
|
.selectAll( 'g' ) |
|
.data([ 'negative', 'positive' ]) |
|
.enter() |
|
.append( 'g' ) |
|
.attr( 'class', String ) |
|
.attr( 'transform', (d,i) => `translate(0,${height/2 + i * 18})` ); |
|
|
|
stain_legend.append( 'text' ) |
|
.attr( 'class', 'label' ) |
|
.attr( 'dy', '.32em' ) |
|
.attr( 'x', 3 ) |
|
.text( d => 'Gram-' + d ); |
|
|
|
stain_legend.append( 'circle' ) |
|
.attr( 'cx', -12 ) |
|
.attr( 'r', 5 ); |
|
|
|
}); |
|
</script> |