chartjs-chart-sankey 0.13.0 → 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.13.0
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,936 +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
- const size = Math.max(n.in, n.out);
169
- y = Math.max(n.y + size, y);
170
- }
171
- 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;
172
240
  }
173
-
174
- /**
175
- * @param {SankeyNode} node
176
- * @param {number} value
177
- * @return {number}
178
- */
179
241
  function setOrGetY(node, value) {
180
- if (defined(node.y)) {
181
- return node.y;
182
- }
183
- node.y = value;
184
- return value;
242
+ if (defined(node.y)) {
243
+ return node.y;
244
+ }
245
+ node.y = value;
246
+ return value;
185
247
  }
186
-
187
- /**
188
- * @param {Array<SankeyNode>} nodeArray
189
- * @param {number} maxX
190
- * @return {number}
191
- */
192
248
  function processRest(nodeArray, maxX) {
193
- const leftNodes = nodeArray.filter(node => node.x === 0);
194
- const rightNodes = nodeArray.filter(node => node.x === maxX);
195
- const leftToDo = leftNodes.filter(node => !defined(node.y));
196
- const rightToDo = rightNodes.filter(node => !defined(node.y));
197
- const centerToDo = nodeArray.filter(node => node.x > 0 && node.x < maxX && !defined(node.y));
198
-
199
- let leftY = leftNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.out) || 0), 0);
200
- let rightY = rightNodes.reduce((acc, cur) => Math.max(acc, (cur.y + cur.in) || 0), 0);
201
- let centerY = 0;
202
-
203
- if (leftY >= rightY) {
204
- leftToDo.forEach(node => {
205
- leftY = setOrGetY(node, leftY);
206
- leftY = Math.max(leftY + node.out, processTo(node, leftY));
207
- });
208
-
209
- rightToDo.forEach(node => {
210
- rightY = setOrGetY(node, rightY);
211
- rightY = Math.max(rightY + node.in, processTo(node, rightY));
212
- });
213
- } else {
214
- rightToDo.forEach(node => {
215
- rightY = setOrGetY(node, rightY);
216
- rightY = Math.max(rightY + node.in, processTo(node, rightY));
217
- });
218
-
219
- leftToDo.forEach(node => {
220
- leftY = setOrGetY(node, leftY);
221
- 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);
222
281
  });
223
- }
224
- centerToDo.forEach(node => {
225
- let y = nodeArray.filter(n => n.x === node.x && defined(n.y))
226
- .reduce((acc, cur) => Math.max(acc, cur.y + Math.max(cur.in, cur.out)), 0);
227
- y = setOrGetY(node, y);
228
- y = Math.max(y + node.in, processFrom(node, y));
229
- y = Math.max(y + node.out, processTo(node, y));
230
- centerY = Math.max(centerY, y);
231
- });
232
-
233
- return Math.max(leftY, rightY, centerY);
282
+ return Math.max(leftY, rightY, centerY);
234
283
  }
235
-
236
- /**
237
- * @param {Array<SankeyNode>} nodeArray
238
- * @param {number} maxX
239
- * @return {number}
240
- */
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
+ };
241
309
  function calculateY(nodeArray, maxX) {
242
- nodeArray.sort((a, b) => Math.max(b.in, b.out) - Math.max(a.in, a.out));
243
- const start = nodeArray[0];
244
- start.y = 0;
245
- const left = processFrom(start, 0);
246
- const right = processTo(start, 0);
247
- const rest = processRest(nodeArray, maxX);
248
- 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);
249
317
  }
250
-
251
- /**
252
- * @param {Array<SankeyNode>} nodeArray
253
- * @param {number} maxX
254
- * @return {number}
255
- */
256
318
  function calculateYUsingPriority(nodeArray, maxX) {
257
- let maxY = 0;
258
- let nextYStart = 0;
259
- for (let x = 0; x <= maxX; x++) {
260
- let y = nextYStart;
261
- const nodes = nodeArray.filter(node => node.x === x).sort((a, b) => a.priority - b.priority);
262
- nextYStart = nodes.length ? nodes[0].to.filter(to => to.node.x > x + 1).reduce((acc, cur) => acc + cur.flow, 0) || 0 : 0;
263
- for (const node of nodes) {
264
- node.y = y;
265
- y += Math.max(node.out, node.in);
266
- }
267
- maxY = Math.max(y, maxY);
268
- }
269
- 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;
270
332
  }
