@internetstiftelsen/charts 0.18.1 → 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 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 = settings.paddingX +
408
- Math.max(0, (availableWidth - row.width) / 2);
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;
@@ -1,6 +1,7 @@
1
1
  import { SPLIT_TOOLTIP_GAP_PX, TOOLTIP_BOX_ARROW_LENGTH_PX, TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX, TOOLTIP_CONNECTOR_ELBOW_RATIO, TOOLTIP_CONNECTOR_INSET_PX, TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX, TOOLTIP_CONNECTOR_PADDING_PX, TOOLTIP_OFFSET_PX, TOOLTIP_TOTAL_BORDER_WIDTH_PX, TOOLTIP_VIEWPORT_PADDING_PX, } from './types.js';
2
2
  const MAX_EXHAUSTIVE_HORIZONTAL_ROW_LAYOUTS = 10;
3
3
  const HORIZONTAL_ROW_ORDER_FLIP_PENALTY_PX = 128;
4
+ const AUTO_BAR_TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX = 8;
4
5
  export function getSplitTooltipViewportBounds() {
5
6
  const visualViewport = window.visualViewport;
6
7
  const viewportLeft = window.scrollX + (visualViewport?.offsetLeft ?? 0);
@@ -98,13 +99,16 @@ export function getAnchoredTooltipPosition(anchor, target, tooltipWidth, tooltip
98
99
  };
99
100
  }
100
101
  export function resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY, anchor) {
101
- const localTargetX = targetX - tooltipLeft;
102
- const localTargetY = targetY - tooltipTop;
102
+ const boxArrowPosition = resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY);
103
+ const arrowTip = resolveTooltipArrowTip(arrowEdge, boxArrowPosition.x, boxArrowPosition.y, TOOLTIP_BOX_ARROW_LENGTH_PX);
104
+ const arrowTipX = tooltipLeft + arrowTip.x;
105
+ const arrowTipY = tooltipTop + arrowTip.y;
106
+ const connectorTarget = resolveTooltipConnectorTarget(arrowEdge, targetX, targetY, arrowTipX, arrowTipY, anchor);
107
+ const localTargetX = connectorTarget.x - tooltipLeft;
108
+ const localTargetY = connectorTarget.y - tooltipTop;
103
109
  if (!Number.isFinite(localTargetX) || !Number.isFinite(localTargetY)) {
104
110
  return null;
105
111
  }
106
- const boxArrowPosition = resolveTooltipBoxArrowPosition(arrowEdge, tooltipLeft, tooltipTop, tooltipWidth, tooltipHeight, targetX, targetY);
107
- const arrowTip = resolveTooltipArrowTip(arrowEdge, boxArrowPosition.x, boxArrowPosition.y, TOOLTIP_BOX_ARROW_LENGTH_PX);
108
112
  const minX = Math.min(arrowTip.x, localTargetX) - TOOLTIP_CONNECTOR_PADDING_PX;
109
113
  const maxX = Math.max(arrowTip.x, localTargetX) + TOOLTIP_CONNECTOR_PADDING_PX;
110
114
  const minY = Math.min(arrowTip.y, localTargetY) - TOOLTIP_CONNECTOR_PADDING_PX;
@@ -117,9 +121,7 @@ export function resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop
117
121
  const startY = arrowTip.y - minY;
118
122
  const endX = localTargetX - minX;
119
123
  const endY = localTargetY - minY;
120
- const arrowTipX = tooltipLeft + arrowTip.x;
121
- const arrowTipY = tooltipTop + arrowTip.y;
122
- const connectorPath = resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor);
124
+ const connectorPath = resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor, connectorTarget.preferDirectPath);
123
125
  if (!hasFiniteNumbers(width, height, boxX, boxY, startX, startY, endX, endY)) {
124
126
  return null;
125
127
  }
@@ -134,6 +136,43 @@ export function resolveTooltipConnectorLayout(arrowEdge, tooltipLeft, tooltipTop
134
136
  arrowY: boxArrowPosition.y,
135
137
  };
136
138
  }
