davidardz07 commited on
Commit
b0b2cd8
·
1 Parent(s): 7db744a
Files changed (1) hide show
  1. script.js +300 -26
script.js CHANGED
@@ -1,4 +1,282 @@
1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  // Sankey diagram data structure with example data
3
  let sankeyData = {
4
  nodes: [
@@ -56,17 +334,14 @@ function initDiagram() {
56
 
57
  const sankey = d3.sankey()
58
  .nodeWidth(parseInt(document.getElementById('node-width').value))
59
- .nodePadding(20)
60
- .extent([[textPadding/2, textPadding/2], [width - textPadding/2, height - textPadding/2]]);
61
-
62
- // Generate the sankey diagram
63
- const graph = sankey({
64
- nodes: sankeyData.nodes.map(d => Object.assign({}, d)),
65
- links: sankeyData.links.map(d => Object.assign({}, d))
66
- });
67
 
68
- const nodes = graph.nodes;
69
- const links = graph.links;
70
 
71
  // Clear previous diagram
72
  svg.attr("width", width)
@@ -80,13 +355,13 @@ function initDiagram() {
80
  .selectAll("path")
81
  .data(links)
82
  .join("path")
83
- .attr("d", d3.sankeyLinkHorizontal())
84
  .attr("stroke", d => {
85
  const srcColor = sankeyData.nodes[d.source.index]?.color || "#93c5fd";
86
  const rgb = hexToRgb(srcColor);
87
  return `rgba(${rgb.r},${rgb.g},${rgb.b},${linkAlpha})`;
88
  })
89
- .attr("stroke-width", d => Math.max(1, d.width))
90
  .style("mix-blend-mode", "multiply")
91
  .on("mouseover", function() {
92
  d3.select(this).attr("stroke-opacity", 0.8);
@@ -100,17 +375,17 @@ function initDiagram() {
100
  .selectAll("g")
101
  .data(nodes)
102
  .join("g")
103
- .attr("transform", d => `translate(${d.x0},${d.y0})`);
104
 
105
  // Apply stored positions if they exist
106
  node.attr("transform", d => {
107
  const pos = nodePositions[d.index];
108
- return pos ? `translate(${pos.x},${pos.y})` : `translate(${d.x0},${d.y0})`;
109
  });
110
 
111
  node.append("rect")
112
- .attr("height", d => d.y1 - d.y0)
113
- .attr("width", d => d.x1 - d.x0)
114
  .attr("fill", d => d.color || "#4f46e5")
115
  .attr("stroke", "#fff")
116
  .attr("stroke-width", 1)
@@ -119,22 +394,21 @@ function initDiagram() {
119
 
120
  // Add drag behavior
121
  node.call(d3.drag()
122
- .subject(d => ({ x: d.x0, y: d.y0 }))
123
  .on("start", function(event) {
124
  d3.select(this).raise();
125
  })
126
  .on("drag", function(event, d) {
127
- // Update node position
128
- d.x0 = event.x;
129
- d.x1 = event.x + (d.x1 - d.x0);
130
- d.y0 = event.y;
131
- d.y1 = event.y + (d.y1 - d.y0);
132
-
133
- nodePositions[d.index] = { x: d.x0, y: d.y0 };
134
- d3.select(this).attr("transform", `translate(${d.x0},${d.y0})`);
135
 
136
  // Update links
137
- link.attr("d", d3.sankeyLinkHorizontal());
 
 
 
138
  })
139
  );
140
 
 
1
 
2
+ // Define the d3.sankey layout function
3
+ d3.sankey = function() {
4
+ var sankey = {},
5
+ nodeWidth = 24,
6
+ nodePadding = 8,
7
+ size = [1, 1],
8
+ nodes = [],
9
+ links = [];
10
+
11
+ sankey.nodeWidth = function(_) {
12
+ if (!arguments.length) return nodeWidth;
13
+ nodeWidth = +_;
14
+ return sankey;
15
+ };
16
+
17
+ sankey.nodePadding = function(_) {
18
+ if (!arguments.length) return nodePadding;
19
+ nodePadding = +_;
20
+ return sankey;
21
+ };
22
+
23
+ sankey.nodes = function(_) {
24
+ if (!arguments.length) return nodes;
25
+ nodes = _;
26
+ return sankey;
27
+ };
28
+
29
+ sankey.links = function(_) {
30
+ if (!arguments.length) return links;
31
+ links = _;
32
+ return sankey;
33
+ };
34
+
35
+ sankey.size = function(_) {
36
+ if (!arguments.length) return size;
37
+ size = _;
38
+ return sankey;
39
+ };
40
+
41
+ sankey.layout = function(iterations) {
42
+ if (!nodes.length) return sankey;
43
+ computeNodeLinks();
44
+ computeNodeValues();
45
+ computeNodeBreadths();
46
+ computeNodeDepths(iterations || 32);
47
+ computeLinkDepths();
48
+ return sankey;
49
+ };
50
+
51
+ sankey.relayout = function() {
52
+ computeLinkDepths();
53
+ return sankey;
54
+ };
55
+
56
+ sankey.link = function() {
57
+ var curvature = .5;
58
+
59
+ function link(d) {
60
+ var x0 = d.source.x + d.source.dx,
61
+ x1 = d.target.x,
62
+ xi = d3.interpolateNumber(x0, x1),
63
+ x2 = xi(curvature),
64
+ x3 = xi(1 - curvature),
65
+ y0 = d.source.y + d.sy + d.dy / 2,
66
+ y1 = d.target.y + d.ty + d.dy / 2;
67
+ return "M" + x0 + "," + y0
68
+ + "C" + x2 + "," + y0
69
+ + " " + x3 + "," + y1
70
+ + " " + x1 + "," + y1;
71
+ }
72
+
73
+ link.curvature = function(_) {
74
+ if (!arguments.length) return curvature;
75
+ curvature = +_;
76
+ return link;
77
+ };
78
+
79
+ return link;
80
+ };
81
+
82
+ // Populate the sourceLinks and targetLinks for each node.
83
+ function computeNodeLinks() {
84
+ nodes.forEach(function(node) {
85
+ node.sourceLinks = [];
86
+ node.targetLinks = [];
87
+ });
88
+ links.forEach(function(link) {
89
+ var source = link.source,
90
+ target = link.target;
91
+ if (typeof source === "number") source = link.source = nodes[link.source];
92
+ if (typeof target === "number") target = link.target = nodes[link.target];
93
+ source.sourceLinks.push(link);
94
+ target.targetLinks.push(link);
95
+ });
96
+ }
97
+
98
+ // Compute the value (size) of each node by summing the associated links.
99
+ function computeNodeValues() {
100
+ nodes.forEach(function(node) {
101
+ node.value = Math.max(
102
+ d3.sum(node.sourceLinks, value),
103
+ d3.sum(node.targetLinks, value)
104
+ );
105
+ });
106
+ }
107
+
108
+ function computeNodeBreadths() {
109
+ var remainingNodes = nodes,
110
+ nextNodes,
111
+ x = 0;
112
+
113
+ while (remainingNodes.length) {
114
+ nextNodes = [];
115
+ remainingNodes.forEach(function(node) {
116
+ node.x = x;
117
+ node.dx = nodeWidth;
118
+ node.sourceLinks.forEach(function(link) {
119
+ if (nextNodes.indexOf(link.target) < 0) {
120
+ nextNodes.push(link.target);
121
+ }
122
+ });
123
+ });
124
+ remainingNodes = nextNodes;
125
+ ++x;
126
+ }
127
+
128
+ moveSinksRight(x);
129
+ scaleNodeBreadths((size[0] - nodeWidth) / (x - 1));
130
+ }
131
+
132
+ function moveSinksRight(x) {
133
+ nodes.forEach(function(node) {
134
+ if (!node.sourceLinks.length) {
135
+ node.x = x - 1;
136
+ }
137
+ });
138
+ }
139
+
140
+ function scaleNodeBreadths(kx) {
141
+ nodes.forEach(function(node) {
142
+ node.x *= kx;
143
+ });
144
+ }
145
+
146
+ function computeNodeDepths(iterations) {
147
+ // Group nodes by x-coordinate
148
+ var nodesByBreadth = d3.group(nodes, d => d.x);
149
+ // Convert Map to Array and ensure it's sorted
150
+ nodesByBreadth = Array.from(nodesByBreadth.values());
151
+
152
+ initializeNodeDepth();
153
+ resolveCollisions();
154
+ for (var alpha = 1; iterations > 0; --iterations) {
155
+ relaxRightToLeft(alpha *= .99);
156
+ resolveCollisions();
157
+ relaxLeftToRight(alpha);
158
+ resolveCollisions();
159
+ }
160
+
161
+ function initializeNodeDepth() {
162
+ // Calculate vertical scaling factor
163
+ var ky = d3.min(nodesByBreadth, function(nodes) {
164
+ return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value);
165
+ });
166
+
167
+ nodesByBreadth.forEach(function(nodes) {
168
+ nodes.forEach(function(node, i) {
169
+ node.y = i;
170
+ node.dy = node.value * ky;
171
+ });
172
+ });
173
+
174
+ links.forEach(function(link) {
175
+ link.dy = link.value * ky;
176
+ });
177
+ }
178
+
179
+ function relaxLeftToRight(alpha) {
180
+ nodesByBreadth.forEach(function(nodes) {
181
+ nodes.forEach(function(node) {
182
+ if (node.targetLinks.length) {
183
+ var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value);
184
+ node.y += (y - center(node)) * alpha;
185
+ }
186
+ });
187
+ });
188
+
189
+ function weightedSource(link) {
190
+ return center(link.source) * link.value;
191
+ }
192
+ }
193
+
194
+ function relaxRightToLeft(alpha) {
195
+ nodesByBreadth.slice().reverse().forEach(function(nodes) {
196
+ nodes.forEach(function(node) {
197
+ if (node.sourceLinks.length) {
198
+ var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value);
199
+ node.y += (y - center(node)) * alpha;
200
+ }
201
+ });
202
+ });
203
+
204
+ function weightedTarget(link) {
205
+ return center(link.target) * link.value;
206
+ }
207
+ }
208
+
209
+ function resolveCollisions() {
210
+ nodesByBreadth.forEach(function(nodes) {
211
+ var node,
212
+ dy,
213
+ y0 = 0,
214
+ n = nodes.length,
215
+ i;
216
+
217
+ nodes.sort(ascendingDepth);
218
+ for (i = 0; i < n; ++i) {
219
+ node = nodes[i];
220
+ dy = y0 - node.y;
221
+ if (dy > 0) node.y += dy;
222
+ y0 = node.y + node.dy + nodePadding;
223
+ }
224
+
225
+ dy = y0 - nodePadding - size[1];
226
+ if (dy > 0) {
227
+ y0 = node.y -= dy;
228
+ for (i = n - 2; i >= 0; --i) {
229
+ node = nodes[i];
230
+ dy = node.y + node.dy + nodePadding - y0;
231
+ if (dy > 0) node.y -= dy;
232
+ y0 = node.y;
233
+ }
234
+ }
235
+ });
236
+ }
237
+
238
+ function ascendingDepth(a, b) {
239
+ return a.y - b.y;
240
+ }
241
+ }
242
+
243
+ function computeLinkDepths() {
244
+ nodes.forEach(function(node) {
245
+ node.sourceLinks.sort(ascendingTargetDepth);
246
+ node.targetLinks.sort(ascendingSourceDepth);
247
+ });
248
+ nodes.forEach(function(node) {
249
+ var sy = 0, ty = 0;
250
+ node.sourceLinks.forEach(function(link) {
251
+ link.sy = sy;
252
+ sy += link.dy;
253
+ });
254
+ node.targetLinks.forEach(function(link) {
255
+ link.ty = ty;
256
+ ty += link.dy;
257
+ });
258
+ });
259
+
260
+ function ascendingSourceDepth(a, b) {
261
+ return a.source.y - b.source.y;
262
+ }
263
+
264
+ function ascendingTargetDepth(a, b) {
265
+ return a.target.y - b.target.y;
266
+ }
267
+ }
268
+
269
+ function value(link) {
270
+ return link.value;
271
+ }
272
+
273
+ function center(node) {
274
+ return node.y + node.dy / 2;
275
+ }
276
+
277
+ return sankey;
278
+ };
279
+
280
  // Sankey diagram data structure with example data
281
  let sankeyData = {
282
  nodes: [
 
334
 
335
  const sankey = d3.sankey()
336
  .nodeWidth(parseInt(document.getElementById('node-width').value))
337
+ .nodePadding(8)
338
+ .size([width - textPadding, height - textPadding])
339
+ .nodes(sankeyData.nodes.map(d => Object.assign({}, d)))
340
+ .links(sankeyData.links.map(d => Object.assign({}, d)))
341
+ .layout(32); // Increase iterations for better layout
 
 
 
342
 
343
+ const nodes = sankey.nodes();
344
+ const links = sankey.links();
345
 
346
  // Clear previous diagram
347
  svg.attr("width", width)
 
355
  .selectAll("path")
356
  .data(links)
357
  .join("path")
358
+ .attr("d", sankey.link())
359
  .attr("stroke", d => {
360
  const srcColor = sankeyData.nodes[d.source.index]?.color || "#93c5fd";
361
  const rgb = hexToRgb(srcColor);
362
  return `rgba(${rgb.r},${rgb.g},${rgb.b},${linkAlpha})`;
363
  })
364
+ .attr("stroke-width", d => Math.max(1, d.dy))
365
  .style("mix-blend-mode", "multiply")
366
  .on("mouseover", function() {
367
  d3.select(this).attr("stroke-opacity", 0.8);
 
375
  .selectAll("g")
376
  .data(nodes)
377
  .join("g")
378
+ .attr("transform", d => `translate(${d.x},${d.y})`);
379
 
380
  // Apply stored positions if they exist
381
  node.attr("transform", d => {
382
  const pos = nodePositions[d.index];
383
+ return pos ? `translate(${pos.x},${pos.y})` : `translate(${d.x},${d.y})`;
384
  });
385
 
386
  node.append("rect")
387
+ .attr("height", d => d.dy)
388
+ .attr("width", d => d.dx)
389
  .attr("fill", d => d.color || "#4f46e5")
390
  .attr("stroke", "#fff")
391
  .attr("stroke-width", 1)
 
394
 
395
  // Add drag behavior
396
  node.call(d3.drag()
397
+ .subject(d => ({ x: d.x, y: d.y }))
398
  .on("start", function(event) {
399
  d3.select(this).raise();
400
  })
401
  .on("drag", function(event, d) {
402
+ // Keep x position fixed, only allow vertical movement
403
+ d.y = event.y;
404
+ nodePositions[d.index] = { x: d.x, y: d.y };
405
+ d3.select(this).attr("transform", `translate(${d.x},${d.y})`);
 
 
 
 
406
 
407
  // Update links
408
+ link.attr("d", sankey.link());
409
+
410
+ // After dragging, relayout to maintain proper link positions
411
+ sankey.relayout();
412
  })
413
  );
414