271
-
272
- /**
273
- * @param {Array<SankeyNode>} nodeArray
274
- * @param {number} padding
275
- * @return {number}
276
- */
277
- function addPadding(nodeArray, padding) {
278
- let i = 1;
279
- let x = 0;
280
- let prev = 0;
281
- let maxY = 0;
282
- const rows = [];
283
- nodeArray.sort(nodeByXY);
284
- for (const node of nodeArray) {
285
- if (node.y) {
286
- if (node.x === 0) {
287
- rows.push(node.y);
288
- } else {
289
- if (x !== node.x) {
290
- x = node.x;
291
- 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([]);
292
346
  }
293
-
294
- for (i = prev + 1; i < rows.length; i++) {
295
- if (rows[i] > node.y) {
296
- break;
297
- }
298
- }
299
- prev = i;
300
- }
301
- node.y += i * padding;
302
- i++;
303
- }
304
- maxY = Math.max(maxY, node.y + Math.max(node.in, node.out));
305
- }
306
- 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;
307
371
  }
308
-
309
- /**
310
- * @param {Array<SankeyNode>} nodeArray
311
- * @param {'min' | 'max'} size
312
- */
313
- function sortFlows(nodeArray, size) {
314
- nodeArray.forEach((node) => {
315
- const nodeSize = Math[size](node.in || node.out, node.out || node.in);
316
- const overlapFrom = nodeSize < node.in;
317
- const overlapTo = nodeSize < node.out;
318
- let addY = 0;
319
- let len = node.from.length;
320
- node.from.sort((a, b) => (a.node.y + a.node.out / 2) - (b.node.y + b.node.out / 2)).forEach((flow, idx) => {
321
- if (overlapFrom) {
322
- flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
323
- } else {
324
- flow.addY = addY;
325
- addY += flow.flow;
326
- }
327
- });
328
- addY = 0;
329
- len = node.to.length;
330
- node.to.sort((a, b) => (a.node.y + a.node.in / 2) - (b.node.y + b.node.in / 2)).forEach((flow, idx) => {
331
- if (overlapTo) {
332
- flow.addY = idx * (nodeSize - flow.flow) / (len - 1);
333
- } else {
334
- flow.addY = addY;
335
- addY += flow.flow;
336
- }
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
+ });
337
397
  });
338
- });
339
- }
340
-
341
- /**
342
- * @param {Map<string, SankeyNode>} nodes
343
- * @param {Array<SankeyDataPoint>} data
344
- * @param {boolean} priority
345
- * @param {'min' | 'max'} size
346
- * @return {{maxY: number, maxX: number}}
347
- */
348
- function layout(nodes, data, priority, size) {
349
- const nodeArray = [...nodes.values()];
350
- const maxX = calculateX(nodes, data);
351
- const maxY = priority ? calculateYUsingPriority(nodeArray, maxX) : calculateY(nodeArray, maxX);
352
- const padding = maxY * 0.03; // rows;
353
- const maxYWithPadding = addPadding(nodeArray, padding);
354
- sortFlows(nodeArray, size);
355
- return {maxX, maxY: maxYWithPadding};
356
398
  }
