chartjs-chart-sankey 0.14.3 → 0.15.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.
- package/README.md +5 -0
- package/dist/chartjs-chart-sankey.cjs +471 -151
- package/dist/chartjs-chart-sankey.esm.js +472 -152
- package/dist/chartjs-chart-sankey.min.js +2 -2
- package/dist/controller.d.ts +13 -5
- package/dist/flow.d.ts +20 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.esm.d.ts +1 -1
- package/dist/labels.d.ts +22 -0
- package/dist/lib/core.d.ts +1 -2
- package/dist/lib/layout.d.ts +2 -1
- package/dist/types.d.ts +114 -66
- package/package.json +20 -18
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* chartjs-chart-sankey v0.
|
|
2
|
+
* chartjs-chart-sankey v0.15.0
|
|
3
3
|
* https://chartjs-chart-sankey.pages.dev/
|
|
4
4
|
* (c) 2026 Jukka Kurkela
|
|
5
5
|
* Released under the MIT license
|
|
@@ -39,6 +39,99 @@ function validateSizeValue(size) {
|
|
|
39
39
|
return size;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function isPatternOrGradient(value) {
|
|
43
|
+
const type = Object.prototype.toString.call(value);
|
|
44
|
+
return type === '[object CanvasPattern]' || type === '[object CanvasGradient]';
|
|
45
|
+
}
|
|
46
|
+
function resolveNodeLabelOption(option, node) {
|
|
47
|
+
if (typeof option === 'function') {
|
|
48
|
+
return option(node);
|
|
49
|
+
}
|
|
50
|
+
if (option && typeof option === 'object' && !isPatternOrGradient(option)) {
|
|
51
|
+
return option[node.key];
|
|
52
|
+
}
|
|
53
|
+
return option;
|
|
54
|
+
}
|
|
55
|
+
function addRoundedRectPath(ctx, x, y, width, height, radius) {
|
|
56
|
+
const r = Math.max(0, Math.min(radius, width / 2, height / 2));
|
|
57
|
+
ctx.beginPath();
|
|
58
|
+
ctx.moveTo(x + r, y);
|
|
59
|
+
ctx.lineTo(x + width - r, y);
|
|
60
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + r);
|
|
61
|
+
ctx.lineTo(x + width, y + height - r);
|
|
62
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height);
|
|
63
|
+
ctx.lineTo(x + r, y + height);
|
|
64
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - r);
|
|
65
|
+
ctx.lineTo(x, y + r);
|
|
66
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
67
|
+
ctx.closePath();
|
|
68
|
+
}
|
|
69
|
+
function resolvePosition(position, autoPosition) {
|
|
70
|
+
return position === 'auto' ? autoPosition : position;
|
|
71
|
+
}
|
|
72
|
+
function getTextPosition(position, options, totalTextHeight) {
|
|
73
|
+
const { borderWidth, height, padding, width, x, y } = options;
|
|
74
|
+
const text = {
|
|
75
|
+
align: 'center',
|
|
76
|
+
x: x + width / 2,
|
|
77
|
+
y: y + height / 2
|
|
78
|
+
};
|
|
79
|
+
if (position === 'left') {
|
|
80
|
+
text.align = 'right';
|
|
81
|
+
text.x = x - borderWidth - padding;
|
|
82
|
+
} else if (position === 'right') {
|
|
83
|
+
text.align = 'left';
|
|
84
|
+
text.x = x + width + borderWidth + padding;
|
|
85
|
+
} else if (position === 'top') {
|
|
86
|
+
text.y = y - padding - totalTextHeight / 2;
|
|
87
|
+
} else if (position === 'bottom') {
|
|
88
|
+
text.y = y + height + padding + totalTextHeight / 2;
|
|
89
|
+
}
|
|
90
|
+
return text;
|
|
91
|
+
}
|
|
92
|
+
function getBackgroundX(textAlign, textX, textWidth, backgroundWidth, padding) {
|
|
93
|
+
if (textAlign === 'left') return textX - padding;
|
|
94
|
+
if (textAlign === 'right') return textX - textWidth - padding;
|
|
95
|
+
return textX - backgroundWidth / 2;
|
|
96
|
+
}
|
|
97
|
+
function drawBackground(ctx, color, x, y, width, height, borderRadius) {
|
|
98
|
+
ctx.save();
|
|
99
|
+
ctx.fillStyle = color;
|
|
100
|
+
if (borderRadius > 0) {
|
|
101
|
+
addRoundedRectPath(ctx, x, y, width, height, borderRadius);
|
|
102
|
+
ctx.fill();
|
|
103
|
+
} else {
|
|
104
|
+
ctx.fillRect(x, y, width, height);
|
|
105
|
+
}
|
|
106
|
+
ctx.restore();
|
|
107
|
+
}
|
|
108
|
+
function drawLabel(ctx, label, options) {
|
|
109
|
+
const lines = toTextLines(label);
|
|
110
|
+
if (!lines.length) return;
|
|
111
|
+
const { backgroundColor, borderRadius, color, font, lineOffset, padding } = options;
|
|
112
|
+
const position = resolvePosition(options.position, options.autoPosition);
|
|
113
|
+
const textHeight = Number(font.lineHeight);
|
|
114
|
+
ctx.font = font.string;
|
|
115
|
+
const textWidth = Math.max(...lines.map((line)=>ctx.measureText(line).width));
|
|
116
|
+
const totalTextHeight = textHeight * lines.length;
|
|
117
|
+
const text = getTextPosition(position, options, totalTextHeight);
|
|
118
|
+
ctx.textAlign = text.align;
|
|
119
|
+
ctx.textBaseline = 'middle';
|
|
120
|
+
const backgroundWidth = textWidth + padding * 2;
|
|
121
|
+
const backgroundHeight = totalTextHeight + padding * 2;
|
|
122
|
+
const backgroundX = getBackgroundX(text.align, text.x, textWidth, backgroundWidth, padding);
|
|
123
|
+
const firstLineY = lines.length === 1 ? text.y : text.y - totalTextHeight / 2 + lineOffset;
|
|
124
|
+
const textCenterY = firstLineY + (lines.length - 1) * textHeight / 2;
|
|
125
|
+
const backgroundY = textCenterY - backgroundHeight / 2;
|
|
126
|
+
if (backgroundColor !== undefined) {
|
|
127
|
+
drawBackground(ctx, backgroundColor, backgroundX, backgroundY, backgroundWidth, backgroundHeight, borderRadius);
|
|
128
|
+
}
|
|
129
|
+
ctx.fillStyle = color;
|
|
130
|
+
for(let i = 0; i < lines.length; i++){
|
|
131
|
+
ctx.fillText(lines[i], text.x, firstLineY + i * textHeight);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
42
135
|
const flowSort = (a, b)=>{
|
|
43
136
|
if (b.flow === a.flow) return a.index - b.index;
|
|
44
137
|
return b.flow - a.flow;
|
|
@@ -126,6 +219,12 @@ function buildNodesFromData(data, { size, priority, column }) {
|
|
|
126
219
|
}
|
|
127
220
|
|
|
128
221
|
const SMALL_VALUE = 1e-6;
|
|
222
|
+
function nodeX$1(node) {
|
|
223
|
+
return node.x ?? 0;
|
|
224
|
+
}
|
|
225
|
+
function nodeY$1(node) {
|
|
226
|
+
return node.y ?? 0;
|
|
227
|
+
}
|
|
129
228
|
const getAllKeysForward = (nodes, visited = new Set())=>{
|
|
130
229
|
const keys = [];
|
|
131
230
|
for (const node of nodes){
|
|
@@ -183,7 +282,7 @@ function calculateX(nodeMap, data, mode) {
|
|
|
183
282
|
x++;
|
|
184
283
|
}
|
|
185
284
|
}
|
|
186
|
-
const maxX = allNodes.reduce((max, node)=>Math.max(max, node
|
|
285
|
+
const maxX = allNodes.reduce((max, node)=>Math.max(max, nodeX$1(node)), 0);
|
|
187
286
|
if (mode === 'edge') {
|
|
188
287
|
const from = new Set(data.map((dataPoint)=>dataPoint.from));
|
|
189
288
|
allKeys.filter((key)=>!from.has(key)).forEach((key)=>{
|
|
@@ -223,20 +322,26 @@ function processFrom(node, y) {
|
|
|
223
322
|
}
|
|
224
323
|
y = Math.max(n.y + n.out, y);
|
|
225
324
|
}
|
|
226
|
-
return node
|
|
325
|
+
return nodeY$1(node) + node.size;
|
|
227
326
|
}
|
|
327
|
+
const returnsToNearerColumn = (current, next)=>Boolean(next && nodeX$1(next) < nodeX$1(current));
|
|
228
328
|
function processTo(node, y) {
|
|
229
329
|
if (!node.to.length) return y;
|
|
230
330
|
node.to.sort(flowByNodeCount('to'));
|
|
231
|
-
for
|
|
331
|
+
for(let i = 0; i < node.to.length; i++){
|
|
332
|
+
const flow = node.to[i];
|
|
232
333
|
const n = flow.node;
|
|
233
334
|
if (!defined(n.y)) {
|
|
234
335
|
n.y = y;
|
|
235
336
|
processTo(n, y ? y + SMALL_VALUE : 0);
|
|
236
337
|
}
|
|
237
|
-
|
|
338
|
+
if (returnsToNearerColumn(n, node.to[i + 1]?.node)) {
|
|
339
|
+
y += flow.flow;
|
|
340
|
+
} else {
|
|
341
|
+
y = Math.max(n.y + Math.max(n.in, n.out), y);
|
|
342
|
+
}
|
|
238
343
|
}
|
|
239
|
-
return node
|
|
344
|
+
return nodeY$1(node) + node.size;
|
|
240
345
|
}
|
|
241
346
|
function setOrGetY(node, value) {
|
|
242
347
|
if (defined(node.y)) {
|
|
@@ -250,9 +355,9 @@ function processRest(nodeArray, maxX) {
|
|
|
250
355
|
const rightNodes = nodeArray.filter((node)=>node.x === maxX);
|
|
251
356
|
const leftToDo = leftNodes.filter((node)=>!defined(node.y));
|
|
252
357
|
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
|
|
358
|
+
const centerToDo = nodeArray.filter((node)=>nodeX$1(node) > 0 && nodeX$1(node) < maxX && !defined(node.y));
|
|
359
|
+
let leftY = leftNodes.reduce((acc, cur)=>Math.max(acc, nodeY$1(cur) + cur.out || 0), 0) + SMALL_VALUE;
|
|
360
|
+
let rightY = rightNodes.reduce((acc, cur)=>Math.max(acc, nodeY$1(cur) + cur.in || 0), 0) + SMALL_VALUE;
|
|
256
361
|
let centerY = 0;
|
|
257
362
|
if (leftY >= rightY) {
|
|
258
363
|
leftToDo.forEach((node)=>{
|
|
@@ -273,7 +378,7 @@ function processRest(nodeArray, maxX) {
|
|
|
273
378
|
});
|
|
274
379
|
}
|
|
275
380
|
centerToDo.forEach((node)=>{
|
|
276
|
-
let y = nodeArray.filter((n)=>n
|
|
381
|
+
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
382
|
y = setOrGetY(node, y);
|
|
278
383
|
y = Math.max(y + node.in, processFrom(node, y));
|
|
279
384
|
y = Math.max(y + node.out, processTo(node, y));
|
|
@@ -284,25 +389,29 @@ function processRest(nodeArray, maxX) {
|
|
|
284
389
|
const fixTop = (nodeArray, maxX)=>{
|
|
285
390
|
let maxY = 0;
|
|
286
391
|
for(let x = 0; x <= maxX; x++){
|
|
287
|
-
const nodes = nodeArray.filter((n)=>n
|
|
392
|
+
const nodes = nodeArray.filter((n)=>nodeX$1(n) === x).sort((a, b)=>nodeY$1(a) - nodeY$1(b));
|
|
288
393
|
let minY = 0;
|
|
289
394
|
for (const node of nodes){
|
|
290
|
-
if (node
|
|
291
|
-
minY = node
|
|
395
|
+
if (nodeY$1(node) < minY) node.y = minY;
|
|
396
|
+
minY = nodeY$1(node) + node.size;
|
|
292
397
|
}
|
|
293
398
|
maxY = Math.max(maxY, minY);
|
|
294
399
|
}
|
|
295
400
|
return maxY;
|
|
296
401
|
};
|
|
297
402
|
const findStartNode = (nodeArray, maxX)=>{
|
|
298
|
-
const
|
|
403
|
+
const sorted = [
|
|
299
404
|
...nodeArray
|
|
300
|
-
].sort((a, b)=>a.size - b.size)
|
|
405
|
+
].sort((a, b)=>a.size - b.size);
|
|
406
|
+
const largest = sorted[sorted.length - 1];
|
|
407
|
+
const size = largest.size;
|
|
301
408
|
const biggest = nodeArray.filter((n)=>n.size === size);
|
|
302
|
-
|
|
303
|
-
biggest.
|
|
304
|
-
|
|
305
|
-
if (
|
|
409
|
+
const first = biggest[0];
|
|
410
|
+
if (biggest.length === 1) return first;
|
|
411
|
+
biggest.sort((a, b)=>nodeX$1(a) - nodeX$1(b));
|
|
412
|
+
if (nodeX$1(first) === 0) return first;
|
|
413
|
+
const last = biggest[biggest.length - 1];
|
|
414
|
+
if (nodeX$1(last) === maxX) return last;
|
|
306
415
|
const mid = Math.floor(biggest.length / 2);
|
|
307
416
|
return biggest[mid];
|
|
308
417
|
};
|
|
@@ -320,8 +429,11 @@ function calculateYUsingPriority(nodeArray, maxX) {
|
|
|
320
429
|
let nextYStart = 0;
|
|
321
430
|
for(let x = 0; x <= maxX; x++){
|
|
322
431
|
let y = nextYStart;
|
|
323
|
-
const nodes = nodeArray.filter((node)=>node
|
|
324
|
-
|
|
432
|
+
const nodes = nodeArray.filter((node)=>nodeX$1(node) === x).sort((a, b)=>(a.priority ?? 0) - (b.priority ?? 0));
|
|
433
|
+
if (nodes.length) {
|
|
434
|
+
const nextX = nodeArray.reduce((next, node)=>nodeX$1(node) > x ? Math.min(next, nodeX$1(node)) : next, Infinity);
|
|
435
|
+
nextYStart = nodes[0].to.filter((to)=>nodeX$1(to.node) > nextX).reduce((acc, cur)=>acc + cur.flow, 0) || 0;
|
|
436
|
+
}
|
|
325
437
|
for (const node of nodes){
|
|
326
438
|
node.y = y;
|
|
327
439
|
y += Math.max(node.out, node.in);
|
|
@@ -331,9 +443,9 @@ function calculateYUsingPriority(nodeArray, maxX) {
|
|
|
331
443
|
return maxY;
|
|
332
444
|
}
|
|
333
445
|
const nodeByXYSize = (a, b)=>{
|
|
334
|
-
if (a
|
|
335
|
-
if (a
|
|
336
|
-
return a
|
|
446
|
+
if (nodeX$1(a) !== nodeX$1(b)) return nodeX$1(a) - nodeX$1(b);
|
|
447
|
+
if (nodeY$1(a) === nodeY$1(b)) return a.size - b.size;
|
|
448
|
+
return nodeY$1(a) - nodeY$1(b);
|
|
337
449
|
};
|
|
338
450
|
function addPadding(nodeArray, padding) {
|
|
339
451
|
let maxY = 0;
|
|
@@ -341,31 +453,33 @@ const nodeByXYSize = (a, b)=>{
|
|
|
341
453
|
const grid = [];
|
|
342
454
|
const getColIndex = (x)=>{
|
|
343
455
|
if (!columnXs.has(x)) {
|
|
344
|
-
|
|
456
|
+
const index = grid.length;
|
|
457
|
+
columnXs.set(x, index);
|
|
345
458
|
grid.push([]);
|
|
459
|
+
return index;
|
|
346
460
|
}
|
|
347
|
-
return columnXs.get(x);
|
|
461
|
+
return columnXs.get(x) ?? 0;
|
|
348
462
|
};
|
|
349
463
|
nodeArray.sort(nodeByXYSize);
|
|
350
464
|
for (const node of nodeArray){
|
|
351
|
-
const colIdx = getColIndex(node
|
|
352
|
-
const column = grid[colIdx];
|
|
353
|
-
if (node
|
|
354
|
-
column.push(node
|
|
465
|
+
const colIdx = getColIndex(nodeX$1(node));
|
|
466
|
+
const column = grid[colIdx] ?? [];
|
|
467
|
+
if (nodeY$1(node)) {
|
|
468
|
+
column.push(nodeY$1(node));
|
|
355
469
|
let paddings = column.length;
|
|
356
470
|
if (node.in) {
|
|
357
471
|
for(let col = 0; col < colIdx; col++){
|
|
358
|
-
const otherColumn = grid[col];
|
|
472
|
+
const otherColumn = grid[col] ?? [];
|
|
359
473
|
for(let row = 0; row < otherColumn.length; row++){
|
|
360
|
-
if (otherColumn[row] > node
|
|
474
|
+
if (otherColumn[row] > nodeY$1(node)) break;
|
|
361
475
|
paddings = Math.max(row + 1, paddings);
|
|
362
476
|
}
|
|
363
477
|
}
|
|
364
|
-
while(column.length < paddings)column.push(node
|
|
478
|
+
while(column.length < paddings)column.push(nodeY$1(node));
|
|
365
479
|
}
|
|
366
|
-
node.y
|
|
480
|
+
node.y = nodeY$1(node) + paddings * padding;
|
|
367
481
|
}
|
|
368
|
-
maxY = Math.max(maxY, node
|
|
482
|
+
maxY = Math.max(maxY, nodeY$1(node) + Math.max(node.in, node.out));
|
|
369
483
|
}
|
|
370
484
|
return maxY;
|
|
371
485
|
}
|
|
@@ -376,7 +490,7 @@ function sortFlows(nodeArray) {
|
|
|
376
490
|
const overlapTo = nodeSize < node.out;
|
|
377
491
|
let addY = 0;
|
|
378
492
|
let len = node.from.length;
|
|
379
|
-
node.from.sort((a, b)=>a.node
|
|
493
|
+
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
494
|
if (overlapFrom) {
|
|
381
495
|
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
382
496
|
} else {
|
|
@@ -386,7 +500,7 @@ function sortFlows(nodeArray) {
|
|
|
386
500
|
});
|
|
387
501
|
addY = 0;
|
|
388
502
|
len = node.to.length;
|
|
389
|
-
node.to.sort((a, b)=>a.node
|
|
503
|
+
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
504
|
if (overlapTo) {
|
|
391
505
|
flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
|
|
392
506
|
} else {
|
|
@@ -400,7 +514,7 @@ function layout(nodes, data, { priority, height, nodePadding, modeX }) {
|
|
|
400
514
|
const nodeArray = [
|
|
401
515
|
...nodes.values()
|
|
402
516
|
];
|
|
403
|
-
const maxX = calculateX(nodes, data, modeX);
|
|
517
|
+
const maxX = calculateX(nodes, data, modeX ?? 'edge');
|
|
404
518
|
const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
|
|
405
519
|
const padding = maxY / height * nodePadding;
|
|
406
520
|
const maxYWithPadding = addPadding(nodeArray, padding);
|
|
@@ -411,6 +525,21 @@ function layout(nodes, data, { priority, height, nodePadding, modeX }) {
|
|
|
411
525
|
};
|
|
412
526
|
}
|
|
413
527
|
|
|
528
|
+
function nodeX(node) {
|
|
529
|
+
return node.x ?? 0;
|
|
530
|
+
}
|
|
531
|
+
function nodeY(node) {
|
|
532
|
+
return node.y ?? 0;
|
|
533
|
+
}
|
|
534
|
+
function getNodeSize(node, size) {
|
|
535
|
+
return Math[size](node.in || node.out, node.out || node.in);
|
|
536
|
+
}
|
|
537
|
+
function getAutoLabelPosition(x, y, chartArea, orientation) {
|
|
538
|
+
if (orientation === 'vertical') {
|
|
539
|
+
return y < (chartArea.top + chartArea.bottom) / 2 ? 'bottom' : 'top';
|
|
540
|
+
}
|
|
541
|
+
return x < (chartArea.left + chartArea.right) / 2 ? 'right' : 'left';
|
|
542
|
+
}
|
|
414
543
|
function getAddY(arr, key, index) {
|
|
415
544
|
for (const item of arr){
|
|
416
545
|
if (item.key === key && item.index === index) {
|
|
@@ -419,17 +548,113 @@ function getAddY(arr, key, index) {
|
|
|
419
548
|
}
|
|
420
549
|
return 0;
|
|
421
550
|
}
|
|
551
|
+
function parseFlow(from, to, fromY, toY, flow, index, xScale, yScale, orientation) {
|
|
552
|
+
if (orientation === 'vertical') {
|
|
553
|
+
return {
|
|
554
|
+
_custom: {
|
|
555
|
+
flow,
|
|
556
|
+
from,
|
|
557
|
+
height: xScale.parse(flow, index),
|
|
558
|
+
to,
|
|
559
|
+
x: xScale.parse(toY, index),
|
|
560
|
+
y: yScale.parse(nodeX(to), index)
|
|
561
|
+
},
|
|
562
|
+
x: xScale.parse(fromY, index),
|
|
563
|
+
y: yScale.parse(nodeX(from), index)
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
_custom: {
|
|
568
|
+
flow,
|
|
569
|
+
from,
|
|
570
|
+
height: yScale.parse(flow, index),
|
|
571
|
+
to,
|
|
572
|
+
x: xScale.parse(nodeX(to), index),
|
|
573
|
+
y: yScale.parse(toY, index)
|
|
574
|
+
},
|
|
575
|
+
x: xScale.parse(nodeX(from), index),
|
|
576
|
+
y: yScale.parse(fromY, index)
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function getFlowElementProperties(parsed, xScale, yScale, maxColumn, nodeWidth, columnPadding, borderSpace, orientation) {
|
|
580
|
+
const custom = parsed._custom;
|
|
581
|
+
const x = xScale.getPixelForValue(parsed.x);
|
|
582
|
+
const y = yScale.getPixelForValue(parsed.y);
|
|
583
|
+
if (orientation === 'vertical') {
|
|
584
|
+
return {
|
|
585
|
+
flow: custom.flow,
|
|
586
|
+
from: custom.from,
|
|
587
|
+
height: 0,
|
|
588
|
+
to: custom.to,
|
|
589
|
+
width: Math.abs(xScale.getPixelForValue(parsed.x + custom.height) - x),
|
|
590
|
+
x,
|
|
591
|
+
x2: xScale.getPixelForValue(custom.x),
|
|
592
|
+
y: getColumnPixel(yScale, parsed.y, maxColumn, columnPadding) + nodeWidth + borderSpace,
|
|
593
|
+
y2: getColumnPixel(yScale, custom.y, maxColumn, columnPadding) - borderSpace
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
flow: custom.flow,
|
|
598
|
+
from: custom.from,
|
|
599
|
+
height: Math.abs(yScale.getPixelForValue(parsed.y + custom.height) - y),
|
|
600
|
+
to: custom.to,
|
|
601
|
+
width: 0,
|
|
602
|
+
x: getColumnPixel(xScale, parsed.x, maxColumn, columnPadding) + nodeWidth + borderSpace,
|
|
603
|
+
x2: getColumnPixel(xScale, custom.x, maxColumn, columnPadding) - borderSpace,
|
|
604
|
+
y,
|
|
605
|
+
y2: yScale.getPixelForValue(custom.y)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
function getColumnPixel(scale, value, maxColumn, padding) {
|
|
609
|
+
const pixel = scale.getPixelForValue(value);
|
|
610
|
+
return maxColumn ? pixel - value / maxColumn * padding : pixel;
|
|
611
|
+
}
|
|
612
|
+
function getColumnPadding(nodeWidth, orientation, chart) {
|
|
613
|
+
const trailingSpace = orientation === 'vertical' ? chart.height - chart.chartArea.bottom : chart.width - chart.chartArea.right;
|
|
614
|
+
return Math.max(0, nodeWidth + 3 - trailingSpace);
|
|
615
|
+
}
|
|
616
|
+
function getNodeRect(node, size, xScale, yScale, maxColumn, nodeWidth, columnPadding, orientation) {
|
|
617
|
+
if (orientation === 'vertical') {
|
|
618
|
+
const x = xScale.getPixelForValue(nodeY(node));
|
|
619
|
+
return {
|
|
620
|
+
height: nodeWidth,
|
|
621
|
+
width: Math.abs(xScale.getPixelForValue(nodeY(node) + size) - x),
|
|
622
|
+
x,
|
|
623
|
+
y: getColumnPixel(yScale, nodeX(node), maxColumn, columnPadding)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const y = yScale.getPixelForValue(nodeY(node));
|
|
627
|
+
return {
|
|
628
|
+
height: Math.abs(yScale.getPixelForValue(nodeY(node) + size) - y),
|
|
629
|
+
width: nodeWidth,
|
|
630
|
+
x: getColumnPixel(xScale, nodeX(node), maxColumn, columnPadding),
|
|
631
|
+
y
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function resolveNodeLabelStyle(options, node) {
|
|
635
|
+
const { backgroundColor, borderRadius = 0, color, display, font, padding = 4, position } = options.nodeLabels ?? {};
|
|
636
|
+
return {
|
|
637
|
+
backgroundColor: resolveNodeLabelOption(backgroundColor, node),
|
|
638
|
+
borderRadius,
|
|
639
|
+
color: resolveNodeLabelOption(color, node) ?? options.color ?? 'black',
|
|
640
|
+
display: resolveNodeLabelOption(display, node) ?? true,
|
|
641
|
+
font,
|
|
642
|
+
padding,
|
|
643
|
+
position: resolveNodeLabelOption(position, node) ?? 'auto'
|
|
644
|
+
};
|
|
645
|
+
}
|
|
422
646
|
class SankeyController extends chart_js.DatasetController {
|
|
423
647
|
parseObjectData(meta, data, start, count) {
|
|
424
648
|
const sankeyData = getParsedData(data, this.options.parsing);
|
|
425
649
|
const { xScale, yScale } = meta;
|
|
426
650
|
const parsed = [];
|
|
427
651
|
const nodes = buildNodesFromData(sankeyData, this.options);
|
|
652
|
+
const orientation = this.options.orientation ?? 'horizontal';
|
|
428
653
|
this._nodes = nodes;
|
|
429
654
|
const { maxX, maxY } = layout(nodes, sankeyData, {
|
|
430
|
-
height: this.chart.canvas.height,
|
|
655
|
+
height: orientation === 'vertical' ? this.chart.canvas.width : this.chart.canvas.height,
|
|
431
656
|
modeX: this.options.modeX,
|
|
432
|
-
nodePadding: this.options.nodePadding,
|
|
657
|
+
nodePadding: this.options.nodePadding ?? 10,
|
|
433
658
|
priority: !!this.options.priority
|
|
434
659
|
});
|
|
435
660
|
this._maxX = maxX;
|
|
@@ -440,26 +665,17 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
440
665
|
const from = nodes.get(dataPoint.from);
|
|
441
666
|
const to = nodes.get(dataPoint.to);
|
|
442
667
|
if (!from || !to) continue;
|
|
443
|
-
const fromY = (from
|
|
444
|
-
const toY = (to
|
|
445
|
-
parsed.push(
|
|
446
|
-
_custom: {
|
|
447
|
-
flow: dataPoint.flow,
|
|
448
|
-
from,
|
|
449
|
-
height: yScale.parse(dataPoint.flow, i),
|
|
450
|
-
to,
|
|
451
|
-
x: xScale.parse(to.x, i),
|
|
452
|
-
y: yScale.parse(toY, i)
|
|
453
|
-
},
|
|
454
|
-
x: xScale.parse(from.x, i),
|
|
455
|
-
y: yScale.parse(fromY, i)
|
|
456
|
-
});
|
|
668
|
+
const fromY = nodeY(from) + getAddY(from.to, dataPoint.to, i);
|
|
669
|
+
const toY = nodeY(to) + getAddY(to.from, dataPoint.from, i);
|
|
670
|
+
parsed.push(parseFlow(from, to, fromY, toY, dataPoint.flow, i, xScale, yScale, orientation));
|
|
457
671
|
}
|
|
458
672
|
return parsed.slice(start, start + count);
|
|
459
673
|
}
|
|
460
674
|
getMinMax(scale) {
|
|
675
|
+
const vertical = this.options.orientation === 'vertical';
|
|
676
|
+
const columnScale = vertical ? this._cachedMeta.yScale : this._cachedMeta.xScale;
|
|
461
677
|
return {
|
|
462
|
-
max: scale ===
|
|
678
|
+
max: scale === columnScale ? this._maxX : this._maxY,
|
|
463
679
|
min: 0
|
|
464
680
|
};
|
|
465
681
|
}
|
|
@@ -472,79 +688,65 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
472
688
|
if (!xScale || !yScale) return;
|
|
473
689
|
const firstOpts = this.resolveDataElementOptions(start, mode);
|
|
474
690
|
const sharedOptions = this.getSharedOptions(firstOpts);
|
|
475
|
-
const { borderWidth, nodeWidth = 10 } = this.options;
|
|
691
|
+
const { borderWidth, nodeWidth = 10, orientation = 'horizontal' } = this.options;
|
|
692
|
+
const columnPadding = getColumnPadding(nodeWidth, orientation, this.chart);
|
|
476
693
|
const borderSpace = borderWidth ? borderWidth / 2 + 0.5 : 0;
|
|
477
694
|
for(let i = start; i < start + count; i++){
|
|
478
695
|
const parsed = this.getParsed(i);
|
|
479
|
-
const custom = parsed._custom;
|
|
480
|
-
const y = yScale.getPixelForValue(parsed.y);
|
|
481
696
|
this.updateElement(elems[i], i, {
|
|
482
|
-
from: custom.from,
|
|
483
|
-
height: Math.abs(yScale.getPixelForValue(parsed.y + custom.height) - y),
|
|
484
697
|
options: this.resolveDataElementOptions(i, mode),
|
|
485
698
|
progress: mode === 'reset' ? 0 : 1,
|
|
486
|
-
|
|
487
|
-
x: xScale.getPixelForValue(parsed.x) + nodeWidth + borderSpace,
|
|
488
|
-
x2: xScale.getPixelForValue(custom.x) - borderSpace,
|
|
489
|
-
y,
|
|
490
|
-
y2: yScale.getPixelForValue(custom.y)
|
|
699
|
+
...getFlowElementProperties(parsed, xScale, yScale, this._maxX, nodeWidth, columnPadding, borderSpace, orientation)
|
|
491
700
|
}, mode);
|
|
492
701
|
}
|
|
493
|
-
|
|
702
|
+
if (sharedOptions) {
|
|
703
|
+
this.updateSharedOptions(sharedOptions, mode, firstOpts);
|
|
704
|
+
}
|
|
494
705
|
}
|
|
495
706
|
_drawLabels() {
|
|
496
707
|
const ctx = this.chart.ctx;
|
|
497
708
|
const options = this.options;
|
|
498
709
|
const nodes = this._nodes || new Map();
|
|
499
710
|
const size = validateSizeValue(options.size);
|
|
500
|
-
const borderWidth = options.borderWidth ?? 1;
|
|
501
|
-
const nodeWidth = options.nodeWidth ?? 10;
|
|
502
711
|
const labels = options.labels;
|
|
712
|
+
const { borderWidth = 1, nodeWidth = 10, orientation = 'horizontal' } = options;
|
|
713
|
+
const columnPadding = getColumnPadding(nodeWidth, orientation, this.chart);
|
|
714
|
+
const defaultFont = options.font ?? this.chart.options.font ?? chart_js.Chart.defaults.font;
|
|
503
715
|
const { xScale, yScale } = this._cachedMeta;
|
|
504
716
|
if (!xScale || !yScale) return;
|
|
505
717
|
ctx.save();
|
|
506
718
|
const chartArea = this.chart.chartArea;
|
|
507
719
|
for (const node of nodes.values()){
|
|
508
|
-
const
|
|
509
|
-
const y =
|
|
510
|
-
const max = Math[size](node.in || node.out, node.out || node.in);
|
|
511
|
-
const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
|
|
720
|
+
const max = getNodeSize(node, size);
|
|
721
|
+
const { height, width, x, y } = getNodeRect(node, max, xScale, yScale, this._maxX, nodeWidth, columnPadding, orientation);
|
|
512
722
|
const label = labels?.[node.key] ?? node.key;
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
723
|
+
const labelStyle = resolveNodeLabelStyle(options, node);
|
|
724
|
+
if (labelStyle.display) {
|
|
725
|
+
const font = helpers.toFont(labelStyle.font ?? defaultFont);
|
|
726
|
+
drawLabel(ctx, label, {
|
|
727
|
+
autoPosition: getAutoLabelPosition(x, y, chartArea, orientation),
|
|
728
|
+
backgroundColor: labelStyle.backgroundColor,
|
|
729
|
+
borderRadius: labelStyle.borderRadius,
|
|
730
|
+
borderWidth,
|
|
731
|
+
color: labelStyle.color,
|
|
732
|
+
font,
|
|
733
|
+
height,
|
|
734
|
+
lineOffset: helpers.valueOrDefault(options.padding, font.lineHeight / 2),
|
|
735
|
+
padding: labelStyle.padding,
|
|
736
|
+
position: labelStyle.position,
|
|
737
|
+
width,
|
|
738
|
+
x,
|
|
739
|
+
y
|
|
740
|
+
});
|
|
522
741
|
}
|
|
523
|
-
this._drawLabel(label, y, height, ctx, textX);
|
|
524
742
|
}
|
|
525
743
|
ctx.restore();
|
|
526
744
|
}
|
|
527
|
-
_drawLabel(label, y, height, ctx, textX) {
|
|
528
|
-
const font = helpers.toFont(this.options.font, this.chart.options.font);
|
|
529
|
-
const lines = toTextLines(label);
|
|
530
|
-
const lineCount = lines.length;
|
|
531
|
-
const middle = y + height / 2;
|
|
532
|
-
const textHeight = font.lineHeight;
|
|
533
|
-
const padding = helpers.valueOrDefault(this.options.padding, textHeight / 2);
|
|
534
|
-
ctx.font = font.string;
|
|
535
|
-
if (lineCount > 1) {
|
|
536
|
-
const top = middle - textHeight * lineCount / 2 + padding;
|
|
537
|
-
for(let i = 0; i < lineCount; i++){
|
|
538
|
-
ctx.fillText(lines[i], textX, top + i * textHeight);
|
|
539
|
-
}
|
|
540
|
-
} else {
|
|
541
|
-
ctx.fillText(label, textX, middle);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
745
|
_drawNodes() {
|
|
545
746
|
const ctx = this.chart.ctx;
|
|
546
747
|
const nodes = this._nodes || new Map();
|
|
547
|
-
const { borderColor, borderWidth = 0, nodeWidth = 10, size } = this.options;
|
|
748
|
+
const { borderColor, borderWidth = 0, nodeWidth = 10, orientation = 'horizontal', size } = this.options;
|
|
749
|
+
const columnPadding = getColumnPadding(nodeWidth, orientation, this.chart);
|
|
548
750
|
const sizeMethod = validateSizeValue(size);
|
|
549
751
|
const { xScale, yScale } = this._cachedMeta;
|
|
550
752
|
ctx.save();
|
|
@@ -554,14 +756,13 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
554
756
|
}
|
|
555
757
|
for (const node of nodes.values()){
|
|
556
758
|
ctx.fillStyle = node.color ?? 'black';
|
|
557
|
-
|
|
558
|
-
const y = yScale.getPixelForValue(node.y);
|
|
759
|
+
if (!xScale || !yScale) return;
|
|
559
760
|
const max = Math[sizeMethod](node.in || node.out, node.out || node.in);
|
|
560
|
-
const height =
|
|
761
|
+
const { height, width, x, y } = getNodeRect(node, max, xScale, yScale, this._maxX, nodeWidth, columnPadding, orientation);
|
|
561
762
|
if (borderWidth) {
|
|
562
|
-
ctx.strokeRect(x, y,
|
|
763
|
+
ctx.strokeRect(x, y, width, height);
|
|
563
764
|
}
|
|
564
|
-
ctx.fillRect(x, y,
|
|
765
|
+
ctx.fillRect(x, y, width, height);
|
|
565
766
|
}
|
|
566
767
|
ctx.restore();
|
|
567
768
|
}
|
|
@@ -571,13 +772,19 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
571
772
|
const active = [];
|
|
572
773
|
for(let i = 0, ilen = data.length; i < ilen; ++i){
|
|
573
774
|
const flow = data[i];
|
|
574
|
-
flow.from
|
|
775
|
+
if (!flow.from || !flow.to) {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
flow.from.color = flow.options.colorFrom;
|
|
575
779
|
flow.to.color = flow.options.colorTo;
|
|
576
780
|
if (flow.active) {
|
|
577
781
|
active.push(flow);
|
|
578
782
|
}
|
|
579
783
|
}
|
|
580
784
|
for (const flow of active){
|
|
785
|
+
if (!flow.from || !flow.to) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
581
788
|
flow.from.color = flow.options.colorFrom;
|
|
582
789
|
flow.to.color = flow.options.colorTo;
|
|
583
790
|
}
|
|
@@ -587,8 +794,19 @@ class SankeyController extends chart_js.DatasetController {
|
|
|
587
794
|
}
|
|
588
795
|
this._drawLabels();
|
|
589
796
|
}
|
|
797
|
+
constructor(...args){
|
|
798
|
+
super(...args), this._nodes = new Map(), this._maxX = 0, this._maxY = 0;
|
|
799
|
+
}
|
|
590
800
|
}
|
|
591
801
|
SankeyController.id = 'sankey';
|
|
802
|
+
SankeyController.descriptors = {
|
|
803
|
+
_indexable: false,
|
|
804
|
+
_scriptable: true,
|
|
805
|
+
nodeLabels: {
|
|
806
|
+
_indexable: false,
|
|
807
|
+
_scriptable: false
|
|
808
|
+
}
|
|
809
|
+
};
|
|
592
810
|
SankeyController.defaults = {
|
|
593
811
|
animations: {
|
|
594
812
|
colors: {
|
|
@@ -604,13 +822,14 @@ SankeyController.defaults = {
|
|
|
604
822
|
'y',
|
|
605
823
|
'x2',
|
|
606
824
|
'y2',
|
|
607
|
-
'height'
|
|
825
|
+
'height',
|
|
826
|
+
'width'
|
|
608
827
|
],
|
|
609
828
|
type: 'number'
|
|
610
829
|
},
|
|
611
830
|
progress: {
|
|
612
|
-
delay: (ctx)=>ctx.type === 'data' ? ctx.parsed.x * 500 + ctx.dataIndex * 20 : undefined,
|
|
613
|
-
duration: (ctx)=>ctx.type === 'data' ? (ctx.parsed._custom.x - ctx.parsed.x) * 200 : undefined,
|
|
831
|
+
delay: (ctx)=>ctx.type === 'data' ? ctx.parsed[ctx.dataset.orientation === 'vertical' ? 'y' : 'x'] * 500 + ctx.dataIndex * 20 : undefined,
|
|
832
|
+
duration: (ctx)=>ctx.type === 'data' ? (ctx.parsed._custom[ctx.dataset.orientation === 'vertical' ? 'y' : 'x'] - ctx.parsed[ctx.dataset.orientation === 'vertical' ? 'y' : 'x']) * 200 : undefined,
|
|
614
833
|
easing: 'linear'
|
|
615
834
|
}
|
|
616
835
|
},
|
|
@@ -621,6 +840,7 @@ SankeyController.defaults = {
|
|
|
621
840
|
modeX: 'edge',
|
|
622
841
|
nodePadding: 10,
|
|
623
842
|
nodeWidth: 10,
|
|
843
|
+
orientation: 'horizontal',
|
|
624
844
|
transitions: {
|
|
625
845
|
hide: {
|
|
626
846
|
animations: {
|
|
@@ -677,7 +897,7 @@ SankeyController.overrides = {
|
|
|
677
897
|
callbacks: {
|
|
678
898
|
label (context) {
|
|
679
899
|
const parsedCustom = context.parsed._custom;
|
|
680
|
-
return parsedCustom.from.key
|
|
900
|
+
return `${parsedCustom.from.key} -> ${parsedCustom.to.key}: ${parsedCustom.flow}`;
|
|
681
901
|
},
|
|
682
902
|
title () {
|
|
683
903
|
return '';
|
|
@@ -704,7 +924,25 @@ SankeyController.overrides = {
|
|
|
704
924
|
}
|
|
705
925
|
};
|
|
706
926
|
|
|
707
|
-
const controlPoints = (x, y, x2, y2)=>
|
|
927
|
+
const controlPoints = (x, y, x2, y2, orientation)=>orientation === 'vertical' ? y < y2 ? {
|
|
928
|
+
cp1: {
|
|
929
|
+
x,
|
|
930
|
+
y: y + (y2 - y) / 3 * 2
|
|
931
|
+
},
|
|
932
|
+
cp2: {
|
|
933
|
+
x: x2,
|
|
934
|
+
y: y + (y2 - y) / 3
|
|
935
|
+
}
|
|
936
|
+
} : {
|
|
937
|
+
cp1: {
|
|
938
|
+
x: 0,
|
|
939
|
+
y: y - (y - y2) / 3
|
|
940
|
+
},
|
|
941
|
+
cp2: {
|
|
942
|
+
x: 0,
|
|
943
|
+
y: y2 + (y - y2) / 3
|
|
944
|
+
}
|
|
945
|
+
} : x < x2 ? {
|
|
708
946
|
cp1: {
|
|
709
947
|
x: x + (x2 - x) / 3 * 2,
|
|
710
948
|
y
|
|
@@ -729,14 +967,17 @@ const pointInLine = (p1, p2, t)=>({
|
|
|
729
967
|
});
|
|
730
968
|
const applyAlpha = (original, alpha)=>helpers.color(original).alpha(alpha).rgbString();
|
|
731
969
|
const getColorOption = (option, alpha)=>typeof option === 'string' ? applyAlpha(option, alpha) : option;
|
|
732
|
-
|
|
970
|
+
const getHoverColorOption = (option)=>typeof option === 'string' ? helpers.getHoverColor(option) : option;
|
|
971
|
+
function setStyle(ctx, { x, x2, y, y2, options }) {
|
|
733
972
|
let fill = 'black';
|
|
734
|
-
if (options.
|
|
973
|
+
if (options.flowColor !== null) {
|
|
974
|
+
fill = options.flowColor;
|
|
975
|
+
} else if (options.colorMode === 'from') {
|
|
735
976
|
fill = getColorOption(options.colorFrom, options.alpha);
|
|
736
977
|
} else if (options.colorMode === 'to') {
|
|
737
978
|
fill = getColorOption(options.colorTo, options.alpha);
|
|
738
979
|
} else if (typeof options.colorFrom === 'string' && typeof options.colorTo === 'string') {
|
|
739
|
-
fill = ctx.createLinearGradient(x, 0, x2, 0);
|
|
980
|
+
fill = options.orientation === 'vertical' ? ctx.createLinearGradient(0, y, 0, y2) : ctx.createLinearGradient(x, 0, x2, 0);
|
|
740
981
|
fill.addColorStop(0, applyAlpha(options.colorFrom, options.alpha));
|
|
741
982
|
fill.addColorStop(1, applyAlpha(options.colorTo, options.alpha));
|
|
742
983
|
}
|
|
@@ -744,44 +985,104 @@ function setStyle(ctx, { x, x2, options }) {
|
|
|
744
985
|
ctx.strokeStyle = fill;
|
|
745
986
|
ctx.lineWidth = 0.5;
|
|
746
987
|
}
|
|
988
|
+
function clipFlow(ctx, { height, width, x, x2, y, y2 }, progress, orientation) {
|
|
989
|
+
ctx.beginPath();
|
|
990
|
+
if (orientation === 'vertical') {
|
|
991
|
+
ctx.rect(Math.min(x, x2), y, Math.abs(x2 - x) + width + 1, (y2 - y) * progress + 1);
|
|
992
|
+
} else {
|
|
993
|
+
ctx.rect(x, Math.min(y, y2), (x2 - x) * progress + 1, Math.abs(y2 - y) + height + 1);
|
|
994
|
+
}
|
|
995
|
+
ctx.clip();
|
|
996
|
+
}
|
|
997
|
+
function drawFlowPath(ctx, { height, width, x, x2, y, y2 }, { cp1, cp2 }, orientation) {
|
|
998
|
+
ctx.beginPath();
|
|
999
|
+
ctx.moveTo(x, y);
|
|
1000
|
+
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x2, y2);
|
|
1001
|
+
if (orientation === 'vertical') {
|
|
1002
|
+
ctx.lineTo(x2 + width, y2);
|
|
1003
|
+
ctx.bezierCurveTo(cp2.x + width, cp2.y, cp1.x + width, cp1.y, x + width, y);
|
|
1004
|
+
} else {
|
|
1005
|
+
ctx.lineTo(x2, y2 + height);
|
|
1006
|
+
ctx.bezierCurveTo(cp2.x, cp2.y + height, cp1.x, cp1.y + height, x, y + height);
|
|
1007
|
+
}
|
|
1008
|
+
ctx.lineTo(x, y);
|
|
1009
|
+
ctx.stroke();
|
|
1010
|
+
ctx.closePath();
|
|
1011
|
+
ctx.fill();
|
|
1012
|
+
}
|
|
1013
|
+
function getLabelRect({ height, width, x, x2, y, y2 }, orientation) {
|
|
1014
|
+
if (orientation === 'vertical') {
|
|
1015
|
+
return {
|
|
1016
|
+
height: y2 - y,
|
|
1017
|
+
width: Math.abs(x2 - x) + width,
|
|
1018
|
+
x: Math.min(x, x2),
|
|
1019
|
+
y
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
return {
|
|
1023
|
+
height: Math.abs(y2 - y) + height,
|
|
1024
|
+
width: x2 - x,
|
|
1025
|
+
x,
|
|
1026
|
+
y: Math.min(y, y2)
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
747
1029
|
class Flow extends chart_js.Element {
|
|
748
1030
|
draw(ctx) {
|
|
749
|
-
const { x, x2, y, y2, height, progress } = this;
|
|
750
|
-
const
|
|
1031
|
+
const { x, x2, y, y2, height, progress, width } = this;
|
|
1032
|
+
const orientation = this.options.orientation;
|
|
1033
|
+
const controls = controlPoints(x, y, x2, y2, orientation);
|
|
1034
|
+
const geometry = {
|
|
1035
|
+
height,
|
|
1036
|
+
width,
|
|
1037
|
+
x,
|
|
1038
|
+
x2,
|
|
1039
|
+
y,
|
|
1040
|
+
y2
|
|
1041
|
+
};
|
|
751
1042
|
if (progress === 0) {
|
|
752
1043
|
return;
|
|
753
1044
|
}
|
|
754
1045
|
ctx.save();
|
|
755
1046
|
if (progress < 1) {
|
|
756
|
-
ctx
|
|
757
|
-
ctx.rect(x, Math.min(y, y2), (x2 - x) * progress + 1, Math.abs(y2 - y) + height + 1);
|
|
758
|
-
ctx.clip();
|
|
1047
|
+
clipFlow(ctx, geometry, progress, orientation);
|
|
759
1048
|
}
|
|
760
1049
|
setStyle(ctx, this);
|
|
761
|
-
ctx
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1050
|
+
drawFlowPath(ctx, geometry, controls, orientation);
|
|
1051
|
+
const labels = this.options.flowLabels;
|
|
1052
|
+
if (labels.display) {
|
|
1053
|
+
const font = helpers.toFont(labels.font ?? chart_js.Chart.defaults.font);
|
|
1054
|
+
const labelRect = getLabelRect(geometry, orientation);
|
|
1055
|
+
drawLabel(ctx, `${this.flow}`, {
|
|
1056
|
+
autoPosition: 'center',
|
|
1057
|
+
backgroundColor: labels.backgroundColor,
|
|
1058
|
+
borderRadius: labels.borderRadius,
|
|
1059
|
+
borderWidth: 0,
|
|
1060
|
+
color: labels.color,
|
|
1061
|
+
font,
|
|
1062
|
+
height: labelRect.height,
|
|
1063
|
+
lineOffset: font.lineHeight / 2,
|
|
1064
|
+
padding: labels.padding,
|
|
1065
|
+
position: labels.position,
|
|
1066
|
+
width: labelRect.width,
|
|
1067
|
+
x: labelRect.x,
|
|
1068
|
+
y: labelRect.y
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
770
1071
|
ctx.restore();
|
|
771
1072
|
}
|
|
772
1073
|
inRange(mouseX, mouseY, useFinalPosition) {
|
|
773
|
-
const { x, y, x2, y2, height } = this.getProps([
|
|
1074
|
+
const { x, y, x2, y2, height, width } = this.getProps([
|
|
774
1075
|
'x',
|
|
775
1076
|
'y',
|
|
776
1077
|
'x2',
|
|
777
1078
|
'y2',
|
|
778
|
-
'height'
|
|
1079
|
+
'height',
|
|
1080
|
+
'width'
|
|
779
1081
|
], useFinalPosition);
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
const
|
|
784
|
-
const t = (mouseX - x) / (x2 - x);
|
|
1082
|
+
const vertical = this.options.orientation === 'vertical';
|
|
1083
|
+
if (vertical ? mouseY < y || mouseY > y2 : mouseX < x || mouseX > x2) return false;
|
|
1084
|
+
const { cp1, cp2 } = controlPoints(x, y, x2, y2, this.options.orientation);
|
|
1085
|
+
const t = vertical ? (mouseY - y) / (y2 - y) : (mouseX - x) / (x2 - x);
|
|
785
1086
|
const p1 = {
|
|
786
1087
|
x,
|
|
787
1088
|
y
|
|
@@ -795,15 +1096,18 @@ class Flow extends chart_js.Element {
|
|
|
795
1096
|
const c = pointInLine(cp2, p2, t);
|
|
796
1097
|
const d = pointInLine(a, b, t);
|
|
797
1098
|
const e = pointInLine(b, c, t);
|
|
798
|
-
const
|
|
799
|
-
return mouseY >=
|
|
1099
|
+
const edge = pointInLine(d, e, t);
|
|
1100
|
+
return vertical ? mouseX >= edge.x && mouseX <= edge.x + width : mouseY >= edge.y && mouseY <= edge.y + height;
|
|
800
1101
|
}
|
|
801
1102
|
inXRange(mouseX, useFinalPosition) {
|
|
802
|
-
const { x, x2 } = this.getProps([
|
|
1103
|
+
const { x, x2, width } = this.getProps([
|
|
803
1104
|
'x',
|
|
804
|
-
'x2'
|
|
1105
|
+
'x2',
|
|
1106
|
+
'width'
|
|
805
1107
|
], useFinalPosition);
|
|
806
|
-
|
|
1108
|
+
const min = Math.min(x, x2);
|
|
1109
|
+
const max = Math.max(x, x2) + (this.options.orientation === 'vertical' ? width : 0);
|
|
1110
|
+
return mouseX >= min && mouseX <= max;
|
|
807
1111
|
}
|
|
808
1112
|
inYRange(mouseY, useFinalPosition) {
|
|
809
1113
|
const { y, y2, height } = this.getProps([
|
|
@@ -812,30 +1116,34 @@ class Flow extends chart_js.Element {
|
|
|
812
1116
|
'height'
|
|
813
1117
|
], useFinalPosition);
|
|
814
1118
|
const minY = Math.min(y, y2);
|
|
815
|
-
const maxY = Math.max(y, y2) + height;
|
|
1119
|
+
const maxY = Math.max(y, y2) + (this.options.orientation === 'vertical' ? 0 : height);
|
|
816
1120
|
return mouseY >= minY && mouseY <= maxY;
|
|
817
1121
|
}
|
|
818
1122
|
getCenterPoint(useFinalPosition) {
|
|
819
|
-
const { x, y, x2, y2, height } = this.getProps([
|
|
1123
|
+
const { x, y, x2, y2, height, width } = this.getProps([
|
|
820
1124
|
'x',
|
|
821
1125
|
'y',
|
|
822
1126
|
'x2',
|
|
823
1127
|
'y2',
|
|
824
|
-
'height'
|
|
1128
|
+
'height',
|
|
1129
|
+
'width'
|
|
825
1130
|
], useFinalPosition);
|
|
1131
|
+
const vertical = this.options.orientation === 'vertical';
|
|
826
1132
|
return {
|
|
827
|
-
x: (x + x2) / 2,
|
|
828
|
-
y: (y + y2 + height) / 2
|
|
1133
|
+
x: (x + x2 + (vertical ? width : 0)) / 2,
|
|
1134
|
+
y: (y + y2 + (vertical ? 0 : height)) / 2
|
|
829
1135
|
};
|
|
830
1136
|
}
|
|
831
|
-
tooltipPosition(useFinalPosition) {
|
|
1137
|
+
tooltipPosition(useFinalPosition = false) {
|
|
832
1138
|
return this.getCenterPoint(useFinalPosition);
|
|
833
1139
|
}
|
|
834
1140
|
getRange(axis) {
|
|
835
|
-
|
|
1141
|
+
const vertical = this.options.orientation === 'vertical';
|
|
1142
|
+
if (axis === 'x') return vertical ? this.width / 2 : 0;
|
|
1143
|
+
return vertical ? 0 : this.height / 2;
|
|
836
1144
|
}
|
|
837
1145
|
constructor(cfg){
|
|
838
|
-
super();
|
|
1146
|
+
super(), this.flow = 0, this.x2 = 0, this.y2 = 0, this.width = 0, this.height = 0, this.progress = 1;
|
|
839
1147
|
if (cfg) {
|
|
840
1148
|
Object.assign(this, cfg);
|
|
841
1149
|
}
|
|
@@ -847,11 +1155,23 @@ Flow.defaults = {
|
|
|
847
1155
|
colorFrom: 'red',
|
|
848
1156
|
colorMode: 'gradient',
|
|
849
1157
|
colorTo: 'green',
|
|
850
|
-
|
|
851
|
-
|
|
1158
|
+
flowColor: null,
|
|
1159
|
+
flowLabels: {
|
|
1160
|
+
borderRadius: 0,
|
|
1161
|
+
color: 'black',
|
|
1162
|
+
display: false,
|
|
1163
|
+
padding: 4,
|
|
1164
|
+
position: 'center'
|
|
1165
|
+
},
|
|
1166
|
+
hoverColorFrom: (_ctx, options)=>getHoverColorOption(options.colorFrom),
|
|
1167
|
+
hoverColorTo: (_ctx, options)=>getHoverColorOption(options.colorTo),
|
|
1168
|
+
orientation: 'horizontal'
|
|
852
1169
|
};
|
|
853
1170
|
Flow.descriptors = {
|
|
854
|
-
_scriptable: true
|
|
1171
|
+
_scriptable: true,
|
|
1172
|
+
flowLabels: {
|
|
1173
|
+
_scriptable: true
|
|
1174
|
+
}
|
|
855
1175
|
};
|
|
856
1176
|
|
|
857
1177
|
chart_js.Chart.register(SankeyController, Flow);
|