@s-gw/s-gw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.codex-plugin/plugin.json +35 -0
  2. package/.mcp.json +16 -0
  3. package/LICENSE +201 -0
  4. package/NOTICE +7 -0
  5. package/README.md +197 -0
  6. package/TRADEMARKS.md +9 -0
  7. package/assets/icons/aws-ec2.png +0 -0
  8. package/assets/icons/lucide/bot.svg +8 -0
  9. package/assets/icons/lucide/monitor.svg +5 -0
  10. package/assets/icons/lucide/server.svg +6 -0
  11. package/assets/icons/lucide/terminal.svg +4 -0
  12. package/assets/icons/s-gw-128.png +0 -0
  13. package/assets/icons/s-gw-16.png +0 -0
  14. package/assets/icons/s-gw-180.png +0 -0
  15. package/assets/icons/s-gw-192.png +0 -0
  16. package/assets/icons/s-gw-32.png +0 -0
  17. package/assets/icons/s-gw-64.png +0 -0
  18. package/assets/icons/s-gw-menu-bar-template.png +0 -0
  19. package/dist/agent-context.d.ts +17 -0
  20. package/dist/agent-context.js +207 -0
  21. package/dist/agents.d.ts +64 -0
  22. package/dist/agents.js +763 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +1385 -0
  25. package/dist/command-suggest.d.ts +3 -0
  26. package/dist/command-suggest.js +131 -0
  27. package/dist/console-server.d.ts +16 -0
  28. package/dist/console-server.js +978 -0
  29. package/dist/console-ui/assets/codex-DYTPdPxi.png +0 -0
  30. package/dist/console-ui/assets/cursor-CBrUTJD-.png +0 -0
  31. package/dist/console-ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  32. package/dist/console-ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  33. package/dist/console-ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  34. package/dist/console-ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  35. package/dist/console-ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  36. package/dist/console-ui/assets/hermes-B8hNbJPm.png +0 -0
  37. package/dist/console-ui/assets/index-BxUf0Sye.js +96 -0
  38. package/dist/console-ui/assets/index-CmTiBR_w.css +2 -0
  39. package/dist/console-ui/assets/omnigent-Cxa4p2Mq.png +0 -0
  40. package/dist/console-ui/assets/openclaw-C5wL4ZVW.png +0 -0
  41. package/dist/console-ui/assets/opencode-D_wFATSC.png +0 -0
  42. package/dist/console-ui/assets/openhands-DnrlGgev.svg +9 -0
  43. package/dist/console-ui/assets/s-gw-64-ByMUGQ3K.png +0 -0
  44. package/dist/console-ui/assets/vscode-Bdtr9eyf.png +0 -0
  45. package/dist/console-ui/assets/zeptoclaw-DztQW8Sw.png +0 -0
  46. package/dist/console-ui/index.html +13 -0
  47. package/dist/crypto.d.ts +6 -0
  48. package/dist/crypto.js +53 -0
  49. package/dist/executor.d.ts +7 -0
  50. package/dist/executor.js +297 -0
  51. package/dist/gateway.d.ts +31 -0
  52. package/dist/gateway.js +114 -0
  53. package/dist/guard.d.ts +61 -0
  54. package/dist/guard.js +247 -0
  55. package/dist/install.d.ts +146 -0
  56. package/dist/install.js +629 -0
  57. package/dist/mcp-server.d.ts +2 -0
  58. package/dist/mcp-server.js +119 -0
  59. package/dist/native/s-gw-core +0 -0
  60. package/dist/native/s-gw-keychain-helper +0 -0
  61. package/dist/onepassword.d.ts +48 -0
  62. package/dist/onepassword.js +412 -0
  63. package/dist/paths.d.ts +4 -0
  64. package/dist/paths.js +22 -0
  65. package/dist/s-gw Menu Bar.app/Contents/Info.plist +28 -0
  66. package/dist/s-gw Menu Bar.app/Contents/MacOS/s-gw-menu-bar-helper +0 -0
  67. package/dist/s-gw Menu Bar.app/Contents/Resources/AppIcon.icns +0 -0
  68. package/dist/s-gw Menu Bar.app/Contents/Resources/AwsEc2.png +0 -0
  69. package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-bot.svg +8 -0
  70. package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-monitor.svg +5 -0
  71. package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-server.svg +6 -0
  72. package/dist/s-gw Menu Bar.app/Contents/Resources/Lucide-terminal.svg +4 -0
  73. package/dist/s-gw Menu Bar.app/Contents/Resources/MenuBarTemplate.png +0 -0
  74. package/dist/s-gw Menu Bar.app/Contents/_CodeSignature/CodeResources +194 -0
  75. package/dist/s-gw.app/Contents/Info.plist +28 -0
  76. package/dist/s-gw.app/Contents/MacOS/s-gw +0 -0
  77. package/dist/s-gw.app/Contents/Resources/AppIcon.icns +0 -0
  78. package/dist/s-gw.app/Contents/Resources/MenuBarTemplate.png +0 -0
  79. package/dist/s-gw.app/Contents/_CodeSignature/CodeResources +139 -0
  80. package/dist/scanner.d.ts +9 -0
  81. package/dist/scanner.js +437 -0
  82. package/dist/ssh.d.ts +31 -0
  83. package/dist/ssh.js +286 -0
  84. package/dist/store.d.ts +131 -0
  85. package/dist/store.js +1611 -0
  86. package/dist/types.d.ts +196 -0
  87. package/dist/types.js +2 -0
  88. package/dist/unlock.d.ts +29 -0
  89. package/dist/unlock.js +274 -0
  90. package/dist/windows/VERSION.txt +1 -0
  91. package/dist/windows/s-gw-client.cmd +4 -0
  92. package/dist/windows/s-gw-client.ps1 +106 -0
  93. package/dist/windows/s-gw-credential.cmd +4 -0
  94. package/dist/windows/s-gw-credential.ps1 +167 -0
  95. package/dist/windows/s-gw-helper.cmd +4 -0
  96. package/dist/windows/s-gw-helper.ps1 +180 -0
  97. package/docs/README.md +23 -0
  98. package/docs/agents.md +160 -0
  99. package/docs/architecture.md +72 -0
  100. package/docs/deployment.md +447 -0
  101. package/docs/detection.md +44 -0
  102. package/docs/images/s-gw-overview.png +0 -0
  103. package/docs/integrations.md +195 -0
  104. package/docs/keychain.md +39 -0
  105. package/docs/onepassword.md +84 -0
  106. package/docs/quickstart.md +104 -0
  107. package/docs/threat-model.md +100 -0
  108. package/docs/ui/THIRD_PARTY_NOTICES.md +111 -0
  109. package/docs/ui/apple-touch-icon.png +0 -0
  110. package/docs/ui/favicon-32.png +0 -0
  111. package/docs/ui/local-console.html +4477 -0
  112. package/docs/ui/vendor/d3-sankey/d3-array.LICENSE.txt +27 -0
  113. package/docs/ui/vendor/d3-sankey/d3-array.min.js +2 -0
  114. package/docs/ui/vendor/d3-sankey/d3-path.LICENSE.txt +27 -0
  115. package/docs/ui/vendor/d3-sankey/d3-path.min.js +2 -0
  116. package/docs/ui/vendor/d3-sankey/d3-sankey.LICENSE.txt +27 -0
  117. package/docs/ui/vendor/d3-sankey/d3-sankey.min.js +2 -0
  118. package/docs/ui/vendor/d3-sankey/d3-shape.LICENSE.txt +27 -0
  119. package/docs/ui/vendor/d3-sankey/d3-shape.min.js +2 -0
  120. package/docs/ui/vendor/sankeymatic/LICENSE.txt +17 -0
  121. package/docs/ui/vendor/sankeymatic/sankey.js +897 -0
  122. package/package.json +117 -0
  123. package/skills/s-gw/SKILL.md +19 -0
