@internetstiftelsen/charts 0.13.0 → 0.13.2
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/README.md +2 -1
- package/dist/line.d.ts +3 -1
- package/dist/line.js +35 -2
- package/dist/tooltip.d.ts +15 -0
- package/dist/tooltip.js +236 -19
- package/dist/types.d.ts +11 -1
- package/dist/xy-chart.d.ts +3 -0
- package/dist/xy-chart.js +48 -7
- package/docs/components.md +4 -0
- package/docs/xy-chart.md +24 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,8 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
|
|
|
13
13
|
- **Custom Value Labels** - XY, pie, and donut charts support optional on-chart labels with custom formatters
|
|
14
14
|
- **Optional XY Animation** - Animate XY series on first render and `chart.update(...)` with `animate`
|
|
15
15
|
- **Optional Gauge Animation** - Animate gauge value transitions with `gauge.animate`
|
|
16
|
-
- **Stacking Control** - Bar stacking modes with optional reversed visual series order
|
|
16
|
+
- **Stacking Control** - Bar and area stacking modes with optional reversed visual series order
|
|
17
|
+
- **Configurable Tooltips** - Shared or split tooltips with connectors, transitions, and default max-width wrapping
|
|
17
18
|
- **Axis Direction Control** - Use `scales.x.reverse` / `scales.y.reverse` to flip an axis when needed
|
|
18
19
|
- **Flexible Scales** - Band, linear, time, and logarithmic scales (bar value axes stay linear)
|
|
19
20
|
- **Explicit or Responsive Sizing** - Set top-level `width`/`height` or let the container drive size
|
package/dist/line.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { LineConfig, DataItem, D3Scale, ScaleType, ChartTheme, LineValueLabelConfig, ExportHooks, LineConfigBase } from './types.js';
|
|
2
|
+
import type { LineConfig, DataItem, D3Scale, ScaleType, ChartTheme, LineValueLabelConfig, ExportHooks, LineConfigBase, LineCurveType, LinePointsConfig } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
4
|
import type { XYPointAnimationContext, XYPointSnapshot, XYSeriesRenderResult } from './xy-motion/types.js';
|
|
5
5
|
export declare class Line implements ChartComponent<LineConfigBase> {
|
|
@@ -7,6 +7,8 @@ export declare class Line implements ChartComponent<LineConfigBase> {
|
|
|
7
7
|
readonly dataKey: string;
|
|
8
8
|
readonly stroke: string;
|
|
9
9
|
readonly strokeWidth?: number;
|
|
10
|
+
readonly curve: LineCurveType;
|
|
11
|
+
readonly points: Required<LinePointsConfig>;
|
|
10
12
|
readonly valueLabel?: LineValueLabelConfig;
|
|
11
13
|
readonly exportHooks?: ExportHooks<LineConfigBase>;
|
|
12
14
|
constructor(config: LineConfig);
|
package/dist/line.js
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
import { line } from 'd3';
|
|
1
|
+
import { curveBasis, curveCardinal, curveLinear, curveMonotoneX, curveNatural, curveStep, line, } from 'd3';
|
|
2
2
|
import { sanitizeForCSS, mergeDeep } from './utils.js';
|
|
3
3
|
import { getScalePosition } from './scale-utils.js';
|
|
4
4
|
import { buildXYDatumSnapshotKeys, createTransitionCompletionPromise, createLeftToRightRevealTransition, getEnterStaggerTiming, } from './xy-motion/helpers.js';
|
|
5
|
+
const DEFAULT_LINE_POINTS = {
|
|
6
|
+
show: 'always',
|
|
7
|
+
};
|
|
8
|
+
const LINE_CURVE_FACTORIES = {
|
|
9
|
+
linear: curveLinear,
|
|
10
|
+
monotone: curveMonotoneX,
|
|
11
|
+
step: curveStep,
|
|
12
|
+
natural: curveNatural,
|
|
13
|
+
basis: curveBasis,
|
|
14
|
+
cardinal: curveCardinal,
|
|
15
|
+
};
|
|
5
16
|
export class Line {
|
|
6
17
|
constructor(config) {
|
|
7
18
|
Object.defineProperty(this, "type", {
|
|
@@ -28,6 +39,18 @@ export class Line {
|
|
|
28
39
|
writable: true,
|
|
29
40
|
value: void 0
|
|
30
41
|
});
|
|
42
|
+
Object.defineProperty(this, "curve", {
|
|
43
|
+
enumerable: true,
|
|
44
|
+
configurable: true,
|
|
45
|
+
writable: true,
|
|
46
|
+
value: void 0
|
|
47
|
+
});
|
|
48
|
+
Object.defineProperty(this, "points", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: void 0
|
|
53
|
+
});
|
|
31
54
|
Object.defineProperty(this, "valueLabel", {
|
|
32
55
|
enumerable: true,
|
|
33
56
|
configurable: true,
|
|
@@ -43,6 +66,10 @@ export class Line {
|
|
|
43
66
|
this.dataKey = config.dataKey;
|
|
44
67
|
this.stroke = config.stroke || '#8884d8';
|
|
45
68
|
this.strokeWidth = config.strokeWidth;
|
|
69
|
+
this.curve = config.curve || 'linear';
|
|
70
|
+
this.points = {
|
|
71
|
+
show: config.points?.show ?? DEFAULT_LINE_POINTS.show,
|
|
72
|
+
};
|
|
46
73
|
this.valueLabel = config.valueLabel;
|
|
47
74
|
this.exportHooks = config.exportHooks;
|
|
48
75
|
}
|
|
@@ -51,6 +78,8 @@ export class Line {
|
|
|
51
78
|
dataKey: this.dataKey,
|
|
52
79
|
stroke: this.stroke,
|
|
53
80
|
strokeWidth: this.strokeWidth,
|
|
81
|
+
curve: this.curve,
|
|
82
|
+
points: this.points,
|
|
54
83
|
valueLabel: this.valueLabel,
|
|
55
84
|
};
|
|
56
85
|
}
|
|
@@ -80,7 +109,9 @@ export class Line {
|
|
|
80
109
|
});
|
|
81
110
|
const transitions = [
|
|
82
111
|
...this.renderLinePath(plotGroup, lineData, animatedLineData, theme, sanitizeForCSS(this.dataKey), animation),
|
|
83
|
-
...this.
|
|
112
|
+
...(this.points.show === 'always'
|
|
113
|
+
? this.renderLinePoints(plotGroup, validLineData, validAnimatedLineData, theme, animation)
|
|
114
|
+
: []),
|
|
84
115
|
];
|
|
85
116
|
const snapshot = this.createSnapshot(validLineData);
|
|
86
117
|
// Render value labels if enabled (only for valid values)
|
|
@@ -129,8 +160,10 @@ export class Line {
|
|
|
129
160
|
}
|
|
130
161
|
renderLinePath(plotGroup, lineData, animatedLineData, theme, sanitizedKey, animation) {
|
|
131
162
|
const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
|
|
163
|
+
const curveFactory = LINE_CURVE_FACTORIES[this.curve] || curveLinear;
|
|
132
164
|
const lineGenerator = line()
|
|
133
165
|
.defined((entry) => entry.valid)
|
|
166
|
+
.curve(curveFactory)
|
|
134
167
|
.x((entry) => entry.x)
|
|
135
168
|
.y((entry) => entry.y);
|
|
136
169
|
const finalPath = lineGenerator(lineData);
|
package/dist/tooltip.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
14
14
|
readonly mode: TooltipMode;
|
|
15
15
|
readonly position: TooltipPosition;
|
|
16
16
|
readonly barAnchorPosition: TooltipBarAnchorPosition;
|
|
17
|
+
readonly maxWidth: number;
|
|
17
18
|
readonly transition: Required<TooltipTransitionConfig>;
|
|
18
19
|
readonly formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
|
|
19
20
|
readonly labelFormatter?: (label: string, data: DataItem) => string;
|
|
@@ -68,6 +69,8 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
68
69
|
private resolveTooltipConnectorLayout;
|
|
69
70
|
private resolveTooltipBoxArrowPosition;
|
|
70
71
|
private resolveTooltipConnectorPath;
|
|
72
|
+
private isTooltipArrowTipInsideHorizontalAnchorSpan;
|
|
73
|
+
private isTooltipArrowTipInsideVerticalAnchorSpan;
|
|
71
74
|
private resolveTooltipArrowTip;
|
|
72
75
|
private hasFiniteNumbers;
|
|
73
76
|
private resolveSplitTooltipCollisions;
|
|
@@ -75,7 +78,19 @@ export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
|
75
78
|
private getOppositeVerticalArrowEdge;
|
|
76
79
|
private flipTooltipIfItReducesCollisions;
|
|
77
80
|
private countSplitTooltipCollisions;
|
|
81
|
+
private countPlacedLayoutsOnEdge;
|
|
78
82
|
private doSplitTooltipLayoutsOverlap;
|
|
79
83
|
private resolveSplitTooltipPositions;
|
|
84
|
+
private resolveSideSplitTooltipPositions;
|
|
85
|
+
private resolveHorizontalSideSplitTooltipPositions;
|
|
86
|
+
private resolveHorizontalSideSplitTooltipCollisions;
|
|
87
|
+
private flipHorizontalSideTooltipIfItReducesCollisions;
|
|
88
|
+
private findReusableHorizontalTooltipLane;
|
|
89
|
+
private resolveNewHorizontalTooltipLaneTop;
|
|
90
|
+
private getHorizontalTooltipLaneTopCandidates;
|
|
91
|
+
private assignLayoutToHorizontalTooltipLane;
|
|
92
|
+
private doHorizontalTooltipLanesOverlap;
|
|
93
|
+
private doSplitTooltipLayoutsOverlapHorizontally;
|
|
94
|
+
private resolveVerticalSplitTooltipPositions;
|
|
80
95
|
}
|
|
81
96
|
export {};
|
package/dist/tooltip.js
CHANGED
|
@@ -16,6 +16,7 @@ const TOOLTIP_ARROW_FILL_Z_INDEX = 5;
|
|
|
16
16
|
const TOOLTIP_BODY_Z_INDEX = 6;
|
|
17
17
|
const TOOLTIP_TOTAL_BORDER_WIDTH_PX = TOOLTIP_BORDER_WIDTH_PX * 2;
|
|
18
18
|
const SPLIT_TOOLTIP_GAP_PX = 8;
|
|
19
|
+
const DEFAULT_TOOLTIP_MAX_WIDTH_PX = 280;
|
|
19
20
|
const DEFAULT_TOOLTIP_TRANSITION = {
|
|
20
21
|
show: false,
|
|
21
22
|
duration: 120,
|
|
@@ -55,6 +56,12 @@ export class Tooltip {
|
|
|
55
56
|
writable: true,
|
|
56
57
|
value: void 0
|
|
57
58
|
});
|
|
59
|
+
Object.defineProperty(this, "maxWidth", {
|
|
60
|
+
enumerable: true,
|
|
61
|
+
configurable: true,
|
|
62
|
+
writable: true,
|
|
63
|
+
value: void 0
|
|
64
|
+
});
|
|
58
65
|
Object.defineProperty(this, "transition", {
|
|
59
66
|
enumerable: true,
|
|
60
67
|
configurable: true,
|
|
@@ -109,13 +116,17 @@ export class Tooltip {
|
|
|
109
116
|
writable: true,
|
|
110
117
|
value: null
|
|
111
118
|
});
|
|
112
|
-
const { mode = 'split', position = 'side', barAnchorPosition = 'middle', transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
|
|
119
|
+
const { mode = 'split', position = 'side', barAnchorPosition = 'middle', maxWidth, transition, formatter, labelFormatter, customFormatter, exportHooks, } = config;
|
|
113
120
|
const tooltipId = Tooltip.nextTooltipId++;
|
|
114
121
|
this.id = `iisChartTooltip-${tooltipId}`;
|
|
115
122
|
this.splitTooltipOwner = `${this.id}-split`;
|
|
116
123
|
this.mode = mode;
|
|
117
124
|
this.position = position;
|
|
118
125
|
this.barAnchorPosition = barAnchorPosition;
|
|
126
|
+
this.maxWidth =
|
|
127
|
+
maxWidth !== undefined && Number.isFinite(maxWidth) && maxWidth > 0
|
|
128
|
+
? maxWidth
|
|
129
|
+
: DEFAULT_TOOLTIP_MAX_WIDTH_PX;
|
|
119
130
|
this.transition = {
|
|
120
131
|
...DEFAULT_TOOLTIP_TRANSITION,
|
|
121
132
|
...transition,
|
|
@@ -130,6 +141,7 @@ export class Tooltip {
|
|
|
130
141
|
mode: this.mode,
|
|
131
142
|
position: this.position,
|
|
132
143
|
barAnchorPosition: this.barAnchorPosition,
|
|
144
|
+
maxWidth: this.maxWidth,
|
|
133
145
|
transition: this.transition,
|
|
134
146
|
formatter: this.formatter,
|
|
135
147
|
labelFormatter: this.labelFormatter,
|
|
@@ -269,7 +281,11 @@ export class Tooltip {
|
|
|
269
281
|
.attr('aria-hidden', 'true')
|
|
270
282
|
.style('fill', 'none')
|
|
271
283
|
.style('pointer-events', 'all');
|
|
272
|
-
const
|
|
284
|
+
const focusCircleSeries = series.filter((currentSeries) => {
|
|
285
|
+
if (currentSeries.type === 'line' &&
|
|
286
|
+
currentSeries.points.show === 'never') {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
273
289
|
return (currentSeries.type === 'line' ||
|
|
274
290
|
currentSeries.type === 'area' ||
|
|
275
291
|
currentSeries.type === 'scatter');
|
|
@@ -278,7 +294,7 @@ export class Tooltip {
|
|
|
278
294
|
return currentSeries.type === 'bar';
|
|
279
295
|
});
|
|
280
296
|
const hasBarSeries = barSeries.length > 0;
|
|
281
|
-
const focusCircles =
|
|
297
|
+
const focusCircles = focusCircleSeries.map((currentSeries) => {
|
|
282
298
|
const seriesColor = getSeriesColor(currentSeries);
|
|
283
299
|
return svg
|
|
284
300
|
.append('circle')
|
|
@@ -303,7 +319,7 @@ export class Tooltip {
|
|
|
303
319
|
const updateVisualStateAtIndex = (closestIndex) => {
|
|
304
320
|
const dataPoint = data[closestIndex];
|
|
305
321
|
const dataPointPosition = dataPointPositions[closestIndex];
|
|
306
|
-
|
|
322
|
+
focusCircleSeries.forEach((currentSeries, seriesIndex) => {
|
|
307
323
|
const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
|
|
308
324
|
if (!Number.isFinite(value)) {
|
|
309
325
|
focusCircles[seriesIndex].style('opacity', 0);
|
|
@@ -450,11 +466,13 @@ export class Tooltip {
|
|
|
450
466
|
if (layouts.length === 0) {
|
|
451
467
|
return;
|
|
452
468
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
469
|
+
if (!isHorizontal) {
|
|
470
|
+
this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
|
|
471
|
+
this.resolveSplitTooltipCollisions(layouts, 'side', this.getOppositeSideArrowEdge);
|
|
472
|
+
}
|
|
473
|
+
this.resolveSplitTooltipPositions(layouts, isHorizontal);
|
|
456
474
|
layouts.forEach((layout) => {
|
|
457
|
-
this.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY);
|
|
475
|
+
this.renderTooltipWithConnector(layout.div, layout.arrowEdge, layout.left, layout.top, layout.width, layout.height, layout.targetX, layout.targetY, layout.anchor);
|
|
458
476
|
});
|
|
459
477
|
};
|
|
460
478
|
const hideTooltip = () => {
|
|
@@ -702,6 +720,7 @@ export class Tooltip {
|
|
|
702
720
|
theme.tooltip.fontFamily,
|
|
703
721
|
theme.tooltip.fontSize,
|
|
704
722
|
theme.tooltip.fontWeight,
|
|
723
|
+
this.maxWidth,
|
|
705
724
|
this.transition.show,
|
|
706
725
|
this.transition.duration,
|
|
707
726
|
this.transition.easing,
|
|
@@ -720,10 +739,13 @@ export class Tooltip {
|
|
|
720
739
|
.style('font-family', theme.tooltip.fontFamily)
|
|
721
740
|
.style('font-size', `${theme.tooltip.fontSize}px`)
|
|
722
741
|
.style('font-weight', theme.tooltip.fontWeight)
|
|
742
|
+
.style('box-sizing', 'border-box')
|
|
743
|
+
.style('overflow-wrap', 'break-word')
|
|
723
744
|
.style('overflow', 'visible')
|
|
724
745
|
.style('isolation', 'isolate')
|
|
725
746
|
.style('pointer-events', 'none')
|
|
726
747
|
.style('z-index', '1000');
|
|
748
|
+
tooltip.style('max-width', `${this.maxWidth}px`);
|
|
727
749
|
if (this.transition.show) {
|
|
728
750
|
tooltip
|
|
729
751
|
.style('transition', `opacity ${this.transition.duration}ms ${this.transition.easing}, transform ${this.transition.duration}ms ${this.transition.easing}`)
|
|
@@ -754,7 +776,7 @@ export class Tooltip {
|
|
|
754
776
|
height: tooltipRect.height,
|
|
755
777
|
};
|
|
756
778
|
}
|
|
757
|
-
renderTooltipWithConnector(tooltip, arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
779
|
+
renderTooltipWithConnector(tooltip, arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
|
|
758
780
|
if (!Number.isFinite(left) ||
|
|
759
781
|
!Number.isFinite(top) ||
|
|
760
782
|
!Number.isFinite(targetX) ||
|
|
@@ -762,7 +784,7 @@ export class Tooltip {
|
|
|
762
784
|
this.hideTooltipSelection(tooltip);
|
|
763
785
|
return;
|
|
764
786
|
}
|
|
765
|
-
const connectorLayout = this.resolveTooltipConnectorLayout(arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY);
|
|
787
|
+
const connectorLayout = this.resolveTooltipConnectorLayout(arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY, anchor);
|
|
766
788
|
if (!connectorLayout) {
|
|
767
789
|
this.hideTooltipSelection(tooltip);
|
|
768
790
|
return;
|
|
@@ -1096,7 +1118,7 @@ export class Tooltip {
|
|
|
1096
1118
|
top: Math.max(minTop, Math.min(top, maxTop)),
|
|
1097
1119
|
};
|
|
1098
1120
|
}
|
|
1099
|
-
resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
1121
|
+
resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
|
|
1100
1122
|
const localTargetX = targetX - tooltipLeft;
|
|
1101
1123
|
const localTargetY = targetY - tooltipTop;
|
|
1102
1124
|
if (!Number.isFinite(localTargetX) || !Number.isFinite(localTargetY)) {
|
|
@@ -1116,7 +1138,9 @@ export class Tooltip {
|
|
|
1116
1138
|
const startY = arrowTip.y - minY;
|
|
1117
1139
|
const endX = localTargetX - minX;
|
|
1118
1140
|
const endY = localTargetY - minY;
|
|
1119
|
-
const
|
|
1141
|
+
const arrowTipX = tooltipLeft + arrowTip.x;
|
|
1142
|
+
const arrowTipY = tooltipTop + arrowTip.y;
|
|
1143
|
+
const connectorPath = this.resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor);
|
|
1120
1144
|
if (!this.hasFiniteNumbers(width, height, boxX, boxY, startX, startY, endX, endY)) {
|
|
1121
1145
|
return null;
|
|
1122
1146
|
}
|
|
@@ -1159,21 +1183,31 @@ export class Tooltip {
|
|
|
1159
1183
|
};
|
|
1160
1184
|
}
|
|
1161
1185
|
}
|
|
1162
|
-
resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY) {
|
|
1186
|
+
resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor) {
|
|
1163
1187
|
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
1164
|
-
if (
|
|
1165
|
-
TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX) {
|
|
1188
|
+
if (this.isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor)) {
|
|
1166
1189
|
return '';
|
|
1167
1190
|
}
|
|
1168
1191
|
const elbowX = startX + (endX - startX) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
1169
1192
|
return `M ${startX},${startY} L ${elbowX},${startY} L ${endX},${endY}`;
|
|
1170
1193
|
}
|
|
1171
|
-
if (
|
|
1194
|
+
if (this.isTooltipArrowTipInsideHorizontalAnchorSpan(arrowTipX, anchor)) {
|
|
1172
1195
|
return '';
|
|
1173
1196
|
}
|
|
1174
1197
|
const elbowY = startY + (endY - startY) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
1175
1198
|
return `M ${startX},${startY} L ${startX},${elbowY} L ${endX},${endY}`;
|
|
1176
1199
|
}
|
|
1200
|
+
isTooltipArrowTipInsideHorizontalAnchorSpan(arrowTipX, anchor) {
|
|
1201
|
+
return (arrowTipX >=
|
|
1202
|
+
anchor.left - TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
|
|
1203
|
+
arrowTipX <= anchor.right + TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
|
|
1204
|
+
}
|
|
1205
|
+
isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor) {
|
|
1206
|
+
return (arrowTipY >=
|
|
1207
|
+
anchor.top - TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
|
|
1208
|
+
arrowTipY <=
|
|
1209
|
+
anchor.bottom + TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
|
|
1210
|
+
}
|
|
1177
1211
|
resolveTooltipArrowTip(arrowEdge, boxX, boxY, length) {
|
|
1178
1212
|
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
1179
1213
|
return {
|
|
@@ -1194,7 +1228,12 @@ export class Tooltip {
|
|
|
1194
1228
|
return;
|
|
1195
1229
|
}
|
|
1196
1230
|
const placedLayouts = [];
|
|
1197
|
-
const orderedLayouts = [...layouts].sort((a, b) =>
|
|
1231
|
+
const orderedLayouts = [...layouts].sort((a, b) => {
|
|
1232
|
+
if (position === 'vertical') {
|
|
1233
|
+
return a.targetX - b.targetX || a.targetY - b.targetY;
|
|
1234
|
+
}
|
|
1235
|
+
return a.targetY - b.targetY || a.targetX - b.targetX;
|
|
1236
|
+
});
|
|
1198
1237
|
orderedLayouts.forEach((layout) => {
|
|
1199
1238
|
this.flipTooltipIfItReducesCollisions(layout, placedLayouts, getOppositeArrowEdge);
|
|
1200
1239
|
placedLayouts.push(layout);
|
|
@@ -1238,7 +1277,12 @@ export class Tooltip {
|
|
|
1238
1277
|
top: flippedPosition.top,
|
|
1239
1278
|
};
|
|
1240
1279
|
const flippedCollisions = this.countSplitTooltipCollisions(flippedLayout, placedLayouts);
|
|
1241
|
-
if (flippedCollisions
|
|
1280
|
+
if (flippedCollisions > currentCollisions) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (flippedCollisions === currentCollisions &&
|
|
1284
|
+
this.countPlacedLayoutsOnEdge(placedLayouts, flippedArrowEdge) >=
|
|
1285
|
+
this.countPlacedLayoutsOnEdge(placedLayouts, layout.arrowEdge)) {
|
|
1242
1286
|
return;
|
|
1243
1287
|
}
|
|
1244
1288
|
layout.arrowEdge = flippedArrowEdge;
|
|
@@ -1248,13 +1292,28 @@ export class Tooltip {
|
|
|
1248
1292
|
countSplitTooltipCollisions(layout, placedLayouts) {
|
|
1249
1293
|
return placedLayouts.filter((placedLayout) => this.doSplitTooltipLayoutsOverlap(layout, placedLayout)).length;
|
|
1250
1294
|
}
|
|
1295
|
+
countPlacedLayoutsOnEdge(placedLayouts, arrowEdge) {
|
|
1296
|
+
return placedLayouts.filter((layout) => layout.arrowEdge === arrowEdge)
|
|
1297
|
+
.length;
|
|
1298
|
+
}
|
|
1251
1299
|
doSplitTooltipLayoutsOverlap(a, b) {
|
|
1252
1300
|
return (a.left < b.left + b.width + SPLIT_TOOLTIP_GAP_PX &&
|
|
1253
1301
|
a.left + a.width + SPLIT_TOOLTIP_GAP_PX > b.left &&
|
|
1254
1302
|
a.top < b.top + b.height + SPLIT_TOOLTIP_GAP_PX &&
|
|
1255
1303
|
a.top + a.height + SPLIT_TOOLTIP_GAP_PX > b.top);
|
|
1256
1304
|
}
|
|
1257
|
-
resolveSplitTooltipPositions(layouts) {
|
|
1305
|
+
resolveSplitTooltipPositions(layouts, isHorizontal) {
|
|
1306
|
+
if (isHorizontal && this.position === 'side') {
|
|
1307
|
+
this.resolveHorizontalSideSplitTooltipPositions(layouts);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
if (this.position === 'vertical') {
|
|
1311
|
+
this.resolveVerticalSplitTooltipPositions(layouts);
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
this.resolveSideSplitTooltipPositions(layouts);
|
|
1315
|
+
}
|
|
1316
|
+
resolveSideSplitTooltipPositions(layouts) {
|
|
1258
1317
|
const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1259
1318
|
const maxBottom = window.scrollY + window.innerHeight - TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1260
1319
|
const tooltipsByEdge = {
|
|
@@ -1305,6 +1364,164 @@ export class Tooltip {
|
|
|
1305
1364
|
});
|
|
1306
1365
|
});
|
|
1307
1366
|
}
|
|
1367
|
+
resolveHorizontalSideSplitTooltipPositions(layouts) {
|
|
1368
|
+
this.resolveHorizontalSideSplitTooltipCollisions(layouts);
|
|
1369
|
+
const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1370
|
+
const maxBottom = window.scrollY + window.innerHeight - TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1371
|
+
const lanes = [];
|
|
1372
|
+
const orderedLayouts = [...layouts].sort((a, b) => a.targetX - b.targetX || a.left - b.left);
|
|
1373
|
+
orderedLayouts.forEach((layout) => {
|
|
1374
|
+
const maxTop = maxBottom - layout.height;
|
|
1375
|
+
const preferredTop = Math.max(minTop, Math.min(layout.top, maxTop));
|
|
1376
|
+
const reusableLane = this.findReusableHorizontalTooltipLane(lanes, layout, preferredTop);
|
|
1377
|
+
if (reusableLane) {
|
|
1378
|
+
this.assignLayoutToHorizontalTooltipLane(layout, reusableLane);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
const nextLane = {
|
|
1382
|
+
top: this.resolveNewHorizontalTooltipLaneTop(preferredTop, layout.height, minTop, maxTop, lanes),
|
|
1383
|
+
layouts: [],
|
|
1384
|
+
};
|
|
1385
|
+
lanes.push(nextLane);
|
|
1386
|
+
this.assignLayoutToHorizontalTooltipLane(layout, nextLane);
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
resolveHorizontalSideSplitTooltipCollisions(layouts) {
|
|
1390
|
+
const placedLayouts = [];
|
|
1391
|
+
const orderedLayouts = [...layouts].sort((a, b) => a.targetX - b.targetX || a.left - b.left);
|
|
1392
|
+
orderedLayouts.forEach((layout) => {
|
|
1393
|
+
this.flipHorizontalSideTooltipIfItReducesCollisions(layout, placedLayouts);
|
|
1394
|
+
placedLayouts.push(layout);
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
flipHorizontalSideTooltipIfItReducesCollisions(layout, placedLayouts) {
|
|
1398
|
+
const currentCollisions = this.countSplitTooltipCollisions(layout, placedLayouts);
|
|
1399
|
+
if (currentCollisions === 0) {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
const flippedArrowEdge = this.getOppositeSideArrowEdge(layout.arrowEdge);
|
|
1403
|
+
if (!flippedArrowEdge) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const flippedPosition = this.getAnchoredTooltipPosition(layout.anchor, { x: layout.targetX, y: layout.targetY }, layout.width, layout.height, flippedArrowEdge);
|
|
1407
|
+
if (!flippedPosition) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const flippedLayout = {
|
|
1411
|
+
...layout,
|
|
1412
|
+
arrowEdge: flippedArrowEdge,
|
|
1413
|
+
left: flippedPosition.left,
|
|
1414
|
+
top: flippedPosition.top,
|
|
1415
|
+
};
|
|
1416
|
+
const flippedCollisions = this.countSplitTooltipCollisions(flippedLayout, placedLayouts);
|
|
1417
|
+
if (flippedCollisions > currentCollisions) {
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (flippedCollisions === currentCollisions &&
|
|
1421
|
+
this.countPlacedLayoutsOnEdge(placedLayouts, flippedArrowEdge) >=
|
|
1422
|
+
this.countPlacedLayoutsOnEdge(placedLayouts, layout.arrowEdge)) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
layout.arrowEdge = flippedArrowEdge;
|
|
1426
|
+
layout.left = flippedPosition.left;
|
|
1427
|
+
layout.top = flippedPosition.top;
|
|
1428
|
+
}
|
|
1429
|
+
findReusableHorizontalTooltipLane(lanes, layout, preferredTop) {
|
|
1430
|
+
const reusableLanes = lanes
|
|
1431
|
+
.filter((lane) => lane.layouts.every((placedLayout) => !this.doSplitTooltipLayoutsOverlapHorizontally(layout, placedLayout)))
|
|
1432
|
+
.sort((a, b) => Math.abs(a.top - preferredTop) -
|
|
1433
|
+
Math.abs(b.top - preferredTop));
|
|
1434
|
+
return reusableLanes[0] ?? null;
|
|
1435
|
+
}
|
|
1436
|
+
resolveNewHorizontalTooltipLaneTop(preferredTop, tooltipHeight, minTop, maxTop, lanes) {
|
|
1437
|
+
const usedTops = new Set(lanes.map((lane) => Math.round(lane.top)));
|
|
1438
|
+
const candidates = this.getHorizontalTooltipLaneTopCandidates(preferredTop, tooltipHeight, minTop, maxTop);
|
|
1439
|
+
return (candidates.find((candidate) => !usedTops.has(Math.round(candidate)) &&
|
|
1440
|
+
lanes.every((lane) => !this.doHorizontalTooltipLanesOverlap(candidate, tooltipHeight, lane))) ??
|
|
1441
|
+
candidates[0] ??
|
|
1442
|
+
preferredTop);
|
|
1443
|
+
}
|
|
1444
|
+
getHorizontalTooltipLaneTopCandidates(preferredTop, tooltipHeight, minTop, maxTop) {
|
|
1445
|
+
const step = tooltipHeight + SPLIT_TOOLTIP_GAP_PX;
|
|
1446
|
+
const candidates = [preferredTop];
|
|
1447
|
+
for (let index = 1; index <= 8; index++) {
|
|
1448
|
+
candidates.push(preferredTop - step * index);
|
|
1449
|
+
candidates.push(preferredTop + step * index);
|
|
1450
|
+
}
|
|
1451
|
+
return Array.from(new Set(candidates.map((candidate) => Math.round(Math.max(minTop, Math.min(candidate, maxTop)))))).sort((a, b) => Math.abs(a - preferredTop) - Math.abs(b - preferredTop));
|
|
1452
|
+
}
|
|
1453
|
+
assignLayoutToHorizontalTooltipLane(layout, lane) {
|
|
1454
|
+
layout.top = lane.top;
|
|
1455
|
+
lane.layouts.push(layout);
|
|
1456
|
+
}
|
|
1457
|
+
doHorizontalTooltipLanesOverlap(top, height, lane) {
|
|
1458
|
+
const laneHeight = lane.layouts.reduce((maxHeight, layout) => Math.max(maxHeight, layout.height), 0);
|
|
1459
|
+
return (top < lane.top + laneHeight + SPLIT_TOOLTIP_GAP_PX &&
|
|
1460
|
+
top + height + SPLIT_TOOLTIP_GAP_PX > lane.top);
|
|
1461
|
+
}
|
|
1462
|
+
doSplitTooltipLayoutsOverlapHorizontally(a, b) {
|
|
1463
|
+
return (a.left < b.left + b.width + SPLIT_TOOLTIP_GAP_PX &&
|
|
1464
|
+
a.left + a.width + SPLIT_TOOLTIP_GAP_PX > b.left);
|
|
1465
|
+
}
|
|
1466
|
+
resolveVerticalSplitTooltipPositions(layouts) {
|
|
1467
|
+
const minLeft = window.scrollX + TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1468
|
+
const maxRight = window.scrollX + window.innerWidth - TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1469
|
+
const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1470
|
+
const maxBottom = window.scrollY + window.innerHeight - TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1471
|
+
const tooltipsByEdge = {
|
|
1472
|
+
left: [],
|
|
1473
|
+
right: [],
|
|
1474
|
+
top: [],
|
|
1475
|
+
bottom: [],
|
|
1476
|
+
};
|
|
1477
|
+
layouts.forEach((layout) => {
|
|
1478
|
+
tooltipsByEdge[layout.arrowEdge].push(layout);
|
|
1479
|
+
});
|
|
1480
|
+
[tooltipsByEdge.top, tooltipsByEdge.bottom].forEach((edgeLayouts) => {
|
|
1481
|
+
if (edgeLayouts.length === 0) {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
edgeLayouts.sort((a, b) => a.left - b.left);
|
|
1485
|
+
edgeLayouts[0].left = Math.max(minLeft, edgeLayouts[0].left);
|
|
1486
|
+
for (let i = 1; i < edgeLayouts.length; i++) {
|
|
1487
|
+
const previousLayout = edgeLayouts[i - 1];
|
|
1488
|
+
const currentLayout = edgeLayouts[i];
|
|
1489
|
+
const minAllowedLeft = previousLayout.left +
|
|
1490
|
+
previousLayout.width +
|
|
1491
|
+
SPLIT_TOOLTIP_GAP_PX;
|
|
1492
|
+
currentLayout.left = Math.max(currentLayout.left, minAllowedLeft);
|
|
1493
|
+
}
|
|
1494
|
+
const lastLayout = edgeLayouts[edgeLayouts.length - 1];
|
|
1495
|
+
const overflow = lastLayout.left + lastLayout.width - maxRight;
|
|
1496
|
+
if (overflow > 0) {
|
|
1497
|
+
lastLayout.left -= overflow;
|
|
1498
|
+
for (let i = edgeLayouts.length - 2; i >= 0; i--) {
|
|
1499
|
+
const currentLayout = edgeLayouts[i];
|
|
1500
|
+
const nextLayout = edgeLayouts[i + 1];
|
|
1501
|
+
const maxAllowedLeft = nextLayout.left -
|
|
1502
|
+
currentLayout.width -
|
|
1503
|
+
SPLIT_TOOLTIP_GAP_PX;
|
|
1504
|
+
currentLayout.left = Math.min(currentLayout.left, maxAllowedLeft);
|
|
1505
|
+
}
|
|
1506
|
+
const underflow = minLeft - edgeLayouts[0].left;
|
|
1507
|
+
if (underflow > 0) {
|
|
1508
|
+
edgeLayouts.forEach((layout) => {
|
|
1509
|
+
layout.left += underflow;
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
edgeLayouts.forEach((layout) => {
|
|
1514
|
+
const maxLeft = maxRight - layout.width;
|
|
1515
|
+
const maxTop = maxBottom - layout.height;
|
|
1516
|
+
layout.left = Math.max(minLeft, Math.min(layout.left, maxLeft));
|
|
1517
|
+
layout.top = Math.max(minTop, Math.min(layout.top, maxTop));
|
|
1518
|
+
});
|
|
1519
|
+
});
|
|
1520
|
+
this.resolveSideSplitTooltipPositions([
|
|
1521
|
+
...tooltipsByEdge.left,
|
|
1522
|
+
...tooltipsByEdge.right,
|
|
1523
|
+
]);
|
|
1524
|
+
}
|
|
1308
1525
|
}
|
|
1309
1526
|
Object.defineProperty(Tooltip, "nextTooltipId", {
|
|
1310
1527
|
enumerable: true,
|
package/dist/types.d.ts
CHANGED
|
@@ -184,16 +184,25 @@ export type ValueLabelConfig = {
|
|
|
184
184
|
export type LineValueLabelConfig = ValueLabelConfig & {
|
|
185
185
|
show?: boolean;
|
|
186
186
|
};
|
|
187
|
+
export type LinePointVisibility = 'always' | 'hover' | 'never';
|
|
188
|
+
export type LinePointsConfig = {
|
|
189
|
+
show?: LinePointVisibility;
|
|
190
|
+
};
|
|
187
191
|
export type BarValueLabelConfig = ValueLabelConfig & {
|
|
188
192
|
show?: boolean;
|
|
189
193
|
position?: 'inside' | 'outside';
|
|
190
194
|
insidePosition?: 'top' | 'middle' | 'bottom';
|
|
191
195
|
};
|
|
192
196
|
export type BarSide = 'left' | 'right';
|
|
197
|
+
export type CurveType = 'linear' | 'monotone' | 'step' | 'natural' | 'basis' | 'cardinal';
|
|
198
|
+
export type LineCurveType = CurveType;
|
|
199
|
+
export type AreaCurveType = CurveType;
|
|
193
200
|
export type LineConfigBase = {
|
|
194
201
|
dataKey: string;
|
|
195
202
|
stroke?: string;
|
|
196
203
|
strokeWidth?: number;
|
|
204
|
+
curve?: LineCurveType;
|
|
205
|
+
points?: LinePointsConfig;
|
|
197
206
|
valueLabel?: LineValueLabelConfig;
|
|
198
207
|
};
|
|
199
208
|
export type LineConfig = LineConfigBase & {
|
|
@@ -219,7 +228,6 @@ export type BarConfigBase = {
|
|
|
219
228
|
export type BarConfig = BarConfigBase & {
|
|
220
229
|
exportHooks?: ExportHooks<BarConfigBase>;
|
|
221
230
|
};
|
|
222
|
-
export type AreaCurveType = 'linear' | 'monotone' | 'step' | 'natural' | 'basis' | 'cardinal';
|
|
223
231
|
export type AreaConfigBase = {
|
|
224
232
|
dataKey: string;
|
|
225
233
|
fill?: string;
|
|
@@ -245,6 +253,7 @@ export type BarStackConfig = {
|
|
|
245
253
|
export type AreaStackMode = 'none' | 'normal' | 'percent';
|
|
246
254
|
export type AreaStackConfig = {
|
|
247
255
|
mode?: AreaStackMode;
|
|
256
|
+
reverseSeries?: boolean;
|
|
248
257
|
};
|
|
249
258
|
export declare function getSeriesColor(series: {
|
|
250
259
|
stroke?: string;
|
|
@@ -300,6 +309,7 @@ export type TooltipConfigBase = {
|
|
|
300
309
|
mode?: TooltipMode;
|
|
301
310
|
position?: TooltipPosition;
|
|
302
311
|
barAnchorPosition?: TooltipBarAnchorPosition;
|
|
312
|
+
maxWidth?: number;
|
|
303
313
|
transition?: TooltipTransitionConfig;
|
|
304
314
|
formatter?: SeriesValueFormatter;
|
|
305
315
|
labelFormatter?: (label: string, data: DataItem) => string;
|
package/dist/xy-chart.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare class XYChart extends BaseChart {
|
|
|
15
15
|
private barStackGap;
|
|
16
16
|
private barStackReverseSeries;
|
|
17
17
|
private areaStackMode;
|
|
18
|
+
private areaStackReverseSeries;
|
|
18
19
|
private readonly orientation;
|
|
19
20
|
private readonly motionDriver;
|
|
20
21
|
private scaleConfigOverride;
|
|
@@ -39,6 +40,8 @@ export declare class XYChart extends BaseChart {
|
|
|
39
40
|
private resolveValueAxisDomain;
|
|
40
41
|
private getVisibleSeries;
|
|
41
42
|
private getDisplaySeries;
|
|
43
|
+
private getBarDisplaySeries;
|
|
44
|
+
private getAreaDisplaySeries;
|
|
42
45
|
private resolveSeriesDefaults;
|
|
43
46
|
private shouldReplaceSeriesColor;
|
|
44
47
|
private cloneSeriesWithOverride;
|
package/dist/xy-chart.js
CHANGED
|
@@ -14,8 +14,11 @@ function resolveBarStackSettings(config) {
|
|
|
14
14
|
reverseSeries: config.barStack?.reverseSeries ?? false,
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
function
|
|
18
|
-
return
|
|
17
|
+
function resolveAreaStackSettings(config) {
|
|
18
|
+
return {
|
|
19
|
+
mode: config.areaStack?.mode ?? 'none',
|
|
20
|
+
reverseSeries: config.areaStack?.reverseSeries ?? false,
|
|
21
|
+
};
|
|
19
22
|
}
|
|
20
23
|
function isXYSeries(component) {
|
|
21
24
|
return (component.type === 'line' ||
|
|
@@ -56,6 +59,12 @@ export class XYChart extends BaseChart {
|
|
|
56
59
|
writable: true,
|
|
57
60
|
value: void 0
|
|
58
61
|
});
|
|
62
|
+
Object.defineProperty(this, "areaStackReverseSeries", {
|
|
63
|
+
enumerable: true,
|
|
64
|
+
configurable: true,
|
|
65
|
+
writable: true,
|
|
66
|
+
value: void 0
|
|
67
|
+
});
|
|
59
68
|
Object.defineProperty(this, "orientation", {
|
|
60
69
|
enumerable: true,
|
|
61
70
|
configurable: true,
|
|
@@ -75,11 +84,13 @@ export class XYChart extends BaseChart {
|
|
|
75
84
|
value: null
|
|
76
85
|
});
|
|
77
86
|
const barStack = resolveBarStackSettings(config);
|
|
87
|
+
const areaStack = resolveAreaStackSettings(config);
|
|
78
88
|
this.orientation = config.orientation ?? 'vertical';
|
|
79
89
|
this.barStackMode = barStack.mode;
|
|
80
90
|
this.barStackGap = barStack.gap;
|
|
81
91
|
this.barStackReverseSeries = barStack.reverseSeries;
|
|
82
|
-
this.areaStackMode =
|
|
92
|
+
this.areaStackMode = areaStack.mode;
|
|
93
|
+
this.areaStackReverseSeries = areaStack.reverseSeries;
|
|
83
94
|
this.motionDriver = createXYMotionDriver(config.animate);
|
|
84
95
|
}
|
|
85
96
|
addChild(component) {
|
|
@@ -120,6 +131,7 @@ export class XYChart extends BaseChart {
|
|
|
120
131
|
},
|
|
121
132
|
areaStack: {
|
|
122
133
|
mode: this.areaStackMode,
|
|
134
|
+
reverseSeries: this.areaStackReverseSeries,
|
|
123
135
|
},
|
|
124
136
|
animate: false,
|
|
125
137
|
});
|
|
@@ -289,18 +301,22 @@ export class XYChart extends BaseChart {
|
|
|
289
301
|
});
|
|
290
302
|
}
|
|
291
303
|
getDisplaySeries() {
|
|
304
|
+
const barOrderedSeries = this.getBarDisplaySeries(this.series);
|
|
305
|
+
return this.getAreaDisplaySeries(barOrderedSeries);
|
|
306
|
+
}
|
|
307
|
+
getBarDisplaySeries(series) {
|
|
292
308
|
if (!this.barStackReverseSeries) {
|
|
293
|
-
return
|
|
309
|
+
return series;
|
|
294
310
|
}
|
|
295
|
-
const barSeries =
|
|
311
|
+
const barSeries = series.filter((entry) => {
|
|
296
312
|
return entry.type === 'bar';
|
|
297
313
|
});
|
|
298
314
|
if (barSeries.length < 2) {
|
|
299
|
-
return
|
|
315
|
+
return series;
|
|
300
316
|
}
|
|
301
317
|
const reversedBars = [...barSeries].reverse();
|
|
302
318
|
let reversedBarIndex = 0;
|
|
303
|
-
return
|
|
319
|
+
return series.map((entry) => {
|
|
304
320
|
if (entry.type !== 'bar') {
|
|
305
321
|
return entry;
|
|
306
322
|
}
|
|
@@ -309,6 +325,31 @@ export class XYChart extends BaseChart {
|
|
|
309
325
|
return nextBar;
|
|
310
326
|
});
|
|
311
327
|
}
|
|
328
|
+
getAreaDisplaySeries(series) {
|
|
329
|
+
if (!this.areaStackReverseSeries || this.areaStackMode === 'none') {
|
|
330
|
+
return series;
|
|
331
|
+
}
|
|
332
|
+
const areaSeries = series.filter((entry) => {
|
|
333
|
+
return entry.type === 'area';
|
|
334
|
+
});
|
|
335
|
+
const stackGroups = this.getStackedAreaGroups(areaSeries);
|
|
336
|
+
if (stackGroups.size === 0) {
|
|
337
|
+
return series;
|
|
338
|
+
}
|
|
339
|
+
const reversedSeriesByOriginal = new Map();
|
|
340
|
+
stackGroups.forEach((stackSeries) => {
|
|
341
|
+
const reversedStackSeries = [...stackSeries].reverse();
|
|
342
|
+
stackSeries.forEach((entry, index) => {
|
|
343
|
+
reversedSeriesByOriginal.set(entry, reversedStackSeries[index]);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
return series.map((entry) => {
|
|
347
|
+
if (entry.type !== 'area') {
|
|
348
|
+
return entry;
|
|
349
|
+
}
|
|
350
|
+
return reversedSeriesByOriginal.get(entry) ?? entry;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
312
353
|
resolveSeriesDefaults(series) {
|
|
313
354
|
const colorIndex = this.series.length % this.theme.colorPalette.length;
|
|
314
355
|
const paletteColor = this.theme.colorPalette[colorIndex];
|
package/docs/components.md
CHANGED
|
@@ -122,6 +122,7 @@ new Tooltip({
|
|
|
122
122
|
mode?: 'shared' | 'split',
|
|
123
123
|
position?: 'side' | 'vertical',
|
|
124
124
|
barAnchorPosition?: 'top' | 'middle',
|
|
125
|
+
maxWidth?: number, // default: 280
|
|
125
126
|
transition?: {
|
|
126
127
|
show?: boolean,
|
|
127
128
|
duration?: number,
|
|
@@ -155,6 +156,9 @@ possible.
|
|
|
155
156
|
For bars, `barAnchorPosition` controls whether split tooltips point to the `top`
|
|
156
157
|
or `middle` of each bar.
|
|
157
158
|
|
|
159
|
+
Tooltips default to a `280px` max width so longer content wraps. Set `maxWidth`
|
|
160
|
+
to choose a different cap in pixels.
|
|
161
|
+
|
|
158
162
|
Set `transition.show: true` to fade tooltips in and out. Tooltip position and
|
|
159
163
|
connector geometry update immediately; only opacity and the small entrance
|
|
160
164
|
offset transition.
|
package/docs/xy-chart.md
CHANGED
|
@@ -20,7 +20,7 @@ new XYChart(config: XYChartConfig)
|
|
|
20
20
|
| `orientation` | `'vertical' \| 'horizontal'` | `'vertical'` | Chart orientation. Horizontal mode currently supports bar-only charts |
|
|
21
21
|
| `responsive` | `ResponsiveConfig` | - | Container-query responsive overrides (theme + components) |
|
|
22
22
|
| `barStack` | `BarStackConfig` | `{ mode: 'normal', gap: 0.1, reverseSeries: false }` | Bar stacking configuration |
|
|
23
|
-
| `areaStack` | `AreaStackConfig` | `{ mode: 'none' }`
|
|
23
|
+
| `areaStack` | `AreaStackConfig` | `{ mode: 'none', reverseSeries: false }` | Area stacking configuration |
|
|
24
24
|
| `animate` | `boolean \| XYAnimationConfig` | `false` | Opt-in XY series animation for initial render and `update()` |
|
|
25
25
|
|
|
26
26
|
### Theme Options
|
|
@@ -338,6 +338,10 @@ new Line({
|
|
|
338
338
|
dataKey: string, // Key in data objects for Y values (required)
|
|
339
339
|
stroke?: string, // Line color (auto-assigned if omitted)
|
|
340
340
|
strokeWidth?: number, // Line width in pixels (default: 2)
|
|
341
|
+
curve?: 'linear' | 'monotone' | 'step' | 'natural' | 'basis' | 'cardinal',
|
|
342
|
+
points?: {
|
|
343
|
+
show?: 'always' | 'hover' | 'never'
|
|
344
|
+
}, // Data point visibility (default: 'always')
|
|
341
345
|
valueLabel?: {
|
|
342
346
|
show?: boolean,
|
|
343
347
|
formatter?: (dataKey, value, data) => string
|
|
@@ -355,8 +359,20 @@ chart.addChild(new Line({ dataKey: 'expenses' }));
|
|
|
355
359
|
// Manual colors
|
|
356
360
|
chart.addChild(new Line({ dataKey: 'revenue', stroke: '#00ff00' }));
|
|
357
361
|
chart.addChild(new Line({ dataKey: 'expenses', stroke: '#ff0000' }));
|
|
362
|
+
|
|
363
|
+
// Smooth line interpolation
|
|
364
|
+
chart.addChild(new Line({ dataKey: 'revenue', curve: 'monotone' }));
|
|
365
|
+
|
|
366
|
+
// Show point circles only while hovering or focusing a tooltip target
|
|
367
|
+
chart
|
|
368
|
+
.addChild(new Line({ dataKey: 'revenue', points: { show: 'hover' } }))
|
|
369
|
+
.addChild(new Tooltip());
|
|
358
370
|
```
|
|
359
371
|
|
|
372
|
+
`points.show: 'hover'` uses `Tooltip` focus markers, so add a `Tooltip`
|
|
373
|
+
component when hover-only points should be visible. Use `'never'` to suppress
|
|
374
|
+
both static line points and tooltip focus markers for that line.
|
|
375
|
+
|
|
360
376
|
---
|
|
361
377
|
|
|
362
378
|
## Scatter
|
|
@@ -499,6 +515,8 @@ series at the hovered category. Use
|
|
|
499
515
|
to override the grouping and split-tooltip placement.
|
|
500
516
|
For bars, set `barAnchorPosition: 'top' | 'middle'` to choose whether split
|
|
501
517
|
tooltips point to the top or middle of each bar.
|
|
518
|
+
Tooltips default to a `280px` max width. Set `maxWidth` to choose a different
|
|
519
|
+
cap in pixels.
|
|
502
520
|
Use `transition: { show: true, duration: 120, easing: 'ease-out' }` to opt in
|
|
503
521
|
to softer tooltip opacity transitions without delaying position updates.
|
|
504
522
|
|
|
@@ -560,12 +578,16 @@ Area charts support stacking when series share the same `stackId`:
|
|
|
560
578
|
```javascript
|
|
561
579
|
const chart = new XYChart({
|
|
562
580
|
data,
|
|
563
|
-
areaStack: { mode: 'percent' },
|
|
581
|
+
areaStack: { mode: 'percent', reverseSeries: true },
|
|
564
582
|
});
|
|
565
583
|
|
|
566
584
|
chart.addChild(new Area({ dataKey: 'desktop', stackId: 'traffic' })).addChild(new Area({ dataKey: 'mobile', stackId: 'traffic' }));
|
|
567
585
|
```
|
|
568
586
|
|
|
587
|
+
Use `areaStack.reverseSeries: true` to reverse stacked area series display order
|
|
588
|
+
for rendering, legend entries, and split tooltip ordering without changing data
|
|
589
|
+
exports. The reversal applies within each shared `stackId` group.
|
|
590
|
+
|
|
569
591
|
---
|
|
570
592
|
|
|
571
593
|
## Mixed Charts
|
package/package.json
CHANGED