chartjs-chart-sankey 0.12.1 → 0.14.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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * chartjs-chart-sankey v0.12.1
2
+ * chartjs-chart-sankey v0.14.0
3
3
  * https://github.com/kurkle/chartjs-chart-sankey#readme
4
4
  * (c) 2024 Jukka Kurkela
5
5
  * Released under the MIT license
@@ -10,931 +10,847 @@ typeof define === 'function' && define.amd ? define(['chart.js', 'chart.js/helpe
10
10
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Chart, global.Chart.helpers));
11
11
  })(this, (function (chart_js, helpers) { 'use strict';
12
12
 
13
- /**
14
- * @param {string | Array<string>} raw
15
- * @return {Array<string>}
16
- */
13
+ const defined = (x)=>x !== undefined;
17
14
  function toTextLines(raw) {
18
- const lines = [];
19
- const inputs = helpers.isArray(raw) ? raw : helpers.isNullOrUndef(raw) ? [] : [raw];
20
-
21
- while (inputs.length) {
22
- const input = inputs.pop();
23
- if (typeof input === 'string') {
24
- lines.unshift.apply(lines, input.split('\n'));
25
- } else if (Array.isArray(input)) {
26
- inputs.push.apply(inputs, input);
27
- } else if (!helpers.isNullOrUndef(inputs)) {
28
- lines.unshift('' + input);
29
- }
30
- }
31
-
32
- return lines;
15
+ if (!raw) return [];
16
+ const lines = [];
17
+ const inputs = Array.isArray(raw) ? raw : [
18
+ raw
19
+ ];
20
+ while(inputs.length){
21
+ const input = inputs.pop();
22
+ if (typeof input === 'string') {
23
+ lines.unshift(...input.split('\n'));
24
+ } else if (Array.isArray(input)) {
25
+ inputs.push(...input);
26
+ } else if (input) {
27
+ lines.unshift(`${input}`);
28
+ }
29
+ }
30
+ return lines;
33
31
  }
34
-
35
- /**
36
- * @param {any} size
37
- * @return {'min' | 'max'}
38
- */
39
32
  function validateSizeValue(size) {
40
- if (!size || ['min', 'max'].indexOf(size) === -1) {
41
- return 'max';
42
- }
43
- return size;
33
+ if (!size || [
34
+ 'min',
35
+ 'max'
36
+ ].indexOf(size) === -1) {
37
+ return 'max';
38
+ }
39
+ return size;
44
40
  }
45
41
 
46
- /**
47
- * @param x {any}
48
- * @return {boolean}
49
- */
50
- const defined = x => x !== undefined;
51
-
52
- /**
53
- * @param {Map<string, SankeyNode>} nodes
54
- * @param {Array<SankeyDataPoint>} data
55
- * @return {number}
56
- */
57
- function calculateX(nodes, data) {
58
- const to = new Set(data.map(dataPoint => dataPoint.to));
59
- const from = new Set(data.map(dataPoint => dataPoint.from));
60
- const keys = new Set([...nodes.keys()]);
61
- let x = 0;
62
- while (keys.size) {
63
- const column = nextColumn([...keys], to);
64
- for (const key of column) {
65
- const node = nodes.get(key);
66
- if (!defined(node.x)) {
67
- node.x = x;
68
- }
69
- keys.delete(key);
70
- }
71
- if (keys.size) {
72
- to.clear();
73
- data.filter(flow => keys.has(flow.from)).forEach(flow => to.add(flow.to));
74
- x++;
75
- }
76
- }
77
- [...nodes.keys()]
78
- .filter(key => !from.has(key))
79
- .forEach(key => {
80
- const node = nodes.get(key);
81
- // Only move the node to right edge, if it's column is not defined
82
- if (!node.column) {
83
- node.x = x;
84
- }
85
- });
86
-
87
- return [...nodes.values()]
88
- .reduce((max, node) => Math.max(max, node.x), 0);
42
+ const flowSort = (a, b)=>{
43
+ if (b.flow === a.flow) return a.index - b.index;
44
+ return b.flow - a.flow;
45
+ };
46
+ const setSizes = (nodes, size)=>{
47
+ const sizeMethod = validateSizeValue(size);
48
+ for (const node of nodes.values()){
49
+ node.from.sort(flowSort);
50
+ node.to.sort(flowSort);
51
+ node.size = Math[sizeMethod](node.in || node.out, node.out || node.in);
52
+ }
53
+ };
54
+ const setPriorities = (nodes, priority)=>{
55
+ if (!priority) return;
56
+ for (const node of nodes.values()){
57
+ if (node.key in priority) {
58
+ node.priority = priority[node.key];
59
+ }
60
+ }
61
+ };
62
+ const setColumns = (nodes, column)=>{
63
+ if (!column) return;
64
+ for (const node of nodes.values()){
65
+ if (node.key in column) {
66
+ node.column = true;
67
+ node.x = column[node.key];
68
+ }
69
+ }
70
+ };
71
+ const getParsedData = (data, parsing)=>{
72
+ const { from: fromKey = 'from', to: toKey = 'to', flow: flowKey = 'flow' } = parsing;
73
+ return data.map(({ [fromKey]: from, [toKey]: to, [flowKey]: flow })=>({
74
+ from,
75
+ to,
76
+ flow
77
+ }));
78
+ };
79
+ function buildNodesFromData(data, { size, priority, column }) {
80
+ const nodes = new Map();
81
+ for(let i = 0; i < data.length; i++){
82
+ const { from, to, flow } = data[i];
83
+ const fromNode = nodes.get(from) ?? {
84
+ key: from,
85
+ in: 0,
86
+ out: 0,
87
+ size: 0,
88
+ from: [],
89
+ to: []
90
+ };
91
+ const toNode = (from === to ? fromNode : nodes.get(to)) ?? {
92
+ key: to,
93
+ in: 0,
94
+ out: 0,
95
+ size: 0,
96
+ from: [],
97
+ to: []
98
+ };
99
+ fromNode.out += flow;
100
+ fromNode.to.push({
101
+ key: to,
102
+ flow: flow,
103
+ index: i,
104
+ node: toNode,
105
+ addY: 0
106
+ });
107
+ if (fromNode.to.length === 1) {
108
+ nodes.set(from, fromNode);
109
+ }
110
+ toNode.in += flow;
111
+ toNode.from.push({
112
+ key: from,
113
+ flow: flow,
114
+ index: i,
115
+ node: fromNode,
116
+ addY: 0
117
+ });
118
+ if (toNode.from.length === 1) {
119
+ nodes.set(to, toNode);
120
+ }
121
+ }
122
+ setSizes(nodes, size);
123
+ setPriorities(nodes, priority);
124
+ setColumns(nodes, column);
125
+ return nodes;
89
126
  }
90
127
 
91
- /**
92
- * @param {Array<string>} keys
93
- * @param {Set<string>} to
94
- * @return {Array<string>}
95
- */
96
- function nextColumn(keys, to) {
97
- const columnsNotInTo = keys.filter(key => !to.has(key));
98
- return columnsNotInTo.length ? columnsNotInTo : keys.slice(0, 1);
128
+ const SMALL_VALUE = 1e-6;
129
+ const getAllKeysForward = (nodes, visited = new Set())=>{
130
+ const keys = [];
131
+ for (const node of nodes){
132
+ if (visited.has(node.key)) continue;
133
+ visited.add(node.key);
134
+ keys.push(node.key, ...getAllKeysForward(node.to.map((to)=>to.node), visited));
135
+ }
136
+ return keys;
137
+ };
138
+ const startColumn = (data, nodes)=>{
139
+ const startNodes = nodes.filter((node)=>node.from.length === 0);
140
+ const column = startNodes.map((node)=>node.key);
141
+ const startRef = getAllKeysForward(startNodes);
142
+ const referencedNodes = new Set(startRef);
143
+ for (const point of data){
144
+ if (!referencedNodes.has(point.from) && !referencedNodes.has(point.to)) {
145
+ column.push(point.from);
146
+ referencedNodes.add(point.from);
147
+ }
148
+ referencedNodes.add(point.to);
149
+ }
150
+ return column;
151
+ };
152
+ const nextColumn = (dataWithoutDirectLoops, remainingKeys)=>{
153
+ const remainingTo = new Set(dataWithoutDirectLoops.filter((flow)=>remainingKeys.has(flow.from)).map((flow)=>flow.to));
154
+ const remainingKeyArray = [
155
+ ...remainingKeys
156
+ ];
157
+ const columnsNotInTo = remainingKeyArray.filter((key)=>!remainingTo.has(key));
158
+ return columnsNotInTo.length ? columnsNotInTo : remainingKeyArray.slice(0, 1);
159
+ };
160
+ function calculateX(nodeMap, data, mode) {
161
+ const dataWithoutDirectLoops = data.filter((dp)=>dp.from !== dp.to);
162
+ const allKeys = [
163
+ ...nodeMap.keys()
164
+ ];
165
+ const allNodes = [
166
+ ...nodeMap.values()
167
+ ];
168
+ const keysToPlace = new Set(allKeys);
169
+ let x = 0;
170
+ while(keysToPlace.size){
171
+ const column = x === 0 ? startColumn(data, allNodes) : nextColumn(dataWithoutDirectLoops, keysToPlace);
172
+ if (!column.length) {
173
+ throw new Error('Fatal error: Unable to place nodes to columns. Please report this issue.');
174
+ }
175
+ for (const key of column){
176
+ const node = nodeMap.get(key);
177
+ if (node && !defined(node.x)) {
178
+ node.x = x;
179
+ }
180
+ keysToPlace.delete(key);
181
+ }
182
+ if (keysToPlace.size) {
183
+ x++;
184
+ }
185
+ }
186
+ const maxX = allNodes.reduce((max, node)=>Math.max(max, node.x), 0);
187
+ if (mode === 'edge') {
188
+ const from = new Set(data.map((dataPoint)=>dataPoint.from));
189
+ allKeys.filter((key)=>!from.has(key)).forEach((key)=>{
190
+ const node = nodeMap.get(key);
191
+ if (node && !node.column) {
192
+ node.x = maxX;
193
+ }
194
+ });
195
+ }
196
+ return maxX;
99
197
  }
100
-
101
- /**
102
- * @param {SankeyNode} a
103
- * @param {SankeyNode} b
104
- * @return {number}
105
- */
106
- const nodeByXY = (a, b) => a.x !== b.x ? a.x - b.x : a.y - b.y;
107
-
108
198
  let prevCountId = -1;
109
199
  function getCountId() {
110
- prevCountId = prevCountId < 100 ? prevCountId + 1 : 0;
111
- return prevCountId;
200
+ prevCountId = prevCountId < 100 ? prevCountId + 1 : 0;
201
+ return prevCountId;
112
202
  }
113
-
114
- /**
115
- * @param {Array<FromToElement>} list
116
- * @param {string} prop
117
- * @return {number}
118
- */
119
203
  function nodeCount(list, prop, countId = getCountId()) {
120
- let count = 0;
121
- for (const elem of list) {
122
- if (elem.node._visited === countId) {
123
- continue;
124
- }
125
- elem.node._visited = countId;
126
- count += elem.node[prop].length + nodeCount(elem.node[prop], prop, countId);
127
- }
128
- return count;
204
+ let count = 0;
205
+ for (const elem of list){
206
+ if (elem.node._visited === countId) {
207
+ continue;
208
+ }
209
+ elem.node._visited = countId;
210
+ count += elem.node[prop].length + nodeCount(elem.node[prop], prop, countId);
211
+ }
212
+ return count;
129
213
  }
130
-
131
- /**
132
- * @param {string} prop
133
- * @return {function(FromToElement, FromToElement): number}
134
- */
135
- const flowByNodeCount = (prop) => (a, b) => (nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop)) || (a.node[prop].length - b.node[prop].length);
136
-
137
- /**
138
- * @param {SankeyNode} node
139
- * @param {number} y
140
- * @return {number}
141
- */
214
+ const flowByNodeCount = (prop)=>(a, b)=>nodeCount(a.node[prop], prop) - nodeCount(b.node[prop], prop) || a.node[prop].length - b.node[prop].length;
142
215
  function processFrom(node, y) {
143
- node.from.sort(flowByNodeCount('from'));
144
- for (const flow of node.from) {
145
- const n = flow.node;
146
- if (!defined(n.y)) {
147
- n.y = y;
148
- processFrom(n, y);
149
- }
150
- y = Math.max(n.y + n.out, y);
151
- }
152
- return y;
216
+ if (!node.from.length) return y;
217
+ node.from.sort(flowByNodeCount('from'));
218
+ for (const flow of node.from){
219
+ const n = flow.node;
220
+ if (!defined(n.y)) {
221
+ n.y = y;
222
+ processFrom(n, y ? y + SMALL_VALUE : 0);
223
+ }
224
+ y = Math.max(n.y + n.out, y);
225
+ }
226
+ return node.y + node.size;
153
227
  }
154
-
155
- /**
156
- * @param {SankeyNode} node
157
- * @param {number} y
158
- * @return {number}
159
- */
160
228
  function processTo(node, y) {
161
- node.to.sort(flowByNodeCount('to'));
162
- for (const flow of node.to) {
163
- const n = flow.node;
164
- if (!defined(n.y)) {
165
- n.y = y;
166
- processTo(n, y);
167
- }
168
- y = Math.max(n.y + n.in, y);
169
- }
170
- return y;
229
+ if (!node.to.length) return y;
230
+ node.to.sort(flowByNodeCount('to'));
231
+ for (const flow of node.to){
232
+ const n = flow.node;
233
+ if (!defined(n.y)) {
234
+ n.y = y;
235
+ processTo(n, y ? y + SMALL_VALUE : 0);
236
+ }
237
+ y = Math.max(n.y + Math.max(n.in, n.out), y);
238
+ }
239
+ return node.y + node.size;
171
240
  }
172
-
173
- /**
174
- * @param {SankeyNode} node
175
- * @param {number} value
176
- * @return {number}
177
- */
178
241
  function setOrGetY(node, value) {
179
- if (defined(node.y)) {
180
- return node.y;
181
- }
182
- node.y = value;
183
- return value;
242
+ if (defined(node.y)) {
243
+ return node.y;
244
+ }
245
+ node.y = value;
246
+ return value;
184
247
  }
185
-
186
- /**
187
- * @param {Array<SankeyNode>} nodeArray
188
- * @param {number} maxX
189
- * @return {number}
190
- */
191
248
  function processRest(nodeArray, maxX) {
192
- const leftNodes = nodeArray.filter(node => node.x === 0);
193
- const rightNodes = nodeArray.filter(node => node.x === maxX);
194
- const leftToDo = leftNodes.filter(node => !defined(node.y));
195
- const rightToDo = rightNodes.filter(node => !defined(node.y));
196
- const centerToDo = nodeArray.filter(node => node.x > 0 && node.x < maxX && !defined(node.y));
197
-
198
- let leftY = leftNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.out) || 0), 0);
199
- let rightY = rightNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.in) || 0), 0);
200
- let centerY = 0;
201
-
202
- if (leftY >= rightY) {
203
- leftToDo.forEach(node => {
204
- leftY = setOrGetY(node, leftY);
205
- leftY = Math.max(leftY + node.out, processTo(node, leftY));
206
- });
207
-
208
- rightToDo.forEach(node => {
209
- rightY = setOrGetY(node, rightY);
210
- rightY = Math.max(rightY + node.in, processTo(node, rightY));
211
- });
212
- } else {
213
- rightToDo.forEach(node => {
214
- rightY = setOrGetY(node, rightY);
215
- rightY = Math.max(rightY + node.in, processTo(node, rightY));
216
- });
217
-
218
- leftToDo.forEach(node => {
219
- leftY = setOrGetY(node, leftY);
220
- leftY = Math.max(leftY + node.out, processTo(node, leftY));
249
+ const leftNodes = nodeArray.filter((node)=>node.x === 0);
250
+ const rightNodes = nodeArray.filter((node)=>node.x === maxX);
251
+ const leftToDo = leftNodes.filter((node)=>!defined(node.y));
252
+ const rightToDo = rightNodes.filter((node)=>!defined(node.y));
253
+ const centerToDo = nodeArray.filter((node)=>node.x > 0 && node.x < maxX && !defined(node.y));
254
+ let leftY = leftNodes.reduce((acc, cur)=>Math.max(acc, cur.y + cur.out || 0), 0) + SMALL_VALUE;
255
+ let rightY = rightNodes.reduce((acc, cur)=>Math.max(acc, cur.y + cur.in || 0), 0) + SMALL_VALUE;
256
+ let centerY = 0;
257
+ if (leftY >= rightY) {
258
+ leftToDo.forEach((node)=>{
259
+ leftY = setOrGetY(node, leftY);
260
+ leftY = Math.max(leftY + node.out, processTo(node, leftY));
261
+ });
262
+ rightToDo.forEach((node)=>{
263
+ rightY = setOrGetY(node, rightY);
264
+ rightY = Math.max(rightY + node.in, processFrom(node, rightY));
265
+ });
266
+ } else {
267
+ leftToDo.forEach((node)=>{
268
+ leftY = setOrGetY(node, leftY);
269
+ });
270
+ rightToDo.forEach((node)=>{
271
+ rightY = setOrGetY(node, rightY);
272
+ rightY = Math.max(rightY + node.in, processFrom(node, rightY));
273
+ });
274
+ }
275
+ centerToDo.forEach((node)=>{
276
+ let y = nodeArray.filter((n)=>n.x === node.x && defined(n.y)).reduce((acc, cur)=>Math.max(acc, cur.y + Math.max(cur.in, cur.out)), 0);
277
+ y = setOrGetY(node, y);
278
+ y = Math.max(y + node.in, processFrom(node, y));
279
+ y = Math.max(y + node.out, processTo(node, y));
280
+ centerY = Math.max(centerY, y);
221
281
  });
222
- }
223
- centerToDo.forEach(node => {
224
- let y = nodeArray.filter(n => n.x === node.x && defined(n.y))
225
- .reduce((acc, cur) => Math.max(acc, cur.y + Math.max(cur.in, cur.out)), 0);
226
- y = setOrGetY(node, y);
227
- y = Math.max(y + node.in, processFrom(node, y));
228
- y = Math.max(y + node.out, processTo(node, y));
229
- centerY = Math.max(centerY, y);
230
- });
231
-
232
- return Math.max(leftY, rightY, centerY);
282
+ return Math.max(leftY, rightY, centerY);
233
283
  }
234
-
235
- /**
236
- * @param {Array<SankeyNode>} nodeArray
237
- * @param {number} maxX
238
- * @return {number}
239
- */
284
+ const fixTop = (nodeArray, maxX)=>{
285
+ let maxY = 0;
286
+ for(let x = 0; x <= maxX; x++){
287
+ const nodes = nodeArray.filter((n)=>n.x === x).sort((a, b)=>a.y - b.y);
288
+ let minY = 0;
289
+ for (const node of nodes){
290
+ if (node.y < minY) node.y = minY;
291
+ minY = node.y + node.size;
292
+ }
293
+ maxY = Math.max(maxY, minY);
294
+ }
295
+ return maxY;
296
+ };
297
+ const findStartNode = (nodeArray, maxX)=>{
298
+ const size = [
299
+ ...nodeArray
300
+ ].sort((a, b)=>a.size - b.size).pop().size;
301
+ const biggest = nodeArray.filter((n)=>n.size === size);
302
+ if (biggest.length === 1) return biggest[0];
303
+ biggest.sort((a, b)=>a.x - b.x);
304
+ if (biggest[0].x === 0) return biggest[0];
305
+ if (biggest[biggest.length - 1].x === maxX) return biggest.pop();
306
+ const mid = Math.floor(biggest.length / 2);
307
+ return biggest[mid];
308
+ };
240
309
  function calculateY(nodeArray, maxX) {
241
- nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out));
242
- const start = nodeArray[0];
243
- start.y = 0;
244
- const left = processFrom(start, 0);
245
- const right = processTo(start, 0);
246
- const rest = processRest(nodeArray, maxX);
247
- return Math.max(left, right, rest);
310
+ if (!nodeArray.length) return 0;
311
+ const start = findStartNode(nodeArray, maxX);
312
+ start.y = 0;
313
+ processFrom(start, 0);
314
+ processTo(start, 0);
315
+ processRest(nodeArray, maxX);
316
+ return fixTop(nodeArray, maxX);
248
317
  }