@@ -0,0 +1,897 @@
1
+ d3.sankey = () => {
2
+ 'use strict';
3
+
4
+ const sankey = {},
5
+ // Set up some handy constants (acting as enums)
6
+ // These numbers are relatively prime so each cross-product is unique
7
+ // (when we need that)
8
+ [SOURCES, TARGETS, TOP, BOTTOM, NEAREST] = [2, 3, 5, 7, 11];
9
+
10
+ // Set by inputs:
11
+ let nodeWidth = 9,
12
+ nodeHeightFactor = 0.5,
13
+ nodeSpacingFactor = 0.85,
14
+ size = { w: 1, h: 1 },
15
+ nodes = [],
16
+ flows = [],
17
+ rightJustifyEndpoints = false,
18
+ leftJustifyOrigins = false,
19
+ autoLayout = true,
20
+ attachIncompletesTo = NEAREST,
21
+ // Calculated:
22
+ stagesArr = [],
23
+ maximumNodeSpacing = 0,
24
+ actualNodeSpacing = 0,
25
+ maxStage = -1;
26
+
27
+ // ACCESSORS //
28
+ /* eslint-disable func-names */
29
+ sankey.nodeWidth = function (x) {
30
+ if (arguments.length) { nodeWidth = +x; return sankey; }
31
+ return nodeWidth;
32
+ };
33
+
34
+ sankey.nodeHeightFactor = function (x) {
35
+ if (arguments.length) { nodeHeightFactor = +x; return sankey; }
36
+ return nodeHeightFactor;
37
+ };
38
+
39
+ sankey.nodeSpacingFactor = function (x) {
40
+ if (arguments.length) { nodeSpacingFactor = +x; return sankey; }
41
+ return nodeSpacingFactor;
42
+ };
43
+
44
+ sankey.nodes = function (x) {
45
+ if (arguments.length) { nodes = x; return sankey; }
46
+ return nodes;
47
+ };
48
+
49
+ sankey.flows = function (x) {
50
+ if (arguments.length) { flows = x; return sankey; }
51
+ return flows;
52
+ };
53
+
54
+ sankey.size = function (x) {
55
+ if (arguments.length) { size = x; return sankey; }
56
+ return size;
57
+ };
58
+
59
+ sankey.rightJustifyEndpoints = function (x) {
60
+ if (arguments.length) { rightJustifyEndpoints = x; return sankey; }
61
+ return rightJustifyEndpoints;
62
+ };
63
+
64
+ sankey.leftJustifyOrigins = function (x) {
65
+ if (arguments.length) { leftJustifyOrigins = x; return sankey; }
66
+ return leftJustifyOrigins;
67
+ };
68
+
69
+ sankey.autoLayout = function (x) {
70
+ if (arguments.length) { autoLayout = x; return sankey; }
71
+ return autoLayout;
72
+ };
73
+
74
+ sankey.attachIncompletesTo = function (x) {
75
+ if (arguments.length) {
76
+ switch (x.toLowerCase()) {
77
+ case 'leading': attachIncompletesTo = TOP; break;
78
+ case 'trailing': attachIncompletesTo = BOTTOM; break;
79
+ case 'nearest': attachIncompletesTo = NEAREST; break;
80
+ // no default
81
+ }
82
+ return sankey;
83
+ }
84
+ return attachIncompletesTo;
85
+ };
86
+
87
+ // Getters:
88
+ sankey.stages = () => stagesArr;
89
+
90
+ // FUNCTIONS //
91
+
92
+ // valueSum: Add up all the 'value' keys from a list of objects:
93
+ function valueSum(list) { return d3.sum(list, (d) => d.value); }
94
+
95
+ // divide: Substitute MIN_VALUE if a denominator would be 0:
96
+ function divide(a, b) { return a / (b || Number.MIN_VALUE); }
97
+
98
+ // yCenter & yBottom: Y-position of the middle and end of a node.
99
+ function yCenter(n) { return n.y + n.dy / 2; }
100
+ function yBottom(n) { return n.y + n.dy; }
101
+
102
+ // source___/target___: return the ___ of one end of a flow:
103
+ function sourceTop(f) { return f.source.y + f.sy; }
104
+ function targetTop(f) { return f.target.y + f.ty; }
105
+ function sourceCenter(f) { return f.source.y + f.sy + (f.dy / 2); }
106
+ function targetCenter(f) { return f.target.y + f.ty + (f.dy / 2); }
107
+ function sourceBottom(f) { return f.source.y + f.sy + f.dy; }
108
+ function targetBottom(f) { return f.target.y + f.ty + f.dy; }
109
+
110
+ // Get the extreme bounds across a list of Nodes:
111
+ function leastY(nodeList) { return d3.min(nodeList, (n) => n.y); }
112
+ function greatestY(nodeList) { return d3.max(nodeList, (n) => yBottom(n)); }
113
+
114
+ // Sorting functions:
115
+ function bySourceOrder(a, b) { return a.sourceRow - b.sourceRow; }
116
+ function byTopEdges(a, b) { return a.y - b.y; }
117
+
118
+ // connectFlowsToNodes: Populate flows in & out for each node.
119
+ function connectFlowsToNodes() {
120
+ // Initialize the flow buckets:
121
+ nodes.forEach((n) => {
122
+ // Lists of flows which use this node as their target or source:
123
+ n.flows = { [IN]: [], [OUT]: [] };
124
+ // Mark these as real nodes we want to see:
125
+ n.isAShadow = false;
126
+ });
127
+
128
+ // Connect each flow to its two nodes:
129
+ flows.forEach((f) => {
130
+ // When the source or target is a number, that's an index;
131
+ // convert it to the referenced object:
132
+ if (typeof f.source === 'number') { f.source = nodes[f.source]; }
133
+ if (typeof f.target === 'number') { f.target = nodes[f.target]; }
134
+
135
+ // Add this flow to the affected source & target:
136
+ f.source.flows[OUT].push(f);
137
+ f.target.flows[IN].push(f);
138
+ // By default, real flows are used when sorting/placing within a node.
139
+ f.useForVisiblePlacing = true;
140
+ // Mark these as real flows we want to see:
141
+ f.isAShadow = false;
142
+ f.hasAShadow = false;
143
+ });
144
+ }
145
+
146
+ // computeNodeValues: Compute the value of each node by summing the
147
+ // associated flows:
148
+ function computeNodeValues() {
149
+ nodes.forEach((n) => {
150
+ // Remember the totals in & out:
151
+ n.total = { [IN]: valueSum(n.flows[IN]), [OUT]: valueSum(n.flows[OUT]) };
152
+ // Each node's value will be the greater of the two (or else the
153
+ // smallest positive value):
154
+ n.value = Math.max(n.total[IN], n.total[OUT], Number.MIN_VALUE);
155
+ });
156
+ }
157
+
158
+ // allFlowStats(nodeList): provides all components necessary to make
159
+ // weighted-center calculations. These are used to decide where a
160
+ // group of nodes would ideally 'want' to be.
161
+ function allFlowStats(nodeList) {
162
+ // flowSetStats: get the total weight+value from a group of flows
163
+ function flowSetStats(whichFlows) {
164
+ // Get every flow touching one side & treat them as one list:
165
+ const flowList
166
+ = nodeList
167
+ .map((n) => n.flows[whichFlows])
168
+ .flat()
169
+ // Use the weighted value of a flow (this handles shadows):
170
+ .filter((f) => f.weightedValue > 0);
171
+ // If 0 flows, return enough structure to satisfy the caller:
172
+ if (flowList.length === 0) {
173
+ return { value: 0, sources: { weight: 0 }, targets: { weight: 0 } };
174
+ }
175
+
176
+ return {
177
+ value: d3.sum(flowList, (f) => f.weightedValue),
178
+ sources: {
179
+ weight: d3.sum(flowList, (f) => sourceCenter(f) * f.weightedValue),
180
+ maxSourceStage: d3.max(flowList, (f) => f.source.stage),
181
+ },
182
+ targets: {
183
+ weight: d3.sum(flowList, (f) => targetCenter(f) * f.weightedValue),
184
+ minTargetStage: d3.min(flowList, (f) => f.target.stage),
185
+ },
186
+ };
187
+ }
188
+
189
+ // Return the stats for the set of all flows touching these nodes:
190
+ return { [IN]: flowSetStats(IN), [OUT]: flowSetStats(OUT) };
191
+ }
192
+
193
+ // placeFlowsInsideNodes(nodeList):
194
+ // Compute the y-offset of every flow's source and target endpoints,
195
+ // relative to the each node's y-position.
196
+ function placeFlowsInsideNodes(nodeList) {
197
+ // sortFlows(node, placing):
198
+ // Given a node & a side, reorder that group of flows as best we can.
199
+ // 'placing' indicates which end of the flows we're working on here:
200
+ // - TARGETS = we're placing the targets of n.flows[IN]
201
+ // - SOURCES = we're placing the sources of n.flows[OUT]
202
+ function sortFlows(n, placing) {
203
+ const dir = placing === TARGETS ? IN : OUT,
204
+ fStats = allFlowStats([n]),
205
+ [flowsToSort, totalFlowValue] = [n.flows[dir], n.total[dir]],
206
+ totalFlowWeight
207
+ = (dir === IN ? fStats[IN].sources : fStats[OUT].targets).weight,
208
+ // Make a Set of flow IDs we can delete from as we go:
209
+ flowsRemaining = new Set(flowsToSort.map((f) => f.index)),
210
+ // Calculate how tall the flow group is which will attach to this
211
+ // node (may be less than n.dy):
212
+ totalFlowSpan = d3.sum(
213
+ // Only count the space which is needed for visible flows (when
214
+ // the node is real) OR for flows meeting a shadow node:
215
+ flowsToSort.filter((f) => !f.isAShadow || n.isAShadow),
216
+ (f) => f.dy
217
+ ),
218
+ // Attach flows to the *top* of the range, *except* when:
219
+ // the entire node's value is not all flowing somewhere, AND
220
+ // - The caller says to attach them to the bottom, OR
221
+ // - The caller says to use the 'nearest' end AND
222
+ // - the center-of-all-attached-flows is below the node's
223
+ // own center.
224
+ flowPosition
225
+ = totalFlowValue < n.value
226
+ && (attachIncompletesTo === BOTTOM
227
+ || (attachIncompletesTo === NEAREST
228
+ && divide(totalFlowWeight, totalFlowValue) > yCenter(n)))
229
+ ? BOTTOM
230
+ : TOP,
231
+ // upper/lower bounds = the range where flows may attach
232
+ bounds
233
+ = flowPosition === TOP
234
+ ? { upper: n.y, lower: n.y + totalFlowSpan }
235
+ : { upper: yBottom(n) - totalFlowSpan, lower: yBottom(n) };
236
+ // Reminder: In SVG-land, y-axis coordinates are inverted...
237
+ // "upper" & "lower" are meant visually here, not numerically.
238
+
239
+ // placeFlow(f, y): Update a flow's position
240
+ function placeFlow(f, newTopY) {
241
+ // Is the flow actually in the queue? Exit if not. (This can happen
242
+ // when we're placing a shadow flow and offer to update the original
243
+ // flow's Y, but it's in some other stage.)
244
+ if (!flowsRemaining.has(f.index)) { return; }
245
+ // sy & ty (source/target y) are the vertical *offsets* at each end
246
+ // of a flow, determining where below the node's top edge the flow's
247
+ // top will meet.
248
+ if (placing === TARGETS) {
249
+ f.ty = newTopY - f.target.y;
250
+ } else {
251
+ f.sy = newTopY - f.source.y;
252
+ }
253
+ // Drop the flow we just placed from the queue:
254
+ flowsRemaining.delete(f.index);
255
+ }
256
+
257
+ // placeFlowAt(edge, fIndex):
258
+ // Update the bound, set this flow's offset, update the queue.
259
+ function placeFlowAt(edge, fIndex) {
260
+ const f = flows[fIndex];
261
+ let newY = 0;
262
+ if (edge === TOP) {
263
+ newY = bounds.upper;
264
+ // If this is real, move the upper bound DOWN.
265
+ if (f.useForVisiblePlacing || n.isAShadow) { bounds.upper += f.dy; }
266
+ } else { // edge === BOTTOM
267
+ // Make room at the bottom of the range for this flow:
268
+ newY = bounds.lower - f.dy;
269
+ // If this is real, move the lower bound UP to match:
270
+ if (f.useForVisiblePlacing || n.isAShadow) { bounds.lower = newY; }
271
+ }
272
+
273
+ // Put the flow where we just decided & drop it from the queue:
274
+ placeFlow(f, newY);
275
+
276
+ if (f.useForVisiblePlacing && f.isAShadow) {
277
+ // If this flow should be used for placing a real one AND is a
278
+ // shadow flow, then copy its new position to the true flow & drop
279
+ // that other flow from the queue too:
280
+ placeFlow(flows[f.shadowOf], newY);
281
+ }
282
+ }
283
+
284
+ // slopeData keys are the product of an 'edge' & a 'placing' value:
285
+ const slopeData = {
286
+ [TOP * TARGETS]: { f: (f) => (bounds.upper - sourceTop(f)) / f.dx, dir: -1 },
287
+ [TOP * SOURCES]: { f: (f) => (targetTop(f) - bounds.upper) / f.dx, dir: 1 },
288
+ [BOTTOM * TARGETS]: { f: (f) => (bounds.lower - sourceBottom(f)) / f.dx, dir: 1 },
289
+ [BOTTOM * SOURCES]: { f: (f) => (targetBottom(f) - bounds.lower) / f.dx, dir: -1 },
290
+ };
291
+
292
+ // placeUnhappiestFlowAt(edge):
293
+ // Figure out which flow is worst off (slope-wise) and place it.
294
+ // edge = TOP or BOTTOM
295
+ function placeUnhappiestFlowAt(edge) {
296
+ // The queue may have been drained early. Guard against that:
297
+ if (!flowsRemaining.size) { return; }
298
+ const sKey = edge * placing,
299
+ slopeOf = slopeData[sKey].f,
300
+ // flowIndex = the ID of the unhappiest flow
301
+ flowIndex = Array.from(flowsRemaining)
302
+ // Exclude flows with shadows; they'll get their position
303
+ // assigned when their shadow gets placed:
304
+ .filter((i) => !flows[i].hasAShadow)
305
+ .sort((a, b) => (
306
+ // For autolayout, use the right slopes in the correct order (asc/dsc):
307
+ autoLayout
308
+ ? (slopeData[sKey].dir * (slopeOf(flows[a]) - slopeOf(flows[b]))
309
+ // If there is a tie, sort by x-distance (ascending):
310
+ || flows[a].dx - flows[b].dx)
311
+ : 0)
312
+ // If we are using exact order (OR if there is still a tie),
313
+ // sort by sourceRow (which is also set for shadow flows)
314
+ || flows[a].sourceRow - flows[b].sourceRow)[0];
315
+ // If we found a flow, place it at the correct edge:
316
+ if (flowIndex !== undefined) { placeFlowAt(edge, flowIndex); }
317
+ }
318
+
319
+ // Loop through the flow set, placing them from the outside in.
320
+ // If there are at least 2 flows to be placed, we figure out which is
321
+ // best suited to occupy the top & bottom edge spots.
322
+ // After placing those, the remaining range is reduced & we repeat.
323
+ while (flowsRemaining.size > 1) {
324
+ // Place the least fortunate flows, then subtract their size from
325
+ // the available range:
326
+ placeUnhappiestFlowAt(TOP);
327
+ if (autoLayout) { placeUnhappiestFlowAt(BOTTOM); }
328
+ // (If using exact order, we want to place top->bottom, NOT alternate.)
329
+ }
330
+
331
+ // After that loop, we have 0-1 flows. If there is one, place it:
332
+ flowsRemaining.forEach((i) => placeFlowAt(TOP, i));
333
+ }
334
+
335
+ // We have the utility functions defined now; time to actually use them.
336
+
337
+ // First, update the x-distance (dx) values for all flows -- they may
338
+ // have moved since their initial placement, due to drags. Two notes:
339
+ // 1) We use the *absolute* value of the x-distance, so even when a node
340
+ // is dragged to the opposite side of a connected node, the ordering
341
+ // will remain stable.
342
+ // 2) Denominator dx must not be 0, so MIN_VALUE is substituted if needed.
343
+ flows.forEach((f) => {
344
+ f.dx = Math.abs(f.target.x - f.source.x) || Number.MIN_VALUE;
345
+ });
346
+
347
+ // Gather all the distinct batches of flows we'll need to process (each
348
+ // node may have 0-2 batches):
349
+ const flowBatches = [
350
+ ...nodeList.filter((n) => n.flows[IN].length)
351
+ .map((n) => (
352
+ { i: n.index, len: n.flows[IN].length, placing: TARGETS }
353
+ )),
354
+ ...nodeList.filter((n) => n.flows[OUT].length)
355
+ .map((n) => (
356
+ { i: n.index, len: n.flows[OUT].length, placing: SOURCES }
357
+ )),
358
+ ];
359
+
360
+ // Sort the flow batches so that we start with those having the FEWEST
361
+ // flows and work upward.
362
+ // Reason: a 1-flow placement is certain; a 2-flow set is simple; etc.
363
+ // By settling easier cases first, the harder cases end up with fewer
364
+ // wild possibilities for how they may be arranged.
365
+ flowBatches.sort((a, b) => a.len - b.len)
366
+ // Finally: Go through every batch & sort its flows anew:
367
+ .forEach((fBatch) => { sortFlows(nodes[fBatch.i], fBatch.placing); });
368
+ }
369
+
370
+ // assignNodesToStages: Iteratively assign the stage (x-group) for each node.
371
+ // Nodes are assigned the maximum stage of their incoming neighbors + 1,
372
+ // then any nodes which can be nudged forward are.
373
+ function assignNodesToStages() {
374
+ const nodesToCheckAgain = new Set();
375
+ // updateNode: Set a node's stage & make sure its targets get another look.
376
+ function updateNode(n) {
377
+ n.stage = maxStage;
378
+ n.flows[OUT].forEach((f) => { nodesToCheckAgain.add(f.target); });
379
+ }
380
+
381
+ // Work from left to right.
382
+ // Assign every node to stage 0, then keep updating the stage of every node
383
+ // that was a target of a known node. Repeat and fade.
384
+ let nodesToPlace = nodes;
385
+ // The maxStage check is to avoid an infinite loop when there is a cycle:
386
+ while (nodesToPlace.length && maxStage < nodes.length - 1) {
387
+ maxStage += 1;
388
+ nodesToPlace.forEach((n) => updateNode(n));
389
+ nodesToPlace = Array.from(nodesToCheckAgain);
390
+ nodesToCheckAgain.clear();
391
+ }
392
+
393
+ // Pull any source nodes to the right which have room to move.
394
+ // First, get a COPY of the list of all nodes with targets:
395
+ nodes
396
+ .filter((n) => n.flows[OUT].length).slice()
397
+ .sort((a, b) => b.stage - a.stage) // Sort that by stage, descending
398
+ .forEach((n) => {
399
+ // Find n's minimum target stage and use the one right before that:
400
+ const maxNewStage = d3.min(n.flows[OUT], (f) => f.target.stage) - 1;
401
+ if (n.stage < maxNewStage) { n.stage = maxNewStage; }
402
+ });
403
+
404
+ // Handle layout checkboxes:
405
+ function setStageWhenNoFlows(direction, newStage) {
406
+ // For nodes with no flows going {direction}...
407
+ nodes.filter((n) => !n.flows[direction].length)
408
+ // ...set their stages to newStage:
409
+ .forEach((n) => { n.stage = newStage; });
410
+ }
411
+
412
+ // Force origins to appear all the way to the left?
413
+ if (leftJustifyOrigins) { setStageWhenNoFlows(IN, 0); }
414
+
415
+ // Force endpoints all the way to the right?
416
+ if (rightJustifyEndpoints) { setStageWhenNoFlows(OUT, maxStage); }
417
+
418
+ // Now that the main nodes and flows are in place, we also fill in
419
+ // SHADOW nodes & flows to occupy space whenever stages are skipped.
420
+ // To get started, fill in the 'ds' (stage distance) for all flows:
421
+ flows.forEach((f) => { f.ds = f.target.stage - f.source.stage; });
422
+
423
+ // Next, operate on flows which cross more than one stage:
424
+ const shadowNodeNames = new Map();
425
+ flows
426
+ .filter((f) => Math.abs(f.ds) > 1)
427
+ .forEach((f) => {
428
+ const nodesForThisFlow = [f.source];
429
+ // Duplicate the source node as many times as needed (though only
430
+ // as large as this individual flow)
431
+ for (let i = 1; i < f.ds; i += 1) {
432
+ const shadowStage = f.source.stage + i,
433
+ // Create a custom name for the shadow which will still group
434
+ // multiple flows between the same 2 places.
435
+ newNodeName
436
+ = `sh_${f.source.index}_${f.target.index}_s${shadowStage}`,
437
+ fVal = Number(f.value);
438
+ let shadowNode;
439
+ // Have we already made a shadow node for this source/target?
440
+ if (shadowNodeNames.has(newNodeName)) {
441
+ // If so, let's add value to the node we've already made:
442
+ shadowNode = nodes[shadowNodeNames.get(newNodeName)];
443
+ shadowNode.value += fVal;
444
+ shadowNode.total[IN] += fVal;
445
+ shadowNode.total[OUT] += fVal;
446
+ } else {
447
+ // A shadow node doesn't exist, so we make a fresh one with the
448
+ // same sourceRow as the original flow:
449
+ shadowNode = {
450
+ index: nodes.length,
451
+ stage: shadowStage,
452
+ name: newNodeName,
453
+ sourceRow: f.sourceRow,
454
+ isAShadow: true,
455
+ flows: { [IN]: [], [OUT]: [] },
456
+ total: { [IN]: fVal, [OUT]: fVal },
457
+ value: fVal,
458
+ };
459
+ // Add this to the big list and to our shadow-tracking list:
460
+ nodes.push(shadowNode);
461
+ shadowNodeNames.set(newNodeName, shadowNode.index);
462
+ }
463
+ nodesForThisFlow.push(shadowNode);
464
+ }
465
+ nodesForThisFlow.push(f.target);
466
+
467
+ // Now that we have a list of all nodes along the way, add shadow
468
+ // flows between each pair (starting from the 2nd item in the list).
469
+ for (let i = 1; i < nodesForThisFlow.length; i += 1) {
470
+ const sourceNode = nodesForThisFlow[i - 1],
471
+ targetNode = nodesForThisFlow[i],
472
+ origSourceRow = Number(f.sourceRow),
473
+ // Take values from the original flow, then override some:
474
+ newFlow = {
475
+ ...f,
476
+ source: sourceNode,
477
+ target: targetNode,
478
+ index: flows.length,
479
+ shadowOf: f.index,
480
+ isAShadow: true,
481
+ hasAShadow: false,
482
+ // Make artificial sourceRow numbers so these get prioritized
483
+ // *with* the original flow:
484
+ sourceRow: origSourceRow + i / (f.ds + 1),
485
+ // Should we propagate this shadow's y position to the original
486
+ // flow? Only at the ends of the shadow path.
487
+ useForVisiblePlacing:
488
+ sourceNode.stage === f.source.stage
489
+ || targetNode.stage === f.target.stage,
490
+ };
491
+ flows.push(newFlow);
492
+ newFlow.source.flows[OUT].push(newFlow);
493
+ newFlow.target.flows[IN].push(newFlow);
494
+ }
495
+
496
+ // Now that we're done adopting various values from original flow f,
497
+ // tell f itself that Things have Changed:
498
+ f.useForVisiblePlacing = false;
499
+ f.hasAShadow = true;
500
+ });
501
+ }
502
+
503
+ // Set up stagesArr: one array element for each stage, containing that
504
+ // stage's nodes, in stage order.
505
+ // This can also be called when nodes' info may have been updated elsewhere
506
+ // & we need a fresh map generated.
507
+ function updateStagesArray() {
508
+ stagesArr = d3.groups(nodes, (d) => d.stage) // [stage, [nodes]]
509
+ .sort((a, b) => a[0] - b[0])
510
+ // Extract each stage and sort its nodes by sourceRow.
511
+ // (This raises shadow nodes to the same rank the original flow is at)
512
+ .map((d) => d[1].sort(bySourceOrder)); // [[nodes]]
513
+ }
514
+
515
+ // placeNodes(iterations):
516
+ // Set (and then adjust) the y-position for each node and flow, based
517
+ // on their connections to other points in the diagram.
518
+ function placeNodes(iterations) {
519
+ // nodeSetStats(nodeList):
520
+ // Get the total weight+value from an assortment of Nodes.
521
+ // The Nodes are expected to all be in the same Stage.
522
+ function nodeSetStats(nodeList) {
523
+ const weight = d3.sum(nodeList, (n) => yCenter(n) * n.value),
524
+ value = valueSum(nodeList);
525
+ return {
526
+ stage: nodeList[0].stage,
527
+ weight: weight,
528
+ value: value,
529
+ center: divide(weight, value),
530
+ };
531
+ }
532
+
533
+ // Set up the scaling factor and the initial x & y of all the Nodes:
534
+ function initializeNodePositions() {
535
+ // First, calculate the spacing values.
536
+ // How many nodes are in the 'busiest' stage?
537
+ const greatestNodeCount = d3.max(stagesArr, (s) => s.length);
538
+
539
+ let ky = 0;
540
+ // Special case: What if there's only one node in every stage?
541
+ // That calculation is very different:
542
+ if (greatestNodeCount === 1) {
543
+ [maximumNodeSpacing, actualNodeSpacing] = [0, 0];
544
+ ky = nodeHeightFactor
545
+ * d3.min(stagesArr, (s) => divide(size.h, valueSum(s)));
546
+ } else {
547
+ // What if each node in the busiest stage got 1 pixel?
548
+ // Figure out how many pixels would be left over.
549
+ // (If pixels < 2, use 2; otherwise the slider has nothing to do.)
550
+ const allAvailablePadding = Math.max(2, size.h - greatestNodeCount);
551
+
552
+ // A nodeHeightFactor of 0 means: 'pad as much as possible
553
+ // without making any node less than 1 pixel tall'.
554
+ // Formula for the initial spacing value when nHF = 0:
555
+ // allAvailablePadding / (# of spaces in the busiest stage)
556
+ maximumNodeSpacing
557
+ = ((1 - nodeHeightFactor) * allAvailablePadding)
558
+ / (greatestNodeCount - 1);
559
+ actualNodeSpacing = maximumNodeSpacing * nodeSpacingFactor;
560
+ // Finally, calculate the vertical scaling factor for all
561
+ // nodes, given maximumNodeSpacing & the diagram's height:
562
+ ky = d3.min(
563
+ stagesArr,
564
+ (s) => divide(size.h - (s.length - 1) * maximumNodeSpacing, valueSum(s))
565
+ );
566
+ }
567
+ if (ky === Infinity) { ky = 1; } // This happens if all Node values are 0
568
+
569
+ // Compute all the dy & weighted values using the now-known scale
570
+ // of the graph:
571
+ flows.forEach((f) => {
572
+ f.dy = f.value * ky;
573
+ f.weightedValue = f.hasAShadow ? 0 : f.value;
574
+ });
575
+ // Also: Ensure each node has a nonzero height:
576
+ nodes.forEach((n) => {
577
+ n.dy = Math.max(n.value * ky, Number.MIN_VALUE);
578
+ });
579
+
580
+ // Set the initial positions of all nodes within each stage.
581
+ // The initial stage will start with all nodes centered vertically,
582
+ // separated by the actualNodeSpacing.
583
+ // Each stage afterwards will center on its combined source nodes.
584
+ let targetY;
585
+ stagesArr.forEach((s, stageIndex) => {
586
+ const stageSize
587
+ = (valueSum(s) * ky) + (actualNodeSpacing * (s.length - 1));
588
+ targetY = size.h / 2; // default case = center this batch of nodes
589
+ // If we have any flows into the current set of nodes, we have a
590
+ // chicken/egg problem: We want to use weighted centers based on
591
+ // flows (i.e. flowSetStats), but at this point 0 flows are placed.
592
+ // Simpler approach: use the weighted center of nodes flowing in.
593
+ const allFlowsIn = s.map((n) => n.flows[IN]).flat();
594
+ if (allFlowsIn.length > 0) {
595
+ const uniqueSourceNodes = new Set(
596
+ allFlowsIn.map((f) => f.source)
597
+ // Since shadows are in every stage, don't look back more than
598
+ // 1 stage. (And self-loops may mean there are flows from the
599
+ // *same* stage, currently.)
600
+ .filter((n) => n.stage >= stageIndex - 1)
601
+ );
602
+ targetY = nodeSetStats(Array.from(uniqueSourceNodes)).center;
603
+ }
604
+
605
+ // Calculate the first-node-in-this-stage's y position (while not
606
+ // letting it be placed where the stage will exceed either boundary):
607
+ let nextNodePos
608
+ = Math.max(
609
+ 0,
610
+ Math.min(targetY - (stageSize / 2), size.h - stageSize)
611
+ );
612
+ s.forEach((n) => {
613
+ n.y = nextNodePos;
614
+ // Find the y position of the next node:
615
+ nextNodePos = yBottom(n) + actualNodeSpacing;
616
+ });
617
+ });
618
+
619
+ // Set up x-values too.
620
+ // Apply a scaling factor based on width per stage:
621
+ const widthPerStage = maxStage > 0 ? (size.w - nodeWidth) / maxStage : 0;
622
+ nodes.forEach((n) => {
623
+ n.x = widthPerStage * n.stage;
624
+ n.dx = nodeWidth;
625
+ });
626
+
627
+ // With nodes placed, we *also* have to provide an initial
628
+ // placement for all flows, so that their weights can be measured
629
+ // realistically in the placeNodes() routine.
630
+ nodes.forEach((n) => {
631
+ // Each flow is initially placed naively, just using the input order.
632
+ // Any misfires will be corrected soon by placeFlowsInsideNodes()
633
+ let [sy, ty] = [0, 0];
634
+ // Shadows touching a real node adopt the same position as their
635
+ // 'true' flow. (NOTE: This works because all shadows initially
636
+ // *follow* all real flows.):
637
+ n.flows[OUT].forEach((f) => {
638
+ if (f.isAShadow && !n.isAShadow) {
639
+ f.sy = flows[f.shadowOf].sy;
640
+ } else {
641
+ f.sy = sy; sy += f.dy;
642
+ }
643
+ });
644
+ n.flows[IN].forEach((f) => {
645
+ if (f.isAShadow && !n.isAShadow) {
646
+ f.ty = flows[f.shadowOf].ty;
647
+ } else {
648
+ f.ty = ty; ty += f.dy;
649
+ }
650
+ });
651
+ });
652
+ }
653
+
654
+ // findNodeGroupOffset(nodeList):
655
+ // Figure out where these Nodes want to be, and return the
656
+ // appropriate y-offset value.
657
+ function findNodeGroupOffset(nodeList) {
658
+ // The population of flows to test = the combination of every
659
+ // last flow touching this group of Nodes:
660
+ const fStats = allFlowStats(nodeList),
661
+ totalIn = fStats[IN].value,
662
+ totalOut = fStats[OUT].value;
663
+ // If there are no flows touching *either* side here, there's nothing
664
+ // to offset ourselves relative to, so we can exit early:
665
+ if (totalIn === 0 && totalOut === 0) { return 0; }
666
+
667
+ const nStats = nodeSetStats(nodeList),
668
+ // projectedSourceCenter =
669
+ // the current Node group's weighted center
670
+ // MINUS the weighted center of incoming Flows' targets
671
+ // PLUS the weighted center of incoming Flows' sources.
672
+ // Thought exercise:
673
+ // If 100% of the value of the Node group is flowing in, then this is
674
+ // exactly equivalent to: *the weighted center of all sources*.
675
+ projectedSourceCenter = divide(
676
+ nStats.weight - fStats[IN].targets.weight + fStats[IN].sources.weight,
677
+ nStats.value
678
+ ),
679
+ // projectedTargetCenter = the same idea in the other direction:
680
+ // current Node group's weighted center
681
+ // - outgoing weights' center
682
+ // + final center of those weights
683
+ projectedTargetCenter = divide(
684
+ nStats.weight - fStats[OUT].sources.weight + fStats[OUT].targets.weight,
685
+ nStats.value
686
+ );
687
+
688
+ // Time to do the positioning calculations.
689
+ let goalY = 0;
690
+ if (totalOut === 0) {
691
+ // If we have only in-flows, it's simple:
692
+ // Center the current group relative only to its sources.
693
+ goalY = projectedSourceCenter;
694
+ } else if (totalIn === 0) {
695
+ // Only out-flows? Center this group on its targets:
696
+ goalY = projectedTargetCenter;
697
+ } else {
698
+ // There are flows both in & out. Find the slope between the centers:
699
+ const startStage = fStats[IN].sources.maxSourceStage,
700
+ endStage = fStats[OUT].targets.minTargetStage,
701
+ stageDistance = endStage - startStage,
702
+ slopeBetweenCenters
703
+ = stageDistance !== 0 // Avoid divide-by-0 error
704
+ ? (projectedTargetCenter - projectedSourceCenter) / stageDistance
705
+ : 0;
706
+ // Where along that line should this current group be centered?
707
+ goalY
708
+ = projectedSourceCenter
709
+ + (nStats.stage - startStage) * slopeBetweenCenters;
710
+ }
711
+
712
+ // We have a goal Y value! Return the offset from the current center:
713
+ return goalY - nStats.center;
714
+ }
715
+
716
+ // updateStageCentering(stage):
717
+ // Make sure nodes are spaced far enough apart from each other,
718
+ // AND, after some have been nudged apart, put those
719
+ // now-locked-together groups of nodes in the best available
720
+ // position given their group's *overall* connections in & out.
721
+ function updateStageCentering(s) {
722
+ // enforceValidNodePositions():
723
+ // Make sure this stage doesn't extend past either the top or
724
+ // bottom, and preserve the required spacing between nodes.
725
+ function enforceValidNodePositions() {
726
+ // Nudge down any nodes which are past the top:
727
+ let yPos = 0; // = the current available y closest to the top
728
+ s.forEach((n) => {
729
+ // If this node's top is above yPos, nudge the node down:
730
+ if (n.y < yPos) { n.y = yPos; }
731
+ // Set yPos to the next available y toward the bottom:
732
+ yPos = yBottom(n) + actualNodeSpacing;
733
+ });
734
+
735
+ // ... if we've gone *past* the bottom, bump nodes back up.
736
+ yPos = size.h; // = the current available y closest to the bottom
737
+ s.slice().reverse().forEach((n) => {
738
+ // if this node's bottom is below yPos, nudge it up:
739
+ if (yBottom(n) > yPos) { n.y = yPos - n.dy; }
740
+ // Set yPos to the next available y toward the top:
741
+ yPos = n.y - actualNodeSpacing;
742
+ });
743
+ }
744
+
745
+ // nodesAreAdjacent: Given two nodes *in height order*, is the top of n2
746
+ // bumping up against n1's bottom edge?
747
+ function nodesAreAdjacent(n1, n2) {
748
+ // Is the bottom of the 1st node + the node spacing essentially
749
+ // the same as the 2nd node's top? (i.e. within a tenth of a 'pixel')
750
+ return (n2.y - actualNodeSpacing - yBottom(n1)) < 0.1;
751
+ }
752
+
753
+ function centerNeighborGroups() {
754
+ // First, Gather groups of neighbors. This loop produces arrays
755
+ // of 1 or more nodes which need to be nudged together.
756
+ const neighborGroups = [];
757
+ s.forEach((n, i) => {
758
+ // Can we include this node as a neighbor of its predecessor?
759
+ if (i > 0 && nodesAreAdjacent(s[i - 1], n)) {
760
+ // Yes? Then append it to the 'current' group:
761
+ const lastGroup = neighborGroups.length - 1;
762
+ neighborGroups[lastGroup].push(n);
763
+ } else {
764
+ // No? Start a new group:
765
+ neighborGroups.push([n]);
766
+ }
767
+ });
768
+
769
+ // At this point we *may* have node groups which need nudges.
770
+ // For each multi-node group, find the weighted center of its
771
+ // sources/targets, and place that group's center along that
772
+ // line:
773
+ neighborGroups.filter((g) => g.length > 1)
774
+ .forEach((nodeGroup) => {
775
+ // Apply the offset to the entire node group:
776
+ const yOffset = findNodeGroupOffset(nodeGroup);
777
+ nodeGroup.forEach((n) => { n.y += yOffset; });
778
+ });
779
+ }
780
+
781
+ // First, sort this stage's nodes based on either their current
782
+ // positions or on the order they appeared in the data:
783
+ s.sort(autoLayout ? byTopEdges : bySourceOrder);
784
+
785
+ // Make sure any overlapping nodes preserve the required spacing.
786
+ // Run the first nudge of all to see what bumps against each other:
787
+ enforceValidNodePositions();
788
+
789
+ // Look for sets of neighbors and center them as best we can:
790
+ centerNeighborGroups();
791
+ // Make sure we're still on the canvas:
792
+ enforceValidNodePositions();
793
+
794
+ // Since we may have just created more neighbors, iterate 1 more time:
795
+ centerNeighborGroups();
796
+ enforceValidNodePositions();
797
+ // We could keep doing more rounds! But have to stop somewhere.
798
+ // Someday I hope to update this to notice when we've either:
799
+ // 1) stopped bumping into more nodes, or else
800
+ // 2) reached the maximum group (all nodes in 1 neighbor group)
801
+ // For now, this will do.
802
+ }
803
+
804
+ // processStages(stageList, factor):
805
+ // Iterate over a list of stages in the given order, moving Nodes
806
+ // and Flows around according to the given factor (which proceeds
807
+ // from 0.99 downwards as the iterations continue).
808
+ function processStages(stageList, factor) {
809
+ stageList.forEach((s) => {
810
+ // Move each node to its ideal vertical position:
811
+ s.forEach((n) => { n.y += findNodeGroupOffset([n]) * factor; });
812
+ // Update this stage's node positions to incorporate their proximity
813
+ // & required spacing *now*, since they'll be used as the basis for
814
+ // weights in the very next stage:
815
+ updateStageCentering(s);
816
+ // Update the flow sorting too; same reason:
817
+ placeFlowsInsideNodes(s);
818
+ });
819
+ // At the end of each round, do a proper final flow placement
820
+ // across the whole diagram. (Some locally-optimized flow choices
821
+ // don't work across the whole and need this resolution step
822
+ // before doing more balancing).
823
+ placeFlowsInsideNodes(nodes);
824
+ }
825
+
826
+ // reCenterDiagram:
827
+ // If (the vertical size of the space occupied by the nodes)
828
+ // < (the total diagram's Height),
829
+ // then offset ALL Nodes' y positions to center the diagram:
830
+ function reCenterDiagram() {
831
+ const minY = leastY(nodes),
832
+ yH = greatestY(nodes) - minY;
833
+ if (yH < size.h) {
834
+ const yOffset = (size.h / 2) - (minY + (yH / 2));
835
+ nodes.forEach((n) => { n.y += yOffset; });
836
+ }
837
+ }
838
+
839
+ // Enough preamble. Lay out the nodes:
840
+
841
+ initializeNodePositions();
842
+ // Resolve all collisions/spacing & place all flows to start:
843
+ stagesArr.forEach((s) => { updateStageCentering(s); });
844
+ placeFlowsInsideNodes(nodes);
845
+
846
+ let [alpha, counter] = [1, 0];
847
+ while (counter < iterations) {
848
+ counter += 1;
849
+ // Make each round of moves progressively weaker:
850
+ alpha *= 0.99;
851
+ // Run through stages left-to-right, then right-to-left:
852
+ processStages(stagesArr, alpha);
853
+ processStages(stagesArr.slice().reverse(), alpha);
854
+ reCenterDiagram();
855
+ }
856
+
857
+ // After the last layout adjustment, remember these node coordinates
858
+ // (for reference when the user is dragging nodes):
859
+ nodes.forEach((n) => {
860
+ n.origPos = { x: n.x, y: n.y };
861
+ n.lastPos = { x: n.x, y: n.y };
862
+ n.move = [0, 0];
863
+ });
864
+ }
865
+
866
+ // setup() = define the *skeleton* of the diagram -- which nodes link to
867
+ // which, and in which stages -- but no specific positions yet:
868
+ sankey.setup = () => {
869
+ connectFlowsToNodes();
870
+ computeNodeValues();
871
+ assignNodesToStages();
872
+ updateStagesArray();
873
+ return sankey;
874
+ };
875
+
876
+ // layout() = Given a complete skeleton, use the given total width/height and
877
+ // set the exact positions of all nodes and flows:
878
+ sankey.layout = (iterations) => {
879
+ // In case anything's changed since setup, re-generate our map:
880
+ updateStagesArray();
881
+ // Iterate over the structure several times to make the layout nice:
882
+ placeNodes(iterations);
883
+ return sankey;
884
+ };
885
+
886
+ // relayout() = Given a complete diagram with some new node positions,
887
+ // calculate where the flows must now start/end:
888
+ sankey.relayout = () => {
889
+ placeFlowsInsideNodes(nodes);
890
+ return sankey;
891
+ };
892
+
893
+ return sankey;
894
+ };
895
+
896
+ // Make the linter happy about imported objects:
897
+ /* global d3 IN OUT */