Skip to content

Instantly share code, notes, and snippets.

@eesur
Last active February 2, 2022 17:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save eesur/287b5700b5881e8899cc7301a5fefb94 to your computer and use it in GitHub Desktop.
Save eesur/287b5700b5881e8899cc7301a5fefb94 to your computer and use it in GitHub Desktop.
d3js | single stacked bar
license: mit
height: 500
border: no

single stacked bar, removes 0 values (as they can't be seen on the bar), groups data, and cumulates values for stacked placement (instead of using d3.stack()) and calculates percentage (all in groupData.js file/function)

overrides defaults via passing in config:

var myConfig: {
    f: d3.format('.1f'),
    margin: {top: 20, right: 10, bottom: 20, left: 10},
    width: 800,
    height: 200,
    barHeight: 100,
    colors: ['#457c39', '#ffeb00', '#1aa9bc'],
  }

stackedBar('.someDiv', data, myConfig)
*{box-sizing:border-box}body{font-family:-apple-system,monospace;color:#454545;width:800px;margin:0 auto}.chart{padding-top:100px}.text-value{font-size:24px;fill:#fff;opacity:.9}
!function(t){function n(e){if(a[e])return a[e].exports;var c=a[e]={i:e,l:!1,exports:{}};return t[e].call(c.exports,c,c.exports,n),c.l=!0,c.exports}var a={};n.m=t,n.c=a,n.i=function(t){return t},n.d=function(t,a,e){n.o(t,a)||Object.defineProperty(t,a,{configurable:!1,enumerable:!0,get:e})},n.n=function(t){var a=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(a,"a",a),a},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=2)}([function(module,exports,__webpack_require__){"use strict";eval('\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\n\nexports.default = function (data, total) {\n // use scale to get percent values\n var percent = d3.scaleLinear().domain([0, total]).range([0, 100]);\n // filter out data that has zero values\n // also get mapping for next placement\n // (save having to format data for d3 stack)\n var cumulative = 0;\n var _data = data.map(function (d) {\n cumulative += d.value;\n return {\n value: d.value,\n // want the cumulative to prior value (start of rect)\n cumulative: cumulative - d.value,\n label: d.label,\n percent: percent(d.value)\n };\n }).filter(function (d) {\n return d.value > 0;\n });\n return _data;\n};\n\n// group data for chart and filter out zero values\nvar d3 = window.d3;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9ncm91cERhdGEuanM/MDA4OSJdLCJzb3VyY2VzQ29udGVudCI6WyIvLyBncm91cCBkYXRhIGZvciBjaGFydCBhbmQgZmlsdGVyIG91dCB6ZXJvIHZhbHVlc1xuY29uc3QgZDMgPSB3aW5kb3cuZDNcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gKGRhdGEsIHRvdGFsKSB7XG4gIC8vIHVzZSBzY2FsZSB0byBnZXQgcGVyY2VudCB2YWx1ZXNcbiAgY29uc3QgcGVyY2VudCA9IGQzLnNjYWxlTGluZWFyKClcbiAgICAuZG9tYWluKFswLCB0b3RhbF0pXG4gICAgLnJhbmdlKFswLCAxMDBdKVxuICAvLyBmaWx0ZXIgb3V0IGRhdGEgdGhhdCBoYXMgemVybyB2YWx1ZXNcbiAgLy8gYWxzbyBnZXQgbWFwcGluZyBmb3IgbmV4dCBwbGFjZW1lbnRcbiAgLy8gKHNhdmUgaGF2aW5nIHRvIGZvcm1hdCBkYXRhIGZvciBkMyBzdGFjaylcbiAgbGV0IGN1bXVsYXRpdmUgPSAwXG4gIGNvbnN0IF9kYXRhID0gZGF0YS5tYXAoZCA9PiB7XG4gICAgY3VtdWxhdGl2ZSArPSBkLnZhbHVlXG4gICAgcmV0dXJuIHtcbiAgICAgIHZhbHVlOiBkLnZhbHVlLFxuICAgICAgLy8gd2FudCB0aGUgY3VtdWxhdGl2ZSB0byBwcmlvciB2YWx1ZSAoc3RhcnQgb2YgcmVjdClcbiAgICAgIGN1bXVsYXRpdmU6IGN1bXVsYXRpdmUgLSBkLnZhbHVlLFxuICAgICAgbGFiZWw6IGQubGFiZWwsXG4gICAgICBwZXJjZW50OiBwZXJjZW50KGQudmFsdWUpXG4gICAgfVxuICB9KS5maWx0ZXIoZCA9PiBkLnZhbHVlID4gMClcbiAgcmV0dXJuIF9kYXRhXG59XG5cblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gZ3JvdXBEYXRhLmpzIl0sIm1hcHBpbmdzIjoiOzs7Ozs7QUFHQTtBQUNBO0FBQ0E7QUFHQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFMQTtBQU9BO0FBQUE7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQXhCQTtBQUNBIiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///0\n')},function(module,exports,__webpack_require__){"use strict";eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nvar sampleData = [{ label: 'Test 0 which should not render', value: 0 }, { label: 'Group-1', value: 55 }, { label: 'Group-2', value: 233 }, { label: 'Test 0 AGAIN which should not render', value: 0 }, { label: 'Group-3', value: 89 }];\n\nexports.default = sampleData;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMS5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zYW1wbGVEYXRhLmpzPzM5N2YiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc3Qgc2FtcGxlRGF0YSA9IFtcbiAgeyBsYWJlbDogJ1Rlc3QgMCB3aGljaCBzaG91bGQgbm90IHJlbmRlcicsIHZhbHVlOiAwIH0sXG4gIHsgbGFiZWw6ICdHcm91cC0xJywgdmFsdWU6IDU1IH0sXG4gIHsgbGFiZWw6ICdHcm91cC0yJywgdmFsdWU6IDIzMyB9LFxuICB7IGxhYmVsOiAnVGVzdCAwIEFHQUlOIHdoaWNoIHNob3VsZCBub3QgcmVuZGVyJywgdmFsdWU6IDAgfSxcbiAgeyBsYWJlbDogJ0dyb3VwLTMnLCB2YWx1ZTogODkgfVxuXVxuXG5leHBvcnQgZGVmYXVsdCBzYW1wbGVEYXRhXG5cblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gc2FtcGxlRGF0YS5qcyJdLCJtYXBwaW5ncyI6Ijs7Ozs7QUFBQTtBQUNBO0FBT0EiLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///1\n")},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 _sampleData = __webpack_require__(1);\n\nvar _sampleData2 = _interopRequireDefault(_sampleData);\n\nvar _groupData = __webpack_require__(0);\n\nvar _groupData2 = _interopRequireDefault(_groupData);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar d3 = window.d3;\n\nfunction stackedBar(bind, data, config) {\n config = _extends({\n f: d3.format('.1f'),\n margin: { top: 20, right: 10, bottom: 20, left: 10 },\n width: 800,\n height: 200,\n barHeight: 100,\n colors: ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33']\n }, config);\n var _config = config,\n f = _config.f,\n margin = _config.margin,\n width = _config.width,\n height = _config.height,\n barHeight = _config.barHeight,\n colors = _config.colors;\n\n var w = width - margin.left - margin.right;\n var h = height - margin.top - margin.bottom;\n var halfBarHeight = barHeight / 2;\n\n var total = d3.sum(data, function (d) {\n return d.value;\n });\n var _data = (0, _groupData2.default)(data, total);\n\n // set up scales for horizontal placement\n var xScale = d3.scaleLinear().domain([0, total]).range([0, w]);\n\n // create svg in passed in div\n var selection = d3.select(bind).append('svg').attr('width', width).attr('height', height).append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');\n\n // stack rect for each data value\n selection.selectAll('rect').data(_data).enter().append('rect').attr('class', 'rect-stacked').attr('x', function (d) {\n return xScale(d.cumulative);\n }).attr('y', h / 2 - halfBarHeight).attr('height', barHeight).attr('width', function (d) {\n return xScale(d.value);\n }).style('fill', function (d, i) {\n return colors[i];\n });\n\n // add values on bar\n selection.selectAll('.text-value').data(_data).enter().append('text').attr('class', 'text-value').attr('text-anchor', 'middle').attr('x', function (d) {\n return xScale(d.cumulative) + xScale(d.value) / 2;\n }).attr('y', h / 2 + 5).text(function (d) {\n return d.value;\n });\n\n // add some labels for percentages\n selection.selectAll('.text-percent').data(_data).enter().append('text').attr('class', 'text-percent').attr('text-anchor', 'middle').attr('x', function (d) {\n return xScale(d.cumulative) + xScale(d.value) / 2;\n }).attr('y', h / 2 - halfBarHeight * 1.1).text(function (d) {\n return f(d.percent) + ' %';\n });\n\n // add the labels\n selection.selectAll('.text-label').data(_data).enter().append('text').attr('class', 'text-label').attr('text-anchor', 'middle').attr('x', function (d) {\n return xScale(d.cumulative) + xScale(d.value) / 2;\n }).attr('y', h / 2 + halfBarHeight * 1.1 + 20).style('fill', function (d, i) {\n return colors[i];\n }).text(function (d) {\n return d.label;\n });\n}\n// render chart\nstackedBar('.chart', _sampleData2.default);//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMi5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zY3JpcHQuanM/OWE5NSJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgc2FtcGxlRGF0YSBmcm9tICcuL3NhbXBsZURhdGEnXG5pbXBvcnQgZ3JvdXBEYXRhIGZyb20gJy4vZ3JvdXBEYXRhJ1xuY29uc3QgZDMgPSB3aW5kb3cuZDNcblxuZnVuY3Rpb24gc3RhY2tlZEJhciAoYmluZCwgZGF0YSwgY29uZmlnKSB7XG4gIGNvbmZpZyA9IHtcbiAgICBmOiBkMy5mb3JtYXQoJy4xZicpLFxuICAgIG1hcmdpbjoge3RvcDogMjAsIHJpZ2h0OiAxMCwgYm90dG9tOiAyMCwgbGVmdDogMTB9LFxuICAgIHdpZHRoOiA4MDAsXG4gICAgaGVpZ2h0OiAyMDAsXG4gICAgYmFySGVpZ2h0OiAxMDAsXG4gICAgY29sb3JzOiBbJyNlNDFhMWMnLCAnIzM3N2ViOCcsICcjNGRhZjRhJywgJyM5ODRlYTMnLCAnI2ZmN2YwMCcsICcjZmZmZjMzJ10sXG4gICAgLi4uY29uZmlnXG4gIH1cbiAgY29uc3QgeyBmLCBtYXJnaW4sIHdpZHRoLCBoZWlnaHQsIGJhckhlaWdodCwgY29sb3JzIH0gPSBjb25maWdcbiAgY29uc3QgdyA9IHdpZHRoIC0gbWFyZ2luLmxlZnQgLSBtYXJnaW4ucmlnaHRcbiAgY29uc3QgaCA9IGhlaWdodCAtIG1hcmdpbi50b3AgLSBtYXJnaW4uYm90dG9tXG4gIGNvbnN0IGhhbGZCYXJIZWlnaHQgPSBiYXJIZWlnaHQgLyAyXG5cbiAgY29uc3QgdG90YWwgPSBkMy5zdW0oZGF0YSwgZCA9PiBkLnZhbHVlKVxuICBjb25zdCBfZGF0YSA9IGdyb3VwRGF0YShkYXRhLCB0b3RhbClcblxuICAvLyBzZXQgdXAgc2NhbGVzIGZvciBob3Jpem9udGFsIHBsYWNlbWVudFxuICBjb25zdCB4U2NhbGUgPSBkMy5zY2FsZUxpbmVhcigpXG4gICAgLmRvbWFpbihbMCwgdG90YWxdKVxuICAgIC5yYW5nZShbMCwgd10pXG5cbiAgLy8gY3JlYXRlIHN2ZyBpbiBwYXNzZWQgaW4gZGl2XG4gIGNvbnN0IHNlbGVjdGlvbiA9IGQzLnNlbGVjdChiaW5kKVxuICAgIC5hcHBlbmQoJ3N2ZycpXG4gICAgLmF0dHIoJ3dpZHRoJywgd2lkdGgpXG4gICAgLmF0dHIoJ2hlaWdodCcsIGhlaWdodClcbiAgICAuYXBwZW5kKCdnJylcbiAgICAuYXR0cigndHJhbnNmb3JtJywgJ3RyYW5zbGF0ZSgnICsgbWFyZ2luLmxlZnQgKyAnLCcgKyBtYXJnaW4udG9wICsgJyknKVxuXG4gIC8vIHN0YWNrIHJlY3QgZm9yIGVhY2ggZGF0YSB2YWx1ZVxuICBzZWxlY3Rpb24uc2VsZWN0QWxsKCdyZWN0JylcbiAgICAuZGF0YShfZGF0YSlcbiAgICAuZW50ZXIoKS5hcHBlbmQoJ3JlY3QnKVxuICAgIC5hdHRyKCdjbGFzcycsICdyZWN0LXN0YWNrZWQnKVxuICAgIC5hdHRyKCd4JywgZCA9PiB4U2NhbGUoZC5jdW11bGF0aXZlKSlcbiAgICAuYXR0cigneScsIGggLyAyIC0gaGFsZkJhckhlaWdodClcbiAgICAuYXR0cignaGVpZ2h0JywgYmFySGVpZ2h0KVxuICAgIC5hdHRyKCd3aWR0aCcsIGQgPT4geFNjYWxlKGQudmFsdWUpKVxuICAgIC5zdHlsZSgnZmlsbCcsIChkLCBpKSA9PiBjb2xvcnNbaV0pXG5cbiAgLy8gYWRkIHZhbHVlcyBvbiBiYXJcbiAgc2VsZWN0aW9uLnNlbGVjdEFsbCgnLnRleHQtdmFsdWUnKVxuICAgIC5kYXRhKF9kYXRhKVxuICAgIC5lbnRlcigpLmFwcGVuZCgndGV4dCcpXG4gICAgLmF0dHIoJ2NsYXNzJywgJ3RleHQtdmFsdWUnKVxuICAgIC5hdHRyKCd0ZXh0LWFuY2hvcicsICdtaWRkbGUnKVxuICAgIC5hdHRyKCd4JywgZCA9PiB4U2NhbGUoZC5jdW11bGF0aXZlKSArICh4U2NhbGUoZC52YWx1ZSkgLyAyKSlcbiAgICAuYXR0cigneScsIChoIC8gMikgKyA1KVxuICAgIC50ZXh0KGQgPT4gZC52YWx1ZSlcblxuICAvLyBhZGQgc29tZSBsYWJlbHMgZm9yIHBlcmNlbnRhZ2VzXG4gIHNlbGVjdGlvbi5zZWxlY3RBbGwoJy50ZXh0LXBlcmNlbnQnKVxuICAgIC5kYXRhKF9kYXRhKVxuICAgIC5lbnRlcigpLmFwcGVuZCgndGV4dCcpXG4gICAgLmF0dHIoJ2NsYXNzJywgJ3RleHQtcGVyY2VudCcpXG4gICAgLmF0dHIoJ3RleHQtYW5jaG9yJywgJ21pZGRsZScpXG4gICAgLmF0dHIoJ3gnLCBkID0+IHhTY2FsZShkLmN1bXVsYXRpdmUpICsgKHhTY2FsZShkLnZhbHVlKSAvIDIpKVxuICAgIC5hdHRyKCd5JywgKGggLyAyKSAtIChoYWxmQmFySGVpZ2h0ICogMS4xKSlcbiAgICAudGV4dChkID0+IGYoZC5wZXJjZW50KSArICcgJScpXG5cbiAgLy8gYWRkIHRoZSBsYWJlbHNcbiAgc2VsZWN0aW9uLnNlbGVjdEFsbCgnLnRleHQtbGFiZWwnKVxuICAgIC5kYXRhKF9kYXRhKVxuICAgIC5lbnRlcigpLmFwcGVuZCgndGV4dCcpXG4gICAgLmF0dHIoJ2NsYXNzJywgJ3RleHQtbGFiZWwnKVxuICAgIC5hdHRyKCd0ZXh0LWFuY2hvcicsICdtaWRkbGUnKVxuICAgIC5hdHRyKCd4JywgZCA9PiB4U2NhbGUoZC5jdW11bGF0aXZlKSArICh4U2NhbGUoZC52YWx1ZSkgLyAyKSlcbiAgICAuYXR0cigneScsIChoIC8gMikgKyAoaGFsZkJhckhlaWdodCAqIDEuMSkgKyAyMClcbiAgICAuc3R5bGUoJ2ZpbGwnLCAoZCwgaSkgPT4gY29sb3JzW2ldKVxuICAgIC50ZXh0KGQgPT4gZC5sYWJlbClcbn1cbi8vIHJlbmRlciBjaGFydFxuc3RhY2tlZEJhcignLmNoYXJ0Jywgc2FtcGxlRGF0YSlcblxuXG5cbi8vIFdFQlBBQ0sgRk9PVEVSIC8vXG4vLyBzY3JpcHQuanMiXSwibWFwcGluZ3MiOiI7Ozs7QUFBQTtBQUNBOzs7QUFBQTtBQUNBOzs7OztBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBTkE7QUFEQTtBQUFBO0FBQUE7QUFBQTtBQUFBO0FBQUE7QUFBQTtBQUNBO0FBVUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBR0E7QUFDQTtBQUNBO0FBTUE7QUFDQTtBQUlBO0FBQUE7QUFHQTtBQUFBO0FBQ0E7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUtBO0FBQUE7QUFFQTtBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBS0E7QUFBQTtBQUVBO0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFLQTtBQUFBO0FBRUE7QUFBQTtBQUNBO0FBQUE7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///2\n")}]);
// group data for chart and filter out zero values
const d3 = window.d3
export default function (data, total) {
// use scale to get percent values
const percent = d3.scaleLinear()
.domain([0, total])
.range([0, 100])
// filter out data that has zero values
// also get mapping for next placement
// (save having to format data for d3 stack)
let cumulative = 0
const _data = data.map(d => {
cumulative += d.value
return {
value: d.value,
// want the cumulative to prior value (start of rect)
cumulative: cumulative - d.value,
label: d.label,
percent: percent(d.value)
}
}).filter(d => d.value > 0)
return _data
}
<!DOCTYPE html>
<title>eesur | stacked bar</title>
<link href='dist.css' rel='stylesheet' />
<body>
<div class="chart"></div>
<script src='https://d3js.org/d3.v4.min.js'></script>
<script src='dist.js'></script>
</body>
const sampleData = [
{ label: 'Test 0 which should not render', value: 0 },
{ label: 'Group-1', value: 55 },
{ label: 'Group-2', value: 233 },
{ label: 'Test 0 AGAIN which should not render', value: 0 },
{ label: 'Group-3', value: 89 }
]
export default sampleData
import sampleData from './sampleData'
import groupData from './groupData'
const d3 = window.d3
function stackedBar (bind, data, config) {
config = {
f: d3.format('.1f'),
margin: {top: 20, right: 10, bottom: 20, left: 10},
width: 800,
height: 200,
barHeight: 100,
colors: ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33'],
...config
}
const { f, margin, width, height, barHeight, colors } = config
const w = width - margin.left - margin.right
const h = height - margin.top - margin.bottom
const halfBarHeight = barHeight / 2
const total = d3.sum(data, d => d.value)
const _data = groupData(data, total)
// set up scales for horizontal placement
const xScale = d3.scaleLinear()
.domain([0, total])
.range([0, w])
// create svg in passed in div
const selection = d3.select(bind)
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
// stack rect for each data value
selection.selectAll('rect')
.data(_data)
.enter().append('rect')
.attr('class', 'rect-stacked')
.attr('x', d => xScale(d.cumulative))
.attr('y', h / 2 - halfBarHeight)
.attr('height', barHeight)
.attr('width', d => xScale(d.value))
.style('fill', (d, i) => colors[i])
// add values on bar
selection.selectAll('.text-value')
.data(_data)
.enter().append('text')
.attr('class', 'text-value')
.attr('text-anchor', 'middle')
.attr('x', d => xScale(d.cumulative) + (xScale(d.value) / 2))
.attr('y', (h / 2) + 5)
.text(d => d.value)
// add some labels for percentages
selection.selectAll('.text-percent')
.data(_data)
.enter().append('text')
.attr('class', 'text-percent')
.attr('text-anchor', 'middle')
.attr('x', d => xScale(d.cumulative) + (xScale(d.value) / 2))
.attr('y', (h / 2) - (halfBarHeight * 1.1))
.text(d => f(d.percent) + ' %')
// add the labels
selection.selectAll('.text-label')
.data(_data)
.enter().append('text')
.attr('class', 'text-label')
.attr('text-anchor', 'middle')
.attr('x', d => xScale(d.cumulative) + (xScale(d.value) / 2))
.attr('y', (h / 2) + (halfBarHeight * 1.1) + 20)
.style('fill', (d, i) => colors[i])
.text(d => d.label)
}
// render chart
stackedBar('.chart', sampleData)
*
box-sizing border-box
body
font-family:-apple-system,monospace
color: #454545
width: 800px
margin: 0 auto
.chart
padding-top: 100px
.text-value
font-size: 24px
fill: #fff
opacity: 0.9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment