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