@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 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;
@@ -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.2",
2
+ "version": "0.19.0",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
5
  "sideEffects": false,