Skip to content

Instantly share code, notes, and snippets.

@eesur
Last active December 19, 2017 16:20
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 eesur/cc40e7d583346b2a8a5a2f9e296d3007 to your computer and use it in GitHub Desktop.
Save eesur/cc40e7d583346b2a8a5a2f9e296d3007 to your computer and use it in GitHub Desktop.
d3js | box and whiskers (boxplot)
license: mit
height: 500
border: no

Box and whiskers with the following config options:

    config = {
        colors: ['#eee'], // array of colours (one if you want all one colour)
        fixedScale: null, // pass in a fixed scale e.g. [0, 100]
        customColor: true, // boolean if using `colors` 
        boxWidth: null, // null or number for set width e.g. 30
        width: 800, // svg width
        height: 500, // svg height
        margin: {top: 50, right: 50, bottom: 50, left: 80}
    }
*{box-sizing:border-box}body{font-family:-apple-system,monospace;color:#454545;width:800px;margin:0 auto}.axis text{font-family:-apple-system,monospace;font-size:12px;fill:#888}.axis line,.axis path{fill:none;stroke:#ccc;shape-rendering:crispEdges}
!function(g){function n(c){if(t[c])return t[c].exports;var l=t[c]={i:c,l:!1,exports:{}};return g[c].call(l.exports,l,l.exports,n),l.l=!0,l.exports}var t={};n.m=g,n.c=t,n.i=function(g){return g},n.d=function(g,t,c){n.o(g,t)||Object.defineProperty(g,t,{configurable:!1,enumerable:!0,get:c})},n.n=function(g){var t=g&&g.__esModule?function(){return g.default}:function(){return g};return n.d(t,"a",t),t},n.o=function(g,n){return Object.prototype.hasOwnProperty.call(g,n)},n.p="",n(n.s=0)}([function(module,exports,__webpack_require__){"use strict";eval("\n\nvar _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };\n\nvar d3 = window.d3;\n\nd3.boxPlot = function (bind, data, config) {\n config = _extends({\n colors: ['#eee'], // pass in more colours to change each box\n fixedScale: null, // e.g. [0, 100]\n customColor: true, // false will use a rainbow :)\n boxWidth: null,\n width: 800,\n height: 500\n }, config, {\n margin: _extends({\n top: 30,\n right: 50,\n bottom: 50,\n left: 80\n }, (config || {}).margin)\n });\n var _config = config,\n width = _config.width,\n height = _config.height,\n margin = _config.margin;\n\n // helpers\n\n var w = width - margin.left - margin.right;\n var h = height - margin.top - margin.bottom;\n var boxWidth = config.boxWidth === null ? w / data.length * 0.5 : config.boxWidth;\n var boxWidthHalf = boxWidth / 2;\n var selection = d3.select(bind);\n // dom elements\n var dom = {};\n // append the svg and groups if first render\n if (selection.select('svg').empty()) {\n dom.svg = selection.append('svg').attr('width', width).attr('height', height);\n dom.yAxis = dom.svg.append('g').attr('transform', 'translate(' + (margin.left - boxWidth) + ', ' + margin.top + ')').attr('class', 'axis yAxisG');\n dom.xAxis = dom.svg.append('g').attr('transform', 'translate(' + margin.left + ', ' + (h + margin.top) + ')').attr('class', 'axis xAxisG');\n dom.plotarea = dom.svg.append('g').attr('class', 'plotarea').attr('transform', 'translate(' + margin.left + ', ' + margin.top + ')');\n } else {\n dom.svg = selection.select('svg');\n dom.yAxis = selection.select('.yAxisG');\n dom.xAxis = selection.select('.xAxisG');\n dom.plotarea = selection.select('.plotarea');\n }\n\n // have list of labels to pass into xAxis\n var labels = data.map(function (d) {\n return d.id;\n });\n // either return a fixed domain or calculate it via the data values\n var xScaleDomain = config.fixedScale === null ? [d3.min(data, function (d) {\n return d.low;\n }), d3.max(data, function (d) {\n return d.high;\n })] : config.fixedScale;\n // setup scales\n var sequentialScale = d3.scaleSequential().domain([0, data.length]).interpolator(d3.interpolateRainbow);\n var colorScale = d3.scaleOrdinal().domain(labels).range(config.colors);\n var xScale = d3.scalePoint().domain(labels).range([0, w]);\n var yScale = d3.scaleLinear().domain(xScaleDomain).range([h, 0]);\n // setup axis\n var yAxis = d3.axisLeft().scale(yScale).ticks(5);\n var xAxis = d3.axisBottom().scale(xScale).tickSize(5);\n // check if using custom colour(s) or rainbow\n function setColor(i) {\n if (config.customColor) {\n return colorScale(labels[i]);\n } else {\n return sequentialScale(i);\n }\n }\n\n function render() {\n // add the y axis\n dom.yAxis.call(yAxis);\n // add the x axis\n dom.xAxis.call(xAxis);\n // destroy and add the boxplots to plotarea\n dom.plotarea.selectAll('g.boxplot').remove();\n dom.plotarea.selectAll('g.boxplot').data(data, function (d) {\n return d.id;\n }).enter().append('g').attr('class', 'boxplot').attr('transform', function (d) {\n return 'translate( ' + xScale(d.id) + ' , ' + yScale(d.median) + ' )';\n }).transition().each(createBoxPlot);\n }\n\n function createBoxPlot(d, i) {\n // vertical line\n 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', function () {\n return d3.color(setColor(i)).darker();\n }).style('stroke-width', '4px');\n // top whisker\n 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', function () {\n return d3.color(setColor(i)).darker();\n }).style('stroke-width', '4px');\n // bottom whisker\n 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', function () {\n return d3.color(setColor(i)).darker();\n }).style('stroke-width', '4px');\n // create box\n 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', function () {\n return d3.color(setColor(i)).darker();\n }).style('stroke-width', '4px');\n // median line\n d3.select(this).append('line').attr('x1', -boxWidthHalf).attr('x2', boxWidthHalf).attr('y1', 0).attr('y2', 0).style('stroke', function () {\n return d3.color(setColor(i)).darker();\n }).style('stroke-width', '4px');\n }\n\n return render();\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zY3JpcHQuanM/OWE5NSJdLCJzb3VyY2VzQ29udGVudCI6WyJjb25zdCBkMyA9IHdpbmRvdy5kM1xuXG5kMy5ib3hQbG90ID0gZnVuY3Rpb24gKGJpbmQsIGRhdGEsIGNvbmZpZykge1xuICBjb25maWcgPSB7XG4gICAgY29sb3JzOiBbJyNlZWUnXSwgLy8gcGFzcyBpbiBtb3JlIGNvbG91cnMgdG8gY2hhbmdlIGVhY2ggYm94XG4gICAgZml4ZWRTY2FsZTogbnVsbCwgLy8gZS5nLiBbMCwgMTAwXVxuICAgIGN1c3RvbUNvbG9yOiB0cnVlLCAvLyBmYWxzZSB3aWxsIHVzZSBhIHJhaW5ib3cgOilcbiAgICBib3hXaWR0aDogbnVsbCxcbiAgICB3aWR0aDogODAwLFxuICAgIGhlaWdodDogNTAwLFxuICAgIC4uLmNvbmZpZyxcbiAgICBtYXJnaW46IHtcbiAgICAgIHRvcDogMzAsXG4gICAgICByaWdodDogNTAsXG4gICAgICBib3R0b206IDUwLFxuICAgICAgbGVmdDogODAsXG4gICAgICAuLi4oY29uZmlnIHx8IHt9KS5tYXJnaW5cbiAgICB9XG4gIH1cbiAgY29uc3QgeyB3aWR0aCwgaGVpZ2h0LCBtYXJnaW4gfSA9IGNvbmZpZ1xuXG4gIC8vIGhlbHBlcnNcbiAgY29uc3QgdyA9IHdpZHRoIC0gbWFyZ2luLmxlZnQgLSBtYXJnaW4ucmlnaHRcbiAgY29uc3QgaCA9IGhlaWdodCAtIG1hcmdpbi50b3AgLSBtYXJnaW4uYm90dG9tXG4gIGNvbnN0IGJveFdpZHRoID0gY29uZmlnLmJveFdpZHRoID09PSBudWxsID8gKHcgLyBkYXRhLmxlbmd0aCkgKiAwLjUgOiBjb25maWcuYm94V2lkdGhcbiAgY29uc3QgYm94V2lkdGhIYWxmID0gYm94V2lkdGggLyAyXG4gIGNvbnN0IHNlbGVjdGlvbiA9IGQzLnNlbGVjdChiaW5kKVxuICAvLyBkb20gZWxlbWVudHNcbiAgY29uc3QgZG9tID0ge31cbiAgLy8gYXBwZW5kIHRoZSBzdmcgYW5kIGdyb3VwcyBpZiBmaXJzdCByZW5kZXJcbiAgaWYgKHNlbGVjdGlvbi5zZWxlY3QoJ3N2ZycpLmVtcHR5KCkpIHtcbiAgICBkb20uc3ZnID0gc2VsZWN0aW9uLmFwcGVuZCgnc3ZnJylcbiAgICAgIC5hdHRyKCd3aWR0aCcsIHdpZHRoKVxuICAgICAgLmF0dHIoJ2hlaWdodCcsIGhlaWdodClcbiAgICBkb20ueUF4aXMgPSBkb20uc3ZnLmFwcGVuZCgnZycpXG4gICAgICAuYXR0cigndHJhbnNmb3JtJywgYHRyYW5zbGF0ZSgke21hcmdpbi5sZWZ0IC0gYm94V2lkdGh9LCAke21hcmdpbi50b3B9KWApXG4gICAgICAuYXR0cignY2xhc3MnLCAnYXhpcyB5QXhpc0cnKVxuICAgIGRvbS54QXhpcyA9IGRvbS5zdmcuYXBwZW5kKCdnJylcbiAgICAgIC5hdHRyKCd0cmFuc2Zvcm0nLCBgdHJhbnNsYXRlKCR7bWFyZ2luLmxlZnR9LCAke2ggKyBtYXJnaW4udG9wfSlgKVxuICAgICAgLmF0dHIoJ2NsYXNzJywgJ2F4aXMgeEF4aXNHJylcbiAgICBkb20ucGxvdGFyZWEgPSBkb20uc3ZnLmFwcGVuZCgnZycpXG4gICAgICAuYXR0cignY2xhc3MnLCAncGxvdGFyZWEnKVxuICAgICAgLmF0dHIoJ3RyYW5zZm9ybScsIGB0cmFuc2xhdGUoJHttYXJnaW4ubGVmdH0sICR7bWFyZ2luLnRvcH0pYClcbiAgfSBlbHNlIHtcbiAgICBkb20uc3ZnID0gc2VsZWN0aW9uLnNlbGVjdCgnc3ZnJylcbiAgICBkb20ueUF4aXMgPSBzZWxlY3Rpb24uc2VsZWN0KCcueUF4aXNHJylcbiAgICBkb20ueEF4aXMgPSBzZWxlY3Rpb24uc2VsZWN0KCcueEF4aXNHJylcbiAgICBkb20ucGxvdGFyZWEgPSBzZWxlY3Rpb24uc2VsZWN0KCcucGxvdGFyZWEnKVxuICB9XG5cbiAgLy8gaGF2ZSBsaXN0IG9mIGxhYmVscyB0byBwYXNzIGludG8geEF4aXNcbiAgY29uc3QgbGFiZWxzID0gZGF0YS5tYXAoZCA9PiBkLmlkKVxuICAvLyBlaXRoZXIgcmV0dXJuIGEgZml4ZWQgZG9tYWluIG9yIGNhbGN1bGF0ZSBpdCB2aWEgdGhlIGRhdGEgdmFsdWVzXG4gIGNvbnN0IHhTY2FsZURvbWFpbiA9IGNvbmZpZy5maXhlZFNjYWxlID09PSBudWxsID8gW2QzLm1pbihkYXRhLCBkID0+IGQubG93KSwgZDMubWF4KGRhdGEsIGQgPT4gZC5oaWdoKV0gOiBjb25maWcuZml4ZWRTY2FsZVxuICAvLyBzZXR1cCBzY2FsZXNcbiAgY29uc3Qgc2VxdWVudGlhbFNjYWxlID0gZDMuc2NhbGVTZXF1ZW50aWFsKCkuZG9tYWluKFswLCBkYXRhLmxlbmd0aF0pLmludGVycG9sYXRvcihkMy5pbnRlcnBvbGF0ZVJhaW5ib3cpXG4gIGNvbnN0IGNvbG9yU2NhbGUgPSBkMy5zY2FsZU9yZGluYWwoKS5kb21haW4obGFiZWxzKS5yYW5nZShjb25maWcuY29sb3JzKVxuICBjb25zdCB4U2NhbGUgPSBkMy5zY2FsZVBvaW50KCkuZG9tYWluKGxhYmVscykucmFuZ2UoWzAsIHddKVxuICBjb25zdCB5U2NhbGUgPSBkMy5zY2FsZUxpbmVhcigpLmRvbWFpbih4U2NhbGVEb21haW4pLnJhbmdlKFtoLCAwXSlcbiAgLy8gc2V0dXAgYXhpc1xuICBjb25zdCB5QXhpcyA9IGQzLmF4aXNMZWZ0KCkuc2NhbGUoeVNjYWxlKS50aWNrcyg1KVxuICBjb25zdCB4QXhpcyA9IGQzLmF4aXNCb3R0b20oKS5zY2FsZSh4U2NhbGUpLnRpY2tTaXplKDUpXG4gIC8vIGNoZWNrIGlmIHVzaW5nIGN1c3RvbSBjb2xvdXIocykgb3IgcmFpbmJvd1xuICBmdW5jdGlvbiBzZXRDb2xvciAoaSkge1xuICAgIGlmIChjb25maWcuY3VzdG9tQ29sb3IpIHtcbiAgICAgIHJldHVybiBjb2xvclNjYWxlKGxhYmVsc1tpXSlcbiAgICB9IGVsc2Uge1xuICAgICAgcmV0dXJuIHNlcXVlbnRpYWxTY2FsZShpKVxuICAgIH1cbiAgfVxuXG4gIGZ1bmN0aW9uIHJlbmRlciAoKSB7XG4gICAgLy8gYWRkIHRoZSB5IGF4aXNcbiAgICBkb20ueUF4aXMuY2FsbCh5QXhpcylcbiAgICAvLyBhZGQgdGhlIHggYXhpc1xuICAgIGRvbS54QXhpcy5jYWxsKHhBeGlzKVxuICAgIC8vIGRlc3Ryb3kgYW5kIGFkZCB0aGUgYm94cGxvdHMgdG8gcGxvdGFyZWFcbiAgICBkb20ucGxvdGFyZWEuc2VsZWN0QWxsKCdnLmJveHBsb3QnKS5yZW1vdmUoKVxuICAgIGRvbS5wbG90YXJlYS5zZWxlY3RBbGwoJ2cuYm94cGxvdCcpXG4gICAgICAuZGF0YShkYXRhLCBkID0+IGQuaWQpXG4gICAgICAuZW50ZXIoKS5hcHBlbmQoJ2cnKVxuICAgICAgLmF0dHIoJ2NsYXNzJywgJ2JveHBsb3QnKVxuICAgICAgLmF0dHIoJ3RyYW5zZm9ybScsIGQgPT4gYHRyYW5zbGF0ZSggJHt4U2NhbGUoZC5pZCl9ICwgJHt5U2NhbGUoZC5tZWRpYW4pfSApYClcbiAgICAgIC50cmFuc2l0aW9uKClcbiAgICAgIC5lYWNoKGNyZWF0ZUJveFBsb3QpXG4gIH1cblxuICBmdW5jdGlvbiBjcmVhdGVCb3hQbG90IChkLCBpKSB7XG4gICAgLy8gdmVydGljYWwgbGluZVxuICAgIGQzLnNlbGVjdCh0aGlzKVxuICAgICAgLmFwcGVuZCgnbGluZScpXG4gICAgICAuYXR0cignY2xhc3MnLCAncmFuZ2UnKVxuICAgICAgLmF0dHIoJ3gxJywgMClcbiAgICAgIC5hdHRyKCd4MicsIDApXG4gICAgICAuYXR0cigneTEnLCB5U2NhbGUoZC5oaWdoKSAtIHlTY2FsZShkLm1lZGlhbikpXG4gICAgICAuYXR0cigneTInLCB5U2NhbGUoZC5sb3cpIC0geVNjYWxlKGQubWVkaWFuKSlcbiAgICAgIC5zdHlsZSgnc3Ryb2tlJywgKCkgPT4gZDMuY29sb3Ioc2V0Q29sb3IoaSkpLmRhcmtlcigpKVxuICAgICAgLnN0eWxlKCdzdHJva2Utd2lkdGgnLCAnNHB4JylcbiAgICAvLyB0b3Agd2hpc2tlclxuICAgIGQzLnNlbGVjdCh0aGlzKVxuICAgICAgLmFwcGVuZCgnbGluZScpXG4gICAgICAuYXR0cignY2xhc3MnLCAnaGlnaCcpXG4gICAgICAuYXR0cigneDEnLCAtKGJveFdpZHRoIC8gMykpXG4gICAgICAuYXR0cigneDInLCAoYm94V2lkdGggLyAzKSlcbiAgICAgIC5hdHRyKCd5MScsIHlTY2FsZShkLmhpZ2gpIC0geVNjYWxlKGQubWVkaWFuKSlcbiAgICAgIC5hdHRyKCd5MicsIHlTY2FsZShkLmhpZ2gpIC0geVNjYWxlKGQubWVkaWFuKSlcbiAgICAgIC5zdHlsZSgnc3Ryb2tlJywgKCkgPT4gZDMuY29sb3Ioc2V0Q29sb3IoaSkpLmRhcmtlcigpKVxuICAgICAgLnN0eWxlKCdzdHJva2Utd2lkdGgnLCAnNHB4JylcbiAgICAvLyBib3R0b20gd2hpc2tlclxuICAgIGQzLnNlbGVjdCh0aGlzKVxuICAgICAgLmFwcGVuZCgnbGluZScpXG4gICAgICAuYXR0cignY2xhc3MnLCAnbG93JylcbiAgICAgIC5hdHRyKCd4MScsIC0oYm94V2lkdGggLyAzKSlcbiAgICAgIC5hdHRyKCd4MicsIChib3hXaWR0aCAvIDMpKVxuICAgICAgLmF0dHIoJ3kxJywgeVNjYWxlKGQubG93KSAtIHlTY2FsZShkLm1lZGlhbikpXG4gICAgICAuYXR0cigneTInLCB5U2NhbGUoZC5sb3cpIC0geVNjYWxlKGQubWVkaWFuKSlcbiAgICAgIC5zdHlsZSgnc3Ryb2tlJywgKCkgPT4gZDMuY29sb3Ioc2V0Q29sb3IoaSkpLmRhcmtlcigpKVxuICAgICAgLnN0eWxlKCdzdHJva2Utd2lkdGgnLCAnNHB4JylcbiAgICAvLyBjcmVhdGUgYm94XG4gICAgZDMuc2VsZWN0KHRoaXMpXG4gICAgICAuYXBwZW5kKCdyZWN0JylcbiAgICAgIC5hdHRyKCdjbGFzcycsICdyYW5nZScpXG4gICAgICAuYXR0cignd2lkdGgnLCBib3hXaWR0aClcbiAgICAgIC5hdHRyKCd4JywgLWJveFdpZHRoSGFsZilcbiAgICAgIC5hdHRyKCd5JywgeVNjYWxlKGQudXBwZXJfcXVhcnRpbGUpIC0geVNjYWxlKGQubWVkaWFuKSlcbiAgICAgIC5hdHRyKCdoZWlnaHQnLCB5U2NhbGUoZC5sb3dlcl9xdWFydGlsZSkgLSB5U2NhbGUoZC51cHBlcl9xdWFydGlsZSkpXG4gICAgICAuc3R5bGUoJ2ZpbGwnLCBzZXRDb2xvcihpKSlcbiAgICAgIC5zdHlsZSgnc3Ryb2tlJywgKCkgPT4gZDMuY29sb3Ioc2V0Q29sb3IoaSkpLmRhcmtlcigpKVxuICAgICAgLnN0eWxlKCdzdHJva2Utd2lkdGgnLCAnNHB4JylcbiAgICAvLyBtZWRpYW4gbGluZVxuICAgIGQzLnNlbGVjdCh0aGlzKVxuICAgICAgLmFwcGVuZCgnbGluZScpXG4gICAgICAuYXR0cigneDEnLCAtYm94V2lkdGhIYWxmKVxuICAgICAgLmF0dHIoJ3gyJywgYm94V2lkdGhIYWxmKVxuICAgICAgLmF0dHIoJ3kxJywgMClcbiAgICAgIC5hdHRyKCd5MicsIDApXG4gICAgICAuc3R5bGUoJ3N0cm9rZScsICgpID0+IGQzLmNvbG9yKHNldENvbG9yKGkpKS5kYXJrZXIoKSlcbiAgICAgIC5zdHlsZSgnc3Ryb2tlLXdpZHRoJywgJzRweCcpXG4gIH1cblxuICByZXR1cm4gcmVuZGVyKClcbn1cblxuXG5cbi8vIFdFQlBBQ0sgRk9PVEVSIC8vXG4vLyBzY3JpcHQuanMiXSwibWFwcGluZ3MiOiI7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQU5BO0FBUUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUpBO0FBUkE7QUFEQTtBQUFBO0FBQUE7QUFBQTtBQUNBO0FBa0JBO0FBQ0E7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUdBO0FBR0E7QUFHQTtBQUdBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBQUE7QUFDQTtBQUNBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFHQTtBQUFBO0FBR0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQU9BO0FBQUE7QUFFQTtBQUNBO0FBT0E7QUFBQTtBQUVBO0FBQ0E7QUFPQTtBQUFBO0FBRUE7QUFDQTtBQVFBO0FBQUE7QUFFQTtBQUNBO0FBTUE7QUFBQTtBQUVBO0FBQ0E7QUFDQTtBQUNBIiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///0\n")}]);
<!DOCTYPE html>
<title>blockup</title>
<link href='dist.css' rel='stylesheet' />
<body>
<div id="vis"></div>
<script src='https://d3js.org/d3.v4.min.js'></script>
<!-- <script src="http://d3js.org/d3.v3.min.js"></script> -->
<!-- <script src="boxplot.js"></script> -->
<script src='dist.js'></script>
<script>
var d3 = window.d3
var data = [
{
"id": "id-1",
"low": 14,
"high": 65,
"median": 33,
"lower_quartile": 20,
"upper_quartile": 35,
"value": 22
},
{
"id": "id-2",
"low": 20,
"high": 73,
"median": 35,
"lower_quartile": 25,
"upper_quartile": 50,
"value": 170
},
{
"id": "id-3",
"low": 15,
"high": 40,
"median": 25,
"lower_quartile": 17,
"upper_quartile": 28,
"value": 185
},
{
"id": "id-4",
"low": 18,
"high": 55,
"median": 33,
"lower_quartile": 28,
"upper_quartile": 42,
"value": 135
},
{
"id": "id-5",
"low": 14,
"high": 66,
"median": 35,
"lower_quartile": 22,
"upper_quartile": 45,
"value": 150
},
{
"id": "id-6",
"low": 22,
"high": 70,
"median": 34,
"lower_quartile": 28,
"upper_quartile": 42,
"value": 170
},
{
"id": "id-7",
"low": 14,
"high": 65,
"median": 33,
"lower_quartile": 30,
"upper_quartile": 50,
"value": 28
}
]
// render a chart
var chart = d3.boxPlot
chart(
'#vis', // dom element
data, // data
{ // config overrides
fixedScale: [0, 100],
colors: ['#f77e5e']
}
)
function createData (amount) {
return d3.range(amount).map(d => {
return {
id: 'id-' + d,
low: Math.round(d3.randomUniform(2, 20)()),
high: Math.round(d3.randomUniform(70, 99)()),
median: Math.round(d3.randomUniform(40, 50)()),
lower_quartile: Math.round(d3.randomUniform(20, 40)()),
upper_quartile: Math.round(d3.randomUniform(55, 70)()),
value: Math.round(d3.randomUniform(50, 150)())
}
})
}
// tet some updates and config
d3.interval(function () {
var _data = createData(Math.round(d3.randomUniform(5, 10)()))
chart(
'#vis',
_data,
{
fixedScale: null, // let the scale work it out
customColor: false // show the rainbow!
}
)
}, 4000)
</script>
</body>
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()
}
*
box-sizing border-box
body
font-family:-apple-system,monospace
color: #454545
width: 800px
margin: 0 auto
.axis text
font-family:-apple-system,monospace
font-size: 12px
fill: #888
.axis path, .axis line
fill: none
stroke: #ccc
shape-rendering: crispEdges
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment