@progress/kendo-charts 2.3.0-dev.202402161236 → 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
- import { drawing } from '@progress/kendo-drawing';
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';
@@ -10,6 +10,7 @@ import Box from '../core/box';
10
10
  import rectToBox from '../core/utils/rect-to-box';
11
11
  import { Observable } from '../common/observable';
12
12
  import { Legend } from './legend';
13
+ import { defined } from '../drawing-utils';
13
14
 
14
15
  const LINK = 'link';
15
16
  const NODE = 'node';
@@ -59,6 +60,7 @@ export class Sankey extends Observable {
59
60
  }
60
61
  this.size = { width, height };
61
62
  this.surface.setSize(this.size);
63
+ this.resize = true;
62
64
  this._redraw();
63
65
  });
64
66
  });
@@ -242,11 +244,11 @@ export class Sankey extends Observable {
242
244
  return legendVisual.chartElement.box;
243
245
  }
244
246
 
245
- calculateSankey(options) {
246
- const { title, legend, data } = this.options;
247
- const { nodes, labels, nodesColors } = this.options;
247
+ calculateSankey(calcOptions, sankeyOptions) {
248
+ const { title, legend, data, nodes, labels, nodesColors, disableAutoLayout } = sankeyOptions;
249
+ const autoLayout = !disableAutoLayout;
248
250
 
249
- const sankeyBox = new Box(0, 0, options.width, options.height);
251
+ const sankeyBox = new Box(0, 0, calcOptions.width, calcOptions.height);
250
252
  const titleBox = this.titleBox(title, sankeyBox);
251
253
 
252
254
  let legendArea = sankeyBox.clone();
@@ -255,34 +257,35 @@ export class Sankey extends Observable {
255
257
  const titleHeight = titleBox.height();
256
258
  if (title.position === TOP) {
257
259
  sankeyBox.unpad({ top: titleHeight });
258
- legendArea = new Box(0, titleHeight, options.width, options.height);
260
+ legendArea = new Box(0, titleHeight, calcOptions.width, calcOptions.height);
259
261
  } else {
260
262
  sankeyBox.shrink(0, titleHeight);
261
- legendArea = new Box(0, 0, options.width, options.height - titleHeight);
263
+ legendArea = new Box(0, 0, calcOptions.width, calcOptions.height - titleHeight);
262
264
  }
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({}, options, {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,34 +293,90 @@ 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: options.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({}, options, {offsetX, offsetY, width, height})),
360
+ sankey: result,
308
361
  legendBox,
309
362
  titleBox
310
363
  };
311
364
  }
312
365
 
313
- _render() {
314
- const { data, labels: labelOptions, nodes: nodesOptions, links: linkOptions, nodesColors, title, legend } = this.options;
315
- const { width, height } = this.size;
366
+ _render(options, context) {
367
+ const sankeyOptions = options || this.options;
368
+ const sankeyContext = context || this;
369
+
370
+ const { data, labels: labelOptions, nodes: nodesOptions, links: linkOptions, nodesColors, title, legend } = sankeyOptions;
371
+ const { width, height } = sankeyContext.size;
372
+
316
373
  const calcOptions = Object.assign({}, data, {width, height, nodesOptions, title, legend});
317
- const { sankey, titleBox, legendBox } = this.calculateSankey(calcOptions);
374
+ const { sankey, titleBox, legendBox } = this.calculateSankey(calcOptions, sankeyOptions);
318
375
  const { nodes, links } = sankey;
319
376
 
320
- const visual = new drawing.Group();
377
+ const visual = new drawing.Group({
378
+ clip: drawing.Path.fromRect(new geometry.Rect([0, 0], [width, height]))
379
+ });
321
380
 
322
381
  if (titleBox) {
323
382
  const titleElement = new Title(Object.assign({}, title, {drawingRect: titleBox}));
@@ -332,6 +391,7 @@ export class Sankey extends Observable {
332
391
  }
333
392
 
334
393
  const visualNodes = new Map();
394
+ sankeyContext.nodesVisuals = visualNodes;
335
395
 
336
396
  nodes.forEach((node, i) => {
337
397
  const nodeOps = resolveNodeOptions(node, nodesOptions, nodesColors, i);
@@ -354,6 +414,7 @@ export class Sankey extends Observable {
354
414
  const sortedLinks = links.slice().sort((a, b) => b.value - a.value);
355
415
 
356
416
  const linksVisuals = [];
417
+ sankeyContext.linksVisuals = linksVisuals;
357
418
 
358
419
  sortedLinks.forEach(link => {
359
420
  const { source, target } = link;
@@ -377,11 +438,9 @@ export class Sankey extends Observable {
377
438
  visual.append(linkVisual);
378
439
  });
379
440
 
380
- this.linksVisuals = linksVisuals;
381
- this.nodesVisuals = visualNodes;
382
-
441
+ const diagramWidth = nodes.reduce((acc, node) => Math.max(acc, node.x1), 0);
383
442
  nodes.forEach((node) => {
384
- const textOps = resolveLabelOptions(node, labelOptions, width);
443
+ const textOps = resolveLabelOptions(node, labelOptions, diagramWidth);
385
444
  const labelInstance = new Label(textOps);
386
445
  const labelVisual = labelInstance.exportVisual();
387
446
 
@@ -393,8 +452,18 @@ export class Sankey extends Observable {
393
452
  return visual;
394
453
  }
395
454
 
396
- exportVisual() {
397
- return this._render();
455
+ exportVisual(exportOptions) {
456
+ const options = (exportOptions && exportOptions.options) ?
457
+ deepExtend({}, this.options, exportOptions.options) : this.options;
458
+
459
+ const context = {
460
+ size: {
461
+ width: defined(exportOptions && exportOptions.width) ? exportOptions.width : this.size.width,
462
+ height: defined(exportOptions && exportOptions.height) ? exportOptions.height : this.size.height
463
+ }
464
+ };
465
+
466
+ return this._render(options, context);
398
467
  }
399
468
 
400
469
  _setOptions(options) {
@@ -403,6 +472,9 @@ export class Sankey extends Observable {
403
472
  }
404
473
 
405
474
  setDefaultOptions(Sankey, {
475
+ title: {
476
+ position: TOP, // 'top', 'bottom'
477
+ },
406
478
  labels: {
407
479
  visible: true,
408
480
  margin: {
@@ -421,6 +493,7 @@ setDefaultOptions(Sankey, {
421
493
  width: 24,
422
494
  padding: 16,
423
495
  opacity: 1,
496
+ align: 'stretch', // 'left', 'right', 'stretch'
424
497
  offset: { left: 0, top: 0 }
425
498
  },
426
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: {