ForceDirected.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. /**
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. /* eslint-disable react/sort-prop-types, func-names, no-param-reassign */
  20. import d3 from 'd3';
  21. import PropTypes from 'prop-types';
  22. import './ForceDirected.css';
  23. const propTypes = {
  24. data: PropTypes.arrayOf(PropTypes.shape({
  25. source: PropTypes.string,
  26. target: PropTypes.string,
  27. value: PropTypes.number
  28. })),
  29. width: PropTypes.number,
  30. height: PropTypes.number,
  31. linkLength: PropTypes.number,
  32. charge: PropTypes.number
  33. };
  34. /* Modified from http://bl.ocks.org/d3noob/5141278 */
  35. function ForceDirected(element, props) {
  36. const {
  37. data,
  38. width,
  39. height,
  40. linkLength = 200,
  41. charge = -500
  42. } = props;
  43. const div = d3.select(element);
  44. div.classed('superset-legacy-chart-force-directed', true);
  45. const links = data;
  46. const nodes = {}; // Compute the distinct nodes from the links.
  47. links.forEach(link => {
  48. link.source = nodes[link.source] || (nodes[link.source] = {
  49. name: link.source
  50. });
  51. link.target = nodes[link.target] || (nodes[link.target] = {
  52. name: link.target
  53. });
  54. link.value = Number(link.value);
  55. const targetName = link.target.name;
  56. const sourceName = link.source.name;
  57. if (nodes[targetName].total === undefined) {
  58. nodes[targetName].total = link.value;
  59. }
  60. if (nodes[sourceName].total === undefined) {
  61. nodes[sourceName].total = 0;
  62. }
  63. if (nodes[targetName].max === undefined) {
  64. nodes[targetName].max = 0;
  65. }
  66. if (link.value > nodes[targetName].max) {
  67. nodes[targetName].max = link.value;
  68. }
  69. if (nodes[targetName].min === undefined) {
  70. nodes[targetName].min = 0;
  71. }
  72. if (link.value > nodes[targetName].min) {
  73. nodes[targetName].min = link.value;
  74. }
  75. nodes[targetName].total += link.value;
  76. });
  77. /* eslint-disable no-use-before-define */
  78. // add the curvy lines
  79. function tick() {
  80. path.attr('d', d => {
  81. const dx = d.target.x - d.source.x;
  82. const dy = d.target.y - d.source.y;
  83. const dr = Math.sqrt(dx * dx + dy * dy);
  84. return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
  85. });
  86. node.attr('transform', d => "translate(" + d.x + "," + d.y + ")");
  87. }
  88. /* eslint-enable no-use-before-define */
  89. const force = d3.layout.force().nodes(d3.values(nodes)).links(links).size([width, height]).linkDistance(linkLength).charge(charge).on('tick', tick).start();
  90. div.selectAll('*').remove();
  91. const svg = div.append('svg').attr('width', width).attr('height', height); // build the arrow.
  92. svg.append('svg:defs').selectAll('marker').data(['end']) // Different link/path types can be defined here
  93. .enter().append('svg:marker') // This section adds in the arrows
  94. .attr('id', String).attr('viewBox', '0 -5 10 10').attr('refX', 15).attr('refY', -1.5).attr('markerWidth', 6).attr('markerHeight', 6).attr('orient', 'auto').append('svg:path').attr('d', 'M0,-5L10,0L0,5');
  95. const edgeScale = d3.scale.linear().range([0.1, 0.5]); // add the links and the arrows
  96. const path = svg.append('svg:g').selectAll('path').data(force.links()).enter().append('svg:path').attr('class', 'link').style('opacity', d => edgeScale(d.value / d.target.max)).attr('marker-end', 'url(#end)'); // define the nodes
  97. const node = svg.selectAll('.node').data(force.nodes()).enter().append('g').attr('class', 'node').on('mouseenter', function () {
  98. d3.select(this).select('circle').transition().style('stroke-width', 5);
  99. d3.select(this).select('text').transition().style('font-size', 25);
  100. }).on('mouseleave', function () {
  101. d3.select(this).select('circle').transition().style('stroke-width', 1.5);
  102. d3.select(this).select('text').transition().style('font-size', 12);
  103. }).call(force.drag); // add the nodes
  104. const ext = d3.extent(d3.values(nodes), d => Math.sqrt(d.total));
  105. const circleScale = d3.scale.linear().domain(ext).range([3, 30]);
  106. node.append('circle').attr('r', d => circleScale(Math.sqrt(d.total))); // add the text
  107. node.append('text').attr('x', 6).attr('dy', '.35em').text(d => d.name);
  108. }
  109. ForceDirected.displayName = 'ForceDirected';
  110. ForceDirected.propTypes = propTypes;
  111. export default ForceDirected;