Skip to content

Instantly share code, notes, and snippets.

@eesur
Last active February 6, 2018 18:05
Show Gist options
  • Save eesur/a0c31af31b4ae022f61b50af22aa4d30 to your computer and use it in GitHub Desktop.
Save eesur/a0c31af31b4ae022f61b50af22aa4d30 to your computer and use it in GitHub Desktop.
d3js | sankey chart replicating a chord chart
license: mit
height: 500
border: no

sankey chart to replicate a chord chart

click on either the left or right stacked bars to select/highlight paths—double click to reset


notes:

this chart takes data as per sampleData.js:

const sampleData = [
  {
    label: 'Region-a',
    values: [
      {label: 'Ash', value: 4},
      {label: 'Birch', value: 3},
      {label: 'Maple', value: 1}
    ]
  },
  ...
  • it converts it to required format in nodesAndLinks.js for d3-sankey
  • it creates the required DOM elements in createDOM.js, to allow control/ease over internal selections
  • data is displayed in a html elements via listInfo.js and updateTitle.js functions
// using http://basscss.com/ classes
function createDOM (selection) {
const container = selection.append('section')
.attr('class', 'clearfix mx-auto my2 js-wrap')
// create DOM for svg chart
const chart = container.append('div')
.attr('class', 'col col-9')
chart.append('div')
.attr('class', 'js-svg')
// create DOM for displaying info
const info = container.append('div')
.attr('class', 'col col-3')
info.append('h2')
.attr('class', 'h3 regular js-info-title')
info.append('ul')
.attr('class', 'list-reset js-info-ul')
}
export default createDOM
// this will produce the below elements:
/*
<section class="clearfix mx-auto my2">
<div class="col col-9">
<div class="js-svg"></div>
</div>
<div class="col col-3">
<h2 class="h3 regular js-info-title"></h2>
<ul class="list-reset js-info-ul"></ul>
</div>
</section>
*/
*{box-sizing:border-box}body{font-family:-apple-system,monospace;color:#454545;width:800px;margin:0 auto}.chart{padding-top:20px}.node rect{cursor:move;fill-opacity:.9;shape-rendering:crispEdges}.node text{pointer-events:none;text-shadow:0 1px 0 #fff}rect.source{opacity:.8;cursor:pointer}.label{font-size:14px;fill:#454545}.link{fill:none;stroke:#000;stroke-opacity:.2}.link:hover{stroke-opacity:.5}.js-total{padding-left:5px;color:#888}
!function(n){function e(t){if(c[t])return c[t].exports;var a=c[t]={i:t,l:!1,exports:{}};return n[t].call(a.exports,a,a.exports,e),a.l=!0,a.exports}var c={};e.m=n,e.c=c,e.i=function(n){return n},e.d=function(n,c,t){e.o(n,c)||Object.defineProperty(n,c,{configurable:!1,enumerable:!0,get:t})},e.n=function(n){var c=n&&n.__esModule?function(){return n.default}:function(){return n};return e.d(c,"a",c),c},e.o=function(n,e){return Object.prototype.hasOwnProperty.call(n,e)},e.p="",e(e.s=5)}([function(module,exports,__webpack_require__){"use strict";eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n// using http://basscss.com/ classes\n\nfunction createDOM(selection) {\n var container = selection.append('section').attr('class', 'clearfix mx-auto my2 js-wrap');\n // create DOM for svg chart\n var chart = container.append('div').attr('class', 'col col-9');\n chart.append('div').attr('class', 'js-svg');\n // create DOM for displaying info\n var info = container.append('div').attr('class', 'col col-3');\n info.append('h2').attr('class', 'h3 regular js-info-title');\n info.append('ul').attr('class', 'list-reset js-info-ul');\n}\n\nexports.default = createDOM;\n\n// this will produce the below elements:\n/*\n<section class=\"clearfix mx-auto my2\">\n <div class=\"col col-9\">\n <div class=\"js-svg\"></div>\n </div>\n <div class=\"col col-3\">\n <h2 class=\"h3 regular js-info-title\"></h2>\n <ul class=\"list-reset js-info-ul\"></ul>\n </div>\n</section>\n*///# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9jcmVhdGVET00uanM/MmE4YSJdLCJzb3VyY2VzQ29udGVudCI6WyIvLyB1c2luZyBodHRwOi8vYmFzc2Nzcy5jb20vIGNsYXNzZXNcblxuZnVuY3Rpb24gY3JlYXRlRE9NIChzZWxlY3Rpb24pIHtcbiAgY29uc3QgY29udGFpbmVyID0gc2VsZWN0aW9uLmFwcGVuZCgnc2VjdGlvbicpXG4gICAgLmF0dHIoJ2NsYXNzJywgJ2NsZWFyZml4IG14LWF1dG8gbXkyIGpzLXdyYXAnKVxuICAvLyBjcmVhdGUgRE9NIGZvciBzdmcgY2hhcnRcbiAgY29uc3QgY2hhcnQgPSBjb250YWluZXIuYXBwZW5kKCdkaXYnKVxuICAgIC5hdHRyKCdjbGFzcycsICdjb2wgY29sLTknKVxuICBjaGFydC5hcHBlbmQoJ2RpdicpXG4gICAgLmF0dHIoJ2NsYXNzJywgJ2pzLXN2ZycpXG4gIC8vIGNyZWF0ZSBET00gZm9yIGRpc3BsYXlpbmcgaW5mb1xuICBjb25zdCBpbmZvID0gY29udGFpbmVyLmFwcGVuZCgnZGl2JylcbiAgICAuYXR0cignY2xhc3MnLCAnY29sIGNvbC0zJylcbiAgaW5mby5hcHBlbmQoJ2gyJylcbiAgICAuYXR0cignY2xhc3MnLCAnaDMgcmVndWxhciBqcy1pbmZvLXRpdGxlJylcbiAgaW5mby5hcHBlbmQoJ3VsJylcbiAgICAuYXR0cignY2xhc3MnLCAnbGlzdC1yZXNldCBqcy1pbmZvLXVsJylcbn1cblxuZXhwb3J0IGRlZmF1bHQgY3JlYXRlRE9NXG5cbi8vIHRoaXMgd2lsbCBwcm9kdWNlIHRoZSBiZWxvdyBlbGVtZW50czpcbi8qXG48c2VjdGlvbiBjbGFzcz1cImNsZWFyZml4IG14LWF1dG8gbXkyXCI+XG4gIDxkaXYgY2xhc3M9XCJjb2wgY29sLTlcIj5cbiAgICA8ZGl2IGNsYXNzPVwianMtc3ZnXCI+PC9kaXY+XG4gIDwvZGl2PlxuICA8ZGl2IGNsYXNzPVwiY29sIGNvbC0zXCI+XG4gICAgPGgyIGNsYXNzPVwiaDMgcmVndWxhciBqcy1pbmZvLXRpdGxlXCI+PC9oMj5cbiAgICA8dWwgY2xhc3M9XCJsaXN0LXJlc2V0IGpzLWluZm8tdWxcIj48L3VsPlxuICA8L2Rpdj5cbjwvc2VjdGlvbj5cbiovXG5cblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gY3JlYXRlRE9NLmpzIl0sIm1hcHBpbmdzIjoiOzs7OztBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBRUE7QUFDQTtBQUVBO0FBRUE7QUFDQTtBQUVBO0FBRUE7QUFFQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7Ozs7Ozs7Ozs7QSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///0\n")},function(module,exports,__webpack_require__){"use strict";eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\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\n// create list items for data paths\n\nfunction listInfo(selection, data, config) {\n config = _extends({\n type: 'source',\n colorScale: null\n }, config);\n var _config = config,\n type = _config.type,\n colorScale = _config.colorScale;\n\n\n var join = selection.select('.js-info-ul').selectAll('li').data(data);\n\n join.enter().append('li').attr('class', 'mb1').merge(join).html(function (d) {\n var index = d.source.node;\n if (type === 'source') {\n return createColorBlock(index) + ' ' + d.target.label + ': <span class=\\'bold\\'>' + d.value + '</span>';\n } else {\n return createColorBlock(index) + ' ' + d.source.label + ': <span class=\\'bold\\'>' + d.value + '</span>';\n }\n });\n\n join.exit().remove();\n\n function createColorBlock(index) {\n return '\\n <span \\n class=\\'inline-block li-' + index + '\\' \\n style=\\'background: ' + colorScale(index) + '; width: 8px; opacity:0.7;\\'\\n >\\n &nbsp;\\n </span>\\n ';\n }\n}\n\nexports.default = listInfo;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMS5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9saXN0SW5mby5qcz8wM2I2Il0sInNvdXJjZXNDb250ZW50IjpbIi8vIGNyZWF0ZSBsaXN0IGl0ZW1zIGZvciBkYXRhIHBhdGhzXG5cbmZ1bmN0aW9uIGxpc3RJbmZvIChzZWxlY3Rpb24sIGRhdGEsIGNvbmZpZykge1xuICBjb25maWcgPSB7XG4gICAgdHlwZTogJ3NvdXJjZScsXG4gICAgY29sb3JTY2FsZTogbnVsbCxcbiAgICAuLi5jb25maWdcbiAgfVxuICBjb25zdCB7dHlwZSwgY29sb3JTY2FsZX0gPSBjb25maWdcblxuICBjb25zdCBqb2luID0gc2VsZWN0aW9uLnNlbGVjdCgnLmpzLWluZm8tdWwnKVxuICAgIC5zZWxlY3RBbGwoJ2xpJylcbiAgICAuZGF0YShkYXRhKVxuXG4gIGpvaW4uZW50ZXIoKS5hcHBlbmQoJ2xpJylcbiAgICAuYXR0cignY2xhc3MnLCAnbWIxJylcbiAgICAubWVyZ2Uoam9pbilcbiAgICAuaHRtbChkID0+IHtcbiAgICAgIGNvbnN0IGluZGV4ID0gZC5zb3VyY2Uubm9kZVxuICAgICAgaWYgKHR5cGUgPT09ICdzb3VyY2UnKSB7XG4gICAgICAgIHJldHVybiBgJHtjcmVhdGVDb2xvckJsb2NrKGluZGV4KX0gJHtkLnRhcmdldC5sYWJlbH06IDxzcGFuIGNsYXNzPSdib2xkJz4ke2QudmFsdWV9PC9zcGFuPmBcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJldHVybiBgJHtjcmVhdGVDb2xvckJsb2NrKGluZGV4KX0gJHtkLnNvdXJjZS5sYWJlbH06IDxzcGFuIGNsYXNzPSdib2xkJz4ke2QudmFsdWV9PC9zcGFuPmBcbiAgICAgIH1cbiAgICB9KVxuXG4gIGpvaW4uZXhpdCgpLnJlbW92ZSgpXG5cbiAgZnVuY3Rpb24gY3JlYXRlQ29sb3JCbG9jayAoaW5kZXgpIHtcbiAgICByZXR1cm4gKGBcbiAgICAgIDxzcGFuIFxuICAgICAgY2xhc3M9J2lubGluZS1ibG9jayBsaS0ke2luZGV4fScgXG4gICAgICBzdHlsZT0nYmFja2dyb3VuZDogJHtjb2xvclNjYWxlKGluZGV4KX07IHdpZHRoOiA4cHg7IG9wYWNpdHk6MC43OydcbiAgICAgID5cbiAgICAgICZuYnNwO1xuICAgICAgPC9zcGFuPlxuICAgIGApXG4gIH1cbn1cblxuZXhwb3J0IGRlZmF1bHQgbGlzdEluZm9cblxuXG5cbi8vIFdFQlBBQ0sgRk9PVEVSIC8vXG4vLyBsaXN0SW5mby5qcyJdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFGQTtBQURBO0FBQUE7QUFBQTtBQUNBO0FBQ0E7QUFNQTtBQUNBO0FBR0E7QUFJQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFRQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///1\n")},function(module,exports,__webpack_require__){"use strict";eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nexports.default = function (data) {\n var targetsArray = listTargets(data);\n var sourceNode = createSourceNodes(data);\n var targetNodes = createTargetNodes(data);\n var linksArray = createLinks(data);\n console.log('sourceNode', sourceNode);\n console.log('targetNodes', targetNodes);\n console.log('linksArray', linksArray);\n console.log('sourceNode.concat(targetNodes)', sourceNode.concat(targetNodes));\n\n function createSourceNodes(data) {\n var nodes = data.map(function (d, i) {\n return {\n node: i,\n subIndex: i,\n label: d.label,\n type: 'source'\n };\n });\n return nodes;\n }\n\n function listTargets(data) {\n // list potential targets\n var _targets = [];\n data.forEach(function (o) {\n // push as flat array\n _targets.push.apply(_targets, _toConsumableArray(o.values.map(function (d) {\n return d.label;\n })));\n });\n // remove duplicates\n var targetsArray = [].concat(_toConsumableArray(new Set(_targets)));\n console.log('targetsArray', targetsArray);\n return targetsArray;\n }\n\n function createTargetNodes(data) {\n // create required structure\n var nodes = targetsArray.map(function (d, i) {\n return {\n node: data.length + i,\n subIndex: i,\n label: d,\n type: 'target'\n };\n });\n return nodes;\n }\n\n function createLinks(data) {\n var links = [];\n data.forEach(function (o, index) {\n var val = o.values.map(function (d) {\n return {\n source: index,\n target: targetsArray.indexOf(d.label) + data.length,\n value: d.value,\n targetLabel: o.label,\n sourceLabel: d.label\n };\n }).filter(function (d) {\n return d.value !== null;\n });\n links.push(val);\n });\n // return a flattened array\n return links.reduce(function (a, b) {\n return a.concat(b);\n }, []);\n }\n\n // return the formatted data\n return {\n nodes: sourceNode.concat(targetNodes),\n links: linksArray\n };\n};\n\nfunction _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } // format data//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMi5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9ub2Rlc0FuZExpbmtzLmpzPzJlOTMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gZm9ybWF0IGRhdGFcbmV4cG9ydCBkZWZhdWx0IGZ1bmN0aW9uIChkYXRhKSB7XG4gIGNvbnN0IHRhcmdldHNBcnJheSA9IGxpc3RUYXJnZXRzKGRhdGEpXG4gIGNvbnN0IHNvdXJjZU5vZGUgPSBjcmVhdGVTb3VyY2VOb2RlcyhkYXRhKVxuICBjb25zdCB0YXJnZXROb2RlcyA9IGNyZWF0ZVRhcmdldE5vZGVzKGRhdGEpXG4gIGNvbnN0IGxpbmtzQXJyYXkgPSBjcmVhdGVMaW5rcyhkYXRhKVxuICBjb25zb2xlLmxvZygnc291cmNlTm9kZScsIHNvdXJjZU5vZGUpXG4gIGNvbnNvbGUubG9nKCd0YXJnZXROb2RlcycsIHRhcmdldE5vZGVzKVxuICBjb25zb2xlLmxvZygnbGlua3NBcnJheScsIGxpbmtzQXJyYXkpXG4gIGNvbnNvbGUubG9nKCdzb3VyY2VOb2RlLmNvbmNhdCh0YXJnZXROb2RlcyknLCBzb3VyY2VOb2RlLmNvbmNhdCh0YXJnZXROb2RlcykpXG5cbiAgZnVuY3Rpb24gY3JlYXRlU291cmNlTm9kZXMgKGRhdGEpIHtcbiAgICBjb25zdCBub2RlcyA9IGRhdGEubWFwKChkLCBpKSA9PiB7XG4gICAgICByZXR1cm4ge1xuICAgICAgICBub2RlOiBpLFxuICAgICAgICBzdWJJbmRleDogaSxcbiAgICAgICAgbGFiZWw6IGQubGFiZWwsXG4gICAgICAgIHR5cGU6ICdzb3VyY2UnXG4gICAgICB9XG4gICAgfSlcbiAgICByZXR1cm4gbm9kZXNcbiAgfVxuXG4gIGZ1bmN0aW9uIGxpc3RUYXJnZXRzIChkYXRhKSB7XG4gICAgLy8gbGlzdCBwb3RlbnRpYWwgdGFyZ2V0c1xuICAgIGxldCBfdGFyZ2V0cyA9IFtdXG4gICAgZGF0YS5mb3JFYWNoKG8gPT4ge1xuICAgICAgLy8gcHVzaCBhcyBmbGF0IGFycmF5XG4gICAgICBfdGFyZ2V0cy5wdXNoKC4uLm8udmFsdWVzLm1hcChkID0+IGQubGFiZWwpKVxuICAgIH0pXG4gICAgLy8gcmVtb3ZlIGR1cGxpY2F0ZXNcbiAgICBjb25zdCB0YXJnZXRzQXJyYXkgPSBbLi4uKG5ldyBTZXQoX3RhcmdldHMpKV1cbiAgICBjb25zb2xlLmxvZygndGFyZ2V0c0FycmF5JywgdGFyZ2V0c0FycmF5KVxuICAgIHJldHVybiB0YXJnZXRzQXJyYXlcbiAgfVxuXG4gIGZ1bmN0aW9uIGNyZWF0ZVRhcmdldE5vZGVzIChkYXRhKSB7XG4gICAgLy8gY3JlYXRlIHJlcXVpcmVkIHN0cnVjdHVyZVxuICAgIGNvbnN0IG5vZGVzID0gdGFyZ2V0c0FycmF5Lm1hcCgoZCwgaSkgPT4ge1xuICAgICAgcmV0dXJuIHtcbiAgICAgICAgbm9kZTogKGRhdGEubGVuZ3RoKSArIGksXG4gICAgICAgIHN1YkluZGV4OiBpLFxuICAgICAgICBsYWJlbDogZCxcbiAgICAgICAgdHlwZTogJ3RhcmdldCdcbiAgICAgIH1cbiAgICB9KVxuICAgIHJldHVybiBub2Rlc1xuICB9XG5cbiAgZnVuY3Rpb24gY3JlYXRlTGlua3MgKGRhdGEpIHtcbiAgICBjb25zdCBsaW5rcyA9IFtdXG4gICAgZGF0YS5mb3JFYWNoKChvLCBpbmRleCkgPT4ge1xuICAgICAgY29uc3QgdmFsID0gby52YWx1ZXMubWFwKGQgPT4ge1xuICAgICAgICByZXR1cm4ge1xuICAgICAgICAgIHNvdXJjZTogaW5kZXgsXG4gICAgICAgICAgdGFyZ2V0OiB0YXJnZXRzQXJyYXkuaW5kZXhPZihkLmxhYmVsKSArIGRhdGEubGVuZ3RoLFxuICAgICAgICAgIHZhbHVlOiBkLnZhbHVlLFxuICAgICAgICAgIHRhcmdldExhYmVsOiBvLmxhYmVsLFxuICAgICAgICAgIHNvdXJjZUxhYmVsOiBkLmxhYmVsXG4gICAgICAgIH1cbiAgICAgIH0pLmZpbHRlcihkID0+IGQudmFsdWUgIT09IG51bGwpXG4gICAgICBsaW5rcy5wdXNoKHZhbClcbiAgICB9KVxuICAgIC8vIHJldHVybiBhIGZsYXR0ZW5lZCBhcnJheVxuICAgIHJldHVybiBsaW5rcy5yZWR1Y2UoKGEsIGIpID0+IGEuY29uY2F0KGIpLCBbXSlcbiAgfVxuXG4gIC8vIHJldHVybiB0aGUgZm9ybWF0dGVkIGRhdGFcbiAgcmV0dXJuIHtcbiAgICBub2Rlczogc291cmNlTm9kZS5jb25jYXQodGFyZ2V0Tm9kZXMpLFxuICAgIGxpbmtzOiBsaW5rc0FycmF5XG4gIH1cbn1cblxuXG5cbi8vIFdFQlBBQ0sgRk9PVEVSIC8vXG4vLyBub2Rlc0FuZExpbmtzLmpzIl0sIm1hcHBpbmdzIjoiOzs7Ozs7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBSkE7QUFNQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUFBO0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFKQTtBQU1BO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFMQTtBQU9BO0FBQUE7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQUE7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUZBO0FBSUE7QUFDQTtBQXpFQSIsInNvdXJjZVJvb3QiOiIifQ==\n//# sourceURL=webpack-internal:///2\n")},function(module,exports,__webpack_require__){"use strict";eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nvar sampleData = [{\n label: 'Region-a',\n values: [{ label: 'Ash', value: 4 }, { label: 'Birch', value: 3 }, { label: 'Maple', value: 1 }]\n}, {\n label: 'Region-b',\n values: [{ label: 'Ash', value: 3 }, { label: 'Birch', value: 1 }, { label: 'Maple', value: 1 }]\n}, {\n label: 'Region-c',\n values: [{ label: 'Ash', value: 4 }, { label: 'Birch', value: 3 }]\n}, {\n label: 'Region-d',\n values: [{ label: 'Maple', value: 1 }, { label: 'Ash', value: null }, { label: 'Birch', value: 2 }]\n}];\n\nexports.default = sampleData;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMy5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zYW1wbGVEYXRhLmpzPzM5N2YiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc3Qgc2FtcGxlRGF0YSA9IFtcbiAge1xuICAgIGxhYmVsOiAnUmVnaW9uLWEnLFxuICAgIHZhbHVlczogW1xuICAgICAge2xhYmVsOiAnQXNoJywgdmFsdWU6IDR9LFxuICAgICAge2xhYmVsOiAnQmlyY2gnLCB2YWx1ZTogM30sXG4gICAgICB7bGFiZWw6ICdNYXBsZScsIHZhbHVlOiAxfVxuICAgIF1cbiAgfSxcbiAge1xuICAgIGxhYmVsOiAnUmVnaW9uLWInLFxuICAgIHZhbHVlczogW1xuICAgICAge2xhYmVsOiAnQXNoJywgdmFsdWU6IDN9LFxuICAgICAge2xhYmVsOiAnQmlyY2gnLCB2YWx1ZTogMX0sXG4gICAgICB7bGFiZWw6ICdNYXBsZScsIHZhbHVlOiAxfVxuICAgIF1cbiAgfSxcbiAge1xuICAgIGxhYmVsOiAnUmVnaW9uLWMnLFxuICAgIHZhbHVlczogW1xuICAgICAge2xhYmVsOiAnQXNoJywgdmFsdWU6IDR9LFxuICAgICAge2xhYmVsOiAnQmlyY2gnLCB2YWx1ZTogM31cbiAgICBdXG4gIH0sXG4gIHtcbiAgICBsYWJlbDogJ1JlZ2lvbi1kJyxcbiAgICB2YWx1ZXM6IFtcbiAgICAgIHtsYWJlbDogJ01hcGxlJywgdmFsdWU6IDF9LFxuICAgICAge2xhYmVsOiAnQXNoJywgdmFsdWU6IG51bGx9LFxuICAgICAge2xhYmVsOiAnQmlyY2gnLCB2YWx1ZTogMn1cbiAgICBdXG4gIH1cbl1cblxuZXhwb3J0IGRlZmF1bHQgc2FtcGxlRGF0YVxuXG5cblxuLy8gV0VCUEFDSyBGT09URVIgLy9cbi8vIHNhbXBsZURhdGEuanMiXSwibWFwcGluZ3MiOiI7Ozs7O0FBQUE7QUFFQTtBQUNBO0FBRkE7QUFTQTtBQUNBO0FBRkE7QUFTQTtBQUNBO0FBRkE7QUFRQTtBQUNBO0FBRkE7QUFDQTtBQVNBIiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///3\n")},function(module,exports,__webpack_require__){"use strict";eval("\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n// output the header\n\nfunction updateTitle(selection, label, value) {\n selection.select('.js-info-title').html(label + ' <span class=\\'js-total\\'>(total: ' + value + ')</span>');\n}\nexports.default = updateTitle;//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNC5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy91cGRhdGVUaXRsZS5qcz9mZjYxIl0sInNvdXJjZXNDb250ZW50IjpbIi8vIG91dHB1dCB0aGUgaGVhZGVyXG5cbmZ1bmN0aW9uIHVwZGF0ZVRpdGxlIChzZWxlY3Rpb24sIGxhYmVsLCB2YWx1ZSkge1xuICBzZWxlY3Rpb24uc2VsZWN0KCcuanMtaW5mby10aXRsZScpXG4gICAgLmh0bWwoYCR7bGFiZWx9IDxzcGFuIGNsYXNzPSdqcy10b3RhbCc+KHRvdGFsOiAke3ZhbHVlfSk8L3NwYW4+YClcbn1cbmV4cG9ydCBkZWZhdWx0IHVwZGF0ZVRpdGxlXG5cblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gdXBkYXRlVGl0bGUuanMiXSwibWFwcGluZ3MiOiI7Ozs7O0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFFQTtBQUNBIiwic291cmNlUm9vdCI6IiJ9\n//# sourceURL=webpack-internal:///4\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__(3);\n\nvar _sampleData2 = _interopRequireDefault(_sampleData);\n\nvar _nodesAndLinks = __webpack_require__(2);\n\nvar _nodesAndLinks2 = _interopRequireDefault(_nodesAndLinks);\n\nvar _listInfo = __webpack_require__(1);\n\nvar _listInfo2 = _interopRequireDefault(_listInfo);\n\nvar _updateTitle = __webpack_require__(4);\n\nvar _updateTitle2 = _interopRequireDefault(_updateTitle);\n\nvar _createDOM = __webpack_require__(0);\n\nvar _createDOM2 = _interopRequireDefault(_createDOM);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar d3 = window.d3;\n\nfunction sankey(bind, data, config) {\n config = _extends({\n margin: { top: 20, right: 100, bottom: 20, left: 100 },\n width: 600,\n height: 500,\n sourceColors: ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33'],\n targetColors: ['#bbb']\n }, config);\n var _config = config,\n margin = _config.margin,\n width = _config.width,\n height = _config.height;\n\n var w = width - margin.left - margin.right;\n var h = height - margin.top - margin.bottom;\n var sourceColors = d3.scaleOrdinal().range(config.sourceColors);\n var targetColors = d3.scaleOrdinal().range(config.targetColors);\n var _data = (0, _nodesAndLinks2.default)(data);\n\n console.log('data', data);\n console.log('_data formatted', _data);\n\n // set up dom\n var selection = d3.select(bind);\n // destroy/wipe first\n d3.select(bind).select('.js-wrap').remove();\n (0, _createDOM2.default)(selection);\n\n // create svg in passed in div\n var svg = selection.select('.js-svg').append('svg').attr('width', width).attr('height', height).append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');\n\n var sankey = d3.sankey().nodeWidth(40).nodePadding(10).extent([[1, 1], [w - 1, h - 6]]);\n sankey(_data);\n // console.log('sankey(_data)', sankey(_data))\n\n var link = svg.append('g').attr('class', 'links').attr('fill', 'none').attr('stroke-opacity', 0.2).selectAll('path').data(_data.links).enter().append('path').attr('class', function (d) {\n // console.log('d', d)\n return 'source-' + d.source.node + ' target-' + d.target.node;\n }).attr('d', d3.sankeyLinkHorizontal()).attr('stroke', function (d) {\n return sourceColors(d.source.node);\n }).attr('stroke-width', function (d) {\n return Math.max(1, d.width);\n });\n\n var node = svg.append('g').attr('class', 'nodes').attr('font-family', 'sans-serif').attr('font-size', 10).selectAll('g').data(_data.nodes).enter().append('g');\n\n node.append('rect').attr('x', function (d) {\n return d.x0;\n }).attr('y', function (d) {\n return d.y0;\n }).attr('class', function (d) {\n return d.type + ' bar';\n }).attr('height', function (d) {\n return d.y1 - d.y0;\n }).attr('width', function (d) {\n return d.x1 - d.x0;\n })\n // use colours depending on source or target\n .attr('fill', function (d) {\n return d.type === 'source' ? sourceColors(d.subIndex) : targetColors(d.subIndex);\n }).on('click', function (d) {\n if (d.type === 'source') {\n highlightSourcePaths(svg, d.index, d.type);\n (0, _updateTitle2.default)(selection, d.label, d.value);\n (0, _listInfo2.default)(selection, d.sourceLinks, {\n type: 'source',\n colorScale: sourceColors\n });\n } else {\n highlightSourcePaths(svg, d.index, d.type);\n (0, _updateTitle2.default)(selection, d.label, d.value);\n (0, _listInfo2.default)(selection, d.targetLinks, {\n type: 'target',\n colorScale: sourceColors\n });\n }\n }).on('dblclick', function (d) {\n svg.selectAll('path').attr('stroke-opacity', 0.2);\n });\n\n node.append('text').attr('x', function (d) {\n return d.x0 + 50;\n }).attr('y', function (d) {\n return (d.y1 + d.y0) / 2;\n }).attr('dy', '0.35em').attr('class', 'label').attr('text-anchor', 'start').text(function (d) {\n return d.label;\n }).filter(function (d) {\n return d.x0 < width / 2;\n }).attr('x', function (d) {\n return d.x1 - 50;\n }).attr('text-anchor', 'end');\n\n function highlightSourcePaths(sel, index, type) {\n sel.selectAll('path').attr('stroke-opacity', 0.05)\n // pass in type of source/target\n .filter('path.' + type + '-' + index).attr('stroke-opacity', 0.6);\n }\n}\n// render chart\nsankey('.chart', _sampleData2.default);//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiNS5qcyIsInNvdXJjZXMiOlsid2VicGFjazovLy9zY3JpcHQuanM/OWE5NSJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgc2FtcGxlRGF0YSBmcm9tICcuL3NhbXBsZURhdGEnXG5pbXBvcnQgbm9kZXNBbmRMaW5rcyBmcm9tICcuL25vZGVzQW5kTGlua3MnXG5pbXBvcnQgbGlzdEluZm8gZnJvbSAnLi9saXN0SW5mbydcbmltcG9ydCB1cGRhdGVUaXRsZSBmcm9tICcuL3VwZGF0ZVRpdGxlJ1xuaW1wb3J0IGNyZWF0ZURPTSBmcm9tICcuL2NyZWF0ZURPTSdcbmNvbnN0IGQzID0gd2luZG93LmQzXG5cbmZ1bmN0aW9uIHNhbmtleSAoYmluZCwgZGF0YSwgY29uZmlnKSB7XG4gIGNvbmZpZyA9IHtcbiAgICBtYXJnaW46IHt0b3A6IDIwLCByaWdodDogMTAwLCBib3R0b206IDIwLCBsZWZ0OiAxMDB9LFxuICAgIHdpZHRoOiA2MDAsXG4gICAgaGVpZ2h0OiA1MDAsXG4gICAgc291cmNlQ29sb3JzOiBbJyNlNDFhMWMnLCAnIzM3N2ViOCcsICcjNGRhZjRhJywgJyM5ODRlYTMnLCAnI2ZmN2YwMCcsICcjZmZmZjMzJ10sXG4gICAgdGFyZ2V0Q29sb3JzOiBbJyNiYmInXSxcbiAgICAuLi5jb25maWdcbiAgfVxuICBjb25zdCB7bWFyZ2luLCB3aWR0aCwgaGVpZ2h0fSA9IGNvbmZpZ1xuICBjb25zdCB3ID0gd2lkdGggLSBtYXJnaW4ubGVmdCAtIG1hcmdpbi5yaWdodFxuICBjb25zdCBoID0gaGVpZ2h0IC0gbWFyZ2luLnRvcCAtIG1hcmdpbi5ib3R0b21cbiAgY29uc3Qgc291cmNlQ29sb3JzID0gZDMuc2NhbGVPcmRpbmFsKClcbiAgICAucmFuZ2UoY29uZmlnLnNvdXJjZUNvbG9ycylcbiAgY29uc3QgdGFyZ2V0Q29sb3JzID0gZDMuc2NhbGVPcmRpbmFsKClcbiAgICAucmFuZ2UoY29uZmlnLnRhcmdldENvbG9ycylcbiAgY29uc3QgX2RhdGEgPSBub2Rlc0FuZExpbmtzKGRhdGEpXG5cbiAgY29uc29sZS5sb2coJ2RhdGEnLCBkYXRhKVxuICBjb25zb2xlLmxvZygnX2RhdGEgZm9ybWF0dGVkJywgX2RhdGEpXG5cbiAgLy8gc2V0IHVwIGRvbVxuICBjb25zdCBzZWxlY3Rpb24gPSBkMy5zZWxlY3QoYmluZClcbiAgLy8gZGVzdHJveS93aXBlIGZpcnN0XG4gIGQzLnNlbGVjdChiaW5kKS5zZWxlY3QoJy5qcy13cmFwJykucmVtb3ZlKClcbiAgY3JlYXRlRE9NKHNlbGVjdGlvbilcblxuICAvLyBjcmVhdGUgc3ZnIGluIHBhc3NlZCBpbiBkaXZcbiAgY29uc3Qgc3ZnID0gc2VsZWN0aW9uLnNlbGVjdCgnLmpzLXN2ZycpXG4gICAgLmFwcGVuZCgnc3ZnJylcbiAgICAuYXR0cignd2lkdGgnLCB3aWR0aClcbiAgICAuYXR0cignaGVpZ2h0JywgaGVpZ2h0KVxuICAgIC5hcHBlbmQoJ2cnKVxuICAgIC5hdHRyKCd0cmFuc2Zvcm0nLCAndHJhbnNsYXRlKCcgKyBtYXJnaW4ubGVmdCArICcsJyArIG1hcmdpbi50b3AgKyAnKScpXG5cbiAgY29uc3Qgc2Fua2V5ID0gZDMuc2Fua2V5KClcbiAgICAubm9kZVdpZHRoKDQwKVxuICAgIC5ub2RlUGFkZGluZygxMClcbiAgICAuZXh0ZW50KFtbMSwgMV0sIFt3IC0gMSwgaCAtIDZdXSlcbiAgc2Fua2V5KF9kYXRhKVxuICAvLyBjb25zb2xlLmxvZygnc2Fua2V5KF9kYXRhKScsIHNhbmtleShfZGF0YSkpXG5cbiAgY29uc3QgbGluayA9IHN2Zy5hcHBlbmQoJ2cnKVxuICAgIC5hdHRyKCdjbGFzcycsICdsaW5rcycpXG4gICAgLmF0dHIoJ2ZpbGwnLCAnbm9uZScpXG4gICAgLmF0dHIoJ3N0cm9rZS1vcGFjaXR5JywgMC4yKVxuICAuc2VsZWN0QWxsKCdwYXRoJylcbiAgICAuZGF0YShfZGF0YS5saW5rcylcbiAgICAuZW50ZXIoKS5hcHBlbmQoJ3BhdGgnKVxuICAgIC5hdHRyKCdjbGFzcycsIGQgPT4ge1xuICAgICAgLy8gY29uc29sZS5sb2coJ2QnLCBkKVxuICAgICAgcmV0dXJuIGBzb3VyY2UtJHtkLnNvdXJjZS5ub2RlfSB0YXJnZXQtJHtkLnRhcmdldC5ub2RlfWBcbiAgICB9KVxuICAgIC5hdHRyKCdkJywgZDMuc2Fua2V5TGlua0hvcml6b250YWwoKSlcbiAgICAuYXR0cignc3Ryb2tlJywgZCA9PiBzb3VyY2VDb2xvcnMoZC5zb3VyY2Uubm9kZSkpXG4gICAgLmF0dHIoJ3N0cm9rZS13aWR0aCcsIGQgPT4gTWF0aC5tYXgoMSwgZC53aWR0aCkpXG5cbiAgY29uc3Qgbm9kZSA9IHN2Zy5hcHBlbmQoJ2cnKVxuICAgIC5hdHRyKCdjbGFzcycsICdub2RlcycpXG4gICAgLmF0dHIoJ2ZvbnQtZmFtaWx5JywgJ3NhbnMtc2VyaWYnKVxuICAgIC5hdHRyKCdmb250LXNpemUnLCAxMClcbiAgLnNlbGVjdEFsbCgnZycpXG4gIC5kYXRhKF9kYXRhLm5vZGVzKVxuICAgIC5lbnRlcigpLmFwcGVuZCgnZycpXG5cbiAgbm9kZS5hcHBlbmQoJ3JlY3QnKVxuICAgIC5hdHRyKCd4JywgZCA9PiBkLngwKVxuICAgIC5hdHRyKCd5JywgZCA9PiBkLnkwKVxuICAgIC5hdHRyKCdjbGFzcycsIGQgPT4gZC50eXBlICsgJyBiYXInKVxuICAgIC5hdHRyKCdoZWlnaHQnLCBkID0+IGQueTEgLSBkLnkwKVxuICAgIC5hdHRyKCd3aWR0aCcsIGQgPT4gZC54MSAtIGQueDApXG4gICAgLy8gdXNlIGNvbG91cnMgZGVwZW5kaW5nIG9uIHNvdXJjZSBvciB0YXJnZXRcbiAgICAuYXR0cignZmlsbCcsIGQgPT4gKGQudHlwZSA9PT0gJ3NvdXJjZScpXG4gICAgICA/IHNvdXJjZUNvbG9ycyhkLnN1YkluZGV4KVxuICAgICAgOiB0YXJnZXRDb2xvcnMoZC5zdWJJbmRleClcbiAgICApXG4gICAgLm9uKCdjbGljaycsIGZ1bmN0aW9uIChkKSB7XG4gICAgICBpZiAoZC50eXBlID09PSAnc291cmNlJykge1xuICAgICAgICBoaWdobGlnaHRTb3VyY2VQYXRocyhzdmcsIGQuaW5kZXgsIGQudHlwZSlcbiAgICAgICAgdXBkYXRlVGl0bGUoc2VsZWN0aW9uLCBkLmxhYmVsLCBkLnZhbHVlKVxuICAgICAgICBsaXN0SW5mbyhzZWxlY3Rpb24sIGQuc291cmNlTGlua3MsIHtcbiAgICAgICAgICB0eXBlOiAnc291cmNlJyxcbiAgICAgICAgICBjb2xvclNjYWxlOiBzb3VyY2VDb2xvcnNcbiAgICAgICAgfSlcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIGhpZ2hsaWdodFNvdXJjZVBhdGhzKHN2ZywgZC5pbmRleCwgZC50eXBlKVxuICAgICAgICB1cGRhdGVUaXRsZShzZWxlY3Rpb24sIGQubGFiZWwsIGQudmFsdWUpXG4gICAgICAgIGxpc3RJbmZvKHNlbGVjdGlvbiwgZC50YXJnZXRMaW5rcywge1xuICAgICAgICAgIHR5cGU6ICd0YXJnZXQnLFxuICAgICAgICAgIGNvbG9yU2NhbGU6IHNvdXJjZUNvbG9yc1xuICAgICAgICB9KVxuICAgICAgfVxuICAgIH0pXG4gICAgLm9uKCdkYmxjbGljaycsIGZ1bmN0aW9uIChkKSB7XG4gICAgICBzdmcuc2VsZWN0QWxsKCdwYXRoJylcbiAgICAgICAgLmF0dHIoJ3N0cm9rZS1vcGFjaXR5JywgMC4yKVxuICAgIH0pXG5cbiAgbm9kZS5hcHBlbmQoJ3RleHQnKVxuICAgICAgLmF0dHIoJ3gnLCBkID0+IGQueDAgKyA1MClcbiAgICAgIC5hdHRyKCd5JywgZCA9PiAoZC55MSArIGQueTApIC8gMilcbiAgICAgIC5hdHRyKCdkeScsICcwLjM1ZW0nKVxuICAgICAgLmF0dHIoJ2NsYXNzJywgJ2xhYmVsJylcbiAgICAgIC5hdHRyKCd0ZXh0LWFuY2hvcicsICdzdGFydCcpXG4gICAgICAudGV4dChkID0+IGQubGFiZWwpXG4gICAgLmZpbHRlcihkID0+IGQueDAgPCB3aWR0aCAvIDIpXG4gICAgICAuYXR0cigneCcsIGQgPT4gZC54MSAtIDUwKVxuICAgICAgLmF0dHIoJ3RleHQtYW5jaG9yJywgJ2VuZCcpXG5cbiAgZnVuY3Rpb24gaGlnaGxpZ2h0U291cmNlUGF0aHMgKHNlbCwgaW5kZXgsIHR5cGUpIHtcbiAgICBzZWwuc2VsZWN0QWxsKCdwYXRoJylcbiAgICAgIC5hdHRyKCdzdHJva2Utb3BhY2l0eScsIDAuMDUpXG4gICAgICAvLyBwYXNzIGluIHR5cGUgb2Ygc291cmNlL3RhcmdldFxuICAgICAgLmZpbHRlcihgcGF0aC4ke3R5cGV9LSR7aW5kZXh9YClcbiAgICAgIC5hdHRyKCdzdHJva2Utb3BhY2l0eScsIDAuNilcbiAgfVxufVxuLy8gcmVuZGVyIGNoYXJ0XG5zYW5rZXkoJy5jaGFydCcsIHNhbXBsZURhdGEpXG5cblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gc2NyaXB0LmpzIl0sIm1hcHBpbmdzIjoiOzs7O0FBQUE7QUFDQTs7O0FBQUE7QUFDQTs7O0FBQUE7QUFDQTs7O0FBQUE7QUFDQTs7O0FBQUE7QUFDQTs7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFMQTtBQURBO0FBQUE7QUFBQTtBQUFBO0FBQ0E7QUFTQTtBQUNBO0FBQ0E7QUFFQTtBQUVBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFNQTtBQUlBO0FBQ0E7QUFDQTtBQUNBO0FBUUE7QUFDQTtBQUNBO0FBRUE7QUFBQTtBQUNBO0FBQUE7QUFDQTtBQUNBO0FBQ0E7QUFPQTtBQUNBO0FBQUE7QUFDQTtBQUFBO0FBQ0E7QUFBQTtBQUNBO0FBQUE7QUFDQTtBQUFBO0FBQ0E7QUFOQTtBQU9BO0FBQUE7QUFLQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFGQTtBQUlBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUZBO0FBSUE7QUFDQTtBQUVBO0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFBQTtBQUNBO0FBQUE7QUFJQTtBQUFBO0FBQ0E7QUFBQTtBQUNBO0FBQUE7QUFDQTtBQUVBO0FBQ0E7QUFFQTtBQUZBO0FBS0E7QUFDQTtBQUNBO0FBQ0EiLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///5\n")}]);
<!DOCTYPE html>
<title>sankey</title>
<link href="https://unpkg.com/[email protected]/css/basscss.min.css" rel="stylesheet">
<link href='dist.css' rel='stylesheet' />
<body>
<div class="chart"></div>
<script src='https://d3js.org/d3.v4.min.js'></script>
<script src="https://unpkg.com/[email protected]"></script>
<script src='dist.js'></script>
<script>
// change frame height for bl.ocks
d3.select(self.frameElement).style('height', '550px')
</script>
</body>
// create list items for data paths
function listInfo (selection, data, config) {
config = {
type: 'source',
colorScale: null,
...config
}
const {type, colorScale} = config
const join = selection.select('.js-info-ul')
.selectAll('li')
.data(data)
join.enter().append('li')
.attr('class', 'mb1')
.merge(join)
.html(d => {
const index = d.source.node
if (type === 'source') {
return `${createColorBlock(index)} ${d.target.label}: <span class='bold'>${d.value}</span>`
} else {
return `${createColorBlock(index)} ${d.source.label}: <span class='bold'>${d.value}</span>`
}
})
join.exit().remove()
function createColorBlock (index) {
return (`
<span
class='inline-block li-${index}'
style='background: ${colorScale(index)}; width: 8px; opacity:0.7;'
>
&nbsp;
</span>
`)
}
}
export default listInfo
const sampleData = [
{
label: 'Region-a',
values: [
{label: 'Ash', value: 4},
{label: 'Birch', value: 3},
{label: 'Maple', value: 1}
]
},
{
label: 'Region-b',
values: [
{label: 'Ash', value: 3},
{label: 'Birch', value: 1},
{label: 'Maple', value: 1}
]
},
{
label: 'Region-c',
values: [
{label: 'Ash', value: 4},
{label: 'Birch', value: 3}
]
},
{
label: 'Region-d',
values: [
{label: 'Maple', value: 1},
{label: 'Ash', value: null},
{label: 'Birch', value: 2}
]
}
]
export default sampleData
import sampleData from './sampleData'
import nodesAndLinks from './nodesAndLinks'
import listInfo from './listInfo'
import updateTitle from './updateTitle'
import createDOM from './createDOM'
const d3 = window.d3
function sankey (bind, data, config) {
config = {
margin: {top: 20, right: 100, bottom: 20, left: 100},
width: 600,
height: 500,
sourceColors: ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33'],
targetColors: ['#bbb'],
...config
}
const {margin, width, height} = config
const w = width - margin.left - margin.right
const h = height - margin.top - margin.bottom
const sourceColors = d3.scaleOrdinal()
.range(config.sourceColors)
const targetColors = d3.scaleOrdinal()
.range(config.targetColors)
const _data = nodesAndLinks(data)
console.log('data', data)
console.log('_data formatted', _data)
// set up dom
const selection = d3.select(bind)
// destroy/wipe first
d3.select(bind).select('.js-wrap').remove()
createDOM(selection)
// create svg in passed in div
const svg = selection.select('.js-svg')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
const sankey = d3.sankey()
.nodeWidth(40)
.nodePadding(10)
.extent([[1, 1], [w - 1, h - 6]])
sankey(_data)
// console.log('sankey(_data)', sankey(_data))
const link = svg.append('g')
.attr('class', 'links')
.attr('fill', 'none')
.attr('stroke-opacity', 0.2)
.selectAll('path')
.data(_data.links)
.enter().append('path')
.attr('class', d => {
// console.log('d', d)
return `source-${d.source.node} target-${d.target.node}`
})
.attr('d', d3.sankeyLinkHorizontal())
.attr('stroke', d => sourceColors(d.source.node))
.attr('stroke-width', d => Math.max(1, d.width))
const node = svg.append('g')
.attr('class', 'nodes')
.attr('font-family', 'sans-serif')
.attr('font-size', 10)
.selectAll('g')
.data(_data.nodes)
.enter().append('g')
node.append('rect')
.attr('x', d => d.x0)
.attr('y', d => d.y0)
.attr('class', d => d.type + ' bar')
.attr('height', d => d.y1 - d.y0)
.attr('width', d => d.x1 - d.x0)
// use colours depending on source or target
.attr('fill', d => (d.type === 'source')
? sourceColors(d.subIndex)
: targetColors(d.subIndex)
)
.on('click', function (d) {
if (d.type === 'source') {
highlightSourcePaths(svg, d.index, d.type)
updateTitle(selection, d.label, d.value)
listInfo(selection, d.sourceLinks, {
type: 'source',
colorScale: sourceColors
})
} else {
highlightSourcePaths(svg, d.index, d.type)
updateTitle(selection, d.label, d.value)
listInfo(selection, d.targetLinks, {
type: 'target',
colorScale: sourceColors
})
}
})
.on('dblclick', function (d) {
svg.selectAll('path')
.attr('stroke-opacity', 0.2)
})
node.append('text')
.attr('x', d => d.x0 + 50)
.attr('y', d => (d.y1 + d.y0) / 2)
.attr('dy', '0.35em')
.attr('class', 'label')
.attr('text-anchor', 'start')
.text(d => d.label)
.filter(d => d.x0 < width / 2)
.attr('x', d => d.x1 - 50)
.attr('text-anchor', 'end')
function highlightSourcePaths (sel, index, type) {
sel.selectAll('path')
.attr('stroke-opacity', 0.05)
// pass in type of source/target
.filter(`path.${type}-${index}`)
.attr('stroke-opacity', 0.6)
}
}
// render chart
sankey('.chart', sampleData)
*
box-sizing border-box
body
font-family:-apple-system,monospace
color: #454545
width: 800px
margin: 0 auto
.chart
padding-top: 20px
.node rect {
cursor: move;
fill-opacity: .9;
shape-rendering: crispEdges;
}
.node text {
pointer-events: none;
text-shadow: 0 1px 0 #fff;
}
rect.source {
opacity: .8;
cursor: pointer;
}
.label {
font-size: 14px;
fill: #454545
}
.link {
fill: none;
stroke: #000;
stroke-opacity: .2;
}
.link:hover {
stroke-opacity: .5;
}
.js-total {
padding-left: 5px;
color: #888;
}
// output the header
function updateTitle (selection, label, value) {
selection.select('.js-info-title')
.html(`${label} <span class='js-total'>(total: ${value})</span>`)
}
export default updateTitle
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment