@internetstiftelsen/charts 0.18.2 → 0.19.0
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 +3 -1
- package/dist/legend.d.ts +4 -1
- package/dist/legend.js +38 -3
- package/dist/tooltip/xy-interaction.js +24 -0
- package/dist/types.d.ts +4 -1
- package/docs/components.md +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -255,7 +255,9 @@ axis, and responsive behavior. `ChartGroup.height` behaves like chart height:
|
|
|
255
255
|
set it for a fixed total group height, or omit it to size from the render
|
|
256
256
|
container. Set `syncY: true` when you want vertical `XYChart` children to share
|
|
257
257
|
the same Y domain so grid lines stay aligned while only one child renders a
|
|
258
|
-
visible Y axis.
|
|
258
|
+
visible Y axis. Legend rows wrap automatically, with `align` positioning the
|
|
259
|
+
legend block and `textAlign` aligning rows inside it:
|
|
260
|
+
`new Legend({ align: 'center', textAlign: 'left' })`.
|
|
259
261
|
|
|
260
262
|
`ChartGroup` also supports declarative responsive layout overrides. Group
|
|
261
263
|
breakpoints can change `cols` and `gap`, while `addChart(..., options)` can
|
package/dist/legend.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { LegendConfig, ChartTheme, LegendSeries, ExportHooks, LegendConfigBase, LegendMode } from './types.js';
|
|
2
|
+
import type { LegendConfig, ChartTheme, HorizontalAlignment, LegendSeries, ExportHooks, LegendConfigBase, LegendMode } from './types.js';
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
4
|
import { LegendStateController } from './legend-state.js';
|
|
5
5
|
export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
@@ -7,6 +7,8 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
7
7
|
mode: LegendMode;
|
|
8
8
|
readonly position: LegendConfig['position'];
|
|
9
9
|
readonly disconnectedTarget?: string | HTMLElement;
|
|
10
|
+
readonly align: HorizontalAlignment;
|
|
11
|
+
readonly textAlign: HorizontalAlignment;
|
|
10
12
|
readonly exportHooks?: ExportHooks<LegendConfigBase>;
|
|
11
13
|
private readonly marginTop;
|
|
12
14
|
private readonly marginBottom;
|
|
@@ -53,5 +55,6 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
|
53
55
|
private positionRows;
|
|
54
56
|
private getLayoutSignature;
|
|
55
57
|
private getFallbackRowHeight;
|
|
58
|
+
private getAlignmentOffset;
|
|
56
59
|
private bindStateController;
|
|
57
60
|
}
|
package/dist/legend.js
CHANGED
|
@@ -28,6 +28,18 @@ export class Legend {
|
|
|
28
28
|
writable: true,
|
|
29
29
|
value: void 0
|
|
30
30
|
});
|
|
31
|
+
Object.defineProperty(this, "align", {
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true,
|
|
34
|
+
writable: true,
|
|
35
|
+
value: void 0
|
|
36
|
+
});
|
|
37
|
+
Object.defineProperty(this, "textAlign", {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
configurable: true,
|
|
40
|
+
writable: true,
|
|
41
|
+
value: void 0
|
|
42
|
+
});
|
|
31
43
|
Object.defineProperty(this, "exportHooks", {
|
|
32
44
|
enumerable: true,
|
|
33
45
|
configurable: true,
|
|
@@ -106,10 +118,12 @@ export class Legend {
|
|
|
106
118
|
writable: true,
|
|
107
119
|
value: null
|
|
108
120
|
});
|
|
109
|
-
const { mode = 'inline', position = 'bottom', disconnectedTarget, marginTop = 20, marginBottom = 10, paddingX, itemSpacingX, itemSpacingY, exportHooks, } = config ?? {};
|
|
121
|
+
const { mode = 'inline', position = 'bottom', disconnectedTarget, align = 'center', textAlign = 'center', marginTop = 20, marginBottom = 10, paddingX, itemSpacingX, itemSpacingY, exportHooks, } = config ?? {};
|
|
110
122
|
this.mode = mode;
|
|
111
123
|
this.position = position;
|
|
112
124
|
this.disconnectedTarget = disconnectedTarget;
|
|
125
|
+
this.align = align;
|
|
126
|
+
this.textAlign = textAlign;
|
|
113
127
|
this.marginTop = marginTop;
|
|
114
128
|
this.marginBottom = marginBottom;
|
|
115
129
|
this.paddingX = paddingX;
|
|
@@ -124,6 +138,8 @@ export class Legend {
|
|
|
124
138
|
mode: this.mode,
|
|
125
139
|
position: this.position,
|
|
126
140
|
disconnectedTarget: this.disconnectedTarget,
|
|
141
|
+
align: this.align,
|
|
142
|
+
textAlign: this.textAlign,
|
|
127
143
|
marginTop: this.marginTop,
|
|
128
144
|
marginBottom: this.marginBottom,
|
|
129
145
|
paddingX: this.paddingX,
|
|
@@ -335,6 +351,8 @@ export class Legend {
|
|
|
335
351
|
paddingX: this.paddingX ?? theme.legend.paddingX,
|
|
336
352
|
itemSpacingX: this.itemSpacingX ?? theme.legend.itemSpacingX,
|
|
337
353
|
itemSpacingY: this.itemSpacingY ?? theme.legend.itemSpacingY,
|
|
354
|
+
align: this.align,
|
|
355
|
+
textAlign: this.textAlign,
|
|
338
356
|
};
|
|
339
357
|
}
|
|
340
358
|
buildLegendItems(series) {
|
|
@@ -402,10 +420,15 @@ export class Legend {
|
|
|
402
420
|
positionRows(rows, width, settings) {
|
|
403
421
|
const positionedItems = [];
|
|
404
422
|
const availableWidth = Math.max(0, width - settings.paddingX * 2);
|
|
423
|
+
const legendBlockWidth = rows.reduce((maxWidth, row) => {
|
|
424
|
+
return Math.max(maxWidth, row.width);
|
|
425
|
+
}, 0);
|
|
426
|
+
const legendBlockX = settings.paddingX +
|
|
427
|
+
this.getAlignmentOffset(availableWidth, legendBlockWidth, settings.align);
|
|
405
428
|
let accumulatedRowHeight = 0;
|
|
406
429
|
rows.forEach((row, rowIndex) => {
|
|
407
|
-
const rowX =
|
|
408
|
-
|
|
430
|
+
const rowX = legendBlockX +
|
|
431
|
+
this.getAlignmentOffset(legendBlockWidth, row.width, settings.textAlign);
|
|
409
432
|
const rowY = accumulatedRowHeight + rowIndex * settings.itemSpacingY;
|
|
410
433
|
let currentX = rowX;
|
|
411
434
|
row.items.forEach((item, itemIndex) => {
|
|
@@ -433,6 +456,8 @@ export class Legend {
|
|
|
433
456
|
theme.legend.paddingX,
|
|
434
457
|
theme.legend.itemSpacingX,
|
|
435
458
|
theme.legend.itemSpacingY,
|
|
459
|
+
this.align,
|
|
460
|
+
this.textAlign,
|
|
436
461
|
this.paddingX ?? '',
|
|
437
462
|
this.itemSpacingX ?? '',
|
|
438
463
|
this.itemSpacingY ?? '',
|
|
@@ -442,6 +467,16 @@ export class Legend {
|
|
|
442
467
|
getFallbackRowHeight(theme) {
|
|
443
468
|
return Math.max(theme.legend.boxSize, theme.legend.fontSize);
|
|
444
469
|
}
|
|
470
|
+
getAlignmentOffset(availableWidth, rowWidth, align) {
|
|
471
|
+
const remainingWidth = Math.max(0, availableWidth - rowWidth);
|
|
472
|
+
if (align === 'left') {
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
if (align === 'right') {
|
|
476
|
+
return remainingWidth;
|
|
477
|
+
}
|
|
478
|
+
return remainingWidth / 2;
|
|
479
|
+
}
|
|
445
480
|
bindStateController(controller) {
|
|
446
481
|
this.stateControllerCleanup?.();
|
|
447
482
|
this.stateController = controller;
|
|
@@ -517,12 +517,36 @@ function getClosestSingleTooltipCandidate(candidates, request) {
|
|
|
517
517
|
!Number.isFinite(request.pointerY)) {
|
|
518
518
|
return candidates[0];
|
|
519
519
|
}
|
|
520
|
+
const touchedBarCandidate = getTouchedBarTooltipCandidate(candidates, request);
|
|
521
|
+
if (touchedBarCandidate) {
|
|
522
|
+
return touchedBarCandidate;
|
|
523
|
+
}
|
|
520
524
|
return candidates.reduce((closest, candidate) => {
|
|
521
525
|
const closestDistance = getSingleTooltipDistance(closest, request);
|
|
522
526
|
const candidateDistance = getSingleTooltipDistance(candidate, request);
|
|
523
527
|
return candidateDistance < closestDistance ? candidate : closest;
|
|
524
528
|
});
|
|
525
529
|
}
|
|
530
|
+
function getTouchedBarTooltipCandidate(candidates, request) {
|
|
531
|
+
const touchedBarCandidates = candidates.filter((candidate) => {
|
|
532
|
+
return (candidate.series.type === 'bar' &&
|
|
533
|
+
isPointerInsideAnchor(request, candidate.anchor));
|
|
534
|
+
});
|
|
535
|
+
return touchedBarCandidates[touchedBarCandidates.length - 1] ?? null;
|
|
536
|
+
}
|
|
537
|
+
function isPointerInsideAnchor(request, anchor) {
|
|
538
|
+
const { pointerX, pointerY } = request;
|
|
539
|
+
if (pointerX === undefined ||
|
|
540
|
+
pointerY === undefined ||
|
|
541
|
+
!Number.isFinite(pointerX) ||
|
|
542
|
+
!Number.isFinite(pointerY)) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
return (pointerX >= anchor.left &&
|
|
546
|
+
pointerX <= anchor.right &&
|
|
547
|
+
pointerY >= anchor.top &&
|
|
548
|
+
pointerY <= anchor.bottom);
|
|
549
|
+
}
|
|
526
550
|
function getSingleTooltipDistance(candidate, request) {
|
|
527
551
|
const deltaX = (request.pointerX ?? 0) - candidate.target.x;
|
|
528
552
|
const deltaY = (request.pointerY ?? 0) - candidate.target.y;
|
package/dist/types.d.ts
CHANGED
|
@@ -330,10 +330,13 @@ export type TooltipConfigBase = {
|
|
|
330
330
|
export type TooltipConfig = TooltipConfigBase & {
|
|
331
331
|
exportHooks?: ExportHooks<TooltipConfigBase>;
|
|
332
332
|
};
|
|
333
|
+
export type HorizontalAlignment = 'left' | 'center' | 'right';
|
|
333
334
|
export type LegendConfigBase = {
|
|
334
335
|
mode?: LegendMode;
|
|
335
336
|
position?: 'bottom';
|
|
336
337
|
disconnectedTarget?: string | HTMLElement;
|
|
338
|
+
align?: HorizontalAlignment;
|
|
339
|
+
textAlign?: HorizontalAlignment;
|
|
337
340
|
marginTop?: number;
|
|
338
341
|
marginBottom?: number;
|
|
339
342
|
paddingX?: number;
|
|
@@ -355,7 +358,7 @@ export type LegendItem = {
|
|
|
355
358
|
visible: boolean;
|
|
356
359
|
};
|
|
357
360
|
export type TextPosition = 'top' | 'bottom';
|
|
358
|
-
export type TextAlign =
|
|
361
|
+
export type TextAlign = HorizontalAlignment;
|
|
359
362
|
export type TextVariantStyle = {
|
|
360
363
|
fontSize?: number;
|
|
361
364
|
fontWeight?: string;
|
package/docs/components.md
CHANGED
|
@@ -211,6 +211,8 @@ new Legend({
|
|
|
211
211
|
mode?: 'inline' | 'disconnected' | 'hidden', // Rendering mode (default: 'inline')
|
|
212
212
|
position?: 'bottom', // Position (currently only 'bottom')
|
|
213
213
|
disconnectedTarget?: string | HTMLElement, // External mount target when mode is 'disconnected'
|
|
214
|
+
align?: 'left' | 'center' | 'right', // Legend block alignment (default: 'center')
|
|
215
|
+
textAlign?: 'left' | 'center' | 'right', // Wrapped row alignment within the legend block (default: 'center')
|
|
214
216
|
marginTop?: number, // Space above legend (default: 20)
|
|
215
217
|
marginBottom?: number, // Space below legend (default: 10)
|
|
216
218
|
paddingX?: number, // Horizontal inset for legend layout (default: theme.legend.paddingX)
|
|
@@ -220,7 +222,10 @@ new Legend({
|
|
|
220
222
|
```
|
|
221
223
|
|
|
222
224
|
Legend items automatically wrap to new rows when they do not fit available
|
|
223
|
-
width.
|
|
225
|
+
width. The widest wrapped row defines the legend block. Use `align` to position
|
|
226
|
+
that block and `textAlign` to align rows inside it, for example
|
|
227
|
+
`{ align: 'center', textAlign: 'left' }` for a centered legend with
|
|
228
|
+
left-aligned wrapped rows.
|
|
224
229
|
|
|
225
230
|
- `inline`: current default behavior (legend rendered inside the chart SVG)
|
|
226
231
|
- `disconnected`: legend rendered in a separate SVG outside chart layout
|
package/package.json
CHANGED