357
-
358
- /**
359
- * @param {Array<SankeyDataPoint>} data Array of raw data elements
360
- * @return {Map<string, SankeyNode>}
361
- */
362
- function buildNodesFromRawData(data) {
363
- const nodes = new Map();
364
- for (let i = 0; i < data.length; i++) {
365
- const {from, to, flow} = data[i];
366
-
367
- if (!nodes.has(from)) {
368
- nodes.set(from, {
369
- key: from,
370
- in: 0,
371
- out: flow,
372
- from: [],
373
- to: [{key: to, flow: flow, index: i}],
374
- });
375
- } else {
376
- const node = nodes.get(from);
377
- node.out += flow;
378
- node.to.push({key: to, flow: flow, index: i});
379
- }
380
- if (!nodes.has(to)) {
381
- nodes.set(to, {
382
- key: to,
383
- in: flow,
384
- out: 0,
385
- from: [{key: from, flow: flow, index: i}],
386
- to: [],
387
- });
388
- } else {
389
- const node = nodes.get(to);
390
- node.in += flow;
391
- node.from.push({key: from, flow: flow, index: i});
392
- }
393
- }
394
-
395
- const flowSort = (a, b) => b.flow - a.flow;
396
-
397
- [...nodes.values()].forEach(node => {
398
- node.from = node.from.sort(flowSort);
399
- node.from.forEach(x => {
400
- x.node = nodes.get(x.key);
401
- });
402
-
403
- node.to = node.to.sort(flowSort);
404
- node.to.forEach(x => {
405
- x.node = nodes.get(x.key);
406
- });
407
- });
408
-
409
- 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
+ };
410
412
  }
411
413
 
412
- /**
413
- * @param {Array<FromToElement>} arr
414
- * @param {string} key
415
- * @param {number} index
416
- * @return {number}
417
- */
418
414
  function getAddY(arr, key, index) {
419
- for (const item of arr) {
420
- if (item.key === key && item.index === index) {
421
- return item.addY;
415
+ for (const item of arr){
416
+ if (item.key === key && item.index === index) {
417
+ return item.addY;
418
+ }
422
419
  }
423
- }
424
- return 0;
420
+ return 0;
425
421
  }
426
-
427
422
  class SankeyController extends chart_js.DatasetController {
428
- /**
429
- * @param {ChartMeta<Flow, Element>} meta
430
- * @param {Array<SankeyDataPoint>} data Array of original data elements
431
- * @param {number} start
432
- * @param {number} count
433
- * @return {Array<SankeyParsedData>}
434
- */
435
- parseObjectData(meta, data, start, count) {
436
- const {from: fromKey = 'from', to: toKey = 'to', flow: flowKey = 'flow'} = this.options.parsing;
437
- const sankeyData = data.map(({[fromKey]: from, [toKey]: to, [flowKey]: flow}) => ({from, to, flow}));
438
- const {xScale, yScale} = meta;
439
- const parsed = []; /* Array<SankeyParsedData> */
440
- const nodes = this._nodes = buildNodesFromRawData(sankeyData);
441
- /* getDataset() => SankeyControllerDatasetOptions */
442
- const {column, priority, size} = this.getDataset();
443
- if (priority) {
444
- for (const node of nodes.values()) {
445
- if (node.key in priority) {
446
- 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
+ });
447
456
  }
448
- }
457
+ return parsed.slice(start, start + count);
449
458
  }
450
- if (column) {
451
- for (const node of nodes.values()) {
452
- if (node.key in column) {
453
- node.column = true;
454
- 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);
455
491
  }
456
- }
492
+ this.updateSharedOptions(sharedOptions, mode, firstOpts);
457
493
  }
