source: node_modules/d3-sankey/src/sankey.js@ a762898

Last change on this file since a762898 was e4c61dd, checked in by istevanoska <ilinastevanoska@…>, 6 months ago

Prototype 1.1

  • Property mode set to 100644
File size: 10.3 KB
Line 
1import {max, min, sum} from "d3-array";
2import {justify} from "./align.js";
3import constant from "./constant.js";
4
5function ascendingSourceBreadth(a, b) {
6 return ascendingBreadth(a.source, b.source) || a.index - b.index;
7}
8
9function ascendingTargetBreadth(a, b) {
10 return ascendingBreadth(a.target, b.target) || a.index - b.index;
11}
12
13function ascendingBreadth(a, b) {
14 return a.y0 - b.y0;
15}
16
17function value(d) {
18 return d.value;
19}
20
21function defaultId(d) {
22 return d.index;
23}
24
25function defaultNodes(graph) {
26 return graph.nodes;
27}
28
29function defaultLinks(graph) {
30 return graph.links;
31}
32
33function find(nodeById, id) {
34 const node = nodeById.get(id);
35 if (!node) throw new Error("missing: " + id);
36 return node;
37}
38
39function computeLinkBreadths({nodes}) {
40 for (const node of nodes) {
41 let y0 = node.y0;
42 let y1 = y0;
43 for (const link of node.sourceLinks) {
44 link.y0 = y0 + link.width / 2;
45 y0 += link.width;
46 }
47 for (const link of node.targetLinks) {
48 link.y1 = y1 + link.width / 2;
49 y1 += link.width;
50 }
51 }
52}
53
54export default function Sankey() {
55 let x0 = 0, y0 = 0, x1 = 1, y1 = 1; // extent
56 let dx = 24; // nodeWidth
57 let dy = 8, py; // nodePadding
58 let id = defaultId;
59 let align = justify;
60 let sort;
61 let linkSort;
62 let nodes = defaultNodes;
63 let links = defaultLinks;
64 let iterations = 6;
65
66 function sankey() {
67 const graph = {nodes: nodes.apply(null, arguments), links: links.apply(null, arguments)};
68 computeNodeLinks(graph);
69 computeNodeValues(graph);
70 computeNodeDepths(graph);
71 computeNodeHeights(graph);
72 computeNodeBreadths(graph);
73 computeLinkBreadths(graph);
74 return graph;
75 }
76
77 sankey.update = function(graph) {
78 computeLinkBreadths(graph);
79 return graph;
80 };
81
82 sankey.nodeId = function(_) {
83 return arguments.length ? (id = typeof _ === "function" ? _ : constant(_), sankey) : id;
84 };
85
86 sankey.nodeAlign = function(_) {
87 return arguments.length ? (align = typeof _ === "function" ? _ : constant(_), sankey) : align;
88 };
89
90 sankey.nodeSort = function(_) {
91 return arguments.length ? (sort = _, sankey) : sort;
92 };
93
94 sankey.nodeWidth = function(_) {
95 return arguments.length ? (dx = +_, sankey) : dx;
96 };
97
98 sankey.nodePadding = function(_) {
99 return arguments.length ? (dy = py = +_, sankey) : dy;
100 };
101
102 sankey.nodes = function(_) {
103 return arguments.length ? (nodes = typeof _ === "function" ? _ : constant(_), sankey) : nodes;
104 };
105
106 sankey.links = function(_) {
107 return arguments.length ? (links = typeof _ === "function" ? _ : constant(_), sankey) : links;
108 };
109
110 sankey.linkSort = function(_) {
111 return arguments.length ? (linkSort = _, sankey) : linkSort;
112 };
113
114 sankey.size = function(_) {
115 return arguments.length ? (x0 = y0 = 0, x1 = +_[0], y1 = +_[1], sankey) : [x1 - x0, y1 - y0];
116 };
117
118 sankey.extent = function(_) {
119 return arguments.length ? (x0 = +_[0][0], x1 = +_[1][0], y0 = +_[0][1], y1 = +_[1][1], sankey) : [[x0, y0], [x1, y1]];
120 };
121
122 sankey.iterations = function(_) {
123 return arguments.length ? (iterations = +_, sankey) : iterations;
124 };
125
126 function computeNodeLinks({nodes, links}) {
127 for (const [i, node] of nodes.entries()) {
128 node.index = i;
129 node.sourceLinks = [];
130 node.targetLinks = [];
131 }
132 const nodeById = new Map(nodes.map((d, i) => [id(d, i, nodes), d]));
133 for (const [i, link] of links.entries()) {
134 link.index = i;
135 let {source, target} = link;
136 if (typeof source !== "object") source = link.source = find(nodeById, source);
137 if (typeof target !== "object") target = link.target = find(nodeById, target);
138 source.sourceLinks.push(link);
139 target.targetLinks.push(link);
140 }
141 if (linkSort != null) {
142 for (const {sourceLinks, targetLinks} of nodes) {
143 sourceLinks.sort(linkSort);
144 targetLinks.sort(linkSort);
145 }
146 }
147 }
148
149 function computeNodeValues({nodes}) {
150 for (const node of nodes) {
151 node.value = node.fixedValue === undefined
152 ? Math.max(sum(node.sourceLinks, value), sum(node.targetLinks, value))
153 : node.fixedValue;
154 }
155 }
156
157 function computeNodeDepths({nodes}) {
158 const n = nodes.length;
159 let current = new Set(nodes);
160 let next = new Set;
161 let x = 0;
162 while (current.size) {
163 for (const node of current) {
164 node.depth = x;
165 for (const {target} of node.sourceLinks) {
166 next.add(target);
167 }
168 }
169 if (++x > n) throw new Error("circular link");
170 current = next;
171 next = new Set;
172 }
173 }
174
175 function computeNodeHeights({nodes}) {
176 const n = nodes.length;
177 let current = new Set(nodes);
178 let next = new Set;
179 let x = 0;
180 while (current.size) {
181 for (const node of current) {
182 node.height = x;
183 for (const {source} of node.targetLinks) {
184 next.add(source);
185 }
186 }
187 if (++x > n) throw new Error("circular link");
188 current = next;
189 next = new Set;
190 }
191 }
192
193 function computeNodeLayers({nodes}) {
194 const x = max(nodes, d => d.depth) + 1;
195 const kx = (x1 - x0 - dx) / (x - 1);
196 const columns = new Array(x);
197 for (const node of nodes) {
198 const i = Math.max(0, Math.min(x - 1, Math.floor(align.call(null, node, x))));
199 node.layer = i;
200 node.x0 = x0 + i * kx;
201 node.x1 = node.x0 + dx;
202 if (columns[i]) columns[i].push(node);
203 else columns[i] = [node];
204 }
205 if (sort) for (const column of columns) {
206 column.sort(sort);
207 }
208 return columns;
209 }
210
211 function initializeNodeBreadths(columns) {
212 const ky = min(columns, c => (y1 - y0 - (c.length - 1) * py) / sum(c, value));
213 for (const nodes of columns) {
214 let y = y0;
215 for (const node of nodes) {
216 node.y0 = y;
217 node.y1 = y + node.value * ky;
218 y = node.y1 + py;
219 for (const link of node.sourceLinks) {
220 link.width = link.value * ky;
221 }
222 }
223 y = (y1 - y + py) / (nodes.length + 1);
224 for (let i = 0; i < nodes.length; ++i) {
225 const node = nodes[i];
226 node.y0 += y * (i + 1);
227 node.y1 += y * (i + 1);
228 }
229 reorderLinks(nodes);
230 }
231 }
232
233 function computeNodeBreadths(graph) {
234 const columns = computeNodeLayers(graph);
235 py = Math.min(dy, (y1 - y0) / (max(columns, c => c.length) - 1));
236 initializeNodeBreadths(columns);
237 for (let i = 0; i < iterations; ++i) {
238 const alpha = Math.pow(0.99, i);
239 const beta = Math.max(1 - alpha, (i + 1) / iterations);
240 relaxRightToLeft(columns, alpha, beta);
241 relaxLeftToRight(columns, alpha, beta);
242 }
243 }
244
245 // Reposition each node based on its incoming (target) links.
246 function relaxLeftToRight(columns, alpha, beta) {
247 for (let i = 1, n = columns.length; i < n; ++i) {
248 const column = columns[i];
249 for (const target of column) {
250 let y = 0;
251 let w = 0;
252 for (const {source, value} of target.targetLinks) {
253 let v = value * (target.layer - source.layer);
254 y += targetTop(source, target) * v;
255 w += v;
256 }
257 if (!(w > 0)) continue;
258 let dy = (y / w - target.y0) * alpha;
259 target.y0 += dy;
260 target.y1 += dy;
261 reorderNodeLinks(target);
262 }
263 if (sort === undefined) column.sort(ascendingBreadth);
264 resolveCollisions(column, beta);
265 }
266 }
267
268 // Reposition each node based on its outgoing (source) links.
269 function relaxRightToLeft(columns, alpha, beta) {
270 for (let n = columns.length, i = n - 2; i >= 0; --i) {
271 const column = columns[i];
272 for (const source of column) {
273 let y = 0;
274 let w = 0;
275 for (const {target, value} of source.sourceLinks) {
276 let v = value * (target.layer - source.layer);
277 y += sourceTop(source, target) * v;
278 w += v;
279 }
280 if (!(w > 0)) continue;
281 let dy = (y / w - source.y0) * alpha;
282 source.y0 += dy;
283 source.y1 += dy;
284 reorderNodeLinks(source);
285 }
286 if (sort === undefined) column.sort(ascendingBreadth);
287 resolveCollisions(column, beta);
288 }
289 }
290
291 function resolveCollisions(nodes, alpha) {
292 const i = nodes.length >> 1;
293 const subject = nodes[i];
294 resolveCollisionsBottomToTop(nodes, subject.y0 - py, i - 1, alpha);
295 resolveCollisionsTopToBottom(nodes, subject.y1 + py, i + 1, alpha);
296 resolveCollisionsBottomToTop(nodes, y1, nodes.length - 1, alpha);
297 resolveCollisionsTopToBottom(nodes, y0, 0, alpha);
298 }
299
300 // Push any overlapping nodes down.
301 function resolveCollisionsTopToBottom(nodes, y, i, alpha) {
302 for (; i < nodes.length; ++i) {
303 const node = nodes[i];
304 const dy = (y - node.y0) * alpha;
305 if (dy > 1e-6) node.y0 += dy, node.y1 += dy;
306 y = node.y1 + py;
307 }
308 }
309
310 // Push any overlapping nodes up.
311 function resolveCollisionsBottomToTop(nodes, y, i, alpha) {
312 for (; i >= 0; --i) {
313 const node = nodes[i];
314 const dy = (node.y1 - y) * alpha;
315 if (dy > 1e-6) node.y0 -= dy, node.y1 -= dy;
316 y = node.y0 - py;
317 }
318 }
319
320 function reorderNodeLinks({sourceLinks, targetLinks}) {
321 if (linkSort === undefined) {
322 for (const {source: {sourceLinks}} of targetLinks) {
323 sourceLinks.sort(ascendingTargetBreadth);
324 }
325 for (const {target: {targetLinks}} of sourceLinks) {
326 targetLinks.sort(ascendingSourceBreadth);
327 }
328 }
329 }
330
331 function reorderLinks(nodes) {
332 if (linkSort === undefined) {
333 for (const {sourceLinks, targetLinks} of nodes) {
334 sourceLinks.sort(ascendingTargetBreadth);
335 targetLinks.sort(ascendingSourceBreadth);
336 }
337 }
338 }
339
340 // Returns the target.y0 that would produce an ideal link from source to target.
341 function targetTop(source, target) {
342 let y = source.y0 - (source.sourceLinks.length - 1) * py / 2;
343 for (const {target: node, width} of source.sourceLinks) {
344 if (node === target) break;
345 y += width + py;
346 }
347 for (const {source: node, width} of target.targetLinks) {
348 if (node === source) break;
349 y -= width;
350 }
351 return y;
352 }
353
354 // Returns the source.y0 that would produce an ideal link from source to target.
355 function sourceTop(source, target) {
356 let y = target.y0 - (target.targetLinks.length - 1) * py / 2;
357 for (const {source: node, width} of target.targetLinks) {
358 if (node === source) break;
359 y += width + py;
360 }
361 for (const {target: node, width} of source.sourceLinks) {
362 if (node === target) break;
363 y -= width;
364 }
365 return y;
366 }
367
368 return sankey;
369}
Note: See TracBrowser for help on using the repository browser.