@internetstiftelsen/charts 0.13.1 → 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/tooltip.d.ts +15 -0
- package/dist/tooltip.js +229 -16
- package/dist/types.d.ts +2 -0
- 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 +8 -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/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,
|
|
@@ -454,11 +466,13 @@ export class Tooltip {
|
|
|
454
466
|
if (layouts.length === 0) {
|
|
455
467
|
return;
|
|
456
468
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
469
|
+
if (!isHorizontal) {
|
|
470
|
+
this.resolveSplitTooltipCollisions(layouts, 'vertical', this.getOppositeVerticalArrowEdge);
|
|
471
|
+
this.resolveSplitTooltipCollisions(layouts, 'side', this.getOppositeSideArrowEdge);
|
|
472
|
+
}
|
|
473
|
+
this.resolveSplitTooltipPositions(layouts, isHorizontal);
|
|
460
474
|
layouts.forEach((layout) => {
|
|
461
|
-
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);
|
|
462
476
|
});
|
|
463
477
|
};
|
|
464
478
|
const hideTooltip = () => {
|
|
@@ -706,6 +720,7 @@ export class Tooltip {
|
|
|
706
720
|
theme.tooltip.fontFamily,
|
|
707
721
|
theme.tooltip.fontSize,
|
|
708
722
|
theme.tooltip.fontWeight,
|
|
723
|
+
this.maxWidth,
|
|
709
724
|
this.transition.show,
|
|
710
725
|
this.transition.duration,
|
|
711
726
|
this.transition.easing,
|
|
@@ -724,10 +739,13 @@ export class Tooltip {
|
|
|
724
739
|
.style('font-family', theme.tooltip.fontFamily)
|
|
725
740
|
.style('font-size', `${theme.tooltip.fontSize}px`)
|
|
726
741
|
.style('font-weight', theme.tooltip.fontWeight)
|
|
742
|
+
.style('box-sizing', 'border-box')
|
|
743
|
+
.style('overflow-wrap', 'break-word')
|
|
727
744
|
.style('overflow', 'visible')
|
|
728
745
|
.style('isolation', 'isolate')
|
|
729
746
|
.style('pointer-events', 'none')
|
|
730
747
|
.style('z-index', '1000');
|
|
748
|
+
tooltip.style('max-width', `${this.maxWidth}px`);
|
|
731
749
|
if (this.transition.show) {
|
|
732
750
|
tooltip
|
|
733
751
|
.style('transition', `opacity ${this.transition.duration}ms ${this.transition.easing}, transform ${this.transition.duration}ms ${this.transition.easing}`)
|
|
@@ -758,7 +776,7 @@ export class Tooltip {
|
|
|
758
776
|
height: tooltipRect.height,
|
|
759
777
|
};
|
|
760
778
|
}
|
|
761
|
-
renderTooltipWithConnector(tooltip, arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
779
|
+
renderTooltipWithConnector(tooltip, arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
|
|
762
780
|
if (!Number.isFinite(left) ||
|
|
763
781
|
!Number.isFinite(top) ||
|
|
764
782
|
!Number.isFinite(targetX) ||
|
|
@@ -766,7 +784,7 @@ export class Tooltip {
|
|
|
766
784
|
this.hideTooltipSelection(tooltip);
|
|
767
785
|
return;
|
|
768
786
|
}
|
|
769
|
-
const connectorLayout = this.resolveTooltipConnectorLayout(arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY);
|
|
787
|
+
const connectorLayout = this.resolveTooltipConnectorLayout(arrowEdge, left, top, tooltipWidth, tooltipHeight, targetX, targetY, anchor);
|
|
770
788
|
if (!connectorLayout) {
|
|
771
789
|
this.hideTooltipSelection(tooltip);
|
|
772
790
|
return;
|
|
@@ -1100,7 +1118,7 @@ export class Tooltip {
|
|
|
1100
1118
|
top: Math.max(minTop, Math.min(top, maxTop)),
|
|
1101
1119
|
};
|
|
1102
1120
|
}
|
|
1103
|
-
resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY) {
|
|
1121
|
+
resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
|
|
1104
1122
|
const localTargetX = targetX - tooltipLeft;
|
|
1105
1123
|
const localTargetY = targetY - tooltipTop;
|
|
1106
1124
|
if (!Number.isFinite(localTargetX) || !Number.isFinite(localTargetY)) {
|
|
@@ -1120,7 +1138,9 @@ export class Tooltip {
|
|
|
1120
1138
|
const startY = arrowTip.y - minY;
|
|
1121
1139
|
const endX = localTargetX - minX;
|
|
1122
1140
|
const endY = localTargetY - minY;
|
|
1123
|
-
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);
|
|
1124
1144
|
if (!this.hasFiniteNumbers(width, height, boxX, boxY, startX, startY, endX, endY)) {
|
|
1125
1145
|
return null;
|
|
1126
1146
|
}
|
|
@@ -1163,21 +1183,31 @@ export class Tooltip {
|
|
|
1163
1183
|
};
|
|
1164
1184
|
}
|
|
1165
1185
|
}
|
|
1166
|
-
resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY) {
|
|
1186
|
+
resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor) {
|
|
1167
1187
|
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
1168
|
-
if (
|
|
1169
|
-
TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX) {
|
|
1188
|
+
if (this.isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor)) {
|
|
1170
1189
|
return '';
|
|
1171
1190
|
}
|
|
1172
1191
|
const elbowX = startX + (endX - startX) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
1173
1192
|
return `M ${startX},${startY} L ${elbowX},${startY} L ${endX},${endY}`;
|
|
1174
1193
|
}
|
|
1175
|
-
if (
|
|
1194
|
+
if (this.isTooltipArrowTipInsideHorizontalAnchorSpan(arrowTipX, anchor)) {
|
|
1176
1195
|
return '';
|
|
1177
1196
|
}
|
|
1178
1197
|
const elbowY = startY + (endY - startY) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
|
|
1179
1198
|
return `M ${startX},${startY} L ${startX},${elbowY} L ${endX},${endY}`;
|
|
1180
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
|
+
}
|
|
1181
1211
|
resolveTooltipArrowTip(arrowEdge, boxX, boxY, length) {
|
|
1182
1212
|
if (arrowEdge === 'left' || arrowEdge === 'right') {
|
|
1183
1213
|
return {
|
|
@@ -1198,7 +1228,12 @@ export class Tooltip {
|
|
|
1198
1228
|
return;
|
|
1199
1229
|
}
|
|
1200
1230
|
const placedLayouts = [];
|
|
1201
|
-
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
|
+
});
|
|
1202
1237
|
orderedLayouts.forEach((layout) => {
|
|
1203
1238
|
this.flipTooltipIfItReducesCollisions(layout, placedLayouts, getOppositeArrowEdge);
|
|
1204
1239
|
placedLayouts.push(layout);
|
|
@@ -1242,7 +1277,12 @@ export class Tooltip {
|
|
|
1242
1277
|
top: flippedPosition.top,
|
|
1243
1278
|
};
|
|
1244
1279
|
const flippedCollisions = this.countSplitTooltipCollisions(flippedLayout, placedLayouts);
|
|
1245
|
-
if (flippedCollisions
|
|
1280
|
+
if (flippedCollisions > currentCollisions) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (flippedCollisions === currentCollisions &&
|
|
1284
|
+
this.countPlacedLayoutsOnEdge(placedLayouts, flippedArrowEdge) >=
|
|
1285
|
+
this.countPlacedLayoutsOnEdge(placedLayouts, layout.arrowEdge)) {
|
|
1246
1286
|
return;
|
|
1247
1287
|
}
|
|
1248
1288
|
layout.arrowEdge = flippedArrowEdge;
|
|
@@ -1252,13 +1292,28 @@ export class Tooltip {
|
|
|
1252
1292
|
countSplitTooltipCollisions(layout, placedLayouts) {
|
|
1253
1293
|
return placedLayouts.filter((placedLayout) => this.doSplitTooltipLayoutsOverlap(layout, placedLayout)).length;
|
|
1254
1294
|
}
|
|
1295
|
+
countPlacedLayoutsOnEdge(placedLayouts, arrowEdge) {
|
|
1296
|
+
return placedLayouts.filter((layout) => layout.arrowEdge === arrowEdge)
|
|
1297
|
+
.length;
|
|
1298
|
+
}
|
|
1255
1299
|
doSplitTooltipLayoutsOverlap(a, b) {
|
|
1256
1300
|
return (a.left < b.left + b.width + SPLIT_TOOLTIP_GAP_PX &&
|
|
1257
1301
|
a.left + a.width + SPLIT_TOOLTIP_GAP_PX > b.left &&
|
|
1258
1302
|
a.top < b.top + b.height + SPLIT_TOOLTIP_GAP_PX &&
|
|
1259
1303
|
a.top + a.height + SPLIT_TOOLTIP_GAP_PX > b.top);
|
|
1260
1304
|
}
|
|
1261
|
-
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) {
|
|
1262
1317
|
const minTop = window.scrollY + TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1263
1318
|
const maxBottom = window.scrollY + window.innerHeight - TOOLTIP_VIEWPORT_PADDING_PX;
|
|
1264
1319
|
const tooltipsByEdge = {
|
|
@@ -1309,6 +1364,164 @@ export class Tooltip {
|
|
|
1309
1364
|
});
|
|
1310
1365
|
});
|
|
1311
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
|
+
}
|
|
1312
1525
|
}
|
|
1313
1526
|
Object.defineProperty(Tooltip, "nextTooltipId", {
|
|
1314
1527
|
enumerable: true,
|
package/dist/types.d.ts
CHANGED
|
@@ -253,6 +253,7 @@ export type BarStackConfig = {
|
|
|
253
253
|
export type AreaStackMode = 'none' | 'normal' | 'percent';
|
|
254
254
|
export type AreaStackConfig = {
|
|
255
255
|
mode?: AreaStackMode;
|
|
256
|
+
reverseSeries?: boolean;
|
|
256
257
|
};
|
|
257
258
|
export declare function getSeriesColor(series: {
|
|
258
259
|
stroke?: string;
|
|
@@ -308,6 +309,7 @@ export type TooltipConfigBase = {
|
|
|
308
309
|
mode?: TooltipMode;
|
|
309
310
|
position?: TooltipPosition;
|
|
310
311
|
barAnchorPosition?: TooltipBarAnchorPosition;
|
|
312
|
+
maxWidth?: number;
|
|
311
313
|
transition?: TooltipTransitionConfig;
|
|
312
314
|
formatter?: SeriesValueFormatter;
|
|
313
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
|
|
@@ -515,6 +515,8 @@ series at the hovered category. Use
|
|
|
515
515
|
to override the grouping and split-tooltip placement.
|
|
516
516
|
For bars, set `barAnchorPosition: 'top' | 'middle'` to choose whether split
|
|
517
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.
|
|
518
520
|
Use `transition: { show: true, duration: 120, easing: 'ease-out' }` to opt in
|
|
519
521
|
to softer tooltip opacity transitions without delaying position updates.
|
|
520
522
|
|
|
@@ -576,12 +578,16 @@ Area charts support stacking when series share the same `stackId`:
|
|
|
576
578
|
```javascript
|
|
577
579
|
const chart = new XYChart({
|
|
578
580
|
data,
|
|
579
|
-
areaStack: { mode: 'percent' },
|
|
581
|
+
areaStack: { mode: 'percent', reverseSeries: true },
|
|
580
582
|
});
|
|
581
583
|
|
|
582
584
|
chart.addChild(new Area({ dataKey: 'desktop', stackId: 'traffic' })).addChild(new Area({ dataKey: 'mobile', stackId: 'traffic' }));
|
|
583
585
|
```
|
|
584
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
|
+
|
|
585
591
|
---
|
|
586
592
|
|
|
587
593
|
## Mixed Charts
|
package/package.json
CHANGED