458
-
459
- const {maxX, maxY} = layout(nodes, sankeyData, !!priority, validateSizeValue(size));
460
-
461
- this._maxX = maxX;
462
- this._maxY = maxY;
463
-
464
- for (let i = 0, ilen = sankeyData.length; i < ilen; ++i) {
465
- const dataPoint = sankeyData[i];
466
- const from = nodes.get(dataPoint.from);
467
- const to = nodes.get(dataPoint.to);
468
- const fromY = from.y + getAddY(from.to, dataPoint.to, i);
469
- const toY = to.y + getAddY(to.from, dataPoint.from, i);
470
- parsed.push({
471
- x: xScale.parse(from.x, i),
472
- y: yScale.parse(fromY, i),
473
- _custom: {
474
- from,
475
- to,
476
- x: xScale.parse(to.x, i),
477
- y: yScale.parse(toY, i),
478
- height: yScale.parse(dataPoint.flow, i),
479
- }
480
- });
481
- }
482
- return parsed.slice(start, start + count);
483
- }
484
-
485
- getMinMax(scale) {
486
- return {
487
- min: 0,
488
- max: scale === this._cachedMeta.xScale ? this._maxX : this._maxY
489
- };
490
- }
491
-
492
- update(mode) {
493
- const {data} = this._cachedMeta;
494
-
495
- this.updateElements(data, 0, data.length, mode);
496
- }
497
-
498
- /**
499
- * @param {Array<Flow>} elems
500
- * @param {number} start
501
- * @param {number} count
502
- * @param {"resize" | "reset" | "none" | "hide" | "show" | "normal" | "active"} mode
503
- */
504
- updateElements(elems, start, count, mode) {
505
- const {xScale, yScale} = this._cachedMeta;
506
- const firstOpts = this.resolveDataElementOptions(start, mode);
507
- const sharedOptions = this.getSharedOptions(mode, elems[start], firstOpts);
508
- const dataset = this.getDataset();
509
- const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1) / 2 + 0.5;
510
- const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
511
-
512
- for (let i = start; i < start + count; i++) {
513
- /* getParsed(idx: number) => SankeyParsedData */
514
- const parsed = this.getParsed(i);
515
- const custom = parsed._custom;
516
- const y = yScale.getPixelForValue(parsed.y);
517
- this.updateElement(
518
- elems[i],
519
- i,
520
- {
521
- x: xScale.getPixelForValue(parsed.x) + nodeWidth + borderWidth,
522
- y,
523
- x2: xScale.getPixelForValue(custom.x) - borderWidth,
524
- y2: yScale.getPixelForValue(custom.y),
525
- from: custom.from,
526
- to: custom.to,
527
- progress: mode === 'reset' ? 0 : 1,
528
- height: Math.abs(yScale.getPixelForValue(parsed.y + custom.height) - y),
529
- options: this.resolveDataElementOptions(i, mode)
530
- },
531
- 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();
532
525
  }
533
-
534
- this.updateSharedOptions(sharedOptions, mode);
535
- }
536
-
537
- _drawLabels() {
538
- const ctx = this._ctx;
539
- const nodes = this._nodes || new Map();
540
- const dataset = this.getDataset(); /* SankeyControllerDatasetOptions */
541
- const size = validateSizeValue(dataset.size);
542
- const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1);
543
- const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
544
- const labels = dataset.labels;
545
- const {xScale, yScale} = this._cachedMeta;
546
-
547
- ctx.save();
548
- const chartArea = this.chart.chartArea;
549
- for (const node of nodes.values()) {
550
- const x = xScale.getPixelForValue(node.x);
551
- const y = yScale.getPixelForValue(node.y);
552
-
553
- const max = Math[size](node.in || node.out, node.out || node.in);
554
- const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
555
- const label = labels && labels[node.key] || node.key;
556
- let textX = x;
557
- ctx.fillStyle = dataset.color || 'black';
558
- ctx.textBaseline = 'middle';
559
- if (x < chartArea.width / 2) {
560
- ctx.textAlign = 'left';
561
- textX += nodeWidth + borderWidth + 4;
562
- } else {
563
- ctx.textAlign = 'right';
564
- textX -= borderWidth + 4;
565
- }
566
- this._drawLabel(label, y, height, ctx, textX);
567
- }
568
- ctx.restore();
569
- }
570
-
571
- /**
572
- * @param {string} label
573
- * @param {number} y
574
- * @param {number} height
575
- * @param {CanvasRenderingContext2D} ctx
576
- * @param {number} textX
577
- * @private
578
- */
579
- _drawLabel(label, y, height, ctx, textX) {
580
- const font = helpers.toFont(this.options.font, this.chart.options.font);
581
- const lines = helpers.isNullOrUndef(label) ? [] : toTextLines(label);
582
- const linesLength = lines.length;
583
- const middle = y + height / 2;
584
- const textHeight = font.lineHeight;
585
- const padding = helpers.valueOrDefault(this.options.padding, textHeight / 2);
586
-
587
- ctx.font = font.string;
588
-
589
- if (linesLength > 1) {
590
- const top = middle - (textHeight * linesLength / 2) + padding;
591
- for (let i = 0; i < linesLength; i++) {
592
- ctx.fillText(lines[i], textX, top + (i * textHeight));
593
- }
594
- } else {
595
- 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
+ }
596
542
  }