249
-
250
- /**
251
- * @param {Array<SankeyNode>} nodeArray
252
- * @param {number} maxX
253
- * @return {number}
254
- */
255
318
  function calculateYUsingPriority(nodeArray, maxX) {
256
- let maxY = 0;
257
- let nextYStart = 0;
258
- for (let x = 0; x <= maxX; x++) {
259
- let y = nextYStart;
260
- const nodes = nodeArray.filter(node => node.x === x).sort((a, b) => a.priority - b.priority);
261
- nextYStart = nodes.length ? nodes[0].to.filter(to => to.node.x > x + 1).reduce((acc, cur) => acc + cur.flow, 0) || 0 : 0;
262
- for (const node of nodes) {
263
- node.y = y;
264
- y += Math.max(node.out, node.in);
265
- }
266
- maxY = Math.max(y, maxY);
267
- }
268
- return maxY;
319
+ let maxY = 0;
320
+ let nextYStart = 0;
321
+ for(let x = 0; x <= maxX; x++){
322
+ let y = nextYStart;
323
+ const nodes = nodeArray.filter((node)=>node.x === x).sort((a, b)=>(a.priority ?? 0) - (b.priority ?? 0));
324
+ nextYStart = nodes.length ? nodes[0].to.filter((to)=>to.node.x > x + 1).reduce((acc, cur)=>acc + cur.flow, 0) || 0 : 0;
325
+ for (const node of nodes){
326
+ node.y = y;
327
+ y += Math.max(node.out, node.in);
328
+ }
329
+ maxY = Math.max(y, maxY);
330
+ }
331
+ return maxY;
269
332
  }
