@internetstiftelsen/charts 0.12.0 → 0.13.1

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
@@ -198,6 +198,7 @@ import { XYChart } from '@internetstiftelsen/charts/xy-chart';
198
198
  import { Line } from '@internetstiftelsen/charts/line';
199
199
  import { Bar } from '@internetstiftelsen/charts/bar';
200
200
  import { Legend } from '@internetstiftelsen/charts/legend';
201
+ import { Text } from '@internetstiftelsen/charts/text';
201
202
  import { Title } from '@internetstiftelsen/charts/title';
202
203
 
203
204
  const lineChart = new XYChart({ data: lineData });
@@ -215,6 +216,14 @@ const group = new ChartGroup({
215
216
 
216
217
  group
217
218
  .addChild(new Title({ text: 'Revenue vs Expenses' }))
219
+ .addChild(
220
+ new Text({
221
+ text: 'Source: finance team',
222
+ position: 'bottom',
223
+ variant: 'caption',
224
+ align: 'left',
225
+ }),
226
+ )
218
227
  .addChart(barChart)
219
228
  .addChart(lineChart)
220
229
  .addChild(new Legend());
@@ -426,7 +435,7 @@ Grouped parsing rules:
426
435
  - [DonutChart](./docs/donut-chart.md) - Donut/pie charts API
427
436
  - [PieChart](./docs/pie-chart.md) - Pie chart API
428
437
  - [GaugeChart](./docs/gauge-chart.md) - Gauge chart API
429
- - [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Title
438
+ - [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Text, Title
430
439
  - [Theming](./docs/theming.md) - Colors, fonts, and styling
431
440
 
432
441
  ## Browser Support
@@ -1,5 +1,5 @@
1
1
  import { type Selection } from 'd3';
2
- import type { ChartData, DataItem, ChartTheme, ResolvedChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext, LegendItem, LegendMode, LegendSeries, ResponsiveConfig, ResponsiveRenderContext, DeepPartial } from './types.js';
2
+ import type { ChartData, DataItem, ChartTheme, ResolvedChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext, LegendItem, LegendMode, LegendSeries, ResponsiveConfig, ResponsiveRenderContext, DeepPartial, TextPosition } from './types.js';
3
3
  import type { ChartComponentBase, LayoutAwareComponentBase } from './chart-interface.js';
4
4
  import type { XAxis } from './x-axis.js';
5
5
  import type { YAxis } from './y-axis.js';
@@ -75,6 +75,13 @@ type ComponentSlot<TComponent extends ChartComponentBase = ChartComponentBase> =
75
75
  set: (component: TComponent | null) => void;
76
76
  onRegister?: (component: TComponent) => void;
77
77
  };
78
+ type TextLayoutComponent = LayoutAwareComponentBase & ChartComponentBase & {
79
+ display: boolean;
80
+ text: string;
81
+ position: TextPosition;
82
+ variant: string;
83
+ render: (svg: Selection<SVGSVGElement, undefined, null, undefined>, theme: ChartTheme, width: number, x?: number, y?: number) => void;
84
+ };
78
85
  export type BaseChartConfig = {
79
86
  data: ChartData;
80
87
  width?: number;
@@ -102,6 +109,7 @@ export declare abstract class BaseChart {
102
109
  protected tooltip: Tooltip | null;
103
110
  protected legend: Legend | null;
104
111
  protected title: Title | null;
112
+ protected textComponents: TextLayoutComponent[];
105
113
  protected svg: Selection<SVGSVGElement, undefined, null, undefined> | null;
106
114
  protected plotGroup: Selection<SVGGElement, undefined, null, undefined> | null;
107
115
  protected container: HTMLElement | null;
@@ -139,6 +147,7 @@ export declare abstract class BaseChart {
139
147
  private pushIfIncluded;
140
148
  private resolveAccessibleLabel;
141
149
  private syncAccessibleLabelFromSvg;
150
+ private resolvePrimaryTextLabel;
142
151
  protected resolveResponsiveContext(context: {
143
152
  width: number;
144
153
  height: number;
@@ -169,6 +178,7 @@ export declare abstract class BaseChart {
169
178
  */
170
179
  protected getLayoutComponents(): LayoutAwareComponentBase[];
171
180
  protected getBaseLayoutComponents(options: BaseLayoutComponentsOptions): LayoutAwareComponentBase[];
181
+ private getTextComponents;
172
182
  protected getExportComponents(): ChartComponentBase[];
173
183
  protected getOverrideableComponents(): ChartComponentBase[];
174
184
  protected getBaseExportComponents(options: BaseExportComponentsOptions): ChartComponentBase[];
@@ -226,6 +236,7 @@ export declare abstract class BaseChart {
226
236
  protected tryRegisterComponent(component: ChartComponentBase, slots: readonly ComponentSlot[]): boolean;
227
237
  protected applySlotOverrides(overrides: Map<ChartComponentBase, ChartComponentBase>, slots: readonly ComponentSlot[]): () => void;
228
238
  protected applyArrayComponentOverrides<TComponent extends ChartComponentBase>(components: TComponent[], overrides: Map<ChartComponentBase, ChartComponentBase>, isComponent: (component: ChartComponentBase) => component is TComponent): () => void;
239
+ private isTextComponent;
229
240
  private getBaseComponentSlots;
230
241
  /**
231
242
  * Exports the chart in the specified format
@@ -135,6 +135,12 @@ export class BaseChart {
135
135
  writable: true,
136
136
  value: null
137
137
  });
138
+ Object.defineProperty(this, "textComponents", {
139
+ enumerable: true,
140
+ configurable: true,
141
+ writable: true,
142
+ value: []
143
+ });
138
144
  Object.defineProperty(this, "svg", {
139
145
  enumerable: true,
140
146
  configurable: true,
@@ -410,20 +416,31 @@ export class BaseChart {
410
416
  }
411
417
  }
412
418
  resolveAccessibleLabel() {
413
- const titleText = this.title?.text.trim();
419
+ const titleText = this.resolvePrimaryTextLabel();
414
420
  if (titleText) {
415
421
  return titleText;
416
422
  }
417
423
  return 'Chart';
418
424
  }
419
425
  syncAccessibleLabelFromSvg(svg) {
420
- const titleText = svg.querySelector('.title text')?.textContent?.trim();
426
+ const titleText = svg
427
+ .querySelector('.title text, .text--title text')
428
+ ?.textContent?.trim();
421
429
  if (titleText) {
422
430
  svg.setAttribute('aria-label', titleText);
423
431
  return;
424
432
  }
425
433
  svg.setAttribute('aria-label', this.resolveAccessibleLabel());
426
434
  }
435
+ resolvePrimaryTextLabel() {
436
+ return this.textComponents
437
+ .find((component) => {
438
+ return (component.display &&
439
+ (component.type === 'title' ||
440
+ component.variant === 'title'));
441
+ })
442
+ ?.text.trim();
443
+ }
427
444
  resolveResponsiveContext(context) {
428
445
  const activeBreakpoints = this.resolveActiveBreakpoints(context.width).map(({ name }) => name);
429
446
  return {
@@ -574,8 +591,8 @@ export class BaseChart {
574
591
  }
575
592
  getBaseLayoutComponents(options) {
576
593
  const components = [];
577
- if (options.title && this.title) {
578
- components.push(this.title);
594
+ if (options.title) {
595
+ components.push(...this.getTextComponents('top'));
579
596
  }
580
597
  if (options.xAxis && this.xAxis) {
581
598
  components.push(this.xAxis);
@@ -586,8 +603,16 @@ export class BaseChart {
586
603
  if (options.inlineLegend && this.legend && this.hasInlineLegend()) {
587
604
  components.push(this.legend);
588
605
  }
606
+ if (options.title) {
607
+ components.push(...this.getTextComponents('bottom'));
608
+ }
589
609
  return components;
590
610
  }
611
+ getTextComponents(position) {
612
+ return this.textComponents.filter((component) => {
613
+ return component.position === position;
614
+ });
615
+ }
591
616
  getExportComponents() {
592
617
  return this.getBaseExportComponents({
593
618
  title: true,
@@ -603,7 +628,9 @@ export class BaseChart {
603
628
  }
604
629
  getBaseExportComponents(options) {
605
630
  const components = [];
606
- this.pushIfIncluded(components, options.title, this.title);
631
+ if (options.title) {
632
+ components.push(...this.textComponents);
633
+ }
607
634
  this.pushIfIncluded(components, options.grid, this.grid);
608
635
  this.pushIfIncluded(components, options.xAxis, this.xAxis);
609
636
  this.pushIfIncluded(components, options.yAxis, this.yAxis);
@@ -612,6 +639,13 @@ export class BaseChart {
612
639
  return components;
613
640
  }
614
641
  registerBaseComponent(component) {
642
+ if (this.isTextComponent(component)) {
643
+ this.textComponents.push(component);
644
+ if (component.type === 'title' && !this.title) {
645
+ this.title = component;
646
+ }
647
+ return true;
648
+ }
615
649
  return this.tryRegisterComponent(component, this.getBaseComponentSlots());
616
650
  }
617
651
  collectExportOverrides(context) {
@@ -739,7 +773,18 @@ export class BaseChart {
739
773
  return overrideComponents;
740
774
  }
741
775
  applyComponentOverrides(overrides) {
742
- return this.applySlotOverrides(overrides, this.getBaseComponentSlots());
776
+ const previousTitle = this.title;
777
+ const restoreSlots = this.applySlotOverrides(overrides, this.getBaseComponentSlots());
778
+ const restoreText = this.applyArrayComponentOverrides(this.textComponents, overrides, this.isTextComponent);
779
+ this.title =
780
+ this.textComponents.find((component) => {
781
+ return component.type === 'title';
782
+ }) ?? null;
783
+ return () => {
784
+ restoreText();
785
+ restoreSlots();
786
+ this.title = previousTitle;
787
+ };
743
788
  }
744
789
  renderExportChart(chart, width, height) {
745
790
  const container = document.createElement('div');
@@ -770,11 +815,10 @@ export class BaseChart {
770
815
  });
771
816
  }
772
817
  renderTitle(svg) {
773
- if (!this.title) {
774
- return;
775
- }
776
- const position = this.layoutManager.getComponentPosition(this.title);
777
- this.title.render(svg, this.renderTheme, this.width, position.x, position.y);
818
+ this.textComponents.forEach((component) => {
819
+ const position = this.layoutManager.getComponentPosition(component);
820
+ component.render(svg, this.renderTheme, this.width, position.x, position.y);
821
+ });
778
822
  }
779
823
  renderInlineLegend(svg) {
780
824
  if (!this.legend || !this.hasInlineLegend()) {
@@ -1060,15 +1104,11 @@ export class BaseChart {
1060
1104
  components.splice(0, components.length, ...previousState);
1061
1105
  };
1062
1106
  }
1107
+ isTextComponent(component) {
1108
+ return component.type === 'text' || component.type === 'title';
1109
+ }
1063
1110
  getBaseComponentSlots() {
1064
1111
  return [
1065
- {
1066
- type: 'title',
1067
- get: () => this.title,
1068
- set: (component) => {
1069
- this.title = component;
1070
- },
1071
- },
1072
1112
  {
1073
1113
  type: 'grid',
1074
1114
  get: () => this.grid,
@@ -56,7 +56,7 @@ export declare class ChartGroup {
56
56
  private readonly warnedColorConflicts;
57
57
  private container;
58
58
  private legend;
59
- private title;
59
+ private readonly textComponents;
60
60
  private resizeObserver;
61
61
  private readyPromise;
62
62
  private childLegendSnapshot;
@@ -69,6 +69,7 @@ export declare class ChartGroup {
69
69
  addChild(component: {
70
70
  type: string;
71
71
  }): this;
72
+ private isTextComponent;
72
73
  render(target: string | HTMLElement): HTMLElement | null;
73
74
  refresh(): HTMLElement | null;
74
75
  whenReady(): Promise<void>;
@@ -79,6 +80,7 @@ export declare class ChartGroup {
79
80
  toggleLegendSeries(dataKey: string): this;
80
81
  setLegendVisibility(visibility: Record<string, boolean>): this;
81
82
  onLegendChange(callback: () => void): () => void;
83
+ private resolveAccessibleLabel;
82
84
  export(format: ChartGroupExportFormat, options?: ExportOptions): Promise<string | Blob | void>;
83
85
  destroy(): void;
84
86
  private bindChartRenderCallback;
@@ -105,15 +107,19 @@ export declare class ChartGroup {
105
107
  private serializeChildLegendState;
106
108
  private serializeChildYDomains;
107
109
  private renderLegendIntoContainer;
108
- private renderTitleSvg;
110
+ private renderTextSvg;
111
+ private renderTextSvgs;
109
112
  private renderLegendSvg;
110
113
  private setupResizeObserver;
111
114
  private exportSVG;
112
115
  private resolveChartOptions;
113
116
  private refreshIfMounted;
114
117
  private prepareRenderState;
118
+ private sumRenderedTextHeight;
115
119
  private createRenderHosts;
116
120
  private appendRenderedSection;
121
+ private appendRenderedTextSections;
122
+ private resolveTextSectionClassName;
117
123
  private renderLayoutItems;
118
124
  private createChartHost;
119
125
  private createReadyPromise;
@@ -122,9 +128,16 @@ export declare class ChartGroup {
122
128
  private createRasterExportOptions;
123
129
  private exportPdfContent;
124
130
  private resolveExportLayoutState;
131
+ private createExportLayoutState;
132
+ private createExportRenderContext;
133
+ private resolveBaseExportHeight;
134
+ private createExportComponent;
135
+ private runExportHooks;
125
136
  private resolveLiveChartHeightOverride;
126
137
  private exportLayoutItems;
127
138
  private composeExportSvg;
139
+ private syncAccessibleLabelFromSvg;
140
+ private appendExportTextSections;
128
141
  private validateResponsiveConfig;
129
142
  private validateItemResponsiveConfig;
130
143
  private resolveResponsiveConfigForWidth;