597
- }
598
-
599
- _drawNodes() {
600
- const ctx = this._ctx;
601
- const nodes = this._nodes || new Map();
602
- const dataset = this.getDataset(); /* SankeyControllerDatasetOptions */
603
- const size = validateSizeValue(dataset.size);
604
- const {xScale, yScale} = this._cachedMeta;
605
- const borderWidth = helpers.valueOrDefault(dataset.borderWidth, 1);
606
- const nodeWidth = helpers.valueOrDefault(dataset.nodeWidth, 10);
607
-
608
- ctx.save();
609
- ctx.strokeStyle = dataset.borderColor || 'black';
610
- ctx.lineWidth = borderWidth;
611
-
612
- for (const node of nodes.values()) {
613
- ctx.fillStyle = node.color;
614
- const x = xScale.getPixelForValue(node.x);
615
- const y = yScale.getPixelForValue(node.y);
616
-
617
- const max = Math[size](node.in || node.out, node.out || node.in);
618
- const height = Math.abs(yScale.getPixelForValue(node.y + max) - y);
619
- if (borderWidth) {
620
- ctx.strokeRect(x, y, nodeWidth, height);
621
- }
622
- ctx.fillRect(x, y, nodeWidth, height);
623
- }
624
- ctx.restore();
625
- }
626
-
627
- /**
628
- * That's where the drawing process happens
629
- */
630
- draw() {
631
- const ctx = this._ctx;
632
- const data = this.getMeta().data || []; /* Array<Flow> */
633
-
634
- // Set node colors
635
- const active = [];
636
- for (let i = 0, ilen = data.length; i < ilen; ++i) {
637
- const flow = data[i]; /* Flow at index i */
638
- flow.from.color = flow.options.colorFrom;
639
- flow.to.color = flow.options.colorTo;
640
- if (flow.active) {
641
- active.push(flow);
642
- }
643
- }
644
- // Make sure nodes connected to hovered flows are using hover colors.
645
- for (const flow of active) {
646
- flow.from.color = flow.options.colorFrom;
647
- 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();
648
566
  }
649
-
650
- /* draw SankeyNodes on the canvas */
651
- this._drawNodes();
652
-
653
- /* draw Flow elements on the canvas */
654
- for (let i = 0, ilen = data.length; i < ilen; ++i) {
655
- 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();
656
588
  }
657
-
658
- /* draw labels (for SankeyNodes) on the canvas */
659
- this._drawLabels();
660
- }
661
589
  }
662
-
663
590
  SankeyController.id = 'sankey';
664
-
665
591
  SankeyController.defaults = {
666
- dataElementType: 'flow',
667
- animations: {
668
- numbers: {
669
- type: 'number',
670
- properties: ['x', 'y', 'x2', 'y2', 'height']
671
- },
672
- progress: {
673
- easing: 'linear',
674
- duration: (ctx) => ctx.type === 'data' ? (ctx.parsed._custom.x - ctx.parsed.x) * 200 : undefined,
675
- delay: (ctx) => ctx.type === 'data' ? ctx.parsed.x * 500 + ctx.dataIndex * 20 : undefined,
676
- },
677
- colors: {
678
- type: 'color',
679
- properties: ['colorFrom', 'colorTo'],
680
- },
681
- },
682
- transitions: {
683
- hide: {
684
- 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
+ },
685
609
  colors: {
686
- type: 'color',
687
- properties: ['colorFrom', 'colorTo'],
688
- to: 'transparent'
610
+ type: 'color',
611
+ properties: [
612
+ 'colorFrom',
613
+ 'colorTo'
614
+ ]
689
615
  }
690
- }
691
616
  },
692
- show: {
693
- animations: {
694
- colors: {
695
- type: 'color',
696
- properties: ['colorFrom', 'colorTo'],
697
- 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
+ }
698
647
  }
699
- }
700
648
  }
701
- }
702
649
  };
