arc.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import { group, sum } from 'd3-array';
  2. import { error } from '../../../utils/helper';
  3. import * as SortMethods from './sort';
  4. const DEFAULT_OPTIONS = {
  5. y: 0,
  6. thickness: 0.05,
  7. weight: false,
  8. marginRatio: 0.1,
  9. id: (node) => node.id,
  10. source: (edge) => edge.source,
  11. target: (edge) => edge.target,
  12. sourceWeight: (edge) => edge.value || 1,
  13. targetWeight: (edge) => edge.value || 1,
  14. sortBy: null,
  15. };
  16. /**
  17. * Layout for Arc / Chord diagram with d3 style.
  18. */
  19. export function Arc(options) {
  20. const { y, thickness, weight, marginRatio, id, source, target, sourceWeight, targetWeight, sortBy, } = Object.assign(Object.assign({}, DEFAULT_OPTIONS), options);
  21. function arc(data) {
  22. // Clone first.
  23. const nodes = data.nodes.map((n) => (Object.assign({}, n)));
  24. const edges = data.edges.map((n) => (Object.assign({}, n)));
  25. // Keep reference in below functions.
  26. preprocess(nodes, edges);
  27. sortNodes(nodes, edges);
  28. layoutNodes(nodes, edges);
  29. layoutEdges(nodes, edges);
  30. return { nodes, edges };
  31. }
  32. /**
  33. * Calculate id, value, frequency for node, and source,target for edge.
  34. */
  35. function preprocess(nodes, edges) {
  36. edges.forEach((edge) => {
  37. edge.source = source(edge);
  38. edge.target = target(edge);
  39. edge.sourceWeight = sourceWeight(edge);
  40. edge.targetWeight = targetWeight(edge);
  41. });
  42. // Group edges by source, target.
  43. const edgesBySource = group(edges, (e) => e.source);
  44. const edgesByTarget = group(edges, (e) => e.target);
  45. nodes.forEach((node) => {
  46. node.id = id(node);
  47. const sources = edgesBySource.has(node.id)
  48. ? edgesBySource.get(node.id)
  49. : [];
  50. const targets = edgesByTarget.has(node.id)
  51. ? edgesByTarget.get(node.id)
  52. : [];
  53. node.frequency = sources.length + targets.length;
  54. node.value =
  55. sum(sources, (d) => d.sourceWeight) +
  56. sum(targets, (d) => d.targetWeight);
  57. });
  58. return { nodes, edges };
  59. }
  60. function sortNodes(nodes, edges) {
  61. const method = typeof sortBy === 'function' ? sortBy : SortMethods[sortBy];
  62. if (method) {
  63. nodes.sort(method);
  64. }
  65. }
  66. function layoutNodes(nodes, edges) {
  67. const size = nodes.length;
  68. if (!size) {
  69. throw error("Invalid nodes: it's empty!");
  70. }
  71. // No weight.
  72. if (!weight) {
  73. const deltaX = 1 / size;
  74. nodes.forEach((node, i) => {
  75. node.x = (i + 0.5) * deltaX;
  76. node.y = y;
  77. });
  78. return { nodes, edges };
  79. }
  80. // todo: marginRatio should be in [0, 1)
  81. // todo: thickness shoule be in (0, 1)
  82. const margin = marginRatio / (2 * size);
  83. const total = nodes.reduce((prev, node) => (prev += node.value), 0);
  84. nodes.reduce((deltaX, node) => {
  85. node.weight = node.value / total;
  86. node.width = node.weight * (1 - marginRatio);
  87. node.height = thickness;
  88. /* points
  89. * 3---2
  90. * | |
  91. * 0---1
  92. */
  93. const minX = margin + deltaX;
  94. const maxX = minX + node.width;
  95. const minY = y - thickness / 2;
  96. const maxY = minY + thickness;
  97. node.x = [minX, maxX, maxX, minX];
  98. node.y = [minY, minY, maxY, maxY];
  99. // Return next deltaX.
  100. return deltaX + node.width + 2 * margin;
  101. }, 0);
  102. return {
  103. nodes,
  104. edges,
  105. };
  106. }
  107. /**
  108. * Get edge layout information from nodes, and save into edge object.
  109. */
  110. function layoutEdges(nodes, edges) {
  111. const nodesMap = new Map(nodes.map((d) => [d.id, d]));
  112. if (!weight) {
  113. edges.forEach((edge) => {
  114. const sourceId = source(edge);
  115. const targetId = target(edge);
  116. const sourceNode = nodesMap.get(sourceId);
  117. const targetNode = nodesMap.get(targetId);
  118. // Edge's layout information is Equal with node.
  119. if (sourceNode && targetNode) {
  120. edge.x = [sourceNode.x, targetNode.x];
  121. edge.y = [sourceNode.y, targetNode.y];
  122. }
  123. });
  124. return { nodes, edges };
  125. }
  126. // Initial edge.x, edge.y.
  127. edges.forEach((edge) => {
  128. edge.x = [0, 0, 0, 0];
  129. edge.y = [y, y, y, y];
  130. });
  131. // Group edges by source, target.
  132. const edgesBySource = group(edges, (e) => e.source);
  133. const edgesByTarget = group(edges, (e) => e.target);
  134. // When weight = true, we need to calculation the bbox of edge start/end.
  135. nodes.forEach((node) => {
  136. const { edges, width, x, y, value, id } = node;
  137. const sourceEdges = edgesBySource.get(id) || [];
  138. const targetEdges = edgesByTarget.get(id) || [];
  139. let offset = 0;
  140. /* points
  141. * 0----------2
  142. * | |
  143. * 1----------3
  144. */
  145. sourceEdges.map((edge) => {
  146. const w = (edge.sourceWeight / value) * width;
  147. edge.x[0] = node.x[0] + offset;
  148. edge.x[1] = node.x[0] + offset + w;
  149. offset += w;
  150. });
  151. targetEdges.forEach((edge) => {
  152. const w = (edge.targetWeight / value) * width;
  153. edge.x[3] = node.x[0] + offset;
  154. edge.x[2] = node.x[0] + offset + w;
  155. offset += w;
  156. });
  157. });
  158. }
  159. return arc;
  160. }
  161. //# sourceMappingURL=arc.js.map