270
-
271
- /**
272
- * @param {Array<SankeyNode>} nodeArray
273
- * @param {number} padding
274
- * @return {number}
275
- */
276
- function addPadding(nodeArray, padding) {
277
- let i = 1;
278
- let x = 0;
279
- let prev = 0;
280
- let maxY = 0;
281
- const rows = [];
282
- nodeArray.sort(nodeByXY);
283
- for (const node of nodeArray) {
284
- if (node.y) {
285
- if (node.x === 0) {
286
- rows.push(node.y);
287
- } else {
288
- if (x !== node.x) {
289
- x = node.x;
290
- prev = 0;
333
+ const nodeByXYSize = (a, b)=>{
334
+ if (a.x !== b.x) return a.x - b.x;
335
+ if (a.y === b.y) return a.size - b.size;
336
+ return a.y - b.y;
337
+ };
338
+ function addPadding(nodeArray, padding) {
339
+ let maxY = 0;
340
+ const columnXs = new Map();
341
+ const grid = [];
342
+ const getColIndex = (x)=>{
343
+ if (!columnXs.has(x)) {
344
+ columnXs.set(x, grid.length);
345
+ grid.push([]);
291
346
  }
292
-
293
- for (i = prev + 1; i < rows.length; i++) {
294
- if (rows[i] > node.y) {
295
- break;
296
- }
297
- }
298
- prev = i;
299
- }
300
- node.y += i * padding;
301
- i++;
302
- }
303
- maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
304
- }
305
- return maxY;
347
+ return columnXs.get(x);
348
+ };
349
+ nodeArray.sort(nodeByXYSize);
350
+ for (const node of nodeArray){
351
+ const colIdx = getColIndex(node.x);
352
+ const column = grid[colIdx];
353
+ if (node.y) {
354
+ column.push(node.y);
355
+ let paddings = column.length;
356
+ if (node.in) {
357
+ for(let col = 0; col < colIdx; col++){
358
+ const otherColumn = grid[col];
359
+ for(let row = 0; row < otherColumn.length; row++){
360
+ if (otherColumn[row] > node.y) break;
361
+ paddings = Math.max(row + 1, paddings);
362
+ }
363
+ }
364
+ while(column.length < paddings)column.push(node.y);
365
+ }
366
+ node.y += paddings * padding;
367
+ }
368
+ maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
369
+ }
370
+ return maxY;
306
371
  }
307
-
308
- /**
309
- * @param {Array<SankeyNode>} nodeArray
310
- * @param {'min' | 'max'} size
311
- */
312
- function sortFlows(nodeArray, size) {
313
- nodeArray.forEach((node) => {
314
- const nodeSize = Math[size](node.in || node.out, node.out || node.in);
315
- const overlapFrom = nodeSize < node.in;
316
- const overlapTo = nodeSize < node.out;
317
- let addY = 0;
318
- let len = node.from.length;
319
- node.from.sort((a, b) => (a.node.y + a.node.out / 2) - (b.node.y + b.node.out / 2)).forEach((flow, idx) => {
320
- if (overlapFrom) {
321
- flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
322
- } else {
323
- flow.addY = addY;
324
- addY += flow.flow;
325
- }
326
- });
327
- addY = 0;
328
- len = node.to.length;
329
- node.to.sort((a, b) => (a.node.y + a.node.in / 2) - (b.node.y + b.node.in / 2)).forEach((flow, idx) => {
330
- if (overlapTo) {
331
- flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
332
- } else {
333
- flow.addY = addY;
334
- addY += flow.flow;
335
- }
372
+ function sortFlows(nodeArray) {
373
+ nodeArray.forEach((node)=>{
374
+ const nodeSize = node.size;
375
+ const overlapFrom = nodeSize < node.in;
376
+ const overlapTo = nodeSize < node.out;
377
+ let addY = 0;
378
+ let len = node.from.length;
379
+ node.from.sort((a, b)=>a.node.y + a.node.out / 2 - (b.node.y + b.node.out / 2)).forEach((flow, idx)=>{
380
+ if (overlapFrom) {
381
+ flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
382
+ } else {
383
+ flow.addY = addY;
384
+ addY += flow.flow;
385
+ }
386
+ });
387
+ addY = 0;
388
+ len = node.to.length;
389
+ node.to.sort((a, b)=>a.node.y + a.node.in / 2 - (b.node.y + b.node.in / 2)).forEach((flow, idx)=>{
390
+ if (overlapTo) {
391
+ flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
392
+ } else {
393
+ flow.addY = addY;
394
+ addY += flow.flow;
395
+ }
396
+ });
336
397
  });
337
- });
338
- }
339
-
340
- /**
341
- * @param {Map<string, SankeyNode>} nodes
342
- * @param {Array<SankeyDataPoint>} data
343
- * @param {boolean} priority
344
- * @param {'min' | 'max'} size
345
- * @return {{maxY: number, maxX: number}}
346
- */
347
- function layout(nodes, data, priority, size) {
348
- const nodeArray = [...nodes.values()];
349
- const maxX = calculateX(nodes, data);
350
- const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
351
- const padding = maxY * 0.03; // rows;
352
- const maxYWithPadding = addPadding(nodeArray, padding);
353
- sortFlows(nodeArray, size);
354
- return {maxX, maxY: maxYWithPadding};
355
398
  }
356
-
357
- /**
358
- * @param {Array<SankeyDataPoint>} data Array of raw data elements
359
- * @return {Map<string, SankeyNode>}
360
- */
361
- function buildNodesFromRawData(data) {
362
- const nodes = new Map();
363
- for (let i = 0; i < data.length; i++) {
364
- const {from, to, flow} = data[i];
365
-
366
- if (!nodes.has(from)) {
367
- nodes.set(from, {
368
- key: from,
369
- in: 0,
370
- out: flow,
371
- from: [],
372
- to: [{key: to, flow: flow, index: i}],
373
- });
374
- } else {
375
- const node = nodes.get(from);
376
- node.out += flow;
377
- node.to.push({key: to, flow: flow, index: i});
378
- }
379
- if (!nodes.has(to)) {
380
- nodes.set(to, {
381
- key: to,
382
- in: flow,
383
- out: 0,
384
- from: [{key: from, flow: flow, index: i}],
385
- to: [],
386
- });
387
- } else {
388
- const node = nodes.get(to);
389
- node.in += flow;
390
- node.from.push({key: from, flow: flow, index: i});
391
- }
392
- }
393
-
394
- const flowSort = (a, b) => b.flow - a.flow;
395
-
396
- [...nodes.values()].forEach(node => {
397
- node.from = node.from.sort(flowSort);
398
- node.from.forEach(x => {
399
- x.node = nodes.get(x.key);
400
- });
401
-
402
- node.to = node.to.sort(flowSort);
403
- node.to.forEach(x => {
404
- x.node = nodes.get(x.key);
405
- });
406
- });
407
-
408
- return nodes;
399
+ function layout(nodes, data, { priority, height, nodePadding, modeX }) {
400
+ const nodeArray = [
401
+ ...nodes.values()
402
+ ];
403
+ const maxX = calculateX(nodes, data, modeX);
404
+ const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
405
+ const padding = maxY / height * nodePadding;
406
+ const maxYWithPadding = addPadding(nodeArray, padding);
407
+ sortFlows(nodeArray);
408
+ return {
409
+ maxX,
410
+ maxY: maxYWithPadding
411
+ };
409
412
  }
410
413
 
411
- /**
412
- * @param {Array<FromToElement>} arr
413
- * @param {string} key
414
- * @param {number} index
415
- * @return {number}
416
- */
417
414
  function getAddY(arr, key, index) {
418
- for (const item of arr) {
419
- if (item.key === key && item.index === index) {
420
- return item.addY;
415
+ for (const item of arr){
416
+ if (item.key === key && item.index === index) {
417
+ return item.addY;
418
+ }
421
419
  }
422
- }
423
- return 0;
420
+ return 0;
424
421
  }
425
-
426
422
  class SankeyController extends chart_js.DatasetController {
427
- /**
428
- * @param {ChartMeta<Flow, Element>} meta
429
- * @param {Array<SankeyDataPoint>} data Array of original data elements
430
- * @param {number} start
431
- * @param {number} count
432
- * @return {Array<SankeyParsedData>}
433
- */
434
- parseObjectData(meta, data, start, count) {
435
- const {from: fromKey = 'from', to: toKey = 'to', flow: flowKey = 'flow'} = this.options.parsing;
436
- const sankeyData = data.map(({[fromKey]: from, [toKey]: to, [flowKey]: flow}) => ({from, to, flow}));
437
- const {xScale, yScale} = meta;
438
- const parsed = []; /* Array<SankeyParsedData> */
439
- const nodes = this._nodes = buildNodesFromRawData(sankeyData);
440
- /* getDataset() => SankeyControllerDatasetOptions */
441
- const {column, priority, size} = this.getDataset();
442
- if (priority) {
443
- for (const node of nodes.values()) {
444
- if (node.key in priority) {
445
- node.priority = priority[node.key];
423
+ parseObjectData(meta, data, start, count) {
424
+ const sankeyData = getParsedData(data, this.options.parsing);
425
+ const { xScale, yScale } = meta;
426
+ const parsed = [];
427
+ const nodes = this._nodes = buildNodesFromData(sankeyData, this.options);
428
+ const { maxX, maxY } = layout(nodes, sankeyData, {
429
+ priority: !!this.options.priority,
430
+ height: this.chart.canvas.height,
431
+ nodePadding: this.options.nodePadding,
432
+ modeX: this.options.modeX
433
+ });
434
+ this._maxX = maxX;
435
+ this._maxY = maxY;
436
+ if (!xScale || !yScale) return [];
437
+ for(let i = 0, ilen = sankeyData.length; i < ilen; ++i){
438
+ const dataPoint = sankeyData[i];
439
+ const from = nodes.get(dataPoint.from);
440
+ const to = nodes.get(dataPoint.to);
441
+ if (!from || !to) continue;
442
+ const fromY = (from.y ?? 0) + getAddY(from.to, dataPoint.to, i);
443
+ const toY = (to.y ?? 0) + getAddY(to.from, dataPoint.from, i);
444
+ parsed.push({
445
+ x: xScale.parse(from.x, i),
446
+ y: yScale.parse(fromY, i),
447
+ _custom: {
448
+ from,
449
+ to,
450
+ x: xScale.parse(to.x, i),
451
+ y: yScale.parse(toY, i),
452
+ height: yScale.parse(dataPoint.flow, i),
453
+ flow: dataPoint.flow
454
+ }
455
+ });
446
456
  }
447
- }
457
+ return parsed.slice(start, start + count);
448
458
  }
449
- if (column) {
450
- for (const node of nodes.values()) {
451
- if (node.key in column) {
452
- node.column = true;
453
- node.x = column[node.key];
459
+ getMinMax(scale) {
460
+ return {
461
+ min: 0,
462
+ max: scale === this._cachedMeta.xScale ? this._maxX : this._maxY
463
+ };
464
+ }
465
+ update(mode) {
466
+ const { data } = this._cachedMeta;
467
+ this.updateElements(data, 0, data.length, mode);
468
+ }
469
+ updateElements(elems, start, count, mode) {
470
+ const { xScale, yScale } = this._cachedMeta;
471
+ if (!xScale || !yScale) return;
472
+ const firstOpts = this.resolveDataElementOptions(start, mode);
473
+ const sharedOptions = this.getSharedOptions(firstOpts);
474
+ const { borderWidth, nodeWidth = 10 } = this.options;
475
+ const borderSpace = borderWidth ? borderWidth / 2 + 0.5 : 0;
476
+ for(let i = start; i < start + count; i++){
477
+ const parsed = this.getParsed(i);
478
+ const custom = parsed._custom;
479
+ const y = yScale.getPixelForValue(parsed.y);
480
+ this.updateElement(elems[i], i, {
481
+ x: xScale.getPixelForValue(parsed.x) + nodeWidth + borderSpace,
482
+ y,
483
+ x2: xScale.getPixelForValue(custom.x) - borderSpace,
484
+ y2: yScale.getPixelForValue(custom.y),
485
+ from: custom.from,
486
+ to: custom.to,
487
+ progress: mode === 'reset' ? 0 : 1,
488
+ height: Math.abs(yScale.getPixelForValue(parsed.y + custom.height) - y),
489
+ options: this.resolveDataElementOptions(i, mode)
490
+ }, mode);
454
491
  }
455
- }
492
+ this.updateSharedOptions(sharedOptions, mode, firstOpts);
456
493
  }
457
-
458
- const {maxX, maxY} = layout(nodes, sankeyData, !!priority, validateSizeValue(size));
459
-
460
- this._maxX = maxX;
461
- this._maxY = maxY;
462
-
463
- for (let i = 0, ilen = sankeyData.length; i < ilen; ++i) {
464
- const dataPoint = sankeyData[i];
465
- const from = nodes.get(dataPoint.from);
466
- const to = nodes.get(dataPoint.to);
467
- const fromY = from.y + getAddY(from.to, dataPoint.to, i);
468
- const toY = to.y + getAddY(to.from, dataPoint.from, i);
469
- parsed.push({
470
- x: xScale.parse(from.x, i),
471
- y: yScale.parse(fromY, i),
472
- _custom: {
473
- from,
474
- to,
475
- x: xScale.parse(to.x, i),
476
- y: yScale.parse(toY, i),
477
- height: yScale.parse(dataPoint.flow, i),
478
- }
479
- });
480
- }
481
- return parsed.slice(start, start + count);
482
- }
483
-
484
- getMinMax(scale) {
485
- return {
486
- min: 0,
487
- max: scale === this._cachedMeta.xScale ? this._maxX : this._maxY
488
- };
489
- }
490
-
491
- update(mode) {
492
- const {data} = this._cachedMeta;
493
-
494
- this.updateElements(data, 0, data.length, mode);
495
- }
496
-
497
- /**
498
- * @param {Array<Flow>} elems
499
- * @param {number} start
500
- * @param {number} count
501
- * @param {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"} mode
502
- */
503
- updateElements(elems, start, count, mode) {
504
- const {xScale, yScale} = this._cachedMeta;
505
- const firstOpts = this.resolveDataElementOptions(start, mode);
506
- const sharedOptions = this.getSharedOptions(mode, elems[start], firstOpts);
507
- const dataset = this.getDataset();
508
- const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1) / 2 + 0.5;
509
- const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
510
-
511
- for (let i = start; i < start + count; i++) {
512
- /* getParsed(idx: number) => SankeyParsedData */
513
- const parsed = this.getParsed(i);
514
- const custom = parsed._custom;
515
- const y = yScale.getPixelForValue(parsed.y);
516
- this.updateElement(
517
- elems[i],
518
- i,
519
- {
520
- x: xScale.getPixelForValue(parsed.x) + nodeWidth + borderWidth,
521
- y,
522
- x2: xScale.getPixelForValue(custom.x) - borderWidth,
523
- y2: yScale.getPixelForValue(custom.y),
524
- from: custom.from,
525
- to: custom.to,
526
- progress: mode === 'reset' ? 0 : 1,
527
- height: Math.abs(yScale.getPixelForValue(parsed.y + custom.height) - y),
528
- options: this.resolveDataElementOptions(i, mode)
529
- },
530
- mode);
494
+ _drawLabels() {
495
+ const ctx = this.chart.ctx;
496
+ const options = this.options;
497
+ const nodes = this._nodes || new Map();
498
+ const size = validateSizeValue(options.size);
499
+ const borderWidth = options.borderWidth ?? 1;
500
+ const nodeWidth = options.nodeWidth ?? 10;
501
+ const labels = options.labels;
502
+ const { xScale, yScale } = this._cachedMeta;
503
+ if (!xScale || !yScale) return;
504
+ ctx.save();
505
+ const chartArea = this.chart.chartArea;
506
+ for (const node of nodes.values()){
507
+ const x = xScale.getPixelForValue(node.x);
508
+ const y = yScale.getPixelForValue(node.y);
509
+ const max = Math[size](node.in || node.out, node.out || node.in);
510
+ const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
511
+ const label = labels?.[node.key] ?? node.key;
512
+ let textX = x;
513
+ ctx.fillStyle = options.color ?? 'black';
514
+ ctx.textBaseline = 'middle';
515
+ if (x < chartArea.width / 2) {
516
+ ctx.textAlign = 'left';
517
+ textX += nodeWidth + borderWidth + 4;
518
+ } else {
519
+ ctx.textAlign = 'right';
520
+ textX -= borderWidth + 4;
521
+ }
522
+ this._drawLabel(label, y, height, ctx, textX);
523
+ }
524
+ ctx.restore();
531
525
  }
532
-
533
- this.updateSharedOptions(sharedOptions, mode);
534
- }
535
-
536
- _drawLabels() {
537
- const ctx = this._ctx;
538
- const nodes = this._nodes || new Map();
539
- const dataset = this.getDataset(); /* SankeyControllerDatasetOptions */
540
- const size = validateSizeValue(dataset.size);
541
- const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1);
542
- const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
543
- const labels = dataset.labels;
544
- const {xScale, yScale} = this._cachedMeta;
545
-
546
- ctx.save();
547
- const chartArea = this.chart.chartArea;
548
- for (const node of nodes.values()) {
549
- const x = xScale.getPixelForValue(node.x);
550
- const y = yScale.getPixelForValue(node.y);
551
-
552
- const max = Math[size](node.in || node.out, node.out || node.in);
553
- const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
554
- const label = labels && labels[node.key] || node.key;
555
- let textX = x;
556
- ctx.fillStyle = dataset.color || 'black';
557
- ctx.textBaseline = 'middle';
558
- if (x < chartArea.width / 2) {
559
- ctx.textAlign = 'left';
560
- textX += nodeWidth + borderWidth + 4;
561
- } else {
562
- ctx.textAlign = 'right';
563
- textX -= borderWidth + 4;
564
- }
565
- this._drawLabel(label, y, height, ctx, textX);
566
- }
567
- ctx.restore();
568
- }
569
-
570
- /**
571
- * @param {string} label
572
- * @param {number} y
573
- * @param {number} height
574
- * @param {CanvasRenderingContext2D} ctx
575
- * @param {number} textX
576
- * @private
577
- */
578
- _drawLabel(label, y, height, ctx, textX) {
579
- const font = helpers.toFont(this.options.font, this.chart.options.font);
580
- const lines = helpers.isNullOrUndef(label) ? [] : toTextLines(label);
581
- const linesLength = lines.length;
582
- const middle = y + height / 2;
583
- const textHeight = font.lineHeight;
584
- const padding = helpers.valueOrDefault(this.options.padding, textHeight / 2);
585
-
586
- ctx.font = font.string;
587
-
588
- if (linesLength > 1) {
589
- const top = middle - (textHeight * linesLength / 2) + padding;
590
- for (let i = 0; i < linesLength; i++) {
591
- ctx.fillText(lines[i], textX, top + (i * textHeight));
592
- }
593
- } else {
594
- ctx.fillText(label, textX, middle);
526
+ _drawLabel(label, y, height, ctx, textX) {
527
+ const font = helpers.toFont(this.options.font, this.chart.options.font);
528
+ const lines = toTextLines(label);
529
+ const lineCount = lines.length;
530
+ const middle = y + height / 2;
531
+ const textHeight = font.lineHeight;
532
+ const padding = helpers.valueOrDefault(this.options.padding, textHeight / 2);
533
+ ctx.font = font.string;
534
+ if (lineCount > 1) {
535
+ const top = middle - textHeight * lineCount / 2 + padding;
536
+ for(let i = 0; i < lineCount; i++){
537
+ ctx.fillText(lines[i], textX, top + i * textHeight);
538
+ }
539
+ } else {
540
+ ctx.fillText(label, textX, middle);
541
+ }
595
542
  }
596
- }
597
-
598
- _drawNodes() {
599
- const ctx = this._ctx;
600
- const nodes = this._nodes || new Map();
601
- const dataset = this.getDataset(); /* SankeyControllerDatasetOptions */
602
- const size = validateSizeValue(dataset.size);
603
- const {xScale, yScale} = this._cachedMeta;
604
- const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1);
605
- const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
606
-
607
- ctx.save();
608
- ctx.strokeStyle = dataset.borderColor || 'black';
609
- ctx.lineWidth = borderWidth;
610
-
611
- for (const node of nodes.values()) {
612
- ctx.fillStyle = node.color;
613
- const x = xScale.getPixelForValue(node.x);
614
- const y = yScale.getPixelForValue(node.y);
615
-
616
- const max = Math[size](node.in || node.out, node.out || node.in);
617
- const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
618
- if (borderWidth) {
619
- ctx.strokeRect(x, y, nodeWidth, height);
620
- }
621
- ctx.fillRect(x, y, nodeWidth, height);
622
- }
623
- ctx.restore();
624
- }
625
-
626
- /**
627
- * That's where the drawing process happens
628
- */
629
- draw() {
630
- const ctx = this._ctx;
631
- const data = this.getMeta().data || []; /* Array<Flow> */
632
-
633
- // Set node colors
634
- const active = [];
635
- for (let i = 0, ilen = data.length; i < ilen; ++i) {
636
- const flow = data[i]; /* Flow at index i */
637
- flow.from.color = flow.options.colorFrom;
638
- flow.to.color = flow.options.colorTo;
639
- if (flow.active) {
640
- active.push(flow);
641
- }
642
- }
643
- // Make sure nodes connected to hovered flows are using hover colors.
644
- for (const flow of active) {
645
- flow.from.color = flow.options.colorFrom;
646
- flow.to.color = flow.options.colorTo;
543
+ _drawNodes() {
544
+ const ctx = this.chart.ctx;
545
+ const nodes = this._nodes || new Map();
546
+ const { borderColor, borderWidth = 0, nodeWidth = 10, size } = this.options;
547
+ const sizeMethod = validateSizeValue(size);
548
+ const { xScale, yScale } = this._cachedMeta;
549
+ ctx.save();
550
+ if (borderColor && borderWidth) {
551
+ ctx.strokeStyle = borderColor;
552
+ ctx.lineWidth = borderWidth;
553
+ }
554
+ for (const node of nodes.values()){
555
+ ctx.fillStyle = node.color ?? 'black';
556
+ const x = xScale.getPixelForValue(node.x);
557
+ const y = yScale.getPixelForValue(node.y);
558
+ const max = Math[sizeMethod](node.in || node.out, node.out || node.in);
559
+ const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
560
+ if (borderWidth) {
561
+ ctx.strokeRect(x, y, nodeWidth, height);
562
+ }
563
+ ctx.fillRect(x, y, nodeWidth, height);
564
+ }
565
+ ctx.restore();
647
566
  }
648
-
649
- /* draw SankeyNodes on the canvas */
650
- this._drawNodes();
651
-
652
- /* draw Flow elements on the canvas */
653
- for (let i = 0, ilen = data.length; i < ilen; ++i) {
654
- data[i].draw(ctx);
567
+ draw() {
568
+ const ctx = this.chart.ctx;
569
+ const data = this.getMeta().data ?? [];
570
+ const active = [];
571
+ for(let i = 0, ilen = data.length; i < ilen; ++i){
572
+ const flow = data[i];
573
+ flow.from.color = flow.options.colorFrom;
574
+ flow.to.color = flow.options.colorTo;
575
+ if (flow.active) {
576
+ active.push(flow);
577
+ }
578
+ }
579
+ for (const flow of active){
580
+ flow.from.color = flow.options.colorFrom;
581
+ flow.to.color = flow.options.colorTo;
582
+ }
583
+ this._drawNodes();
584
+ for(let i = 0, ilen = data.length; i < ilen; ++i){
585
+ data[i].draw(ctx);
586
+ }
587
+ this._drawLabels();
655
588
  }
656
-
657
- /* draw labels (for SankeyNodes) on the canvas */
658
- this._drawLabels();
659
- }
660
589
  }
661
-
662
590
  SankeyController.id = 'sankey';
663
-
664
591
  SankeyController.defaults = {
665
- dataElementType: 'flow',
666
- animations: {
667
- numbers: {
668
- type: 'number',
669
- properties: ['x', 'y', 'x2', 'y2', 'height']
670
- },
671
- progress: {
672
- easing: 'linear',
673
- duration: (ctx) => ctx.type === 'data' ? (ctx.parsed._custom.x - ctx.parsed.x) * 200 : undefined,
674
- delay: (ctx) => ctx.type === 'data' ? ctx.parsed.x * 500 + ctx.dataIndex * 20 : undefined,
675
- },
676
- colors: {
677
- type: 'color',
678
- properties: ['colorFrom', 'colorTo'],
679
- },
680
- },
681
- transitions: {
682
- hide: {
683
- animations: {
592
+ dataElementType: 'flow',
593
+ animations: {
594
+ numbers: {
595
+ type: 'number',
596
+ properties: [
597
+ 'x',
598
+ 'y',
599
+ 'x2',
600
+ 'y2',
601
+ 'height'
602
+ ]
603
+ },
604
+ progress: {
605
+ easing: 'linear',
606
+ duration: (ctx)=>ctx.type === 'data' ? (ctx.parsed._custom.x - ctx.parsed.x) * 200 : undefined,
607
+ delay: (ctx)=>ctx.type === 'data' ? ctx.parsed.x * 500 + ctx.dataIndex * 20 : undefined
608
+ },
684
609
  colors: {
685
- type: 'color',
686
- properties: ['colorFrom', 'colorTo'],
687
- to: 'transparent'
610
+ type: 'color',
611
+ properties: [
612
+ 'colorFrom',
613
+ 'colorTo'
614
+ ]
688
615
  }
689
- }
690
616
  },
691
- show: {
692
- animations: {
693
- colors: {
694
- type: 'color',
695
- properties: ['colorFrom', 'colorTo'],
696
- from: 'transparent'
617
+ color: 'black',
618
+ borderColor: 'black',
619
+ borderWidth: 1,
620
+ modeX: 'edge',
621
+ nodeWidth: 10,
622
+ nodePadding: 10,
623
+ transitions: {
624
+ hide: {
625
+ animations: {
626
+ colors: {
627
+ type: 'color',
628
+ properties: [
629
+ 'colorFrom',
630
+ 'colorTo'
631
+ ],
632
+ to: 'transparent'
633
+ }
634
+ }
635
+ },
636
+ show: {
637
+ animations: {
638
+ colors: {
639
+ type: 'color',
640
+ properties: [
641
+ 'colorFrom',
642
+ 'colorTo'
643
+ ],
644
+ from: 'transparent'
645
+ }
646
+ }
697
647
  }
698
- }
699
648
  }
700
- }
701
649
  };
702
-
703
650
  SankeyController.overrides = {
704
- interaction: {
705
- mode: 'nearest',
706
- intersect: true
707
- },
708
- datasets: {
709
- clip: false,
710
- parsing: true
711
- },
712
- plugins: {
713
- tooltip: {
714
- callbacks: {
715
- title() {
716
- return '';
717
- },
718
- label(context) {
719
- const item = context.dataset.data[context.dataIndex];
720
- return item.from + ' -> ' + item.to + ': ' + item.flow;
721
- }
722
- },
651
+ interaction: {
652
+ mode: 'nearest',
653
+ intersect: true
723
654
  },
724
- legend: {
725
- display: false,
726
- },
727
- },
728
- scales: {
729
- x: {
730
- type: 'linear',
731
- bounds: 'data',
732
- display: false,
733
- min: 0,
734
- offset: false,
655
+ datasets: {
656
+ clip: false,
657
+ parsing: {
658
+ from: 'from',
659
+ to: 'to',
660
+ flow: 'flow'
661
+ }
735
662
  },
736
- y: {
737
- type: 'linear',
738
- bounds: 'data',
739
- display: false,
740
- min: 0,
741
- reverse: true,
742
- offset: false,
663
+ plugins: {
664
+ tooltip: {
665
+ callbacks: {
666
+ title () {
667
+ return '';
668
+ },
669
+ label (context) {
670
+ const parsedCustom = context.parsed._custom;
671
+ return parsedCustom.from.key + ' -> ' + parsedCustom.to.key + ': ' + parsedCustom.flow;
672
+ }
673
+ }
674
+ },
675
+ legend: {
676
+ display: false
677
+ }
743
678
  },
744
- },
745
- layout: {
746
- padding: {
747
- top: 3,
748
- left: 3,
749
- right: 13,
750
- bottom: 3,
679
+ scales: {
680
+ x: {
681
+ type: 'linear',
682
+ bounds: 'data',
683
+ display: false,
684
+ min: 0,
685
+ offset: false
686
+ },
687
+ y: {
688
+ type: 'linear',
689
+ bounds: 'data',
690
+ display: false,
691
+ min: 0,
692
+ reverse: true,
693
+ offset: false
694
+ }
751
695
  },
752
- },
696
+ layout: {
697
+ padding: {
698
+ top: 3,
699
+ left: 3,
700
+ right: 13,
701
+ bottom: 3
702
+ }
703
+ }
753
704
  };
754
705
 
755
- /**
756
- * @typedef {{x: number, y: number}} ControlPoint
757
- * @typedef {{cp1: ControlPoint, cp2: ControlPoint}} ControlPoints
758
- *
759
- * @param {number} x
760
- * @param {number} y
761
- * @param {number} x2
762
- * @param {number} y2
763
- * @return {ControlPoints}
764
- */
765
- const controlPoints = (x, y, x2, y2) => x < x2
766
- ? {
767
- cp1: {x: x + (x2 - x) / 3 * 2, y},
768
- cp2: {x: x + (x2 - x) / 3, y: y2}
769
- }
770
- : {
771
- cp1: {x: x - (x - x2) / 3, y: 0},
772
- cp2: {x: x2 + (x - x2) / 3, y: 0}
773
- };
774
-
775
- /**
776
- *
777
- * @param {ControlPoint} p1
778
- * @param {ControlPoint} p2
779
- * @param {number} t
780
- * @return {ControlPoint}
781
- */
782
- const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
783
-
784
- /**
785
- * @param {CanvasRenderingContext2D} ctx
786
- * @param {Flow} flow
787
- */
788
- function setStyle(ctx, {x, x2, options}) {
789
- let fill;
790
-
791
- if (options.colorMode === 'from') {
792
- fill = helpers.color(options.colorFrom).alpha(0.5).rgbString();
793
- } else if (options.colorMode === 'to') {
794
- fill = helpers.color(options.colorTo).alpha(0.5).rgbString();
795
- } else {
796
- fill = ctx.createLinearGradient(x, 0, x2, 0);
797
- fill.addColorStop(0, helpers.color(options.colorFrom).alpha(0.5).rgbString());
798
- fill.addColorStop(1, helpers.color(options.colorTo).alpha(0.5).rgbString());
799
- }
800
-
801
- ctx.fillStyle = fill;
802
- ctx.strokeStyle = fill;
803
- ctx.lineWidth = 0.5;
706
+ const controlPoints = (x, y, x2, y2)=>x < x2 ? {
707
+ cp1: {
708
+ x: x + (x2 - x) / 3 * 2,
709
+ y
710
+ },
711
+ cp2: {
712
+ x: x + (x2 - x) / 3,
713
+ y: y2
714
+ }
715
+ } : {
716
+ cp1: {
717
+ x: x - (x - x2) / 3,
718
+ y: 0
719
+ },
720
+ cp2: {
721
+ x: x2 + (x - x2) / 3,
722
+ y: 0
723
+ }
724
+ };
725
+ const pointInLine = (p1, p2, t)=>({
726
+ x: p1.x + t * (p2.x - p1.x),
727
+ y: p1.y + t * (p2.y - p1.y)
728
+ });
729
+ const applyAlpha = (original, alpha)=>helpers.color(original).alpha(alpha).rgbString();
730
+ const getColorOption = (option, alpha)=>typeof option === 'string' ? applyAlpha(option, alpha) : option;
731
+ function setStyle(ctx, { x, x2, options }) {
732
+ let fill = 'black';
733
+ if (options.colorMode === 'from') {
734
+ fill = getColorOption(options.colorFrom, options.alpha);
735
+ } else if (options.colorMode === 'to') {
736
+ fill = getColorOption(options.colorTo, options.alpha);
737
+ } else if (typeof options.colorFrom === 'string' && typeof options.colorTo === 'string') {
738
+ fill = ctx.createLinearGradient(x, 0, x2, 0);
739
+ fill.addColorStop(0, applyAlpha(options.colorFrom, options.alpha));
740
+ fill.addColorStop(1, applyAlpha(options.colorTo, options.alpha));
741
+ }
742
+ ctx.fillStyle = fill;
743
+ ctx.strokeStyle = fill;
744
+ ctx.lineWidth = 0.5;
804
745
  }
805
-
806
746
  class Flow extends chart_js.Element {
807
-
808
- /**
809
- * @param {FlowConfig} cfg
810
- */
811
- constructor(cfg) {
812
- super();
813
-
814
- this.options = undefined;
815
- this.x = undefined;
816
- this.y = undefined;
817
- this.x2 = undefined;
818
- this.y2 = undefined;
819
- this.height = undefined;
820
-
821
- if (cfg) {
822
- Object.assign(this, cfg);
747
+ draw(ctx) {
748
+ const { x, x2, y, y2, height, progress } = this;
749
+ const { cp1, cp2 } = controlPoints(x, y, x2, y2);
750
+ if (progress === 0) {
751
+ return;
752
+ }
753
+ ctx.save();
754
+ if (progress < 1) {
755
+ ctx.beginPath();
756
+ ctx.rect(x, Math.min(y, y2), (x2 - x) * progress + 1, Math.abs(y2 - y) + height + 1);
757
+ ctx.clip();
758
+ }
759
+ setStyle(ctx, this);
760
+ ctx.beginPath();
761
+ ctx.moveTo(x, y);
762
+ ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x2, y2);
763
+ ctx.lineTo(x2, y2 + height);
764
+ ctx.bezierCurveTo(cp2.x, cp2.y + height, cp1.x, cp1.y + height, x, y + height);
765
+ ctx.lineTo(x, y);
766
+ ctx.stroke();
767
+ ctx.closePath();
768
+ ctx.fill();
769
+ ctx.restore();
823
770
  }
824
- }
825
-
826
- /**
827
- * @param {CanvasRenderingContext2D} ctx
828
- */
829
- draw(ctx) {
830
- const me = this;
831
- const {x, x2, y, y2, height, progress} = me;
832
- const {cp1, cp2} = controlPoints(x, y, x2, y2);
833
-
834
- if (progress === 0) {
835
- return;
771
+ inRange(mouseX, mouseY, useFinalPosition) {
772
+ const { x, y, x2, y2, height } = this.getProps([
773
+ 'x',
774
+ 'y',
775
+ 'x2',
776
+ 'y2',
777
+ 'height'
778
+ ], useFinalPosition);
779
+ if (mouseX < x || mouseX > x2) {
780
+ return false;
781
+ }
782
+ const { cp1, cp2 } = controlPoints(x, y, x2, y2);
783
+ const t = (mouseX - x) / (x2 - x);
784
+ const p1 = {
785
+ x,
786
+ y
787
+ };
788
+ const p2 = {
789
+ x: x2,
790
+ y: y2
791
+ };
792
+ const a = pointInLine(p1, cp1, t);
793
+ const b = pointInLine(cp1, cp2, t);
794
+ const c = pointInLine(cp2, p2, t);
795
+ const d = pointInLine(a, b, t);
796
+ const e = pointInLine(b, c, t);
797
+ const topY = pointInLine(d, e, t).y;
798
+ return mouseY >= topY && mouseY <= topY + height;
836
799
  }
837
- ctx.save();
838
- if (progress < 1) {
839
- ctx.beginPath();
840
- ctx.rect(x, Math.min(y, y2), (x2 - x) * progress + 1, Math.abs(y2 - y) + height + 1);
841
- ctx.clip();
800
+ inXRange(mouseX, useFinalPosition) {
801
+ const { x, x2 } = this.getProps([
802
+ 'x',
803
+ 'x2'
804
+ ], useFinalPosition);
805
+ return mouseX >= x && mouseX <= x2;
806
+ }
807
+ inYRange(mouseY, useFinalPosition) {
808
+ const { y, y2, height } = this.getProps([
809
+ 'y',
810
+ 'y2',
811
+ 'height'
812
+ ], useFinalPosition);
813
+ const minY = Math.min(y, y2);
814
+ const maxY = Math.max(y, y2) + height;
815
+ return mouseY >= minY && mouseY <= maxY;
816
+ }
817
+ getCenterPoint(useFinalPosition) {
818
+ const { x, y, x2, y2, height } = this.getProps([
819
+ 'x',
820
+ 'y',
821
+ 'x2',
822
+ 'y2',
823
+ 'height'
824
+ ], useFinalPosition);
825
+ return {
826
+ x: (x + x2) / 2,
827
+ y: (y + y2 + height) / 2
828
+ };
829
+ }
830
+ tooltipPosition(useFinalPosition) {
831
+ return this.getCenterPoint(useFinalPosition);
832
+ }
833
+ getRange(axis) {
834
+ return axis === 'x' ? this.width / 2 : this.height / 2;
835
+ }
836
+ constructor(cfg){
837
+ super();
838
+ if (cfg) {
839
+ Object.assign(this, cfg);
840
+ }
842
841
  }
843
-
844
- setStyle(ctx, me);
845
-
846
- ctx.beginPath();
847
- ctx.moveTo(x, y);
848
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x2, y2);
849
- ctx.lineTo(x2, y2 + height);
850
- ctx.bezierCurveTo(cp2.x, cp2.y + height, cp1.x, cp1.y + height, x, y + height);
851
- ctx.lineTo(x, y);
852
- ctx.stroke();
853
- ctx.closePath();
854
-
855
- ctx.fill();
856
-
857
- ctx.restore();
858
- }
859
-
860
- /**
861
- * @param {number} mouseX
862
- * @param {number} mouseY
863
- * @param {boolean} useFinalPosition
864
- * @return {boolean}
865
- */
866
- inRange(mouseX, mouseY, useFinalPosition) {
867
- const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
868
- if (mouseX < x || mouseX > x2) {
869
- return false;
870
- }
871
- const {cp1, cp2} = controlPoints(x, y, x2, y2);
872
- const t = (mouseX - x) / (x2 - x);
873
- const p1 = {x, y};
874
- const p2 = {x: x2, y: y2};
875
- const a = pointInLine(p1, cp1, t);
876
- const b = pointInLine(cp1, cp2, t);
877
- const c = pointInLine(cp2, p2, t);
878
- const d = pointInLine(a, b, t);
879
- const e = pointInLine(b, c, t);
880
- const topY = pointInLine(d, e, t).y;
881
- return mouseY >= topY && mouseY <= topY + height;
882
- }
883
-
884
- /**
885
- * @param {number} mouseX
886
- * @param {boolean} useFinalPosition
887
- * @return {boolean}
888
- */
889
- inXRange(mouseX, useFinalPosition) {
890
- const {x, x2} = this.getProps(['x', 'x2'], useFinalPosition);
891
- return mouseX >= x && mouseX <= x2;
892
- }
893
-
894
- /**
895
- * @param {number} mouseY
896
- * @param {boolean} useFinalPosition
897
- * @return {boolean}
898
- */
899
- inYRange(mouseY, useFinalPosition) {
900
- const {y, y2, height} = this.getProps(['y', 'y2', 'height'], useFinalPosition);
901
- const minY = Math.min(y, y2);
902
- const maxY = Math.max(y, y2) + height;
903
- return mouseY >= minY && mouseY <= maxY;
904
- }
905
-
906
- /**
907
- * @param {boolean} useFinalPosition
908
- * @return {{x: number, y:number}}
909
- */
910
- getCenterPoint(useFinalPosition) {
911
- const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
912
- return {
913
- x: (x + x2) / 2,
914
- y: (y + y2 + height) / 2
915
- };
916
- }
917
-
918
- tooltipPosition(useFinalPosition) {
919
- return this.getCenterPoint(useFinalPosition);
920
- }
921
-
922
- /**
923
- * @param {"x" | "y"} axis
924
- * @return {number}
925
- */
926
- getRange(axis) {
927
- return axis === 'x' ? this.width / 2 : this.height / 2;
928
- }
929
842
  }
930
-
931
843
  Flow.id = 'flow';
932
844
  Flow.defaults = {
933
- colorFrom: 'red',
934
- colorTo: 'green',
935
- colorMode: 'gradient',
936
- hoverColorFrom: (ctx, options) => helpers.getHoverColor(options.colorFrom),
937
- hoverColorTo: (ctx, options) => helpers.getHoverColor(options.colorTo)
845
+ colorFrom: 'red',
846
+ colorTo: 'green',
847
+ colorMode: 'gradient',
848
+ alpha: 0.5,
849
+ hoverColorFrom: (_ctx, options)=>helpers.getHoverColor(options.colorFrom),
850
+ hoverColorTo: (_ctx, options)=>helpers.getHoverColor(options.colorTo)
851
+ };
852
+ Flow.descriptors = {
853
+ _scriptable: true
938
854
  };
939
855
 
940
856
  chart_js.Chart.register(SankeyController, Flow);