cats-charts 0.0.99 → 0.1.1
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.
- package/fesm2022/cats-charts.mjs +119 -45
- package/fesm2022/cats-charts.mjs.map +1 -1
- package/index.d.ts +3 -1
- package/package.json +1 -1
package/fesm2022/cats-charts.mjs
CHANGED
|
@@ -3987,6 +3987,17 @@ class SankeyChart {
|
|
|
3987
3987
|
legendSeries = [];
|
|
3988
3988
|
previousReactiveSignature = '';
|
|
3989
3989
|
resizeObserver;
|
|
3990
|
+
stopNodeDragPropagation = (event) => {
|
|
3991
|
+
// Labels can forward interaction to their node through __hostTarget.
|
|
3992
|
+
const node = event.target?.__hostTarget ?? event.target;
|
|
3993
|
+
if (!node?.draggable)
|
|
3994
|
+
return;
|
|
3995
|
+
// ZRender maps mouse, pointer, and touch starts to mousedown. Keep its
|
|
3996
|
+
// node dragging active while blocking ancestor drag handlers and the
|
|
3997
|
+
// browser's native drag of a draggable parent element.
|
|
3998
|
+
event.event?.stopPropagation();
|
|
3999
|
+
event.event?.preventDefault();
|
|
4000
|
+
};
|
|
3990
4001
|
/* ------------------ CHANGE HANDLING ------------------ */
|
|
3991
4002
|
ngOnChanges(changes) {
|
|
3992
4003
|
if (!this.config)
|
|
@@ -4024,11 +4035,39 @@ class SankeyChart {
|
|
|
4024
4035
|
const totalValue = this.totalSourceValue(visibleLinks, visibleNodeMeta);
|
|
4025
4036
|
this.legendSeries = this.buildLegendSeries(visibleNodeMeta, visibleLinks, palette);
|
|
4026
4037
|
const selected = this.readLegendSelected();
|
|
4027
|
-
const legendEnabled = this.isLegendEnabled();
|
|
4028
|
-
const legendConfig = styling?.legend;
|
|
4029
4038
|
const nodeOpacity = this.percentToRatio(styling?.node?.opacity, 100);
|
|
4030
4039
|
const labelTypography = styling?.labels?.typography;
|
|
4031
4040
|
const fontColor = this.withOpacity(labelTypography?.reset ? undefined : labelTypography?.color, this.percentToRatio(labelTypography?.reset ? undefined : labelTypography?.opacity, 100), '#333');
|
|
4041
|
+
const label = {
|
|
4042
|
+
show: styling?.labels?.showLabels ?? true,
|
|
4043
|
+
// Keep labels within the space between Sankey columns. ECharts then
|
|
4044
|
+
// adds an ellipsis whenever a larger font, wider node, or long name
|
|
4045
|
+
// does not fit.
|
|
4046
|
+
width: this.resolveSankeyLabelWidth(visibleNodeMeta),
|
|
4047
|
+
overflow: 'truncate',
|
|
4048
|
+
ellipsis: '...',
|
|
4049
|
+
color: fontColor,
|
|
4050
|
+
fontFamily: labelTypography?.reset ? undefined : labelTypography?.fontFamily,
|
|
4051
|
+
fontSize: this.clamp(labelTypography?.reset ? undefined : labelTypography?.fontSize, 8, 40),
|
|
4052
|
+
fontWeight: labelTypography?.reset
|
|
4053
|
+
? undefined
|
|
4054
|
+
: labelTypography?.bold
|
|
4055
|
+
? 'bold'
|
|
4056
|
+
: undefined,
|
|
4057
|
+
fontStyle: labelTypography?.reset
|
|
4058
|
+
? undefined
|
|
4059
|
+
: labelTypography?.italic
|
|
4060
|
+
? 'italic'
|
|
4061
|
+
: undefined,
|
|
4062
|
+
textDecoration: labelTypography?.reset
|
|
4063
|
+
? undefined
|
|
4064
|
+
: labelTypography?.underline
|
|
4065
|
+
? 'underline'
|
|
4066
|
+
: undefined,
|
|
4067
|
+
position: this.resolveLabelPosition(),
|
|
4068
|
+
formatter: (params) => this.formatNodeLabel(String(params.name ?? ''), visibleNodeMeta.get(String(params.name ?? '')), totalValue),
|
|
4069
|
+
};
|
|
4070
|
+
const labelMargins = this.resolveSankeyLabelMargins(visibleNodes, label, visibleNodeMeta);
|
|
4032
4071
|
this.chartOption = {
|
|
4033
4072
|
backgroundColor: this.config?.backgroundColor || 'transparent',
|
|
4034
4073
|
color: palette,
|
|
@@ -4056,11 +4095,8 @@ class SankeyChart {
|
|
|
4056
4095
|
},
|
|
4057
4096
|
series: [
|
|
4058
4097
|
{
|
|
4059
|
-
left: 16,
|
|
4060
|
-
right: 16,
|
|
4061
|
-
top: legendEnabled && legendConfig?.position === 'top' ? 32 : 16,
|
|
4062
|
-
bottom: legendEnabled && legendConfig?.position === 'bottom' ? 32 : 16,
|
|
4063
4098
|
...this.config.seriesOption,
|
|
4099
|
+
...labelMargins,
|
|
4064
4100
|
type: 'sankey',
|
|
4065
4101
|
...((styling?.node?.thickness ?? this.config.seriesOption?.nodeWidth)
|
|
4066
4102
|
? {
|
|
@@ -4077,7 +4113,7 @@ class SankeyChart {
|
|
|
4077
4113
|
return {
|
|
4078
4114
|
...n,
|
|
4079
4115
|
name: n.name,
|
|
4080
|
-
label: this.mergeSingleNodeLabel(n, nodeStyle?.label),
|
|
4116
|
+
label: this.mergeSingleNodeLabel(n, nodeStyle?.label, visibleNodeMeta.get(n.name)),
|
|
4081
4117
|
itemStyle: {
|
|
4082
4118
|
color: this.resolveNodeColor(n, visibleNodeMeta.get(n.name), index, palette),
|
|
4083
4119
|
opacity: nodeOpacity,
|
|
@@ -4114,41 +4150,10 @@ class SankeyChart {
|
|
|
4114
4150
|
...(this.config.seriesOption?.lineStyle || {}),
|
|
4115
4151
|
...(styling?.lineStyle || {}),
|
|
4116
4152
|
},
|
|
4117
|
-
label
|
|
4118
|
-
show: styling?.labels?.showLabels ?? true,
|
|
4119
|
-
// Keep labels within the space between Sankey columns. ECharts then
|
|
4120
|
-
// adds an ellipsis whenever a larger font, wider node, or long name
|
|
4121
|
-
// does not fit.
|
|
4122
|
-
width: this.resolveSankeyLabelWidth(visibleNodeMeta),
|
|
4123
|
-
overflow: 'truncate',
|
|
4124
|
-
ellipsis: '...',
|
|
4125
|
-
color: fontColor,
|
|
4126
|
-
fontFamily: labelTypography?.reset ? undefined : labelTypography?.fontFamily,
|
|
4127
|
-
fontSize: this.clamp(labelTypography?.reset ? undefined : labelTypography?.fontSize, 8, 40),
|
|
4128
|
-
fontWeight: labelTypography?.reset
|
|
4129
|
-
? undefined
|
|
4130
|
-
: labelTypography?.bold
|
|
4131
|
-
? 'bold'
|
|
4132
|
-
: undefined,
|
|
4133
|
-
fontStyle: labelTypography?.reset
|
|
4134
|
-
? undefined
|
|
4135
|
-
: labelTypography?.italic
|
|
4136
|
-
? 'italic'
|
|
4137
|
-
: undefined,
|
|
4138
|
-
textDecoration: labelTypography?.reset
|
|
4139
|
-
? undefined
|
|
4140
|
-
: labelTypography?.underline
|
|
4141
|
-
? 'underline'
|
|
4142
|
-
: undefined,
|
|
4143
|
-
...(styling?.labels?.alignment !== 'default' && {
|
|
4144
|
-
position: styling?.labels?.alignment,
|
|
4145
|
-
}),
|
|
4146
|
-
formatter: (params) => this.formatNodeLabel(String(params.name ?? ''), visibleNodeMeta.get(String(params.name ?? '')), totalValue),
|
|
4147
|
-
},
|
|
4153
|
+
label,
|
|
4148
4154
|
},
|
|
4149
4155
|
],
|
|
4150
4156
|
};
|
|
4151
|
-
console.log(this.chartOption);
|
|
4152
4157
|
if (this.chartInstance) {
|
|
4153
4158
|
this.chartInstance.setOption(this.chartOption, true);
|
|
4154
4159
|
}
|
|
@@ -4352,10 +4357,14 @@ class SankeyChart {
|
|
|
4352
4357
|
label: nodeStyle.label,
|
|
4353
4358
|
};
|
|
4354
4359
|
}
|
|
4355
|
-
mergeSingleNodeLabel(node, label) {
|
|
4356
|
-
|
|
4360
|
+
mergeSingleNodeLabel(node, label, meta) {
|
|
4361
|
+
const alignment = this.config.styling?.labels?.alignment;
|
|
4362
|
+
const defaultSourcePosition = meta?.role === 'source' &&
|
|
4363
|
+
(!alignment || alignment === 'default');
|
|
4364
|
+
if (!label && !node.label && !defaultSourcePosition)
|
|
4357
4365
|
return undefined;
|
|
4358
4366
|
return {
|
|
4367
|
+
...(defaultSourcePosition ? { position: 'right' } : {}),
|
|
4359
4368
|
...(label || {}),
|
|
4360
4369
|
...(node.label || {}),
|
|
4361
4370
|
};
|
|
@@ -4413,9 +4422,70 @@ class SankeyChart {
|
|
|
4413
4422
|
const nodeWidth = this.clamp(this.config.styling?.node?.thickness ?? this.config.seriesOption?.nodeWidth, 1, 50) ?? 20;
|
|
4414
4423
|
const horizontalPadding = 32;
|
|
4415
4424
|
const availableWidth = Math.max(chartWidth - horizontalPadding - levelCount * nodeWidth, 0);
|
|
4416
|
-
//
|
|
4417
|
-
|
|
4418
|
-
|
|
4425
|
+
// Reserve room for labels at both outer edges as well as between columns.
|
|
4426
|
+
return Math.max(0, Math.min(180, Math.floor(availableWidth / Math.max(levelCount * 2 + 2, 1))));
|
|
4427
|
+
}
|
|
4428
|
+
resolveSankeyLabelMargins(nodes, label, meta) {
|
|
4429
|
+
const width = this.chartInstance?.getWidth() ?? 0;
|
|
4430
|
+
const height = this.chartInstance?.getHeight() ?? 0;
|
|
4431
|
+
const legend = this.config.styling?.legend;
|
|
4432
|
+
const margin = (value, size, fallback) => {
|
|
4433
|
+
if (typeof value === 'number')
|
|
4434
|
+
return value;
|
|
4435
|
+
if (typeof value === 'string' && value.endsWith('%')) {
|
|
4436
|
+
const percent = Number.parseFloat(value);
|
|
4437
|
+
if (Number.isFinite(percent))
|
|
4438
|
+
return (size * percent) / 100;
|
|
4439
|
+
}
|
|
4440
|
+
return fallback;
|
|
4441
|
+
};
|
|
4442
|
+
const series = this.config.seriesOption;
|
|
4443
|
+
const margins = {
|
|
4444
|
+
left: margin(series?.left, width, 16),
|
|
4445
|
+
right: margin(series?.right, width, 16),
|
|
4446
|
+
top: margin(series?.top, height, this.isLegendEnabled() && legend?.position === 'top' ? 32 : 16),
|
|
4447
|
+
bottom: margin(series?.bottom, height, this.isLegendEnabled() && legend?.position === 'bottom' ? 32 : 16),
|
|
4448
|
+
};
|
|
4449
|
+
const measured = nodes.map((node) => {
|
|
4450
|
+
const override = this.mergeSingleNodeLabel(node, this.resolveSingleNodeStyle(node.name)?.label, meta.get(node.name));
|
|
4451
|
+
const effective = {
|
|
4452
|
+
...label,
|
|
4453
|
+
...override,
|
|
4454
|
+
};
|
|
4455
|
+
const font = `${effective.fontStyle ?? 'normal'} ${effective.fontWeight ?? 'normal'} ${effective.fontSize ?? 12}px ${effective.fontFamily ?? 'sans-serif'}`;
|
|
4456
|
+
const text = typeof effective.formatter === 'function'
|
|
4457
|
+
? effective.formatter({ name: node.name, data: node })
|
|
4458
|
+
: typeof effective.formatter === 'string'
|
|
4459
|
+
? effective.formatter.replace(/\{b\}/g, node.name)
|
|
4460
|
+
: node.name;
|
|
4461
|
+
const bounds = echarts.format.getTextRect(String(text), font, undefined, undefined, effective.padding, effective.rich, false, effective.lineHeight);
|
|
4462
|
+
return { effective, bounds, customWidth: override?.width };
|
|
4463
|
+
});
|
|
4464
|
+
// Size the shared label box to the text, within the existing truncation cap.
|
|
4465
|
+
// This makes outer node positions respond to font and formatted text changes.
|
|
4466
|
+
const textWidths = measured
|
|
4467
|
+
.filter(({ effective }) => effective.show !== false)
|
|
4468
|
+
.map(({ bounds }) => bounds.width);
|
|
4469
|
+
label.width = Math.min(label.width, Math.ceil(Math.max(0, ...textWidths)));
|
|
4470
|
+
const alignment = this.config.styling?.labels?.alignment;
|
|
4471
|
+
const useLabelMargins = alignment === 'left' || alignment === 'right';
|
|
4472
|
+
for (const { effective, bounds, customWidth } of measured) {
|
|
4473
|
+
if (effective.show === false)
|
|
4474
|
+
continue;
|
|
4475
|
+
const labelWidth = typeof customWidth === 'number' ? customWidth : label.width;
|
|
4476
|
+
const distance = Number(effective.distance ?? 5);
|
|
4477
|
+
if (effective.position === 'left' || effective.position === 'right') {
|
|
4478
|
+
// Default labels face into the chart; keep the outer margins at 16px.
|
|
4479
|
+
if (useLabelMargins) {
|
|
4480
|
+
const side = effective.position;
|
|
4481
|
+
margins[side] = Math.max(margins[side], Math.ceil(labelWidth + distance + 8));
|
|
4482
|
+
}
|
|
4483
|
+
// Split labels and large fonts also need room above and below nodes.
|
|
4484
|
+
margins.top = Math.max(margins.top, Math.ceil(bounds.height / 2 + 8));
|
|
4485
|
+
margins.bottom = Math.max(margins.bottom, Math.ceil(bounds.height / 2 + 8));
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
return margins;
|
|
4419
4489
|
}
|
|
4420
4490
|
totalSourceValue(links, meta) {
|
|
4421
4491
|
const sourceNames = new Set([...meta.values()].filter((node) => node.role === 'source').map((node) => node.name));
|
|
@@ -4561,6 +4631,9 @@ class SankeyChart {
|
|
|
4561
4631
|
if (!this.chartInstance)
|
|
4562
4632
|
return;
|
|
4563
4633
|
this.chartInstance.off();
|
|
4634
|
+
const zr = this.chartInstance.getZr();
|
|
4635
|
+
zr.off('mousedown', this.stopNodeDragPropagation);
|
|
4636
|
+
zr.on('mousedown', this.stopNodeDragPropagation);
|
|
4564
4637
|
this.bindSankeyDragBounds();
|
|
4565
4638
|
this.events.forEach((eventName) => {
|
|
4566
4639
|
if (eventName === 'contextmenu') {
|
|
@@ -4649,7 +4722,8 @@ class SankeyChart {
|
|
|
4649
4722
|
/* ------------------ DESTROY ------------------ */
|
|
4650
4723
|
ngOnDestroy() {
|
|
4651
4724
|
this.resizeObserver?.disconnect();
|
|
4652
|
-
if (this.chartInstance) {
|
|
4725
|
+
if (this.chartInstance && !this.chartInstance.isDisposed()) {
|
|
4726
|
+
this.chartInstance.getZr()?.off('mousedown', this.stopNodeDragPropagation);
|
|
4653
4727
|
this.chartInstance.off();
|
|
4654
4728
|
this.chartInstance.dispose();
|
|
4655
4729
|
}
|