|
const d3 = window.d3 |
|
|
|
d3.boxPlot = function (bind, data, config) { |
|
config = { |
|
colors: ['#eee'], // pass in more colours to change each box |
|
fixedScale: null, // e.g. [0, 100] |
|
customColor: true, // false will use a rainbow :) |
|
boxWidth: null, |
|
width: 800, |
|
height: 500, |
|
...config, |
|
margin: { |
|
top: 30, |
|
right: 50, |
|
bottom: 50, |
|
left: 80, |
|
...(config || {}).margin |
|
} |
|
} |
|
const { width, height, margin } = config |
|
|
|
// helpers |
|
const w = width - margin.left - margin.right |
|
const h = height - margin.top - margin.bottom |
|
const boxWidth = config.boxWidth === null ? (w / data.length) * 0.5 : config.boxWidth |
|
const boxWidthHalf = boxWidth / 2 |
|
const selection = d3.select(bind) |
|
// dom elements |
|
const dom = {} |
|
// append the svg and groups if first render |
|
if (selection.select('svg').empty()) { |
|
dom.svg = selection.append('svg') |
|
.attr('width', width) |
|
.attr('height', height) |
|
dom.yAxis = dom.svg.append('g') |
|
.attr('transform', `translate(${margin.left - boxWidth}, ${margin.top})`) |
|
.attr('class', 'axis yAxisG') |
|
dom.xAxis = dom.svg.append('g') |
|
.attr('transform', `translate(${margin.left}, ${h + margin.top})`) |
|
.attr('class', 'axis xAxisG') |
|
dom.plotarea = dom.svg.append('g') |
|
.attr('class', 'plotarea') |
|
.attr('transform', `translate(${margin.left}, ${margin.top})`) |
|
} else { |
|
dom.svg = selection.select('svg') |
|
dom.yAxis = selection.select('.yAxisG') |
|
dom.xAxis = selection.select('.xAxisG') |
|
dom.plotarea = selection.select('.plotarea') |
|
} |
|
|
|
// have list of labels to pass into xAxis |
|
const labels = data.map(d => d.id) |
|
// either return a fixed domain or calculate it via the data values |
|
const xScaleDomain = config.fixedScale === null ? [d3.min(data, d => d.low), d3.max(data, d => d.high)] : config.fixedScale |
|
// setup scales |
|
const sequentialScale = d3.scaleSequential().domain([0, data.length]).interpolator(d3.interpolateRainbow) |
|
const colorScale = d3.scaleOrdinal().domain(labels).range(config.colors) |
|
const xScale = d3.scalePoint().domain(labels).range([0, w]) |
|
const yScale = d3.scaleLinear().domain(xScaleDomain).range([h, 0]) |
|
// setup axis |
|
const yAxis = d3.axisLeft().scale(yScale).ticks(5) |
|
const xAxis = d3.axisBottom().scale(xScale).tickSize(5) |
|
// check if using custom colour(s) or rainbow |
|
function setColor (i) { |
|
if (config.customColor) { |
|
return colorScale(labels[i]) |
|
} else { |
|
return sequentialScale(i) |
|
} |
|
} |
|
|
|
function render () { |
|
// add the y axis |
|
dom.yAxis.call(yAxis) |
|
// add the x axis |
|
dom.xAxis.call(xAxis) |
|
// destroy and add the boxplots to plotarea |
|
dom.plotarea.selectAll('g.boxplot').remove() |
|
dom.plotarea.selectAll('g.boxplot') |
|
.data(data, d => d.id) |
|
.enter().append('g') |
|
.attr('class', 'boxplot') |
|
.attr('transform', d => `translate( ${xScale(d.id)} , ${yScale(d.median)} )`) |
|
.transition() |
|
.each(createBoxPlot) |
|
} |
|
|
|
function createBoxPlot (d, i) { |
|
// vertical line |
|
d3.select(this) |
|
.append('line') |
|
.attr('class', 'range') |
|
.attr('x1', 0) |
|
.attr('x2', 0) |
|
.attr('y1', yScale(d.high) - yScale(d.median)) |
|
.attr('y2', yScale(d.low) - yScale(d.median)) |
|
.style('stroke', () => d3.color(setColor(i)).darker()) |
|
.style('stroke-width', '4px') |
|
// top whisker |
|
d3.select(this) |
|
.append('line') |
|
.attr('class', 'high') |
|
.attr('x1', -(boxWidth / 3)) |
|
.attr('x2', (boxWidth / 3)) |
|
.attr('y1', yScale(d.high) - yScale(d.median)) |
|
.attr('y2', yScale(d.high) - yScale(d.median)) |
|
.style('stroke', () => d3.color(setColor(i)).darker()) |
|
.style('stroke-width', '4px') |
|
// bottom whisker |
|
d3.select(this) |
|
.append('line') |
|
.attr('class', 'low') |
|
.attr('x1', -(boxWidth / 3)) |
|
.attr('x2', (boxWidth / 3)) |
|
.attr('y1', yScale(d.low) - yScale(d.median)) |
|
.attr('y2', yScale(d.low) - yScale(d.median)) |
|
.style('stroke', () => d3.color(setColor(i)).darker()) |
|
.style('stroke-width', '4px') |
|
// create box |
|
d3.select(this) |
|
.append('rect') |
|
.attr('class', 'range') |
|
.attr('width', boxWidth) |
|
.attr('x', -boxWidthHalf) |
|
.attr('y', yScale(d.upper_quartile) - yScale(d.median)) |
|
.attr('height', yScale(d.lower_quartile) - yScale(d.upper_quartile)) |
|
.style('fill', setColor(i)) |
|
.style('stroke', () => d3.color(setColor(i)).darker()) |
|
.style('stroke-width', '4px') |
|
// median line |
|
d3.select(this) |
|
.append('line') |
|
.attr('x1', -boxWidthHalf) |
|
.attr('x2', boxWidthHalf) |
|
.attr('y1', 0) |
|
.attr('y2', 0) |
|
.style('stroke', () => d3.color(setColor(i)).darker()) |
|
.style('stroke-width', '4px') |
|
} |
|
|
|
return render() |
|
} |