@progress/kendo-charts 2.3.0-dev.202402161315 → 2.3.0-dev.202403071434

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.
@@ -3,24 +3,39 @@ import { deepExtend } from '../common';
3
3
  const max = (array, mapFn) => Math.max.apply(null, array.map(mapFn));
4
4
  const min = (array, mapFn) => Math.min.apply(null, array.map(mapFn));
5
5
  const sum = (array, mapFn) => array.map(mapFn).reduce((acc, curr) => (acc + curr), 0);
6
- const sortAsc = (a, b) => a.y0 - b.y0;
6
+ const sortAsc = (a, b) => (a.y0 === b.y0 ? a.index - b.index : a.y0 + a.y1 - b.y0 - b.y1);
7
7
  const sortSource = (a, b) => sortAsc(a.source, b.source);
8
8
  const sortTarget = (a, b) => sortAsc(a.target, b.target);
9
9
  const value = (node) => node.value;
10
10
 
11
11
  function sortLinks(nodes) {
12
- nodes.forEach((node) => {
13
- const { sourceLinks, targetLinks } = node;
14
- sourceLinks.sort(sortTarget);
15
- targetLinks.sort(sortSource);
12
+ nodes.forEach(node => {
13
+ node.targetLinks.forEach(link => {
14
+ link.source.sourceLinks.sort(sortTarget);
15
+ });
16
+ node.sourceLinks.forEach(link => {
17
+ link.target.targetLinks.sort(sortSource);
18
+ });
16
19
  });
17
20
  }
18
21
 
22
+ const calcLayer = (node, maxDepth) => {
23
+ if (node.align === 'left') {
24
+ return node.depth;
25
+ }
26
+
27
+ if (node.align === 'right') {
28
+ return maxDepth - node.height;
29
+ }
30
+
31
+ return node.sourceLinks.length ? node.depth : maxDepth;
32
+ };
33
+
19
34
  class Sankey {
20
35
  constructor(options) {
21
- const offset = options.nodesOptions.offset || {};
36
+ const { offset = {}, align } = options.nodesOptions;
22
37
  this.data = {
23
- nodes: options.nodes.map((node) => deepExtend({}, { offset }, node)),
38
+ nodes: options.nodes.map((node) => deepExtend({}, { offset, align }, node)),
24
39
  links: options.links.map((link) => deepExtend({}, link))
25
40
  };
26
41
 
@@ -30,6 +45,10 @@ class Sankey {
30
45
  this.offsetY = options.offsetY || 0;
31
46
  this.nodeWidth = options.nodesOptions.width;
32
47
  this.nodePadding = options.nodesOptions.padding;
48
+ this.reverse = options.reverse;
49
+ this.targetColumnIndex = options.targetColumnIndex;
50
+ this.loops = options.loops;
51
+ this.autoLayout = options.autoLayout;
33
52
  }
34
53
 
35
54
  calculate() {
@@ -37,19 +56,21 @@ class Sankey {
37
56
  this.connectLinksToNodes(nodes, links);
38
57
  this.calculateNodeValues(nodes);
39
58
  this.calculateNodeDepths(nodes);
59
+ this.calculateNodeHeights(nodes);
40
60
 
41
61
  const columns = this.calculateNodeColumns(nodes);
42
62
  this.calculateNodeBreadths(columns);
43
63
  this.applyNodesOffset(nodes);
44
64
  this.calculateLinkBreadths(nodes);
45
65
 
46
- return this.data;
66
+ return Object.assign({}, this.data, {columns});
47
67
  }
48
68
 
49
69
  connectLinksToNodes(nodes, links) {
50
70
  const nodesMap = new Map();
51
71
 
52
- nodes.forEach((node) => {
72
+ nodes.forEach((node, i) => {
73
+ node.index = i;
53
74
  node.sourceLinks = [];
54
75
  node.targetLinks = [];
55
76
  node.id = node.id !== undefined ? node.id : node.label.text;
@@ -93,17 +114,36 @@ class Sankey {
93
114
  }
94
115
  }
95
116
 
117
+ calculateNodeHeights(nodes) {
118
+ let current = new Set(nodes);
119
+ let next = new Set;
120
+ let x = 0;
121
+ const eachNode = (node) => {
122
+ node.height = x;
123
+ node.targetLinks.forEach((link) => {
124
+ next.add(link.source);
125
+ });
126
+ };
127
+ while (current.size) {
128
+ current.forEach(eachNode);
129
+ x++;
130
+ current = next;
131
+ next = new Set;
132
+ }
133
+ }
134
+
96
135
  calculateNodeColumns(nodes) {
97
136
  const maxDepth = max(nodes, (d) => d.depth);
98
137
  const columnWidth = (this.width - this.offsetX - this.nodeWidth) / maxDepth;
99
138
  const columns = new Array(maxDepth + 1);
100
139
  for (let i = 0; i < nodes.length; i++) {
101
140
  const node = nodes[i];
102
- const depth = Math.max(0, Math.min(maxDepth, node.sourceLinks.length ? node.depth : maxDepth));
103
- node.x0 = this.offsetX + depth * columnWidth;
141
+ const layer = Math.max(0, Math.min(maxDepth, calcLayer(node, maxDepth)));
142
+ node.x0 = this.offsetX + layer * columnWidth;
104
143
  node.x1 = node.x0 + this.nodeWidth;
105
- columns[depth] = columns[depth] || [];
106
- columns[depth].push(node);
144
+ node.layer = layer;
145
+ columns[layer] = columns[layer] || [];
146
+ columns[layer].push(node);
107
147
  }
108
148
 
109
149
  return columns;
@@ -112,31 +152,39 @@ class Sankey {
112
152
  calculateNodeBreadths(columns) {
113
153
  const kSize = min(columns, (c) => (this.height - this.offsetY - (c.length - 1) * this.nodePadding) / sum(c, value));
114
154
 
115
- for (let c = 0; c < columns.length; c++) {
116
- const nodes = columns[c];
155
+ columns.forEach(nodes => {
117
156
  let y = this.offsetY;
118
- for (let i = 0; i < nodes.length; i++) {
119
- const node = nodes[i];
157
+ nodes.forEach((node) => {
120
158
  node.y0 = y;
121
159
  node.y1 = y + node.value * kSize;
122
160
  y = node.y1 + this.nodePadding;
123
- for (let l = 0; l < node.sourceLinks.length; l++) {
124
- const link = node.sourceLinks[l];
161
+ node.sourceLinks.forEach((link) => {
125
162
  link.width = link.value * kSize;
126
- }
127
- }
128
-
163
+ });
164
+ });
129
165
  y = (this.height - y + this.nodePadding) / (nodes.length + 1);
130
- for (let i = 0; i < nodes.length; ++i) {
131
- const node = nodes[i];
166
+ nodes.forEach((node, i) => {
132
167
  node.y0 += y * (i + 1);
133
168
  node.y1 += y * (i + 1);
169
+ });
170
+ });
171
+
172
+ if (this.autoLayout !== false) {
173
+ const loops = this.loops !== undefined ? this.loops : columns.length - 1;
174
+ const targetColumnIndex = this.targetColumnIndex || 1;
175
+
176
+ for (let i = 0; i < loops; i++) {
177
+ if (!this.reverse) {
178
+ this.uncurlLinksToLeft(columns, targetColumnIndex);
179
+ this.uncurlLinksToRight(columns, targetColumnIndex);
180
+ } else {
181
+ this.uncurlLinksToRight(columns, targetColumnIndex);
182
+ this.uncurlLinksToLeft(columns, targetColumnIndex);
183
+ }
134
184
  }
135
185
  }
136
186
 
137
- for (let c = 0; c < columns.length; c++) {
138
- sortLinks(columns[c]);
139
- }
187
+ columns.forEach(sortLinks);
140
188
  }
141
189
 
142
190
  applyNodesOffset(nodes) {
@@ -156,15 +204,175 @@ class Sankey {
156
204
  let y = node.y0;
157
205
  let y1 = y;
158
206
  sourceLinks.forEach((link) => {
207
+ link.x0 = link.source.x1;
159
208
  link.y0 = y + link.width / 2;
160
209
  y += link.width;
161
210
  });
162
211
  targetLinks.forEach((link) => {
212
+ link.x1 = link.target.x0;
163
213
  link.y1 = y1 + link.width / 2;
164
214
  y1 += link.width;
165
215
  });
166
216
  });
167
217
  }
218
+
219
+ uncurlLinksToRight(columns, targetColumnIndex) {
220
+ const n = columns.length;
221
+ for (let i = targetColumnIndex; i < n; i++) {
222
+ const column = columns[i];
223
+ column.forEach((target) => {
224
+ let y = 0;
225
+ let sum = 0;
226
+ target.targetLinks.forEach((link) => {
227
+ let kValue = link.value * (target.layer - link.source.layer);
228
+ y += this.targetTopPos(link.source, target) * kValue;
229
+ sum += kValue;
230
+ });
231
+
232
+ let dy = y === 0 ? 0 : (y / sum - target.y0);
233
+ target.y0 += dy;
234
+ target.y1 += dy;
235
+ sortLinks([target]);
236
+ });
237
+ column.sort(sortAsc);
238
+ this.arrangeNodesVertically(column);
239
+ }
240
+ }
241
+
242
+ uncurlLinksToLeft(columns, targetColumnIndex) {
243
+ const l = columns.length;
244
+ const startIndex = l - 1 - targetColumnIndex;
245
+ for (let i = startIndex; i >= 0; i--) {
246
+ const column = columns[i];
247
+ for (let j = 0; j < column.length; j++) {
248
+ const source = column[j];
249
+ let y = 0;
250
+ let sum = 0;
251
+ source.sourceLinks.forEach((link) => {
252
+ let kValue = link.value * (link.target.layer - source.layer);
253
+ y += this.sourceTopPos(source, link.target) * kValue;
254
+ sum += kValue;
255
+ });
256
+ let dy = y === 0 ? 0 : (y / sum - source.y0);
257
+ source.y0 += dy;
258
+ source.y1 += dy;
259
+ sortLinks([source]);
260
+ }
261
+
262
+ column.sort(sortAsc);
263
+ this.arrangeNodesVertically(column);
264
+ }
265
+ }
266
+
267
+ arrangeNodesVertically(nodes) {
268
+ const startIndex = 0;
269
+ const endIndex = nodes.length - 1;
270
+
271
+ this.arrangeUp(nodes, this.height, endIndex);
272
+ this.arrangeDown(nodes, this.offsetY, startIndex);
273
+ }
274
+
275
+ arrangeDown(nodes, yPos, index) {
276
+ let currentY = yPos;
277
+
278
+ for (let i = index; i < nodes.length; i++) {
279
+ const node = nodes[i];
280
+ const dy = Math.max(0, currentY - node.y0);
281
+ node.y0 += dy;
282
+ node.y1 += dy;
283
+ currentY = node.y1 + this.nodePadding;
284
+ }
285
+ }
286
+
287
+ arrangeUp(nodes, yPos, index) {
288
+ let currentY = yPos;
289
+ for (let i = index; i >= 0; --i) {
290
+ const node = nodes[i];
291
+ const dy = Math.max(0, node.y1 - currentY);
292
+ node.y0 -= dy;
293
+ node.y1 -= dy;
294
+ currentY = node.y0 - this.nodePadding;
295
+ }
296
+ }
297
+
298
+ sourceTopPos(source, target) {
299
+ let y = target.y0 - ((target.targetLinks.length - 1) * this.nodePadding) / 2;
300
+ for (let i = 0; i < target.targetLinks.length; i++) {
301
+ const link = target.targetLinks[i];
302
+ if (link.source === source) {
303
+ break;
304
+ }
305
+ y += link.width + this.nodePadding;
306
+ }
307
+ for (let i = 0; i < source.sourceLinks.length; i++) {
308
+ const link = source.sourceLinks[i];
309
+ if (link.target === target) {
310
+ break;
311
+ }
312
+ y -= link.width;
313
+ }
314
+ return y;
315
+ }
316
+
317
+ targetTopPos(source, target) {
318
+ let y = source.y0 - ((source.sourceLinks.length - 1) * this.nodePadding) / 2;
319
+ for (let i = 0; i < source.sourceLinks.length; i++) {
320
+ const link = source.sourceLinks[i];
321
+ if (link.target === target) {
322
+ break;
323
+ }
324
+ y += link.width + this.nodePadding;
325
+ }
326
+ for (let i = 0; i < target.targetLinks.length; i++) {
327
+ const link = target.targetLinks[i];
328
+ if (link.source === source) {
329
+ break;
330
+ }
331
+ y -= link.width;
332
+ }
333
+ return y;
334
+ }
168
335
  }
169
336
 
170
337
  export const calculateSankey = (options) => new Sankey(options).calculate();
338
+
339
+ export const crossesValue = (links) => {
340
+ let value = 0;
341
+ const linksLength = links.length;
342
+
343
+ for (let i = 0; i < linksLength; i++) {
344
+ const link = links[i];
345
+
346
+ for (let lNext = i + 1; lNext < linksLength; lNext++) {
347
+ const nextLink = links[lNext];
348
+
349
+ if (intersect(link, nextLink)) {
350
+ value += Math.min(link.value, nextLink.value);
351
+ }
352
+ }
353
+ }
354
+
355
+ return value;
356
+ };
357
+
358
+ function rotationDirection(p1x, p1y, p2x, p2y, p3x, p3y) {
359
+ const expression1 = (p3y - p1y) * (p2x - p1x);
360
+ const expression2 = (p2y - p1y) * (p3x - p1x);
361
+
362
+ if (expression1 > expression2) {
363
+ return 1;
364
+ } else if (expression1 === expression2) {
365
+ return 0;
366
+ }
367
+
368
+ return -1;
369
+ }
370
+
371
+ function intersect(link1, link2) {
372
+ const f1 = rotationDirection(link1.x0, link1.y0, link1.x1, link1.y1, link2.x1, link2.y1);
373
+ const f2 = rotationDirection(link1.x0, link1.y0, link1.x1, link1.y1, link2.x0, link2.y0);
374
+ const f3 = rotationDirection(link1.x0, link1.y0, link2.x0, link2.y0, link2.x1, link2.y1);
375
+ const f4 = rotationDirection(link1.x1, link1.y1, link2.x0, link2.y0, link2.x1, link2.y1);
376
+
377
+ return f1 !== f2 && f3 !== f4;
378
+ }
@@ -6,11 +6,11 @@ import { defined } from '../drawing-utils';
6
6
  export class Link extends SankeyElement {
7
7
  getElement() {
8
8
  const link = this.options.link;
9
- const { source, target, y0, y1 } = link;
10
- const xC = (source.x0 + target.x1) / 2;
9
+ const { x0, x1, y0, y1 } = link;
10
+ const xC = (x0 + x1) / 2;
11
11
 
12
12
  return new drawing.Path(this.visualOptions())
13
- .moveTo(source.x1, y0).curveTo([xC, y0], [xC, y1], [target.x0, y1]);
13
+ .moveTo(x0, y0).curveTo([xC, y0], [xC, y1], [x1, y1]);
14
14
  }
15
15
 
16
16
  visualOptions() {
@@ -1,6 +1,6 @@
1
1
  import { geometry, drawing } from '@progress/kendo-drawing';
2
2
  import { deepExtend, addClass, setDefaultOptions } from '../common';
3
- import { calculateSankey } from './calculation';
3
+ import { calculateSankey, crossesValue } from './calculation';
4
4
  import { Node, resolveNodeOptions } from './node';
5
5
  import { Link, resolveLinkOptions } from './link';
6
6
  import { Label, resolveLabelOptions } from './label';
@@ -60,6 +60,7 @@ export class Sankey extends Observable {
60
60
  }
61
61
  this.size = { width, height };
62
62
  this.surface.setSize(this.size);
63
+ this.resize = true;
63
64
  this._redraw();
64
65
  });
65
66
  });
@@ -244,7 +245,8 @@ export class Sankey extends Observable {
244
245
  }
245
246
 
246
247
  calculateSankey(calcOptions, sankeyOptions) {
247
- const { title, legend, data, nodes, labels, nodesColors } = sankeyOptions;
248
+ const { title, legend, data, nodes, labels, nodesColors, disableAutoLayout } = sankeyOptions;
249
+ const autoLayout = !disableAutoLayout;
248
250
 
249
251
  const sankeyBox = new Box(0, 0, calcOptions.width, calcOptions.height);
250
252
  const titleBox = this.titleBox(title, sankeyBox);
@@ -263,26 +265,27 @@ export class Sankey extends Observable {
263
265
  }
264
266
 
265
267
  const legendBox = this.legendBox(legend, data.nodes, legendArea);
268
+ const legendPosition = (legend && legend.position) || Legend.prototype.options.position;
266
269
 
267
270
  if (legendBox) {
268
- if (legend.position === LEFT) {
271
+ if (legendPosition === LEFT) {
269
272
  sankeyBox.unpad({ left: legendBox.width() });
270
273
  }
271
274
 
272
- if (legend.position === RIGHT) {
275
+ if (legendPosition === RIGHT) {
273
276
  sankeyBox.shrink(legendBox.width(), 0);
274
277
  }
275
278
 
276
- if (legend.position === TOP) {
279
+ if (legendPosition === TOP) {
277
280
  sankeyBox.unpad({ top: legendBox.height() });
278
281
  }
279
282
 
280
- if (legend.position === BOTTOM) {
283
+ if (legendPosition === BOTTOM) {
281
284
  sankeyBox.shrink(0, legendBox.height());
282
285
  }
283
286
  }
284
287
 
285
- const calculatedNodes = calculateSankey(Object.assign({}, calcOptions, {offsetX: sankeyBox.x1, offsetY: sankeyBox.y1, width: sankeyBox.x2, height: sankeyBox.y2})).nodes;
288
+ const calculatedNodes = calculateSankey(Object.assign({}, calcOptions, {offsetX: 0, offsetY: 0, width: sankeyBox.width(), height: sankeyBox.height()})).nodes;
286
289
  const box = new Box();
287
290
 
288
291
  calculatedNodes.forEach((nodeEl, i) => {
@@ -290,21 +293,71 @@ export class Sankey extends Observable {
290
293
  const nodeInstance = new Node(nodeOps);
291
294
  box.wrap(rectToBox(nodeInstance.exportVisual().rawBBox()));
292
295
 
293
- const labelInstance = new Label(deepExtend({ node: nodeEl, totalWidth: calcOptions.width }, labels));
296
+ const labelInstance = new Label(resolveLabelOptions(nodeEl, labels, sankeyBox.width()));
294
297
  const labelVisual = labelInstance.exportVisual();
295
298
  if (labelVisual) {
296
299
  box.wrap(rectToBox(labelVisual.rawBBox()));
297
300
  }
298
301
  });
299
302
 
300
- let offsetX = (box.x1 < 0 ? -box.x1 : 0) + sankeyBox.x1;
301
- let offsetY = (box.y1 < 0 ? -box.y1 : 0) + sankeyBox.y1;
303
+ let offsetX = sankeyBox.x1;
304
+ let offsetY = sankeyBox.y1;
302
305
 
303
- let width = box.width() > sankeyBox.x2 ? offsetX + sankeyBox.x2 - (box.width() - sankeyBox.x2) : sankeyBox.x2;
304
- let height = box.height() > sankeyBox.y2 ? offsetY + sankeyBox.y2 - (box.height() - sankeyBox.y2) : sankeyBox.y2;
306
+ let width = sankeyBox.width() + offsetX;
307
+ let height = sankeyBox.height() + offsetY;
308
+
309
+ width -= box.x2 > sankeyBox.width() ? box.x2 - sankeyBox.width() : 0;
310
+ height -= box.y2 > sankeyBox.height() ? box.y2 - sankeyBox.height() : 0;
311
+
312
+ offsetX += box.x1 < 0 ? -box.x1 : 0;
313
+ offsetY += box.y1 < 0 ? -box.y1 : 0;
314
+
315
+ if (autoLayout === false) {
316
+ return {
317
+ sankey: calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height, autoLayout: false})),
318
+ legendBox,
319
+ titleBox
320
+ };
321
+ }
322
+
323
+ if (this.resize && autoLayout && this.permutation) {
324
+ this.resize = false;
325
+ return {
326
+ sankey: calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height}, this.permutation)),
327
+ legendBox,
328
+ titleBox
329
+ };
330
+ }
331
+
332
+ const startColumn = 1;
333
+ const loops = 2;
334
+ const columnsLength = calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height, autoLayout: false})).columns.length;
335
+ const results = [];
336
+
337
+ const permutation = (targetColumnIndex, reverse) => {
338
+ let currPerm = calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height, loops: loops, targetColumnIndex, reverse}));
339
+ let crosses = crossesValue(currPerm.links);
340
+ results.push({
341
+ crosses: crosses,
342
+ reverse: reverse,
343
+ targetColumnIndex: targetColumnIndex
344
+ });
345
+ return crosses === 0;
346
+ };
347
+
348
+ for (let index = startColumn; index <= columnsLength - 1; index++) {
349
+ if (permutation(index, false) || permutation(index, true)) {
350
+ break;
351
+ }
352
+ }
353
+
354
+ const minCrosses = Math.min.apply(null, results.map(r => r.crosses));
355
+ const bestResult = results.find(r => r.crosses === minCrosses);
356
+ this.permutation = { targetColumnIndex: bestResult.targetColumnIndex, reverse: bestResult.reverse };
357
+ const result = calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height}, this.permutation));
305
358
 
