Sankey.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. function _extends() { _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; }; return _extends.apply(this, arguments); }
  2. /**
  3. * Licensed to the Apache Software Foundation (ASF) under one
  4. * or more contributor license agreements. See the NOTICE file
  5. * distributed with this work for additional information
  6. * regarding copyright ownership. The ASF licenses this file
  7. * to you under the Apache License, Version 2.0 (the
  8. * "License"); you may not use this file except in compliance
  9. * with the License. You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing,
  14. * software distributed under the License is distributed on an
  15. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  16. * KIND, either express or implied. See the License for the
  17. * specific language governing permissions and limitations
  18. * under the License.
  19. */
  20. /* eslint-disable no-param-reassign */
  21. /* eslint-disable react/sort-prop-types */
  22. import d3 from 'd3';
  23. import PropTypes from 'prop-types';
  24. import { sankey as d3Sankey } from 'd3-sankey';
  25. import { CategoricalColorNamespace } from '@superset-ui/color';
  26. import { getNumberFormatter, NumberFormats } from '@superset-ui/number-format';
  27. import './Sankey.css';
  28. const propTypes = {
  29. data: PropTypes.arrayOf(PropTypes.shape({
  30. source: PropTypes.string,
  31. target: PropTypes.string,
  32. value: PropTypes.number
  33. })),
  34. width: PropTypes.number,
  35. height: PropTypes.number,
  36. colorScheme: PropTypes.string
  37. };
  38. const formatNumber = getNumberFormatter(NumberFormats.FLOAT);
  39. function Sankey(element, props) {
  40. const {
  41. data,
  42. width,
  43. height,
  44. colorScheme
  45. } = props;
  46. const div = d3.select(element);
  47. div.classed('superset-legacy-chart-sankey', true);
  48. const margin = {
  49. top: 5,
  50. right: 5,
  51. bottom: 5,
  52. left: 5
  53. };
  54. const innerWidth = width - margin.left - margin.right;
  55. const innerHeight = height - margin.top - margin.bottom;
  56. div.selectAll('*').remove();
  57. const svg = div.append('svg').attr('width', innerWidth + margin.left + margin.right).attr('height', innerHeight + margin.top + margin.bottom).append('g').attr('transform', "translate(" + margin.left + "," + margin.top + ")");
  58. const tooltip = div.append('div').attr('class', 'sankey-tooltip').style('opacity', 0);
  59. const colorFn = CategoricalColorNamespace.getScale(colorScheme);
  60. const sankey = d3Sankey().nodeWidth(15).nodePadding(10).size([innerWidth, innerHeight]);
  61. const path = sankey.link();
  62. let nodes = {}; // Compute the distinct nodes from the links.
  63. const links = data.map(row => {
  64. const link = _extends({}, row);
  65. link.source = nodes[link.source] || (nodes[link.source] = {
  66. name: link.source
  67. });
  68. link.target = nodes[link.target] || (nodes[link.target] = {
  69. name: link.target
  70. });
  71. link.value = Number(link.value);
  72. return link;
  73. });
  74. nodes = d3.values(nodes);
  75. sankey.nodes(nodes).links(links).layout(32);
  76. function getTooltipHtml(d) {
  77. let html;
  78. if (d.sourceLinks) {
  79. // is node
  80. html = d.name + " Value: <span class='emph'>" + formatNumber(d.value) + "</span>";
  81. } else {
  82. const val = formatNumber(d.value);
  83. const sourcePercent = d3.round(d.value / d.source.value * 100, 1);
  84. const targetPercent = d3.round(d.value / d.target.value * 100, 1);
  85. html = ["<div class=''>Path Value: <span class='emph'>", val, '</span></div>', "<div class='percents'>", "<span class='emph'>", Number.isFinite(sourcePercent) ? sourcePercent : '100', '%</span> of ', d.source.name, '<br/>', "<span class='emph'>" + (Number.isFinite(targetPercent) ? targetPercent : '--') + "%</span> of ", d.target.name, '</div>'].join('');
  86. }
  87. return html;
  88. }
  89. function onmouseover(d) {
  90. tooltip.html(() => getTooltipHtml(d)).transition().duration(200).style('left', d3.event.offsetX + 10 + "px").style('top', d3.event.offsetY + 10 + "px").style('opacity', 0.95);
  91. }
  92. function onmouseout() {
  93. tooltip.transition().duration(100).style('opacity', 0);
  94. }
  95. const link = svg.append('g').selectAll('.link').data(links).enter().append('path').attr('class', 'link').attr('d', path).style('stroke-width', d => Math.max(1, d.dy)).sort((a, b) => b.dy - a.dy).on('mouseover', onmouseover).on('mouseout', onmouseout);
  96. function dragmove(d) {
  97. d3.select(this).attr('transform', "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")");
  98. sankey.relayout();
  99. link.attr('d', path);
  100. }
  101. const node = svg.append('g').selectAll('.node').data(nodes).enter().append('g').attr('class', 'node').attr('transform', d => "translate(" + d.x + "," + d.y + ")").call(d3.behavior.drag().origin(d => d).on('dragstart', function dragStart() {
  102. this.parentNode.append(this);
  103. }).on('drag', dragmove));
  104. const minRectHeight = 5;
  105. node.append('rect').attr('height', d => d.dy > minRectHeight ? d.dy : minRectHeight).attr('width', sankey.nodeWidth()).style('fill', d => {
  106. const name = d.name || 'N/A';
  107. d.color = colorFn(name.replace(/ .*/, ''));
  108. return d.color;
  109. }).style('stroke', d => d3.rgb(d.color).darker(2)).on('mouseover', onmouseover).on('mouseout', onmouseout);
  110. node.append('text').attr('x', -6).attr('y', d => d.dy / 2).attr('dy', '.35em').attr('text-anchor', 'end').attr('transform', null).text(d => d.name).filter(d => d.x < innerWidth / 2).attr('x', 6 + sankey.nodeWidth()).attr('text-anchor', 'start');
  111. }
  112. Sankey.displayName = 'Sankey';
  113. Sankey.propTypes = propTypes;
  114. export default Sankey;