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