chartjs-chart-sankey 0.14.3 → 0.14.4
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.
- package/dist/chartjs-chart-sankey.cjs +86 -55
- package/dist/chartjs-chart-sankey.esm.js +87 -56
- package/dist/chartjs-chart-sankey.min.js +2 -2
- package/dist/controller.d.ts +2 -2
- package/dist/flow.d.ts +6 -6
- package/dist/types.d.ts +5 -1
- package/package.json +18 -18
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.14.
|
|
2
|
+
* chartjs-chart-sankey v0.14.4
|
|
3
3
|
* https://chartjs-chart-sankey.pages.dev/
|
|
4
4
|
* (c) 2026 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
@@ -126,6 +126,12 @@ function buildNodesFromData(data, { size, priority, column }) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
const SMALL_VALUE = 1e-6;
|
|
129
|
+
function nodeX$1(node) {
|
|
130
|
+
return node.x ?? 0;
|
|
131
|
+
}
|
|
132
|
+
function nodeY$1(node) {
|
|
133
|
+
return node.y ?? 0;
|
|
134
|
+
}
|
|
129
135
|
const getAllKeysForward = (nodes, visited = new Set())=>{
|
|
130
136
|
const keys = [];
|
|
131
137
|
for (const node of nodes){
|
|
@@ -183,7 +189,7 @@ function calculateX(nodeMap, data, mode) {
|
|
|
183
189
|
x++;
|
|
184
190
|
}
|
|
185
191
|
}
|
|
186
|
-
const maxX = allNodes.reduce((max, node)=>Math.max(max, node
|
|
192
|
+
const maxX = allNodes.reduce((max, node)=>Math.max(max, nodeX$1(node)), 0);
|
|
187
193
|
if (mode === 'edge') {
|
|
188
194
|
const from = new Set(data.map((dataPoint)=>dataPoint.from));
|
|
189
195
|
allKeys.filter((key)=>!from.has(key)).forEach((key)=>{
|
|
@@ -223,7 +229,7 @@ function processFrom(node, y) {
|
|
|
223
229
|
}
|
|
224
230
|
y = Math.max(n.y + n.out, y);
|
|
225
231
|
}
|
|
226
|
-
return node
|
|
232
|
+
return nodeY$1(node) + node.size;
|
|
227
233
|
}
|
|
228
234
|
function processTo(node, y) {
|
|
229
235
|
if (!node.to.length) return y;
|
|
@@ -236,7 +242,7 @@ function processTo(node, y) {
|
|
|
236
242
|
}
|
|
237
243
|
y = Math.max(n.y + Math.max(n.in, n.out), y);
|
|
238
244
|
}
|
|
239
|
-
return node
|
|
245
|
+
return nodeY$1(node) + node.size;
|
|
240
246
|
}
|
|
241
247
|
function setOrGetY(node, value) {
|
|
242
248
|
if (defined(node.y)) {
|
|
@@ -250,9 +256,9 @@ function processRest(nodeArray, maxX) {
|
|
|
250
256
|
const rightNodes = nodeArray.filter((node)=>node.x === maxX);
|
|
251
257
|
const leftToDo = leftNodes.filter((node)=>!defined(node.y));
|
|
252
258
|
const rightToDo = rightNodes.filter((node)=>!defined(node.y));
|
|
253
|
-
const centerToDo = nodeArray.filter((node)=>node
|
|
254
|
-
let leftY = leftNodes.reduce((acc, cur)=>Math.max(acc, cur
|
|
255
|
-
let rightY = rightNodes.reduce((acc, cur)=>Math.max(acc, cur
|
|
259
|
+
const centerToDo = nodeArray.filter((node)=>nodeX$1(node) > 0 && nodeX$1(node) < maxX && !defined(node.y));
|
|
260
|
+
let leftY = leftNodes.reduce((acc, cur)=>Math.max(acc, nodeY$1(cur) + cur.out || 0), 0) + SMALL_VALUE;
|
|
261
|
+
let rightY = rightNodes.reduce((acc, cur)=>Math.max(acc, nodeY$1(cur) + cur.in || 0), 0) + SMALL_VALUE;
|
|
256
262
|
let centerY = 0;
|
|
257
263
|
if (leftY >= rightY) {
|
|
258
264
|
leftToDo.forEach((node)=>{
|
|
@@ -273,7 +279,7 @@ function processRest(nodeArray, maxX) {
|
|
|
273
279
|
});
|
|
274
280
|
}
|
|
275
281
|
centerToDo.forEach((node)=>{
|
|
276
|
-
let y = nodeArray.filter((n)=>n
|
|
282
|
+
let y = nodeArray.filter((n)=>nodeX$1(n) === nodeX$1(node) && defined(n.y)).reduce((acc, cur)=>Math.max(acc, nodeY$1(cur) + Math.max(cur.in, cur.out)), 0);
|
|
277
283
|
y = setOrGetY(node, y);
|
|
278
284
|
y = Math.max(y + node.in, processFrom(node, y));
|
|
279
285
|
y = Math.max(y + node.out, processTo(node, y));
|
|
@@ -284,25 +290,29 @@ function processRest(nodeArray, maxX) {
|
|
|
284
290
|
const fixTop = (nodeArray, maxX)=>{
|
|
285
291
|
let maxY = 0;
|
|
286
292
|
for(let x = 0; x <= maxX; x++){
|
|
287
|
-
const nodes = nodeArray.filter((n)=>n
|
|
293
|
+
const nodes = nodeArray.filter((n)=>nodeX$1(n) === x).sort((a, b)=>nodeY$1(a) - nodeY$1(b));
|
|
288
294
|
let minY = 0;
|
|
289
295
|
for (const node of nodes){
|
|
290
|
-
if (node
|
|
291
|
-
minY = node
|
|
296
|
+
if (nodeY$1(node) < minY) node.y = minY;
|
|
297
|
+
minY = nodeY$1(node) + node.size;
|
|
292
298
|
}
|
|
293
299
|
maxY = Math.max(maxY, minY);
|
|
294
300
|
}
|
|
295
301
|
return maxY;
|
|
296
302
|
};
|
|
297
303
|
const findStartNode = (nodeArray, maxX)=>{
|
|
298
|
-
const
|
|
304
|
+
const sorted = [
|
|
299
305
|
...nodeArray
|
|
300
|
-
].sort((a, b)=>a.size - b.size)
|
|
306
|
+
].sort((a, b)=>a.size - b.size);
|
|
307
|
+
const largest = sorted[sorted.length - 1];
|
|
308
|
+
const size = largest.size;
|
|
301
309
|
const biggest = nodeArray.filter((n)=>n.size === size);
|
|
302
|
-
|
|
303
|
-
biggest.
|
|
304
|
-
|
|
305
|
-
if (
|
|
310
|
+
const first = biggest[0];
|
|
311
|
+
if (biggest.length === 1) return first;
|
|
312
|
+
biggest.sort((a, b)=>nodeX$1(a) - nodeX$1(b));
|
|
313
|
+
if (nodeX$1(first) === 0) return first;
|
|
314
|
+
const last = biggest[biggest.length - 1];
|
|
315
|
+
if (nodeX$1(last) === maxX) return last;
|
|
306
316
|
const mid = Math.floor(biggest.length / 2);
|
|
307
317
|
return biggest[mid];
|
|
308
318
|
};
|
|
@@ -320,8 +330,8 @@ function calculateYUsingPriority(nodeArray, maxX) {
|
|
|
320
330
|
let nextYStart = 0;
|
|
321
331
|
for(let x = 0; x <= maxX; x++){
|
|
322
332
|
let y = nextYStart;
|
|
323
|
-
const nodes = nodeArray.filter((node)=>node
|
|
324
|
-
nextYStart = nodes.length ? nodes[0].to.filter((to)=>to.node
|
|
333
|
+
const nodes = nodeArray.filter((node)=>nodeX$1(node) === x).sort((a, b)=>(a.priority ?? 0) - (b.priority ?? 0));
|
|
334
|
+
nextYStart = nodes.length ? nodes[0].to.filter((to)=>nodeX$1(to.node) > x + 1).reduce((acc, cur)=>acc + cur.flow, 0) || 0 : 0;
|
|
325
335
|
for (const node of nodes){
|
|
326
336
|
node.y = y;
|
|
327
337
|
y += Math.max(node.out, node.in);
|
|
@@ -331,9 +341,9 @@ function calculateYUsingPriority(nodeArray, maxX) {
|
|
|
331
341
|
return maxY;
|
|
332
342
|
}
|
|
333
343
|
const nodeByXYSize = (a, b)=>{
|
|
334
|
-
if (a
|
|
335
|
-
if (a
|
|
336
|
-
return a
|
|
344
|
+
if (nodeX$1(a) !== nodeX$1(b)) return nodeX$1(a) - nodeX$1(b);
|
|
345
|
+
if (nodeY$1(a) === nodeY$1(b)) return a.size - b.size;
|
|
346
|
+
return nodeY$1(a) - nodeY$1(b);
|
|
337
347
|
};
|
|
338
348
|
function addPadding(nodeArray, padding) {
|
|
339
349
|
let maxY = 0;
|
|
@@ -341,31 +351,33 @@ const nodeByXYSize = (a, b)=>{
|
|
|
341
351
|
const grid = [];
|
|
342
352
|
const getColIndex = (x)=>{
|
|
343
353
|
if (!columnXs.has(x)) {
|
|
344
|
-
|
|
354
|
+
const index = grid.length;
|
|
355
|
+
columnXs.set(x, index);
|
|
345
356
|
grid.push([]);
|
|
357
|
+
return index;
|
|
346
358
|
}
|
|
347
|
-
return columnXs.get(x);
|
|
359
|
+
return columnXs.get(x) ?? 0;
|
|
348
360
|
};
|
|
349
361
|
nodeArray.sort(nodeByXYSize);
|
|
350
362
|
for (const node of nodeArray){
|
|
351
|
-
const colIdx = getColIndex(node
|
|
352
|
-
const column = grid[colIdx];
|
|
353
|
-
if (node
|
|
354
|
-
column.push(node
|
|
363
|
+
const colIdx = getColIndex(nodeX$1(node));
|
|
364
|
+
const column = grid[colIdx] ?? [];
|
|
365
|
+
if (nodeY$1(node)) {
|
|
366
|
+
column.push(nodeY$1(node));
|
|
355
367
|
let paddings = column.length;
|
|
356
368
|
if (node.in) {
|
|
357
369
|
for(let col = 0; col < colIdx; col++){
|
|
358
|
-
const otherColumn = grid[col];
|
|
370
|
+
const otherColumn = grid[col] ?? [];
|
|
359
371
|
for(let row = 0; row < otherColumn.length; row++){
|
|
360
|
-
if (otherColumn[row] > node
|
|
372
|
+
if (otherColumn[row] > nodeY$1(node)) break;
|
|
361
373
|
paddings = Math.max(row + 1, paddings);
|
|
362
374
|
}
|
|
363
375
|
}
|
|
364
|
-
while(column.length < paddings)column.push(node
|
|
376
|
+
while(column.length < paddings)column.push(nodeY$1(node));
|
|
365
377
|
}
|
|
366
|
-
node.y
|
|
378
|
+
node.y = nodeY$1(node) + paddings * padding;
|
|
367
379
|
}
|
|
368
|
-
maxY = Math.max(maxY, node
|
|
380
|
+
maxY = Math.max(maxY, nodeY$1(node) + Math.max(node.in, node.out));
|
|
369
381
|
}
|
|
370
382
|
return maxY;
|
|
371
383
|
}
|
|
@@ -376,7 +388,7 @@ function sortFlows(nodeArray) {
|
|
|
376
388
|
const overlapTo = nodeSize < node.out;
|
|
377
389
|
let addY = 0;
|
|
378
390
|
let len = node.from.length;
|
|
379
|
-
node.from.sort((a, b)=>a.node
|
|
391
|
+
node.from.sort((a, b)=>nodeY$1(a.node) + a.node.out / 2 - (nodeY$1(b.node) + b.node.out / 2)).forEach((flow, idx)=>{
|
|
380
392
|
if (overlapFrom) {
|
|
381
393
|
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
382
394
|
} else {
|
|
@@ -386,7 +398,7 @@ function sortFlows(nodeArray) {
|
|
|
386
398
|
});
|
|
387
399
|
addY = 0;
|
|
388
400
|
len = node.to.length;
|
|
389
|
-
node.to.sort((a, b)=>a.node
|
|
401
|
+
node.to.sort((a, b)=>nodeY$1(a.node) + a.node.in / 2 - (nodeY$1(b.node) + b.node.in / 2)).forEach((flow, idx)=>{
|
|
390
402
|
if (overlapTo) {
|
|
391
403
|
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
392
404
|
} else {
|
|
@@ -400,7 +412,7 @@ function layout(nodes, data, { priority, height, nodePadding, modeX }) {
|
|
|
400
412
|
const nodeArray = [
|
|
401
413
|
...nodes.values()
|
|
402
414
|
];
|
|
403
|
-
const maxX = calculateX(nodes, data, modeX);
|
|
415
|
+
const maxX = calculateX(nodes, data, modeX ?? 'edge');
|
|
404
416
|
const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
|
|
405
417
|
const padding = maxY / height * nodePadding;
|
|
406
418
|
const maxYWithPadding = addPadding(nodeArray, padding);
|
|
@@ -411,6 +423,12 @@ function layout(nodes, data, { priority, height, nodePadding, modeX }) {
|
|
|
411
423
|
};
|
|
412
424
|
}
|
|
413
425
|
|
|
426
|
+
function nodeX(node) {
|
|
427
|
+
return node.x ?? 0;
|
|
428
|
+
}
|
|
429
|
+
function nodeY(node) {
|
|
430
|
+
return node.y ?? 0;
|
|
431
|
+
}
|
|
414
432
|
function getAddY(arr, key, index) {
|
|
415
433
|
for (const item of arr){
|
|
416
434
|
if (item.key === key && item.index === index) {
|
|
@@ -429,7 +447,7 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
429
447
|
const { maxX, maxY } = layout(nodes, sankeyData, {
|
|
430
448
|
height: this.chart.canvas.height,
|
|
431
449
|
modeX: this.options.modeX,
|
|
432
|
-
nodePadding: this.options.nodePadding,
|
|
450
|
+
nodePadding: this.options.nodePadding ?? 10,
|
|
433
451
|
priority: !!this.options.priority
|
|
434
452
|
});
|
|
435
453
|
this._maxX = maxX;
|
|
@@ -440,18 +458,18 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
440
458
|
const from = nodes.get(dataPoint.from);
|
|
441
459
|
const to = nodes.get(dataPoint.to);
|
|
442
460
|
if (!from || !to) continue;
|
|
443
|
-
const fromY = (from
|
|
444
|
-
const toY = (to
|
|
461
|
+
const fromY = nodeY(from) + getAddY(from.to, dataPoint.to, i);
|
|
462
|
+
const toY = nodeY(to) + getAddY(to.from, dataPoint.from, i);
|
|
445
463
|
parsed.push({
|
|
446
464
|
_custom: {
|
|
447
465
|
flow: dataPoint.flow,
|
|
448
466
|
from,
|
|
449
467
|
height: yScale.parse(dataPoint.flow, i),
|
|
450
468
|
to,
|
|
451
|
-
x: xScale.parse(to
|
|
469
|
+
x: xScale.parse(nodeX(to), i),
|
|
452
470
|
y: yScale.parse(toY, i)
|
|
453
471
|
},
|
|
454
|
-
x: xScale.parse(from
|
|
472
|
+
x: xScale.parse(nodeX(from), i),
|
|
455
473
|
y: yScale.parse(fromY, i)
|
|
456
474
|
});
|
|
457
475
|
}
|
|
@@ -490,7 +508,9 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
490
508
|
y2: yScale.getPixelForValue(custom.y)
|
|
491
509
|
}, mode);
|
|
492
510
|
}
|
|
493
|
-
|
|
511
|
+
if (sharedOptions) {
|
|
512
|
+
this.updateSharedOptions(sharedOptions, mode, firstOpts);
|
|
513
|
+
}
|
|
494
514
|
}
|
|
495
515
|
_drawLabels() {
|
|
496
516
|
const ctx = this.chart.ctx;
|
|
@@ -505,10 +525,10 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
505
525
|
ctx.save();
|
|
506
526
|
const chartArea = this.chart.chartArea;
|
|
507
527
|
for (const node of nodes.values()){
|
|
508
|
-
const x = xScale.getPixelForValue(node
|
|
509
|
-
const y = yScale.getPixelForValue(node
|
|
528
|
+
const x = xScale.getPixelForValue(nodeX(node));
|
|
529
|
+
const y = yScale.getPixelForValue(nodeY(node));
|
|
510
530
|
const max = Math[size](node.in || node.out, node.out || node.in);
|
|
511
|
-
const height = Math.abs(yScale.getPixelForValue(node
|
|
531
|
+
const height = Math.abs(yScale.getPixelForValue(nodeY(node) + max) - y);
|
|
512
532
|
const label = labels?.[node.key] ?? node.key;
|
|
513
533
|
let textX = x;
|
|
514
534
|
ctx.fillStyle = options.color ?? 'black';
|
|
@@ -525,7 +545,7 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
525
545
|
ctx.restore();
|
|
526
546
|
}
|
|
527
547
|
_drawLabel(label, y, height, ctx, textX) {
|
|
528
|
-
const font = helpers.toFont(this.options.font
|
|
548
|
+
const font = helpers.toFont(this.options.font ?? this.chart.options.font ?? chart_js.Chart.defaults.font);
|
|
529
549
|
const lines = toTextLines(label);
|
|
530
550
|
const lineCount = lines.length;
|
|
531
551
|
const middle = y + height / 2;
|
|
@@ -554,10 +574,11 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
554
574
|
}
|
|
555
575
|
for (const node of nodes.values()){
|
|
556
576
|
ctx.fillStyle = node.color ?? 'black';
|
|
557
|
-
|
|
558
|
-
const
|
|
577
|
+
if (!xScale || !yScale) return;
|
|
578
|
+
const x = xScale.getPixelForValue(nodeX(node));
|
|
579
|
+
const y = yScale.getPixelForValue(nodeY(node));
|
|
559
580
|
const max = Math[sizeMethod](node.in || node.out, node.out || node.in);
|
|
560
|
-
const height = Math.abs(yScale.getPixelForValue(node
|
|
581
|
+
const height = Math.abs(yScale.getPixelForValue(nodeY(node) + max) - y);
|
|
561
582
|
if (borderWidth) {
|
|
562
583
|
ctx.strokeRect(x, y, nodeWidth, height);
|
|
563
584
|
}
|
|
@@ -571,13 +592,19 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
571
592
|
const active = [];
|
|
572
593
|
for(let i = 0, ilen = data.length; i < ilen; ++i){
|
|
573
594
|
const flow = data[i];
|
|
574
|
-
flow.from
|
|
595
|
+
if (!flow.from || !flow.to) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
flow.from.color = flow.options.colorFrom;
|
|
575
599
|
flow.to.color = flow.options.colorTo;
|
|
576
600
|
if (flow.active) {
|
|
577
601
|
active.push(flow);
|
|
578
602
|
}
|
|
579
603
|
}
|
|
580
604
|
for (const flow of active){
|
|
605
|
+
if (!flow.from || !flow.to) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
581
608
|
flow.from.color = flow.options.colorFrom;
|
|
582
609
|
flow.to.color = flow.options.colorTo;
|
|
583
610
|
}
|
|
@@ -587,6 +614,9 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
587
614
|
}
|
|
588
615
|
this._drawLabels();
|
|
589
616
|
}
|
|
617
|
+
constructor(...args){
|
|
618
|
+
super(...args), this._nodes = new Map(), this._maxX = 0, this._maxY = 0;
|
|
619
|
+
}
|
|
590
620
|
}
|
|
591
621
|
SankeyController.id = 'sankey';
|
|
592
622
|
SankeyController.defaults = {
|
|
@@ -677,7 +707,7 @@ SankeyController.overrides = {
|
|
|
677
707
|
callbacks: {
|
|
678
708
|
label (context) {
|
|
679
709
|
const parsedCustom = context.parsed._custom;
|
|
680
|
-
return parsedCustom.from.key
|
|
710
|
+
return `${parsedCustom.from.key} -> ${parsedCustom.to.key}: ${parsedCustom.flow}`;
|
|
681
711
|
},
|
|
682
712
|
title () {
|
|
683
713
|
return '';
|
|
@@ -729,6 +759,7 @@ const pointInLine = (p1, p2, t)=>({
|
|
|
729
759
|
});
|
|
730
760
|
const applyAlpha = (original, alpha)=>helpers.color(original).alpha(alpha).rgbString();
|
|
731
761
|
const getColorOption = (option, alpha)=>typeof option === 'string' ? applyAlpha(option, alpha) : option;
|
|
762
|
+
const getHoverColorOption = (option)=>typeof option === 'string' ? helpers.getHoverColor(option) : option;
|
|
732
763
|
function setStyle(ctx, { x, x2, options }) {
|
|
733
764
|
let fill = 'black';
|
|
734
765
|
if (options.colorMode === 'from') {
|
|
@@ -828,14 +859,14 @@ class Flow extends chart_js.Element {
|
|
|
828
859
|
y: (y + y2 + height) / 2
|
|
829
860
|
};
|
|
830
861
|
}
|
|
831
|
-
tooltipPosition(useFinalPosition) {
|
|
862
|
+
tooltipPosition(useFinalPosition = false) {
|
|
832
863
|
return this.getCenterPoint(useFinalPosition);
|
|
833
864
|
}
|
|
834
865
|
getRange(axis) {
|
|
835
866
|
return axis === 'x' ? this.width / 2 : this.height / 2;
|
|
836
867
|
}
|
|
837
868
|
constructor(cfg){
|
|
838
|
-
super();
|
|
869
|
+
super(), this.x2 = 0, this.y2 = 0, this.width = 0, this.height = 0, this.progress = 1;
|
|
839
870
|
if (cfg) {
|
|
840
871
|
Object.assign(this, cfg);
|
|
841
872
|
}
|
|
@@ -847,8 +878,8 @@ Flow.defaults = {
|
|
|
847
878
|
colorFrom: 'red',
|
|
848
879
|
colorMode: 'gradient',
|
|
849
880
|
colorTo: 'green',
|
|
850
|
-
hoverColorFrom: (_ctx, options)=>
|
|
851
|
-
hoverColorTo: (_ctx, options)=>
|
|
881
|
+
hoverColorFrom: (_ctx, options)=>getHoverColorOption(options.colorFrom),
|
|
882
|
+
hoverColorTo: (_ctx, options)=>getHoverColorOption(options.colorTo)
|
|
852
883
|
};
|
|
853
884
|
Flow.descriptors = {
|
|
854
885
|
_scriptable: true
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.14.
|
|
2
|
+
* chartjs-chart-sankey v0.14.4
|
|
3
3
|
* https://chartjs-chart-sankey.pages.dev/
|
|
4
4
|
* (c) 2026 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
6
6
|
*/
|
|
7
|
-
import { DatasetController, Element } from 'chart.js';
|
|
7
|
+
import { DatasetController, Chart, Element } from 'chart.js';
|
|
8
8
|
import { toFont, valueOrDefault, getHoverColor, color } from 'chart.js/helpers';
|
|
9
9
|
|
|
10
10
|
const defined = (x)=>x !== undefined;
|
|
@@ -123,6 +123,12 @@ function buildNodesFromData(data, { size, priority, column }) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
const SMALL_VALUE = 1e-6;
|
|
126
|
+
function nodeX$1(node) {
|
|
127
|
+
return node.x ?? 0;
|
|
128
|
+
}
|
|
129
|
+
function nodeY$1(node) {
|
|
130
|
+
return node.y ?? 0;
|
|
131
|
+
}
|
|
126
132
|
const getAllKeysForward = (nodes, visited = new Set())=>{
|
|
127
133
|
const keys = [];
|
|
128
134
|
for (const node of nodes){
|
|
@@ -180,7 +186,7 @@ function calculateX(nodeMap, data, mode) {
|
|
|
180
186
|
x++;
|
|
181
187
|
}
|
|
182
188
|
}
|
|
183
|
-
const maxX = allNodes.reduce((max, node)=>Math.max(max, node
|
|
189
|
+
const maxX = allNodes.reduce((max, node)=>Math.max(max, nodeX$1(node)), 0);
|
|
184
190
|
if (mode === 'edge') {
|
|
185
191
|
const from = new Set(data.map((dataPoint)=>dataPoint.from));
|
|
186
192
|
allKeys.filter((key)=>!from.has(key)).forEach((key)=>{
|
|
@@ -220,7 +226,7 @@ function processFrom(node, y) {
|
|
|
220
226
|
}
|
|
221
227
|
y = Math.max(n.y + n.out, y);
|
|
222
228
|
}
|
|
223
|
-
return node
|
|
229
|
+
return nodeY$1(node) + node.size;
|
|
224
230
|
}
|
|
225
231
|
function processTo(node, y) {
|
|
226
232
|
if (!node.to.length) return y;
|
|
@@ -233,7 +239,7 @@ function processTo(node, y) {
|
|
|
233
239
|
}
|
|
234
240
|
y = Math.max(n.y + Math.max(n.in, n.out), y);
|
|
235
241
|
}
|
|
236
|
-
return node
|
|
242
|
+
return nodeY$1(node) + node.size;
|
|
237
243
|
}
|
|
238
244
|
function setOrGetY(node, value) {
|
|
239
245
|
if (defined(node.y)) {
|
|
@@ -247,9 +253,9 @@ function processRest(nodeArray, maxX) {
|
|
|
247
253
|
const rightNodes = nodeArray.filter((node)=>node.x === maxX);
|
|
248
254
|
const leftToDo = leftNodes.filter((node)=>!defined(node.y));
|
|
249
255
|
const rightToDo = rightNodes.filter((node)=>!defined(node.y));
|
|
250
|
-
const centerToDo = nodeArray.filter((node)=>node
|
|
251
|
-
let leftY = leftNodes.reduce((acc, cur)=>Math.max(acc, cur
|
|
252
|
-
let rightY = rightNodes.reduce((acc, cur)=>Math.max(acc, cur
|
|
256
|
+
const centerToDo = nodeArray.filter((node)=>nodeX$1(node) > 0 && nodeX$1(node) < maxX && !defined(node.y));
|
|
257
|
+
let leftY = leftNodes.reduce((acc, cur)=>Math.max(acc, nodeY$1(cur) + cur.out || 0), 0) + SMALL_VALUE;
|
|
258
|
+
let rightY = rightNodes.reduce((acc, cur)=>Math.max(acc, nodeY$1(cur) + cur.in || 0), 0) + SMALL_VALUE;
|
|
253
259
|
let centerY = 0;
|
|
254
260
|
if (leftY >= rightY) {
|
|
255
261
|
leftToDo.forEach((node)=>{
|
|
@@ -270,7 +276,7 @@ function processRest(nodeArray, maxX) {
|
|
|
270
276
|
});
|
|
271
277
|
}
|
|
272
278
|
centerToDo.forEach((node)=>{
|
|
273
|
-
let y = nodeArray.filter((n)=>n
|
|
279
|
+
let y = nodeArray.filter((n)=>nodeX$1(n) === nodeX$1(node) && defined(n.y)).reduce((acc, cur)=>Math.max(acc, nodeY$1(cur) + Math.max(cur.in, cur.out)), 0);
|
|
274
280
|
y = setOrGetY(node, y);
|
|
275
281
|
y = Math.max(y + node.in, processFrom(node, y));
|
|
276
282
|
y = Math.max(y + node.out, processTo(node, y));
|
|
@@ -281,25 +287,29 @@ function processRest(nodeArray, maxX) {
|
|
|
281
287
|
const fixTop = (nodeArray, maxX)=>{
|
|
282
288
|
let maxY = 0;
|
|
283
289
|
for(let x = 0; x <= maxX; x++){
|
|
284
|
-
const nodes = nodeArray.filter((n)=>n
|
|
290
|
+
const nodes = nodeArray.filter((n)=>nodeX$1(n) === x).sort((a, b)=>nodeY$1(a) - nodeY$1(b));
|
|
285
291
|
let minY = 0;
|
|
286
292
|
for (const node of nodes){
|
|
287
|
-
if (node
|
|
288
|
-
minY = node
|
|
293
|
+
if (nodeY$1(node) < minY) node.y = minY;
|
|
294
|
+
minY = nodeY$1(node) + node.size;
|
|
289
295
|
}
|
|
290
296
|
maxY = Math.max(maxY, minY);
|
|
291
297
|
}
|
|
292
298
|
return maxY;
|
|
293
299
|
};
|
|
294
300
|
const findStartNode = (nodeArray, maxX)=>{
|
|
295
|
-
const
|
|
301
|
+
const sorted = [
|
|
296
302
|
...nodeArray
|
|
297
|
-
].sort((a, b)=>a.size - b.size)
|
|
303
|
+
].sort((a, b)=>a.size - b.size);
|
|
304
|
+
const largest = sorted[sorted.length - 1];
|
|
305
|
+
const size = largest.size;
|
|
298
306
|
const biggest = nodeArray.filter((n)=>n.size === size);
|
|
299
|
-
|
|
300
|
-
biggest.
|
|
301
|
-
|
|
302
|
-
if (
|
|
307
|
+
const first = biggest[0];
|
|
308
|
+
if (biggest.length === 1) return first;
|
|
309
|
+
biggest.sort((a, b)=>nodeX$1(a) - nodeX$1(b));
|
|
310
|
+
if (nodeX$1(first) === 0) return first;
|
|
311
|
+
const last = biggest[biggest.length - 1];
|
|
312
|
+
if (nodeX$1(last) === maxX) return last;
|
|
303
313
|
const mid = Math.floor(biggest.length / 2);
|
|
304
314
|
return biggest[mid];
|
|
305
315
|
};
|
|
@@ -317,8 +327,8 @@ function calculateYUsingPriority(nodeArray, maxX) {
|
|
|
317
327
|
let nextYStart = 0;
|
|
318
328
|
for(let x = 0; x <= maxX; x++){
|
|
319
329
|
let y = nextYStart;
|
|
320
|
-
const nodes = nodeArray.filter((node)=>node
|
|
321
|
-
nextYStart = nodes.length ? nodes[0].to.filter((to)=>to.node
|
|
330
|
+
const nodes = nodeArray.filter((node)=>nodeX$1(node) === x).sort((a, b)=>(a.priority ?? 0) - (b.priority ?? 0));
|
|
331
|
+
nextYStart = nodes.length ? nodes[0].to.filter((to)=>nodeX$1(to.node) > x + 1).reduce((acc, cur)=>acc + cur.flow, 0) || 0 : 0;
|
|
322
332
|
for (const node of nodes){
|
|
323
333
|
node.y = y;
|
|
324
334
|
y += Math.max(node.out, node.in);
|
|
@@ -328,9 +338,9 @@ function calculateYUsingPriority(nodeArray, maxX) {
|
|
|
328
338
|
return maxY;
|
|
329
339
|
}
|
|
330
340
|
const nodeByXYSize = (a, b)=>{
|
|
331
|
-
if (a
|
|
332
|
-
if (a
|
|
333
|
-
return a
|
|
341
|
+
if (nodeX$1(a) !== nodeX$1(b)) return nodeX$1(a) - nodeX$1(b);
|
|
342
|
+
if (nodeY$1(a) === nodeY$1(b)) return a.size - b.size;
|
|
343
|
+
return nodeY$1(a) - nodeY$1(b);
|
|
334
344
|
};
|
|
335
345
|
function addPadding(nodeArray, padding) {
|
|
336
346
|
let maxY = 0;
|
|
@@ -338,31 +348,33 @@ const nodeByXYSize = (a, b)=>{
|
|
|
338
348
|
const grid = [];
|
|
339
349
|
const getColIndex = (x)=>{
|
|
340
350
|
if (!columnXs.has(x)) {
|
|
341
|
-
|
|
351
|
+
const index = grid.length;
|
|
352
|
+
columnXs.set(x, index);
|
|
342
353
|
grid.push([]);
|
|
354
|
+
return index;
|
|
343
355
|
}
|
|
344
|
-
return columnXs.get(x);
|
|
356
|
+
return columnXs.get(x) ?? 0;
|
|
345
357
|
};
|
|
346
358
|
nodeArray.sort(nodeByXYSize);
|
|
347
359
|
for (const node of nodeArray){
|
|
348
|
-
const colIdx = getColIndex(node
|
|
349
|
-
const column = grid[colIdx];
|
|
350
|
-
if (node
|
|
351
|
-
column.push(node
|
|
360
|
+
const colIdx = getColIndex(nodeX$1(node));
|
|
361
|
+
const column = grid[colIdx] ?? [];
|
|
362
|
+
if (nodeY$1(node)) {
|
|
363
|
+
column.push(nodeY$1(node));
|
|
352
364
|
let paddings = column.length;
|
|
353
365
|
if (node.in) {
|
|
354
366
|
for(let col = 0; col < colIdx; col++){
|
|
355
|
-
const otherColumn = grid[col];
|
|
367
|
+
const otherColumn = grid[col] ?? [];
|
|
356
368
|
for(let row = 0; row < otherColumn.length; row++){
|
|
357
|
-
if (otherColumn[row] > node
|
|
369
|
+
if (otherColumn[row] > nodeY$1(node)) break;
|
|
358
370
|
paddings = Math.max(row + 1, paddings);
|
|
359
371
|
}
|
|
360
372
|
}
|
|
361
|
-
while(column.length < paddings)column.push(node
|
|
373
|
+
while(column.length < paddings)column.push(nodeY$1(node));
|
|
362
374
|
}
|
|
363
|
-
node.y
|
|
375
|
+
node.y = nodeY$1(node) + paddings * padding;
|
|
364
376
|
}
|
|
365
|
-
maxY = Math.max(maxY, node
|
|
377
|
+
maxY = Math.max(maxY, nodeY$1(node) + Math.max(node.in, node.out));
|
|
366
378
|
}
|
|
367
379
|
return maxY;
|
|
368
380
|
}
|
|
@@ -373,7 +385,7 @@ function sortFlows(nodeArray) {
|
|
|
373
385
|
const overlapTo = nodeSize < node.out;
|
|
374
386
|
let addY = 0;
|
|
375
387
|
let len = node.from.length;
|
|
376
|
-
node.from.sort((a, b)=>a.node
|
|
388
|
+
node.from.sort((a, b)=>nodeY$1(a.node) + a.node.out / 2 - (nodeY$1(b.node) + b.node.out / 2)).forEach((flow, idx)=>{
|
|
377
389
|
if (overlapFrom) {
|
|
378
390
|
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
379
391
|
} else {
|
|
@@ -383,7 +395,7 @@ function sortFlows(nodeArray) {
|
|
|
383
395
|
});
|
|
384
396
|
addY = 0;
|
|
385
397
|
len = node.to.length;
|
|
386
|
-
node.to.sort((a, b)=>a.node
|
|
398
|
+
node.to.sort((a, b)=>nodeY$1(a.node) + a.node.in / 2 - (nodeY$1(b.node) + b.node.in / 2)).forEach((flow, idx)=>{
|
|
387
399
|
if (overlapTo) {
|
|
388
400
|
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
389
401
|
} else {
|
|
@@ -397,7 +409,7 @@ function layout(nodes, data, { priority, height, nodePadding, modeX }) {
|
|
|
397
409
|
const nodeArray = [
|
|
398
410
|
...nodes.values()
|
|
399
411
|
];
|
|
400
|
-
const maxX = calculateX(nodes, data, modeX);
|
|
412
|
+
const maxX = calculateX(nodes, data, modeX ?? 'edge');
|
|
401
413
|
const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
|
|
402
414
|
const padding = maxY / height * nodePadding;
|
|
403
415
|
const maxYWithPadding = addPadding(nodeArray, padding);
|
|
@@ -408,6 +420,12 @@ function layout(nodes, data, { priority, height, nodePadding, modeX }) {
|
|
|
408
420
|
};
|
|
409
421
|
}
|
|
410
422
|
|
|
423
|
+
function nodeX(node) {
|
|
424
|
+
return node.x ?? 0;
|
|
425
|
+
}
|
|
426
|
+
function nodeY(node) {
|
|
427
|
+
return node.y ?? 0;
|
|
428
|
+
}
|
|
411
429
|
function getAddY(arr, key, index) {
|
|
412
430
|
for (const item of arr){
|
|
413
431
|
if (item.key === key && item.index === index) {
|
|
@@ -426,7 +444,7 @@ class SankeyController extends DatasetController {
|
|
|
426
444
|
const { maxX, maxY } = layout(nodes, sankeyData, {
|
|
427
445
|
height: this.chart.canvas.height,
|
|
428
446
|
modeX: this.options.modeX,
|
|
429
|
-
nodePadding: this.options.nodePadding,
|
|
447
|
+
nodePadding: this.options.nodePadding ?? 10,
|
|
430
448
|
priority: !!this.options.priority
|
|
431
449
|
});
|
|
432
450
|
this._maxX = maxX;
|
|
@@ -437,18 +455,18 @@ class SankeyController extends DatasetController {
|
|
|
437
455
|
const from = nodes.get(dataPoint.from);
|
|
438
456
|
const to = nodes.get(dataPoint.to);
|
|
439
457
|
if (!from || !to) continue;
|
|
440
|
-
const fromY = (from
|
|
441
|
-
const toY = (to
|
|
458
|
+
const fromY = nodeY(from) + getAddY(from.to, dataPoint.to, i);
|
|
459
|
+
const toY = nodeY(to) + getAddY(to.from, dataPoint.from, i);
|
|
442
460
|
parsed.push({
|
|
443
461
|
_custom: {
|
|
444
462
|
flow: dataPoint.flow,
|
|
445
463
|
from,
|
|
446
464
|
height: yScale.parse(dataPoint.flow, i),
|
|
447
465
|
to,
|
|
448
|
-
x: xScale.parse(to
|
|
466
|
+
x: xScale.parse(nodeX(to), i),
|
|
449
467
|
y: yScale.parse(toY, i)
|
|
450
468
|
},
|
|
451
|
-
x: xScale.parse(from
|
|
469
|
+
x: xScale.parse(nodeX(from), i),
|
|
452
470
|
y: yScale.parse(fromY, i)
|
|
453
471
|
});
|
|
454
472
|
}
|
|
@@ -487,7 +505,9 @@ class SankeyController extends DatasetController {
|
|
|
487
505
|
y2: yScale.getPixelForValue(custom.y)
|
|
488
506
|
}, mode);
|
|
489
507
|
}
|
|
490
|
-
|
|
508
|
+
if (sharedOptions) {
|
|
509
|
+
this.updateSharedOptions(sharedOptions, mode, firstOpts);
|
|
510
|
+
}
|
|
491
511
|
}
|
|
492
512
|
_drawLabels() {
|
|
493
513
|
const ctx = this.chart.ctx;
|
|
@@ -502,10 +522,10 @@ class SankeyController extends DatasetController {
|
|
|
502
522
|
ctx.save();
|
|
503
523
|
const chartArea = this.chart.chartArea;
|
|
504
524
|
for (const node of nodes.values()){
|
|
505
|
-
const x = xScale.getPixelForValue(node
|
|
506
|
-
const y = yScale.getPixelForValue(node
|
|
525
|
+
const x = xScale.getPixelForValue(nodeX(node));
|
|
526
|
+
const y = yScale.getPixelForValue(nodeY(node));
|
|
507
527
|
const max = Math[size](node.in || node.out, node.out || node.in);
|
|
508
|
-
const height = Math.abs(yScale.getPixelForValue(node
|
|
528
|
+
const height = Math.abs(yScale.getPixelForValue(nodeY(node) + max) - y);
|
|
509
529
|
const label = labels?.[node.key] ?? node.key;
|
|
510
530
|
let textX = x;
|
|
511
531
|
ctx.fillStyle = options.color ?? 'black';
|
|
@@ -522,7 +542,7 @@ class SankeyController extends DatasetController {
|
|
|
522
542
|
ctx.restore();
|
|
523
543
|
}
|
|
524
544
|
_drawLabel(label, y, height, ctx, textX) {
|
|
525
|
-
const font = toFont(this.options.font
|
|
545
|
+
const font = toFont(this.options.font ?? this.chart.options.font ?? Chart.defaults.font);
|
|
526
546
|
const lines = toTextLines(label);
|
|
527
547
|
const lineCount = lines.length;
|
|
528
548
|
const middle = y + height / 2;
|
|
@@ -551,10 +571,11 @@ class SankeyController extends DatasetController {
|
|
|
551
571
|
}
|
|
552
572
|
for (const node of nodes.values()){
|
|
553
573
|
ctx.fillStyle = node.color ?? 'black';
|
|
554
|
-
|
|
555
|
-
const
|
|
574
|
+
if (!xScale || !yScale) return;
|
|
575
|
+
const x = xScale.getPixelForValue(nodeX(node));
|
|
576
|
+
const y = yScale.getPixelForValue(nodeY(node));
|
|
556
577
|
const max = Math[sizeMethod](node.in || node.out, node.out || node.in);
|
|
557
|
-
const height = Math.abs(yScale.getPixelForValue(node
|
|
578
|
+
const height = Math.abs(yScale.getPixelForValue(nodeY(node) + max) - y);
|
|
558
579
|
if (borderWidth) {
|
|
559
580
|
ctx.strokeRect(x, y, nodeWidth, height);
|
|
560
581
|
}
|
|
@@ -568,13 +589,19 @@ class SankeyController extends DatasetController {
|
|
|
568
589
|
const active = [];
|
|
569
590
|
for(let i = 0, ilen = data.length; i < ilen; ++i){
|
|
570
591
|
const flow = data[i];
|
|
571
|
-
flow.from
|
|
592
|
+
if (!flow.from || !flow.to) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
flow.from.color = flow.options.colorFrom;
|
|
572
596
|
flow.to.color = flow.options.colorTo;
|
|
573
597
|
if (flow.active) {
|
|
574
598
|
active.push(flow);
|
|
575
599
|
}
|
|
576
600
|
}
|
|
577
601
|
for (const flow of active){
|
|
602
|
+
if (!flow.from || !flow.to) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
578
605
|
flow.from.color = flow.options.colorFrom;
|
|
579
606
|
flow.to.color = flow.options.colorTo;
|
|
580
607
|
}
|
|
@@ -584,6 +611,9 @@ class SankeyController extends DatasetController {
|
|
|
584
611
|
}
|
|
585
612
|
this._drawLabels();
|
|
586
613
|
}
|
|
614
|
+
constructor(...args){
|
|
615
|
+
super(...args), this._nodes = new Map(), this._maxX = 0, this._maxY = 0;
|
|
616
|
+
}
|
|
587
617
|
}
|
|
588
618
|
SankeyController.id = 'sankey';
|
|
589
619
|
SankeyController.defaults = {
|
|
@@ -674,7 +704,7 @@ SankeyController.overrides = {
|
|
|
674
704
|
callbacks: {
|
|
675
705
|
label (context) {
|
|
676
706
|
const parsedCustom = context.parsed._custom;
|
|
677
|
-
return parsedCustom.from.key
|
|
707
|
+
return `${parsedCustom.from.key} -> ${parsedCustom.to.key}: ${parsedCustom.flow}`;
|
|
678
708
|
},
|
|
679
709
|
title () {
|
|
680
710
|
return '';
|
|
@@ -726,6 +756,7 @@ const pointInLine = (p1, p2, t)=>({
|
|
|
726
756
|
});
|
|
727
757
|
const applyAlpha = (original, alpha)=>color(original).alpha(alpha).rgbString();
|
|
728
758
|
const getColorOption = (option, alpha)=>typeof option === 'string' ? applyAlpha(option, alpha) : option;
|
|
759
|
+
const getHoverColorOption = (option)=>typeof option === 'string' ? getHoverColor(option) : option;
|
|
729
760
|
function setStyle(ctx, { x, x2, options }) {
|
|
730
761
|
let fill = 'black';
|
|
731
762
|
if (options.colorMode === 'from') {
|
|
@@ -825,14 +856,14 @@ class Flow extends Element {
|
|
|
825
856
|
y: (y + y2 + height) / 2
|
|
826
857
|
};
|
|
827
858
|
}
|
|
828
|
-
tooltipPosition(useFinalPosition) {
|
|
859
|
+
tooltipPosition(useFinalPosition = false) {
|
|
829
860
|
return this.getCenterPoint(useFinalPosition);
|
|
830
861
|
}
|
|
831
862
|
getRange(axis) {
|
|
832
863
|
return axis === 'x' ? this.width / 2 : this.height / 2;
|
|
833
864
|
}
|
|
834
865
|
constructor(cfg){
|
|
835
|
-
super();
|
|
866
|
+
super(), this.x2 = 0, this.y2 = 0, this.width = 0, this.height = 0, this.progress = 1;
|
|
836
867
|
if (cfg) {
|
|
837
868
|
Object.assign(this, cfg);
|
|
838
869
|
}
|
|
@@ -844,8 +875,8 @@ Flow.defaults = {
|
|
|
844
875
|
colorFrom: 'red',
|
|
845
876
|
colorMode: 'gradient',
|
|
846
877
|
colorTo: 'green',
|
|
847
|
-
hoverColorFrom: (_ctx, options)=>
|
|
848
|
-
hoverColorTo: (_ctx, options)=>
|
|
878
|
+
hoverColorFrom: (_ctx, options)=>getHoverColorOption(options.colorFrom),
|
|
879
|
+
hoverColorTo: (_ctx, options)=>getHoverColorOption(options.colorTo)
|
|
849
880
|
};
|
|
850
881
|
Flow.descriptors = {
|
|
851
882
|
_scriptable: true
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.14.
|
|
2
|
+
* chartjs-chart-sankey v0.14.4
|
|
3
3
|
* https://chartjs-chart-sankey.pages.dev/
|
|
4
4
|
* (c) 2026 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
6
6
|
*/
|
|
7
|
-
!function(t,o){"object"==typeof exports&&"undefined"!=typeof module?o(exports,require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["exports","chart.js","chart.js/helpers"],o):o((t="undefined"!=typeof globalThis?globalThis:t||self)["chartjs-chart-sankey"]={},t.Chart,t.Chart.helpers)}(this,function(t,o,e){"use strict";const r=t=>void 0!==t;function n(t){return t&&-1!==["min","max"].indexOf(t)?t:"max"}const s=(t,o)=>o.flow===t.flow?t.index-o.index:o.flow-t.flow;function i(t,{size:o,priority:e,column:r}){const i=new Map;for(let o=0;o<t.length;o++){const{from:e,to:r,flow:n}=t[o],s=i.get(e)??{from:[],in:0,key:e,out:0,size:0,to:[]},a=(e===r?s:i.get(r))??{from:[],in:0,key:r,out:0,size:0,to:[]};s.out+=n,s.to.push({addY:0,flow:n,index:o,key:r,node:a}),1===s.to.length&&i.set(e,s),a.in+=n,a.from.push({addY:0,flow:n,index:o,key:e,node:s}),1===a.from.length&&i.set(r,a)}return((t,o)=>{const e=n(o);for(const o of t.values())o.from.sort(s),o.to.sort(s),o.size=Math[e](o.in||o.out,o.out||o.in)})(i,o),((t,o)=>{if(o)for(const e of t.values())e.key in o&&(e.priority=o[e.key])})(i,e),((t,o)=>{if(o)for(const e of t.values())e.key in o&&(e.column=!0,e.x=o[e.key])})(i,r),i}const a=1e-6,l=(t,o=new Set)=>{const e=[];for(const r of t)o.has(r.key)||(o.add(r.key),e.push(r.key,...l(r.to.map(t=>t.node),o)));return e},c=(t,o)=>{const e=o.filter(t=>0===t.from.length),r=e.map(t=>t.key),n=l(e),s=new Set(n);for(const o of t)s.has(o.from)||s.has(o.to)||(r.push(o.from),s.add(o.from)),s.add(o.to);return r},h=(t,o)=>{const e=new Set(t.filter(t=>o.has(t.from)).map(t=>t.to)),r=[...o],n=r.filter(t=>!e.has(t));return n.length?n:r.slice(0,1)};let f=-1;function d(t,o,e=function(){return f=f<100?f+1:0,f}()){let r=0;for(const n of t)n.node._visited!==e&&(n.node._visited=e,r+=n.node[o].length+d(n.node[o],o,e));return r}const u=t=>(o,e)=>d(o.node[t],t)-d(e.node[t],t)||o.node[t].length-e.node[t].length;function y(t,o){if(!t.from.length)return o;t.from.sort(u("from"));for(const e of t.from){const t=e.node;r(t.y)||(t.y=o,y(t,o?o+a:0)),o=Math.max(t.y+t.out,o)}return t.y+t.size}function p(t,o){if(!t.to.length)return o;t.to.sort(u("to"));for(const e of t.to){const t=e.node;r(t.y)||(t.y=o,p(t,o?o+a:0)),o=Math.max(t.y+Math.max(t.in,t.out),o)}return t.y+t.size}function x(t,o){return r(t.y)?t.y:(t.y=o,o)}function m(t,o){if(!t.length)return 0;const e=((t,o)=>{const e=[...t].sort((t,o)=>t.size-o.size).pop().size,r=t.filter(t=>t.size===e);return 1===r.length?r[0]:(r.sort((t,o)=>t.x-o.x),0===r[0].x?r[0]:r[r.length-1].x===o?r.pop():r[Math.floor(r.length/2)])})(t,o);return e.y=0,y(e,0),p(e,0),function(t,o){const e=t.filter(t=>0===t.x),n=t.filter(t=>t.x===o),s=e.filter(t=>!r(t.y)),i=n.filter(t=>!r(t.y)),l=t.filter(t=>t.x>0&&t.x<o&&!r(t.y));let c=e.reduce((t,o)=>Math.max(t,o.y+o.out||0),0)+a,h=n.reduce((t,o)=>Math.max(t,o.y+o.in||0),0)+a,f=0;c>=h?(s.forEach(t=>{c=x(t,c),c=Math.max(c+t.out,p(t,c))}),i.forEach(t=>{h=x(t,h),h=Math.max(h+t.in,y(t,h))})):(s.forEach(t=>{c=x(t,c)}),i.forEach(t=>{h=x(t,h),h=Math.max(h+t.in,y(t,h))})),l.forEach(o=>{let e=t.filter(t=>t.x===o.x&&r(t.y)).reduce((t,o)=>Math.max(t,o.y+Math.max(o.in,o.out)),0);e=x(o,e),e=Math.max(e+o.in,y(o,e)),e=Math.max(e+o.out,p(o,e)),f=Math.max(f,e)}),Math.max(c,h,f)}(t,o),((t,o)=>{let e=0;for(let r=0;r<=o;r++){const o=t.filter(t=>t.x===r).sort((t,o)=>t.y-o.y);let n=0;for(const t of o)t.y<n&&(t.y=n),n=t.y+t.size;e=Math.max(e,n)}return e})(t,o)}const g=(t,o)=>t.x!==o.x?t.x-o.x:t.y===o.y?t.size-o.size:t.y-o.y;function w(t,o,{priority:e,height:n,nodePadding:s,modeX:i}){const a=[...t.values()],l=function(t,o,e){const n=o.filter(t=>t.from!==t.to),s=[...t.keys()],i=[...t.values()],a=new Set(s);let l=0;for(;a.size;){const e=0===l?c(o,i):h(n,a);if(!e.length)throw new Error("Fatal error: Unable to place nodes to columns. Please report this issue.");for(const o of e){const e=t.get(o);e&&!r(e.x)&&(e.x=l),a.delete(o)}a.size&&l++}const f=i.reduce((t,o)=>Math.max(t,o.x),0);if("edge"===e){const e=new Set(o.map(t=>t.from));s.filter(t=>!e.has(t)).forEach(o=>{const e=t.get(o);e&&!e.column&&(e.x=f)})}return f}(t,o,i),f=e?function(t,o){let e=0,r=0;for(let n=0;n<=o;n++){let o=r;const s=t.filter(t=>t.x===n).sort((t,o)=>(t.priority??0)-(o.priority??0));r=s.length&&s[0].to.filter(t=>t.node.x>n+1).reduce((t,o)=>t+o.flow,0)||0;for(const t of s)t.y=o,o+=Math.max(t.out,t.in);e=Math.max(o,e)}return e}(a,l):m(a,l),d=function(t,o){let e=0;const r=new Map,n=[],s=t=>(r.has(t)||(r.set(t,n.length),n.push([])),r.get(t));t.sort(g);for(const r of t){const t=s(r.x),i=n[t];if(r.y){i.push(r.y);let e=i.length;if(r.in){for(let o=0;o<t;o++){const t=n[o];for(let o=0;o<t.length&&!(t[o]>r.y);o++)e=Math.max(o+1,e)}for(;i.length<e;)i.push(r.y)}r.y+=e*o}e=Math.max(e,r.y+Math.max(r.in,r.out))}return e}(a,f/n*s);return function(t){t.forEach(t=>{const o=t.size,e=o<t.in,r=o<t.out;let n=0,s=t.from.length;t.from.sort((t,o)=>t.node.y+t.node.out/2-(o.node.y+o.node.out/2)).forEach((t,r)=>{e?t.addY=r*(o-t.flow)/(s-1):(t.addY=n,n+=t.flow)}),n=0,s=t.to.length,t.to.sort((t,o)=>t.node.y+t.node.in/2-(o.node.y+o.node.in/2)).forEach((t,e)=>{r?t.addY=e*(o-t.flow)/(s-1):(t.addY=n,n+=t.flow)})})}(a),{maxX:l,maxY:d}}function M(t,o,e){for(const r of t)if(r.key===o&&r.index===e)return r.addY;return 0}class b extends o.DatasetController{parseObjectData(t,o,e,r){const n=((t,o)=>{const{from:e="from",to:r="to",flow:n="flow"}=o;return t.map(({[e]:t,[r]:o,[n]:s})=>({flow:s,from:t,to:o}))})(o,this.options.parsing),{xScale:s,yScale:a}=t,l=[],c=i(n,this.options);this._nodes=c;const{maxX:h,maxY:f}=w(c,n,{height:this.chart.canvas.height,modeX:this.options.modeX,nodePadding:this.options.nodePadding,priority:!!this.options.priority});if(this._maxX=h,this._maxY=f,!s||!a)return[];for(let t=0,o=n.length;t<o;++t){const o=n[t],e=c.get(o.from),r=c.get(o.to);if(!e||!r)continue;const i=(e.y??0)+M(e.to,o.to,t),h=(r.y??0)+M(r.from,o.from,t);l.push({_custom:{flow:o.flow,from:e,height:a.parse(o.flow,t),to:r,x:s.parse(r.x,t),y:a.parse(h,t)},x:s.parse(e.x,t),y:a.parse(i,t)})}return l.slice(e,e+r)}getMinMax(t){return{max:t===this._cachedMeta.xScale?this._maxX:this._maxY,min:0}}update(t){const{data:o}=this._cachedMeta;this.updateElements(o,0,o.length,t)}updateElements(t,o,e,r){const{xScale:n,yScale:s}=this._cachedMeta;if(!n||!s)return;const i=this.resolveDataElementOptions(o,r),a=this.getSharedOptions(i),{borderWidth:l,nodeWidth:c=10}=this.options,h=l?l/2+.5:0;for(let i=o;i<o+e;i++){const o=this.getParsed(i),e=o._custom,a=s.getPixelForValue(o.y);this.updateElement(t[i],i,{from:e.from,height:Math.abs(s.getPixelForValue(o.y+e.height)-a),options:this.resolveDataElementOptions(i,r),progress:"reset"===r?0:1,to:e.to,x:n.getPixelForValue(o.x)+c+h,x2:n.getPixelForValue(e.x)-h,y:a,y2:s.getPixelForValue(e.y)},r)}this.updateSharedOptions(a,r,i)}_drawLabels(){const t=this.chart.ctx,o=this.options,e=this._nodes||new Map,r=n(o.size),s=o.borderWidth??1,i=o.nodeWidth??10,a=o.labels,{xScale:l,yScale:c}=this._cachedMeta;if(!l||!c)return;t.save();const h=this.chart.chartArea;for(const n of e.values()){const e=l.getPixelForValue(n.x),f=c.getPixelForValue(n.y),d=Math[r](n.in||n.out,n.out||n.in),u=Math.abs(c.getPixelForValue(n.y+d)-f),y=a?.[n.key]??n.key;let p=e;t.fillStyle=o.color??"black",t.textBaseline="middle",e<h.width/2?(t.textAlign="left",p+=i+s+4):(t.textAlign="right",p-=s+4),this._drawLabel(y,f,u,t,p)}t.restore()}_drawLabel(t,o,r,n,s){const i=e.toFont(this.options.font,this.chart.options.font),a=function(t){if(!t)return[];const o=[],e=Array.isArray(t)?t:[t];for(;e.length;){const t=e.pop();"string"==typeof t?o.unshift(...t.split("\n")):Array.isArray(t)?e.push(...t):t&&o.unshift(`${t}`)}return o}(t),l=a.length,c=o+r/2,h=i.lineHeight,f=e.valueOrDefault(this.options.padding,h/2);if(n.font=i.string,l>1){const t=c-h*l/2+f;for(let o=0;o<l;o++)n.fillText(a[o],s,t+o*h)}else n.fillText(t,s,c)}_drawNodes(){const t=this.chart.ctx,o=this._nodes||new Map,{borderColor:e,borderWidth:r=0,nodeWidth:s=10,size:i}=this.options,a=n(i),{xScale:l,yScale:c}=this._cachedMeta;t.save(),e&&r&&(t.strokeStyle=e,t.lineWidth=r);for(const e of o.values()){t.fillStyle=e.color??"black";const o=l.getPixelForValue(e.x),n=c.getPixelForValue(e.y),i=Math[a](e.in||e.out,e.out||e.in),h=Math.abs(c.getPixelForValue(e.y+i)-n);r&&t.strokeRect(o,n,s,h),t.fillRect(o,n,s,h)}t.restore()}draw(){const t=this.chart.ctx,o=this.getMeta().data??[],e=[];for(let t=0,r=o.length;t<r;++t){const r=o[t];r.from.color=r.options.colorFrom,r.to.color=r.options.colorTo,r.active&&e.push(r)}for(const t of e)t.from.color=t.options.colorFrom,t.to.color=t.options.colorTo;this._drawNodes();for(let e=0,r=o.length;e<r;++e)o[e].draw(t);this._drawLabels()}}b.id="sankey",b.defaults={animations:{colors:{properties:["colorFrom","colorTo"],type:"color"},numbers:{properties:["x","y","x2","y2","height"],type:"number"},progress:{delay:t=>"data"===t.type?500*t.parsed.x+20*t.dataIndex:void 0,duration:t=>"data"===t.type?200*(t.parsed._custom.x-t.parsed.x):void 0,easing:"linear"}},borderColor:"black",borderWidth:1,color:"black",dataElementType:"flow",modeX:"edge",nodePadding:10,nodeWidth:10,transitions:{hide:{animations:{colors:{properties:["colorFrom","colorTo"],to:"transparent",type:"color"}}},show:{animations:{colors:{from:"transparent",properties:["colorFrom","colorTo"],type:"color"}}}}},b.overrides={datasets:{clip:!1,parsing:{flow:"flow",from:"from",to:"to"}},interaction:{intersect:!0,mode:"nearest"},layout:{padding:{bottom:3,left:3,right:13,top:3}},plugins:{legend:{display:!1},tooltip:{callbacks:{label(t){const o=t.parsed._custom;return o.from.key+" -> "+o.to.key+": "+o.flow},title:()=>""}}},scales:{x:{bounds:"data",display:!1,min:0,offset:!1,type:"linear"},y:{bounds:"data",display:!1,min:0,offset:!1,reverse:!0,type:"linear"}}};const k=(t,o,e,r)=>t<e?{cp1:{x:t+(e-t)/3*2,y:o},cp2:{x:t+(e-t)/3,y:r}}:{cp1:{x:t-(t-e)/3,y:0},cp2:{x:e+(t-e)/3,y:0}},v=(t,o,e)=>({x:t.x+e*(o.x-t.x),y:t.y+e*(o.y-t.y)}),P=(t,o)=>e.color(t).alpha(o).rgbString(),F=(t,o)=>"string"==typeof t?P(t,o):t;class S extends o.Element{draw(t){const{x:o,x2:e,y:r,y2:n,height:s,progress:i}=this,{cp1:a,cp2:l}=k(o,r,e,n);0!==i&&(t.save(),i<1&&(t.beginPath(),t.rect(o,Math.min(r,n),(e-o)*i+1,Math.abs(n-r)+s+1),t.clip()),function(t,{x:o,x2:e,options:r}){let n="black";"from"===r.colorMode?n=F(r.colorFrom,r.alpha):"to"===r.colorMode?n=F(r.colorTo,r.alpha):"string"==typeof r.colorFrom&&"string"==typeof r.colorTo&&(n=t.createLinearGradient(o,0,e,0),n.addColorStop(0,P(r.colorFrom,r.alpha)),n.addColorStop(1,P(r.colorTo,r.alpha))),t.fillStyle=n,t.strokeStyle=n,t.lineWidth=.5}(t,this),t.beginPath(),t.moveTo(o,r),t.bezierCurveTo(a.x,a.y,l.x,l.y,e,n),t.lineTo(e,n+s),t.bezierCurveTo(l.x,l.y+s,a.x,a.y+s,o,r+s),t.lineTo(o,r),t.stroke(),t.closePath(),t.fill(),t.restore())}inRange(t,o,e){const{x:r,y:n,x2:s,y2:i,height:a}=this.getProps(["x","y","x2","y2","height"],e);if(t<r||t>s)return!1;const{cp1:l,cp2:c}=k(r,n,s,i),h=(t-r)/(s-r),f={x:s,y:i},d=v({x:r,y:n},l,h),u=v(l,c,h),y=v(c,f,h),p=v(d,u,h),x=v(u,y,h),m=v(p,x,h).y;return o>=m&&o<=m+a}inXRange(t,o){const{x:e,x2:r}=this.getProps(["x","x2"],o);return t>=e&&t<=r}inYRange(t,o){const{y:e,y2:r,height:n}=this.getProps(["y","y2","height"],o),s=Math.min(e,r),i=Math.max(e,r)+n;return t>=s&&t<=i}getCenterPoint(t){const{x:o,y:e,x2:r,y2:n,height:s}=this.getProps(["x","y","x2","y2","height"],t);return{x:(o+r)/2,y:(e+n+s)/2}}tooltipPosition(t){return this.getCenterPoint(t)}getRange(t){return"x"===t?this.width/2:this.height/2}constructor(t){super(),t&&Object.assign(this,t)}}S.id="flow",S.defaults={alpha:.5,colorFrom:"red",colorMode:"gradient",colorTo:"green",hoverColorFrom:(t,o)=>e.getHoverColor(o.colorFrom),hoverColorTo:(t,o)=>e.getHoverColor(o.colorTo)},S.descriptors={_scriptable:!0},o.Chart.register(b,S),t.Flow=S,t.SankeyController=b});
|
|
7
|
+
!function(t,o){"object"==typeof exports&&"undefined"!=typeof module?o(exports,require("chart.js"),require("chart.js/helpers")):"function"==typeof define&&define.amd?define(["exports","chart.js","chart.js/helpers"],o):o((t="undefined"!=typeof globalThis?globalThis:t||self)["chartjs-chart-sankey"]={},t.Chart,t.Chart.helpers)}(this,function(t,o,e){"use strict";const r=t=>void 0!==t;function n(t){return t&&-1!==["min","max"].indexOf(t)?t:"max"}const s=(t,o)=>o.flow===t.flow?t.index-o.index:o.flow-t.flow;function i(t,{size:o,priority:e,column:r}){const i=new Map;for(let o=0;o<t.length;o++){const{from:e,to:r,flow:n}=t[o],s=i.get(e)??{from:[],in:0,key:e,out:0,size:0,to:[]},a=(e===r?s:i.get(r))??{from:[],in:0,key:r,out:0,size:0,to:[]};s.out+=n,s.to.push({addY:0,flow:n,index:o,key:r,node:a}),1===s.to.length&&i.set(e,s),a.in+=n,a.from.push({addY:0,flow:n,index:o,key:e,node:s}),1===a.from.length&&i.set(r,a)}return((t,o)=>{const e=n(o);for(const o of t.values())o.from.sort(s),o.to.sort(s),o.size=Math[e](o.in||o.out,o.out||o.in)})(i,o),((t,o)=>{if(o)for(const e of t.values())e.key in o&&(e.priority=o[e.key])})(i,e),((t,o)=>{if(o)for(const e of t.values())e.key in o&&(e.column=!0,e.x=o[e.key])})(i,r),i}const a=1e-6;function l(t){return t.x??0}function h(t){return t.y??0}const c=(t,o=new Set)=>{const e=[];for(const r of t)o.has(r.key)||(o.add(r.key),e.push(r.key,...c(r.to.map(t=>t.node),o)));return e},f=(t,o)=>{const e=o.filter(t=>0===t.from.length),r=e.map(t=>t.key),n=c(e),s=new Set(n);for(const o of t)s.has(o.from)||s.has(o.to)||(r.push(o.from),s.add(o.from)),s.add(o.to);return r},d=(t,o)=>{const e=new Set(t.filter(t=>o.has(t.from)).map(t=>t.to)),r=[...o],n=r.filter(t=>!e.has(t));return n.length?n:r.slice(0,1)};let u=-1;function p(t,o,e=function(){return u=u<100?u+1:0,u}()){let r=0;for(const n of t)n.node._visited!==e&&(n.node._visited=e,r+=n.node[o].length+p(n.node[o],o,e));return r}const x=t=>(o,e)=>p(o.node[t],t)-p(e.node[t],t)||o.node[t].length-e.node[t].length;function m(t,o){if(!t.from.length)return o;t.from.sort(x("from"));for(const e of t.from){const t=e.node;r(t.y)||(t.y=o,m(t,o?o+a:0)),o=Math.max(t.y+t.out,o)}return h(t)+t.size}function y(t,o){if(!t.to.length)return o;t.to.sort(x("to"));for(const e of t.to){const t=e.node;r(t.y)||(t.y=o,y(t,o?o+a:0)),o=Math.max(t.y+Math.max(t.in,t.out),o)}return h(t)+t.size}function g(t,o){return r(t.y)?t.y:(t.y=o,o)}function w(t,o){if(!t.length)return 0;const e=((t,o)=>{const e=[...t].sort((t,o)=>t.size-o.size),r=e[e.length-1].size,n=t.filter(t=>t.size===r),s=n[0];if(1===n.length)return s;if(n.sort((t,o)=>l(t)-l(o)),0===l(s))return s;const i=n[n.length-1];return l(i)===o?i:n[Math.floor(n.length/2)]})(t,o);return e.y=0,m(e,0),y(e,0),function(t,o){const e=t.filter(t=>0===t.x),n=t.filter(t=>t.x===o),s=e.filter(t=>!r(t.y)),i=n.filter(t=>!r(t.y)),c=t.filter(t=>l(t)>0&&l(t)<o&&!r(t.y));let f=e.reduce((t,o)=>Math.max(t,h(o)+o.out||0),0)+a,d=n.reduce((t,o)=>Math.max(t,h(o)+o.in||0),0)+a,u=0;f>=d?(s.forEach(t=>{f=g(t,f),f=Math.max(f+t.out,y(t,f))}),i.forEach(t=>{d=g(t,d),d=Math.max(d+t.in,m(t,d))})):(s.forEach(t=>{f=g(t,f)}),i.forEach(t=>{d=g(t,d),d=Math.max(d+t.in,m(t,d))})),c.forEach(o=>{let e=t.filter(t=>l(t)===l(o)&&r(t.y)).reduce((t,o)=>Math.max(t,h(o)+Math.max(o.in,o.out)),0);e=g(o,e),e=Math.max(e+o.in,m(o,e)),e=Math.max(e+o.out,y(o,e)),u=Math.max(u,e)}),Math.max(f,d,u)}(t,o),((t,o)=>{let e=0;for(let r=0;r<=o;r++){const o=t.filter(t=>l(t)===r).sort((t,o)=>h(t)-h(o));let n=0;for(const t of o)h(t)<n&&(t.y=n),n=h(t)+t.size;e=Math.max(e,n)}return e})(t,o)}const M=(t,o)=>l(t)!==l(o)?l(t)-l(o):h(t)===h(o)?t.size-o.size:h(t)-h(o);function b(t,o,{priority:e,height:n,nodePadding:s,modeX:i}){const a=[...t.values()],c=function(t,o,e){const n=o.filter(t=>t.from!==t.to),s=[...t.keys()],i=[...t.values()],a=new Set(s);let h=0;for(;a.size;){const e=0===h?f(o,i):d(n,a);if(!e.length)throw new Error("Fatal error: Unable to place nodes to columns. Please report this issue.");for(const o of e){const e=t.get(o);e&&!r(e.x)&&(e.x=h),a.delete(o)}a.size&&h++}const c=i.reduce((t,o)=>Math.max(t,l(o)),0);if("edge"===e){const e=new Set(o.map(t=>t.from));s.filter(t=>!e.has(t)).forEach(o=>{const e=t.get(o);e&&!e.column&&(e.x=c)})}return c}(t,o,i??"edge"),u=e?function(t,o){let e=0,r=0;for(let n=0;n<=o;n++){let o=r;const s=t.filter(t=>l(t)===n).sort((t,o)=>(t.priority??0)-(o.priority??0));r=s.length&&s[0].to.filter(t=>l(t.node)>n+1).reduce((t,o)=>t+o.flow,0)||0;for(const t of s)t.y=o,o+=Math.max(t.out,t.in);e=Math.max(o,e)}return e}(a,c):w(a,c),p=function(t,o){let e=0;const r=new Map,n=[],s=t=>{if(!r.has(t)){const o=n.length;return r.set(t,o),n.push([]),o}return r.get(t)??0};t.sort(M);for(const r of t){const t=s(l(r)),i=n[t]??[];if(h(r)){i.push(h(r));let e=i.length;if(r.in){for(let o=0;o<t;o++){const t=n[o]??[];for(let o=0;o<t.length&&!(t[o]>h(r));o++)e=Math.max(o+1,e)}for(;i.length<e;)i.push(h(r))}r.y=h(r)+e*o}e=Math.max(e,h(r)+Math.max(r.in,r.out))}return e}(a,u/n*s);return function(t){t.forEach(t=>{const o=t.size,e=o<t.in,r=o<t.out;let n=0,s=t.from.length;t.from.sort((t,o)=>h(t.node)+t.node.out/2-(h(o.node)+o.node.out/2)).forEach((t,r)=>{e?t.addY=r*(o-t.flow)/(s-1):(t.addY=n,n+=t.flow)}),n=0,s=t.to.length,t.to.sort((t,o)=>h(t.node)+t.node.in/2-(h(o.node)+o.node.in/2)).forEach((t,e)=>{r?t.addY=e*(o-t.flow)/(s-1):(t.addY=n,n+=t.flow)})})}(a),{maxX:c,maxY:p}}function k(t){return t.x??0}function v(t){return t.y??0}function _(t,o,e){for(const r of t)if(r.key===o&&r.index===e)return r.addY;return 0}class P extends o.DatasetController{parseObjectData(t,o,e,r){const n=((t,o)=>{const{from:e="from",to:r="to",flow:n="flow"}=o;return t.map(({[e]:t,[r]:o,[n]:s})=>({flow:s,from:t,to:o}))})(o,this.options.parsing),{xScale:s,yScale:a}=t,l=[],h=i(n,this.options);this._nodes=h;const{maxX:c,maxY:f}=b(h,n,{height:this.chart.canvas.height,modeX:this.options.modeX,nodePadding:this.options.nodePadding??10,priority:!!this.options.priority});if(this._maxX=c,this._maxY=f,!s||!a)return[];for(let t=0,o=n.length;t<o;++t){const o=n[t],e=h.get(o.from),r=h.get(o.to);if(!e||!r)continue;const i=v(e)+_(e.to,o.to,t),c=v(r)+_(r.from,o.from,t);l.push({_custom:{flow:o.flow,from:e,height:a.parse(o.flow,t),to:r,x:s.parse(k(r),t),y:a.parse(c,t)},x:s.parse(k(e),t),y:a.parse(i,t)})}return l.slice(e,e+r)}getMinMax(t){return{max:t===this._cachedMeta.xScale?this._maxX:this._maxY,min:0}}update(t){const{data:o}=this._cachedMeta;this.updateElements(o,0,o.length,t)}updateElements(t,o,e,r){const{xScale:n,yScale:s}=this._cachedMeta;if(!n||!s)return;const i=this.resolveDataElementOptions(o,r),a=this.getSharedOptions(i),{borderWidth:l,nodeWidth:h=10}=this.options,c=l?l/2+.5:0;for(let i=o;i<o+e;i++){const o=this.getParsed(i),e=o._custom,a=s.getPixelForValue(o.y);this.updateElement(t[i],i,{from:e.from,height:Math.abs(s.getPixelForValue(o.y+e.height)-a),options:this.resolveDataElementOptions(i,r),progress:"reset"===r?0:1,to:e.to,x:n.getPixelForValue(o.x)+h+c,x2:n.getPixelForValue(e.x)-c,y:a,y2:s.getPixelForValue(e.y)},r)}a&&this.updateSharedOptions(a,r,i)}_drawLabels(){const t=this.chart.ctx,o=this.options,e=this._nodes||new Map,r=n(o.size),s=o.borderWidth??1,i=o.nodeWidth??10,a=o.labels,{xScale:l,yScale:h}=this._cachedMeta;if(!l||!h)return;t.save();const c=this.chart.chartArea;for(const n of e.values()){const e=l.getPixelForValue(k(n)),f=h.getPixelForValue(v(n)),d=Math[r](n.in||n.out,n.out||n.in),u=Math.abs(h.getPixelForValue(v(n)+d)-f),p=a?.[n.key]??n.key;let x=e;t.fillStyle=o.color??"black",t.textBaseline="middle",e<c.width/2?(t.textAlign="left",x+=i+s+4):(t.textAlign="right",x-=s+4),this._drawLabel(p,f,u,t,x)}t.restore()}_drawLabel(t,r,n,s,i){const a=e.toFont(this.options.font??this.chart.options.font??o.Chart.defaults.font),l=function(t){if(!t)return[];const o=[],e=Array.isArray(t)?t:[t];for(;e.length;){const t=e.pop();"string"==typeof t?o.unshift(...t.split("\n")):Array.isArray(t)?e.push(...t):t&&o.unshift(`${t}`)}return o}(t),h=l.length,c=r+n/2,f=a.lineHeight,d=e.valueOrDefault(this.options.padding,f/2);if(s.font=a.string,h>1){const t=c-f*h/2+d;for(let o=0;o<h;o++)s.fillText(l[o],i,t+o*f)}else s.fillText(t,i,c)}_drawNodes(){const t=this.chart.ctx,o=this._nodes||new Map,{borderColor:e,borderWidth:r=0,nodeWidth:s=10,size:i}=this.options,a=n(i),{xScale:l,yScale:h}=this._cachedMeta;t.save(),e&&r&&(t.strokeStyle=e,t.lineWidth=r);for(const e of o.values()){if(t.fillStyle=e.color??"black",!l||!h)return;const o=l.getPixelForValue(k(e)),n=h.getPixelForValue(v(e)),i=Math[a](e.in||e.out,e.out||e.in),c=Math.abs(h.getPixelForValue(v(e)+i)-n);r&&t.strokeRect(o,n,s,c),t.fillRect(o,n,s,c)}t.restore()}draw(){const t=this.chart.ctx,o=this.getMeta().data??[],e=[];for(let t=0,r=o.length;t<r;++t){const r=o[t];r.from&&r.to&&(r.from.color=r.options.colorFrom,r.to.color=r.options.colorTo,r.active&&e.push(r))}for(const t of e)t.from&&t.to&&(t.from.color=t.options.colorFrom,t.to.color=t.options.colorTo);this._drawNodes();for(let e=0,r=o.length;e<r;++e)o[e].draw(t);this._drawLabels()}constructor(...t){super(...t),this._nodes=new Map,this._maxX=0,this._maxY=0}}P.id="sankey",P.defaults={animations:{colors:{properties:["colorFrom","colorTo"],type:"color"},numbers:{properties:["x","y","x2","y2","height"],type:"number"},progress:{delay:t=>"data"===t.type?500*t.parsed.x+20*t.dataIndex:void 0,duration:t=>"data"===t.type?200*(t.parsed._custom.x-t.parsed.x):void 0,easing:"linear"}},borderColor:"black",borderWidth:1,color:"black",dataElementType:"flow",modeX:"edge",nodePadding:10,nodeWidth:10,transitions:{hide:{animations:{colors:{properties:["colorFrom","colorTo"],to:"transparent",type:"color"}}},show:{animations:{colors:{from:"transparent",properties:["colorFrom","colorTo"],type:"color"}}}}},P.overrides={datasets:{clip:!1,parsing:{flow:"flow",from:"from",to:"to"}},interaction:{intersect:!0,mode:"nearest"},layout:{padding:{bottom:3,left:3,right:13,top:3}},plugins:{legend:{display:!1},tooltip:{callbacks:{label(t){const o=t.parsed._custom;return`${o.from.key} -> ${o.to.key}: ${o.flow}`},title:()=>""}}},scales:{x:{bounds:"data",display:!1,min:0,offset:!1,type:"linear"},y:{bounds:"data",display:!1,min:0,offset:!1,reverse:!0,type:"linear"}}};const F=(t,o,e,r)=>t<e?{cp1:{x:t+(e-t)/3*2,y:o},cp2:{x:t+(e-t)/3,y:r}}:{cp1:{x:t-(t-e)/3,y:0},cp2:{x:e+(t-e)/3,y:0}},S=(t,o,e)=>({x:t.x+e*(o.x-t.x),y:t.y+e*(o.y-t.y)}),T=(t,o)=>e.color(t).alpha(o).rgbString(),z=(t,o)=>"string"==typeof t?T(t,o):t,C=t=>"string"==typeof t?e.getHoverColor(t):t;class E extends o.Element{draw(t){const{x:o,x2:e,y:r,y2:n,height:s,progress:i}=this,{cp1:a,cp2:l}=F(o,r,e,n);0!==i&&(t.save(),i<1&&(t.beginPath(),t.rect(o,Math.min(r,n),(e-o)*i+1,Math.abs(n-r)+s+1),t.clip()),function(t,{x:o,x2:e,options:r}){let n="black";"from"===r.colorMode?n=z(r.colorFrom,r.alpha):"to"===r.colorMode?n=z(r.colorTo,r.alpha):"string"==typeof r.colorFrom&&"string"==typeof r.colorTo&&(n=t.createLinearGradient(o,0,e,0),n.addColorStop(0,T(r.colorFrom,r.alpha)),n.addColorStop(1,T(r.colorTo,r.alpha))),t.fillStyle=n,t.strokeStyle=n,t.lineWidth=.5}(t,this),t.beginPath(),t.moveTo(o,r),t.bezierCurveTo(a.x,a.y,l.x,l.y,e,n),t.lineTo(e,n+s),t.bezierCurveTo(l.x,l.y+s,a.x,a.y+s,o,r+s),t.lineTo(o,r),t.stroke(),t.closePath(),t.fill(),t.restore())}inRange(t,o,e){const{x:r,y:n,x2:s,y2:i,height:a}=this.getProps(["x","y","x2","y2","height"],e);if(t<r||t>s)return!1;const{cp1:l,cp2:h}=F(r,n,s,i),c=(t-r)/(s-r),f={x:s,y:i},d=S({x:r,y:n},l,c),u=S(l,h,c),p=S(h,f,c),x=S(d,u,c),m=S(u,p,c),y=S(x,m,c).y;return o>=y&&o<=y+a}inXRange(t,o){const{x:e,x2:r}=this.getProps(["x","x2"],o);return t>=e&&t<=r}inYRange(t,o){const{y:e,y2:r,height:n}=this.getProps(["y","y2","height"],o),s=Math.min(e,r),i=Math.max(e,r)+n;return t>=s&&t<=i}getCenterPoint(t){const{x:o,y:e,x2:r,y2:n,height:s}=this.getProps(["x","y","x2","y2","height"],t);return{x:(o+r)/2,y:(e+n+s)/2}}tooltipPosition(t=!1){return this.getCenterPoint(t)}getRange(t){return"x"===t?this.width/2:this.height/2}constructor(t){super(),this.x2=0,this.y2=0,this.width=0,this.height=0,this.progress=1,t&&Object.assign(this,t)}}E.id="flow",E.defaults={alpha:.5,colorFrom:"red",colorMode:"gradient",colorTo:"green",hoverColorFrom:(t,o)=>C(o.colorFrom),hoverColorTo:(t,o)=>C(o.colorTo)},E.descriptors={_scriptable:!0},o.Chart.register(P,E),t.Flow=E,t.SankeyController=P});
|
package/dist/controller.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ export default class SankeyController extends DatasetController {
|
|
|
15
15
|
type: string;
|
|
16
16
|
};
|
|
17
17
|
progress: {
|
|
18
|
-
delay: (ctx: any) => number;
|
|
19
|
-
duration: (ctx: any) => number;
|
|
18
|
+
delay: (ctx: any) => number | undefined;
|
|
19
|
+
duration: (ctx: any) => number | undefined;
|
|
20
20
|
easing: string;
|
|
21
21
|
};
|
|
22
22
|
};
|
package/dist/flow.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SankeyNode } from 'chart.js';
|
|
1
|
+
import type { Color, SankeyNode } from 'chart.js';
|
|
2
2
|
import type { FlowConfig, FlowOptions, FlowProps } from './types.js';
|
|
3
3
|
import { Element } from 'chart.js';
|
|
4
4
|
export default class Flow extends Element<FlowProps, FlowOptions> {
|
|
@@ -8,8 +8,8 @@ export default class Flow extends Element<FlowProps, FlowOptions> {
|
|
|
8
8
|
colorFrom: string;
|
|
9
9
|
colorMode: string;
|
|
10
10
|
colorTo: string;
|
|
11
|
-
hoverColorFrom: (_ctx:
|
|
12
|
-
hoverColorTo: (_ctx:
|
|
11
|
+
hoverColorFrom: (_ctx: unknown, options: FlowOptions) => Color;
|
|
12
|
+
hoverColorTo: (_ctx: unknown, options: FlowOptions) => Color;
|
|
13
13
|
};
|
|
14
14
|
static readonly descriptors: {
|
|
15
15
|
_scriptable: boolean;
|
|
@@ -19,8 +19,8 @@ export default class Flow extends Element<FlowProps, FlowOptions> {
|
|
|
19
19
|
width: number;
|
|
20
20
|
height: number;
|
|
21
21
|
progress: number;
|
|
22
|
-
from
|
|
23
|
-
to
|
|
22
|
+
from?: SankeyNode;
|
|
23
|
+
to?: SankeyNode;
|
|
24
24
|
constructor(cfg: FlowConfig);
|
|
25
25
|
/**
|
|
26
26
|
* @param {CanvasRenderingContext2D} ctx
|
|
@@ -53,7 +53,7 @@ export default class Flow extends Element<FlowProps, FlowOptions> {
|
|
|
53
53
|
x: number;
|
|
54
54
|
y: number;
|
|
55
55
|
};
|
|
56
|
-
tooltipPosition(useFinalPosition
|
|
56
|
+
tooltipPosition(useFinalPosition?: boolean): {
|
|
57
57
|
x: number;
|
|
58
58
|
y: number;
|
|
59
59
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CartesianScaleTypeRegistry, Color, FontSpec, Scriptable, ScriptableContext } from 'chart.js';
|
|
1
|
+
import type { CartesianScaleTypeRegistry, Color, FontSpec, SankeyNode, Scriptable, ScriptableContext } from 'chart.js';
|
|
2
2
|
export type AnyObject = Record<string, unknown>;
|
|
3
3
|
declare module 'chart.js' {
|
|
4
4
|
interface SankeyDataPoint {
|
|
@@ -98,5 +98,9 @@ export interface FlowConfig {
|
|
|
98
98
|
x2: number;
|
|
99
99
|
y2: number;
|
|
100
100
|
height: number;
|
|
101
|
+
width?: number;
|
|
102
|
+
progress?: number;
|
|
103
|
+
from?: SankeyNode;
|
|
104
|
+
to?: SankeyNode;
|
|
101
105
|
options: FlowOptions;
|
|
102
106
|
}
|
package/package.json
CHANGED
|
@@ -6,41 +6,40 @@
|
|
|
6
6
|
"description": "Chart.js module for creating sankey diagrams",
|
|
7
7
|
"devDependencies": {
|
|
8
8
|
"@astrojs/check": "^0.9.9",
|
|
9
|
-
"@astrojs/mdx": "^
|
|
10
|
-
"@astrojs/starlight": "^0.
|
|
9
|
+
"@astrojs/mdx": "^7.0.2",
|
|
10
|
+
"@astrojs/starlight": "^0.41.3",
|
|
11
11
|
"@biomejs/biome": "^2.4.14",
|
|
12
|
-
"@emnapi/core": "^1.
|
|
13
|
-
"@emnapi/runtime": "^1.
|
|
14
|
-
"@jest/globals": "^29.7.0",
|
|
12
|
+
"@emnapi/core": "^1.11.2",
|
|
13
|
+
"@emnapi/runtime": "^1.11.2",
|
|
15
14
|
"@kurkle/configs": "^1.0.1",
|
|
16
15
|
"@napi-rs/canvas": "^1.0.0",
|
|
17
16
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
18
17
|
"@rollup/plugin-swc": "^0.4.0",
|
|
19
|
-
"@rollup/plugin-terser": "^0.
|
|
18
|
+
"@rollup/plugin-terser": "^1.0.0",
|
|
19
|
+
"@swc/cli": "^0.8.1",
|
|
20
20
|
"@swc/core": "^1.7.40",
|
|
21
|
-
"@types/
|
|
22
|
-
"@types/node": "^
|
|
23
|
-
"astro": "^
|
|
21
|
+
"@types/jasmine": "^6.0.0",
|
|
22
|
+
"@types/node": "^26.1.0",
|
|
23
|
+
"astro": "^7.0.6",
|
|
24
|
+
"c8": "^11.0.0",
|
|
24
25
|
"chart.js": "^4.4.5",
|
|
25
26
|
"chartjs-test-utils": "^0.5.0",
|
|
26
27
|
"cross-env": "^10.1.0",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
28
|
+
"globals": "^17.0.0",
|
|
29
|
+
"jasmine": "^6.2.0",
|
|
29
30
|
"karma": "^6.3.2",
|
|
30
31
|
"karma-chrome-launcher": "^3.1.0",
|
|
31
32
|
"karma-coverage": "^2.0.3",
|
|
32
33
|
"karma-firefox-launcher": "^2.1.3",
|
|
33
34
|
"karma-jasmine": "^5.0.1",
|
|
34
35
|
"karma-jasmine-html-reporter": "^2.0.0",
|
|
35
|
-
"karma-rollup-preprocessor": "7.0.
|
|
36
|
+
"karma-rollup-preprocessor": "^7.0.8",
|
|
36
37
|
"karma-spec-reporter": "^0.0.36",
|
|
37
38
|
"rollup": "^4.24.0",
|
|
38
39
|
"rollup-plugin-cleanup": "^3.2.1",
|
|
39
40
|
"rollup-plugin-istanbul": "^5.0.0",
|
|
40
41
|
"semantic-release": "^25.0.3",
|
|
41
|
-
"
|
|
42
|
-
"ts-node": "^10.9.2",
|
|
43
|
-
"typescript": "^5.0.4"
|
|
42
|
+
"typescript": "^6.0.3"
|
|
44
43
|
},
|
|
45
44
|
"exports": {
|
|
46
45
|
"import": "./dist/chartjs-chart-sankey.esm.js",
|
|
@@ -83,17 +82,18 @@
|
|
|
83
82
|
"predocs": "npm run docs:version",
|
|
84
83
|
"predocs:dev": "npm run docs:version",
|
|
85
84
|
"prepack": "npm run build",
|
|
85
|
+
"pretest:unit": "swc --config-file .swcrc-spec src -d build",
|
|
86
86
|
"test": "npm run test:unit && npm run test:fixture && npm run test:integration:browser-module && npm run test:integration:node-commonjs && npm run test:integration:node-module",
|
|
87
87
|
"test:fixture": "cross-env NODE_ENV=test karma start ./karma.conf.cjs --no-auto-watch --single-run",
|
|
88
88
|
"test:integration:browser-module": "karma start ./karma.integration.cjs --single-run",
|
|
89
89
|
"test:integration:node-commonjs": "node test/integration/node-commonjs/test.cjs",
|
|
90
90
|
"test:integration:node-module": "node test/integration/node-module/test.mjs",
|
|
91
|
-
"test:unit": "
|
|
92
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.json --emitDeclarationOnly && tsc --noEmit -p test/types/",
|
|
91
|
+
"test:unit": "cross-env JASMINE_CONFIG_PATH=jasmine.json c8 --src=src --reporter=text --reporter=lcov -o=coverage/unit jasmine",
|
|
92
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json && tsc -p tsconfig.json --emitDeclarationOnly && tsc --noEmit -p test/types/",
|
|
93
93
|
"typecheck:docs": "astro check"
|
|
94
94
|
},
|
|
95
95
|
"type": "module",
|
|
96
96
|
"types": "dist/index.esm.d.ts",
|
|
97
97
|
"unpkg": "dist/chartjs-chart-sankey.min.js",
|
|
98
|
-
"version": "0.14.
|
|
98
|
+
"version": "0.14.4"
|
|
99
99
|
}
|