703
-
704
650
  SankeyController.overrides = {
705
- interaction: {
706
- mode: 'nearest',
707
- intersect: true
708
- },
709
- datasets: {
710
- clip: false,
711
- parsing: true
712
- },
713
- plugins: {
714
- tooltip: {
715
- callbacks: {
716
- title() {
717
- return '';
718
- },
719
- label(context) {
720
- const item = context.dataset.data[context.dataIndex];
721
- return item.from + ' -> ' + item.to + ': ' + item.flow;
722
- }
723
- },
651
+ interaction: {
652
+ mode: 'nearest',
653
+ intersect: true
724
654
  },
725
- legend: {
726
- display: false,
727
- },
728
- },
729
- scales: {
730
- x: {
731
- type: 'linear',
732
- bounds: 'data',
733
- display: false,
734
- min: 0,
735
- offset: false,
655
+ datasets: {
656
+ clip: false,
657
+ parsing: {
658
+ from: 'from',
659
+ to: 'to',
660
+ flow: 'flow'
661
+ }
736
662
  },
737
- y: {
738
- type: 'linear',
739
- bounds: 'data',
740
- display: false,
741
- min: 0,
742
- reverse: true,
743
- 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
+ }
744
678
  },
745
- },
746
- layout: {
747
- padding: {
748
- top: 3,
749
- left: 3,
750
- right: 13,
751
- 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
+ }
752
695
  },
753
- },
696
+ layout: {
697
+ padding: {
698
+ top: 3,
699
+ left: 3,
700
+ right: 13,
701
+ bottom: 3
702
+ }
703
+ }
754
704
  };
755
705
 
756
- /**
757
- * @typedef {{x: number, y: number}} ControlPoint
758
- * @typedef {{cp1: ControlPoint, cp2: ControlPoint}} ControlPoints
759
- *
760
- * @param {number} x
761
- * @param {number} y
762
- * @param {number} x2
763
- * @param {number} y2
764
- * @return {ControlPoints}
765
- */
766
- const controlPoints = (x, y, x2, y2) => x < x2
767
- ? {
768
- cp1: {x: x + (x2 - x) / 3 * 2, y},
769
- cp2: {x: x + (x2 - x) / 3, y: y2}
770
- }
771
- : {
772
- cp1: {x: x - (x - x2) / 3, y: 0},
773
- cp2: {x: x2 + (x - x2) / 3, y: 0}
774
- };
775
-
776
- /**
777
- *
778
- * @param {ControlPoint} p1
779
- * @param {ControlPoint} p2
780
- * @param {number} t
781
- * @return {ControlPoint}
782
- */
783
- const pointInLine = (p1, p2, t) => ({x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y)});
784
-
785
- /**
786
- * @param {CanvasRenderingContext2D} ctx
787
- * @param {Flow} flow
788
- */
789
- function setStyle(ctx, {x, x2, options}) {
790
- let fill;
791
-
792
- if (options.colorMode === 'from') {
793
- /**
794
- * @todo remove the alpha and use tha alpha provided in colorFrom / colorTo in next major version
795
- */
796
- fill = helpers.color(options.colorFrom).alpha(options.alpha).rgbString();
797
- } else if (options.colorMode === 'to') {
798
- fill = helpers.color(options.colorTo).alpha(options.alpha).rgbString();
799
- } else {
800
- fill = ctx.createLinearGradient(x, 0, x2, 0);
801
- fill.addColorStop(0, helpers.color(options.colorFrom).alpha(options.alpha).rgbString());
802
- fill.addColorStop(1, helpers.color(options.colorTo).alpha(options.alpha).rgbString());
803
- }
804
-
805
- ctx.fillStyle = fill;
806
- ctx.strokeStyle = fill;
807
- 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;
808
745
  }