306
359
  return {
307
- sankey: calculateSankey(Object.assign({}, calcOptions, {offsetX, offsetY, width, height})),
360
+ sankey: result,
308
361
  legendBox,
309
362
  titleBox
310
363
  };
@@ -385,8 +438,9 @@ export class Sankey extends Observable {
385
438
  visual.append(linkVisual);
386
439
  });
387
440
 
441
+ const diagramWidth = nodes.reduce((acc, node) => Math.max(acc, node.x1), 0);
388
442
  nodes.forEach((node) => {
389
- const textOps = resolveLabelOptions(node, labelOptions, width);
443
+ const textOps = resolveLabelOptions(node, labelOptions, diagramWidth);
390
444
  const labelInstance = new Label(textOps);
391
445
  const labelVisual = labelInstance.exportVisual();
392
446
 
@@ -418,6 +472,9 @@ export class Sankey extends Observable {
418
472
  }
419
473
 
420
474
  setDefaultOptions(Sankey, {
475
+ title: {
476
+ position: TOP, // 'top', 'bottom'
477
+ },
421
478
  labels: {
422
479
  visible: true,
423
480
  margin: {
@@ -436,6 +493,7 @@ setDefaultOptions(Sankey, {
436
493
  width: 24,
437
494
  padding: 16,
438
495
  opacity: 1,
496
+ align: 'stretch', // 'left', 'right', 'stretch'
439
497
  offset: { left: 0, top: 0 }
440
498
  },
441
499
  links: {
@@ -26,7 +26,6 @@ export class Title extends SankeyElement {
26
26
  }
27
27
 
28
28
  setDefaultOptions(Title, {
29
- position: TOP, // 'top', 'bottom'
30
29
  align: CENTER, // 'left', 'right', 'center'
31
30
  opacity: 1,
32
31
  border: {