139
+ function resolveTooltipConnectorTarget(arrowEdge, targetX, targetY, arrowTipX, arrowTipY, anchor) {
140
+ if (isPointTooltipAnchor(anchor)) {
141
+ return { x: targetX, y: targetY, preferDirectPath: false };
142
+ }
143
+ if (arrowEdge === 'left') {
144
+ return {
145
+ x: anchor.right,
146
+ y: clampTooltipCoordinate(arrowTipY, anchor.top, anchor.bottom),
147
+ preferDirectPath: true,
148
+ };
149
+ }
150
+ if (arrowEdge === 'right') {
151
+ return {
152
+ x: anchor.left,
153
+ y: clampTooltipCoordinate(arrowTipY, anchor.top, anchor.bottom),
154
+ preferDirectPath: true,
155
+ };
156
+ }
157
+ if (arrowEdge === 'top') {
158
+ return {
159
+ x: clampTooltipCoordinate(arrowTipX, anchor.left, anchor.right),
160
+ y: anchor.bottom,
161
+ preferDirectPath: true,
162
+ };
163
+ }
164
+ return {
165
+ x: clampTooltipCoordinate(arrowTipX, anchor.left, anchor.right),
166
+ y: anchor.top,
167
+ preferDirectPath: true,
168
+ };
169
+ }
170
+ function isPointTooltipAnchor(anchor) {
171
+ return (Math.abs(anchor.left - anchor.right) <=
172
+ TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
173
+ Math.abs(anchor.top - anchor.bottom) <=
174
+ TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
175
+ }
137
176
  export function resolveTooltipArrowPosition(arrowEdge, boxX, boxY, length, halfHeight) {
138
177
  switch (arrowEdge) {
139
178
  case 'left':
@@ -210,17 +249,26 @@ function getTooltipConnectorOffset(start, size, target) {
210
249
  const preferredOffset = target - start;
211
250
  return Math.max(minOffset, Math.min(preferredOffset, maxOffset));
212
251
  }
213
- function resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor) {
252
+ function resolveTooltipConnectorPath(arrowEdge, startX, startY, endX, endY, arrowTipX, arrowTipY, anchor, preferDirectPath) {
214
253
  if (arrowEdge === 'left' || arrowEdge === 'right') {
215
- if (isTooltipArrowTipNearVerticalAnchorSpan(arrowTipY, anchor)) {
254
+ const minPathLength = preferDirectPath
255
+ ? AUTO_BAR_TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX
256
+ : TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX;
257
+ if (isTooltipArrowTipNearVerticalAnchorSpan(arrowTipY, anchor, minPathLength)) {
216
258
  return '';
217
259
  }
260
+ if (preferDirectPath) {
261
+ return `M ${startX},${startY} L ${endX},${endY}`;
262
+ }
218
263
  const elbowX = startX + (endX - startX) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
219
264
  return `M ${startX},${startY} L ${elbowX},${startY} L ${endX},${endY}`;
220
265
  }
221
266
  if (isTooltipArrowTipInsideHorizontalAnchorSpan(arrowTipX, anchor)) {
222
267
  return '';
223
268
  }
269
+ if (preferDirectPath) {
270
+ return `M ${startX},${startY} L ${endX},${endY}`;
271
+ }
224
272
  const elbowY = startY + (endY - startY) * TOOLTIP_CONNECTOR_ELBOW_RATIO;
225
273
  return `M ${startX},${startY} L ${startX},${elbowY} L ${endX},${endY}`;
226
274
  }
@@ -232,12 +280,12 @@ function isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor) {
232
280
  return (arrowTipY >= anchor.top - TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX &&
233
281
  arrowTipY <= anchor.bottom + TOOLTIP_CONNECTOR_ALIGNMENT_TOLERANCE_PX);
234
282
  }
235
- function isTooltipArrowTipNearVerticalAnchorSpan(arrowTipY, anchor) {
283
+ function isTooltipArrowTipNearVerticalAnchorSpan(arrowTipY, anchor, minPathLength = TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX) {
236
284
  if (isTooltipArrowTipInsideVerticalAnchorSpan(arrowTipY, anchor)) {
237
285
  return true;
238
286
  }
239
- return (arrowTipY >= anchor.top - TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX &&
240
- arrowTipY <= anchor.bottom + TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX);
287
+ return (arrowTipY >= anchor.top - minPathLength &&
288
+ arrowTipY <= anchor.bottom + minPathLength);
241
289
  }
242
290
  function resolveTooltipArrowTip(arrowEdge, boxX, boxY, length) {
243
291
  if (arrowEdge === 'left' || arrowEdge === 'right') {
@@ -288,6 +336,7 @@ function getCombinedSplitTooltipTarget(layouts) {
288
336
  function getSideSplitTooltipLayouts(layouts) {
289
337
  if (layouts.every((layout) => layout.targetMode === 'auto')) {
290
338
  return [...layouts].sort((a, b) => a.anchor.top - b.anchor.top ||
339
+ a.anchor.bottom - b.anchor.bottom ||
291
340
  a.anchor.left - b.anchor.left ||
292
341
  a.order - b.order);
293
342
  }
@@ -662,6 +711,15 @@ function getSidePlacementCandidates(layout, placedLayouts, allLayouts, bounds) {
662
711
  ];
663
712
  if (layout.targetMode === 'auto') {
664
713
  const connectorlessRange = getSideConnectorlessTopRange(layout);
714
+ const exactConnectorlessRange = getExactSideConnectorlessTopRange(layout);
715
+ candidates.push({
716
+ top: exactConnectorlessRange.min,
717
+ arrowEdge: layout.arrowEdge,
718
+ });
719
+ candidates.push({
720
+ top: exactConnectorlessRange.max,
721
+ arrowEdge: layout.arrowEdge,
722
+ });
665
723
  candidates.push({
666
724
  top: connectorlessRange.min,
667
725
  arrowEdge: layout.arrowEdge,
@@ -750,6 +808,17 @@ function getFixedPointSideSpreadPreferredTop(layout, direction) {
750
808
  return layout.targetY - TOOLTIP_CONNECTOR_INSET_PX;
751
809
  }
752
810
  function getSideConnectorlessTopRange(layout) {
811
+ const nearPathPadding = layout.targetMode === 'auto'
812
+ ? AUTO_BAR_TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX
813
+ : 0;
814
+ return {
815
+ min: layout.anchor.top -
816
+ getTooltipConnectorMaxOffset(layout.height) -
817
+ nearPathPadding,
818
+ max: layout.anchor.bottom - TOOLTIP_CONNECTOR_INSET_PX + nearPathPadding,
819
+ };
820
+ }
821
+ function getExactSideConnectorlessTopRange(layout) {
753
822
  return {
754
823
  min: layout.anchor.top - getTooltipConnectorMaxOffset(layout.height),
755
824
  max: layout.anchor.bottom - TOOLTIP_CONNECTOR_INSET_PX,
@@ -757,7 +826,9 @@ function getSideConnectorlessTopRange(layout) {
757
826
  }
758
827
  function isSidePlacementConnectorless(candidate, layout) {
759
828
  if (layout.targetMode === 'auto') {
760
- return doRangesOverlap(layout.anchor.top, layout.anchor.bottom, candidate.top + TOOLTIP_CONNECTOR_INSET_PX, candidate.top + getTooltipConnectorMaxOffset(layout.height));
829
+ const connectorlessRange = getSideConnectorlessTopRange(layout);
830
+ return (candidate.top >= connectorlessRange.min &&
831
+ candidate.top <= connectorlessRange.max);
761
832
  }
762
833
  const arrowTipY = getSidePlacementArrowTipY(candidate.top, layout);
763
834
  return (arrowTipY >=
@@ -6,7 +6,7 @@ import type { Scatter } from '../scatter.js';
6
6
  import type { TooltipTransitionConfig } from '../types.js';
7
7
  export declare const TOOLTIP_OFFSET_PX = 12;
8
8
  export declare const TOOLTIP_VIEWPORT_PADDING_PX = 10;
9
- export declare const TOOLTIP_CONNECTOR_INSET_PX = 14;
9
+ export declare const TOOLTIP_CONNECTOR_INSET_PX = 10;
10
10
  export declare const TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX = 16;
11
11
  export declare const TOOLTIP_CONNECTOR_PADDING_PX = 4;
12
12
  export declare const TOOLTIP_CONNECTOR_ELBOW_RATIO = 0.45;
@@ -20,7 +20,7 @@ export declare const TOOLTIP_ARROW_FILL_Z_INDEX = 5;
20
20
  export declare const TOOLTIP_BODY_Z_INDEX = 6;
21
21
  export declare const TOOLTIP_TOTAL_BORDER_WIDTH_PX: number;
22
22
  export declare const TOOLTIP_ROOT_Z_INDEX = 30;
23
- export declare const SPLIT_TOOLTIP_GAP_PX = 8;
23
+ export declare const SPLIT_TOOLTIP_GAP_PX = 4;
24
24
  export declare const DEFAULT_TOOLTIP_MAX_WIDTH_PX = 280;
25
25
  export declare const DEFAULT_TOOLTIP_TRANSITION: Required<TooltipTransitionConfig>;
26
26
  export declare const TOOLTIP_HIDDEN_TRANSFORM = "translateY(2px)";
@@ -1,6 +1,6 @@
1
1
  export const TOOLTIP_OFFSET_PX = 12;
2
2
  export const TOOLTIP_VIEWPORT_PADDING_PX = 10;
3
- export const TOOLTIP_CONNECTOR_INSET_PX = 14;
3
+ export const TOOLTIP_CONNECTOR_INSET_PX = 10;
4
4
  export const TOOLTIP_CONNECTOR_MIN_PATH_LENGTH_PX = 16;
5
5
  export const TOOLTIP_CONNECTOR_PADDING_PX = 4;
6
6
  export const TOOLTIP_CONNECTOR_ELBOW_RATIO = 0.45;
@@ -14,7 +14,7 @@ export const TOOLTIP_ARROW_FILL_Z_INDEX = 5;
14
14
  export const TOOLTIP_BODY_Z_INDEX = 6;
15
15
  export const TOOLTIP_TOTAL_BORDER_WIDTH_PX = TOOLTIP_BORDER_WIDTH_PX * 2;
16
16
  export const TOOLTIP_ROOT_Z_INDEX = 30;
17
- export const SPLIT_TOOLTIP_GAP_PX = 8;
17
+ export const SPLIT_TOOLTIP_GAP_PX = 4;
18
18
  export const DEFAULT_TOOLTIP_MAX_WIDTH_PX = 280;
19
19
  export const DEFAULT_TOOLTIP_TRANSITION = {
20
20
  show: false,
@@ -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 = 'left' | 'center' | 'right';
361
+ export type TextAlign = HorizontalAlignment;
359
362
  export type TextVariantStyle = {
360
363
  fontSize?: number;
361
364
  fontWeight?: string;
@@ -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. Each wrapped row is centered independently.
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.18.1",
2
+ "version": "0.19.0",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
5
  "sideEffects": false,