809
-
810
746
  class Flow extends chart_js.Element {
811
-
812
- /**
813
- * @param {FlowConfig} cfg
814
- */
815
- constructor(cfg) {
816
- super();
817
-
818
- this.options = undefined;
819
- this.x = undefined;
820
- this.y = undefined;
821
- this.x2 = undefined;
822
- this.y2 = undefined;
823
- this.height = undefined;
824
-
825
- if (cfg) {
826
- 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();
827
770
  }
828
- }
829
-
830
- /**
831
- * @param {CanvasRenderingContext2D} ctx
832
- */
833
- draw(ctx) {
834
- const me = this;
835
- const {x, x2, y, y2, height, progress} = me;
836
- const {cp1, cp2} = controlPoints(x, y, x2, y2);
837
-
838
- if (progress === 0) {
839
- 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;
840
799
  }
841
- ctx.save();
842
- if (progress < 1) {
843
- ctx.beginPath();
844
- ctx.rect(x, Math.min(y, y2), (x2 - x) * progress + 1, Math.abs(y2 - y) + height + 1);
845
- 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
+ }
846
841
  }
847
-
848
- setStyle(ctx, me);
849
-
850
- ctx.beginPath();
851
- ctx.moveTo(x, y);
852
- ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, x2, y2);
853
- ctx.lineTo(x2, y2 + height);
854
- ctx.bezierCurveTo(cp2.x, cp2.y + height, cp1.x, cp1.y + height, x, y + height);
855
- ctx.lineTo(x, y);
856
- ctx.stroke();
857
- ctx.closePath();
858
-
859
- ctx.fill();
860
-
861
- ctx.restore();
862
- }
863
-
864
- /**
865
- * @param {number} mouseX
866
- * @param {number} mouseY
867
- * @param {boolean} useFinalPosition
868
- * @return {boolean}
869
- */
870
- inRange(mouseX, mouseY, useFinalPosition) {
871
- const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
872
- if (mouseX < x || mouseX > x2) {
873
- return false;
874
- }
875
- const {cp1, cp2} = controlPoints(x, y, x2, y2);
876
- const t = (mouseX - x) / (x2 - x);
877
- const p1 = {x, y};
878
- const p2 = {x: x2, y: y2};
879
- const a = pointInLine(p1, cp1, t);
880
- const b = pointInLine(cp1, cp2, t);
881
- const c = pointInLine(cp2, p2, t);
882
- const d = pointInLine(a, b, t);
883
- const e = pointInLine(b, c, t);
884
- const topY = pointInLine(d, e, t).y;
885
- return mouseY >= topY && mouseY <= topY + height;
886
- }
887
-
888
- /**
889
- * @param {number} mouseX
890
- * @param {boolean} useFinalPosition
891
- * @return {boolean}
892
- */
893
- inXRange(mouseX, useFinalPosition) {
894
- const {x, x2} = this.getProps(['x', 'x2'], useFinalPosition);
895
- return mouseX >= x && mouseX <= x2;
896
- }
897
-
898
- /**
899
- * @param {number} mouseY
900
- * @param {boolean} useFinalPosition
901
- * @return {boolean}
902
- */
903
- inYRange(mouseY, useFinalPosition) {
904
- const {y, y2, height} = this.getProps(['y', 'y2', 'height'], useFinalPosition);
905
- const minY = Math.min(y, y2);
906
- const maxY = Math.max(y, y2) + height;
907
- return mouseY >= minY && mouseY <= maxY;
908
- }
909
-
910
- /**
911
- * @param {boolean} useFinalPosition
912
- * @return {{x: number, y:number}}
913
- */
914
- getCenterPoint(useFinalPosition) {
915
- const {x, y, x2, y2, height} = this.getProps(['x', 'y', 'x2', 'y2', 'height'], useFinalPosition);
916
- return {
917
- x: (x + x2) / 2,
918
- y: (y + y2 + height) / 2
919
- };
920
- }
921
-
922
- tooltipPosition(useFinalPosition) {
923
- return this.getCenterPoint(useFinalPosition);
924
- }
925
-
926
- /**
927
- * @param {"x" | "y"} axis
928
- * @return {number}
929
- */
930
- getRange(axis) {
931
- return axis === 'x' ? this.width / 2 : this.height / 2;
932
- }
933
842
  }
934
-
935
843
  Flow.id = 'flow';
936
844
  Flow.defaults = {
937
- colorFrom: 'red',
938
- colorTo: 'green',
939
- colorMode: 'gradient',
940
- alpha: 0.5,
941
- hoverColorFrom: (ctx, options) => helpers.getHoverColor(options.colorFrom),
942
- 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
943
854
  };
944
855
 
945
856
  chart_js.Chart.register(SankeyController, Flow);