@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/dist/line.js CHANGED
@@ -1,7 +1,18 @@
1
- import { line } from 'd3';
1
+ import { curveBasis, curveCardinal, curveLinear, curveMonotoneX, curveNatural, curveStep, line, } from 'd3';
2
2
  import { sanitizeForCSS, mergeDeep } from './utils.js';
3
3
  import { getScalePosition } from './scale-utils.js';
4
4
  import { buildXYDatumSnapshotKeys, createTransitionCompletionPromise, createLeftToRightRevealTransition, getEnterStaggerTiming, } from './xy-motion/helpers.js';
5
+ const DEFAULT_LINE_POINTS = {
6
+ show: 'always',
7
+ };
8
+ const LINE_CURVE_FACTORIES = {
9
+ linear: curveLinear,
10
+ monotone: curveMonotoneX,
11
+ step: curveStep,
12
+ natural: curveNatural,
13
+ basis: curveBasis,
14
+ cardinal: curveCardinal,
15
+ };
5
16
  export class Line {
6
17
  constructor(config) {
7
18
  Object.defineProperty(this, "type", {
@@ -28,6 +39,18 @@ export class Line {
28
39
  writable: true,
29
40
  value: void 0
30
41
  });
42
+ Object.defineProperty(this, "curve", {
43
+ enumerable: true,
44
+ configurable: true,
45
+ writable: true,
46
+ value: void 0
47
+ });
48
+ Object.defineProperty(this, "points", {
49
+ enumerable: true,
50
+ configurable: true,
51
+ writable: true,
52
+ value: void 0
53
+ });
31
54
  Object.defineProperty(this, "valueLabel", {
32
55
  enumerable: true,
33
56
  configurable: true,
@@ -43,6 +66,10 @@ export class Line {
43
66
  this.dataKey = config.dataKey;
44
67
  this.stroke = config.stroke || '#8884d8';
45
68
  this.strokeWidth = config.strokeWidth;
69
+ this.curve = config.curve || 'linear';
70
+ this.points = {
71
+ show: config.points?.show ?? DEFAULT_LINE_POINTS.show,
72
+ };
46
73
  this.valueLabel = config.valueLabel;
47
74
  this.exportHooks = config.exportHooks;
48
75
  }
@@ -51,6 +78,8 @@ export class Line {
51
78
  dataKey: this.dataKey,
52
79
  stroke: this.stroke,
53
80
  strokeWidth: this.strokeWidth,
81
+ curve: this.curve,
82
+ points: this.points,
54
83
  valueLabel: this.valueLabel,
55
84
  };
56
85
  }
@@ -80,7 +109,9 @@ export class Line {
80
109
  });
81
110
  const transitions = [
82
111
  ...this.renderLinePath(plotGroup, lineData, animatedLineData, theme, sanitizeForCSS(this.dataKey), animation),
83
- ...this.renderLinePoints(plotGroup, validLineData, validAnimatedLineData, theme, animation),
112
+ ...(this.points.show === 'always'
113
+ ? this.renderLinePoints(plotGroup, validLineData, validAnimatedLineData, theme, animation)
114
+ : []),
84
115
  ];
85
116
  const snapshot = this.createSnapshot(validLineData);
86
117
  // Render value labels if enabled (only for valid values)
@@ -129,8 +160,10 @@ export class Line {
129
160
  }
130
161
  renderLinePath(plotGroup, lineData, animatedLineData, theme, sanitizedKey, animation) {
131
162
  const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
163
+ const curveFactory = LINE_CURVE_FACTORIES[this.curve] || curveLinear;
132
164
  const lineGenerator = line()
133
165
  .defined((entry) => entry.valid)
166
+ .curve(curveFactory)
134
167
  .x((entry) => entry.x)
135
168
  .y((entry) => entry.y);
136
169
  const finalPath = lineGenerator(lineData);
package/dist/text.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { type Selection } from 'd3';
2
+ import type { ChartTheme, ExportHooks, TextAlign, TextConfig, TextConfigBase, TextPosition } from './types.js';
3
+ import type { ComponentSpace, LayoutAwareComponent } from './chart-interface.js';
4
+ type TextComponentType = 'text' | 'title';
5
+ type ResolvedTextStyle = {
6
+ fontSize: number;
7
+ fontWeight: string;
8
+ fontFamily: string;
9
+ color: string;
10
+ lineHeight: number;
11
+ marginTop: number;
12
+ marginBottom: number;
13
+ };
14
+ export declare class Text implements LayoutAwareComponent<TextConfigBase> {
15
+ readonly type: TextComponentType;
16
+ readonly display: boolean;
17
+ readonly text: string;
18
+ readonly position: TextPosition;
19
+ readonly variant: string;
20
+ readonly align: TextAlign;
21
+ readonly exportHooks?: ExportHooks<TextConfigBase>;
22
+ protected readonly fontSize?: number;
23
+ protected readonly fontWeight?: string;
24
+ protected readonly fontFamily?: string;
25
+ protected readonly color?: string;
26
+ protected readonly lineHeight?: number;
27
+ protected readonly marginTop?: number;
28
+ protected readonly marginBottom?: number;
29
+ constructor(config: TextConfig, componentType?: TextComponentType);
30
+ getExportConfig(): TextConfigBase;
31
+ createExportComponent(override?: Partial<TextConfigBase>): LayoutAwareComponent<TextConfigBase>;
32
+ getRequiredSpace(theme?: ChartTheme): ComponentSpace;
33
+ render(svg: Selection<SVGSVGElement, undefined, null, undefined>, theme: ChartTheme, width: number, x?: number, y?: number): void;
34
+ protected resolveStyle(theme?: ChartTheme): ResolvedTextStyle;
35
+ private resolveStyleValue;
36
+ private getLines;
37
+ private resolveTextPosition;
38
+ private resolveClassName;
39
+ }
40
+ export {};
package/dist/text.js ADDED
@@ -0,0 +1,217 @@
1
+ import { mergeDeep, sanitizeForCSS } from './utils.js';
2
+ export class Text {
3
+ constructor(config, componentType = 'text') {
4
+ Object.defineProperty(this, "type", {
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true,
8
+ value: void 0
9
+ });
10
+ Object.defineProperty(this, "display", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: void 0
15
+ });
16
+ Object.defineProperty(this, "text", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: void 0
21
+ });
22
+ Object.defineProperty(this, "position", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: void 0
27
+ });
28
+ Object.defineProperty(this, "variant", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: void 0
33
+ });
34
+ Object.defineProperty(this, "align", {
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ value: void 0
39
+ });
40
+ Object.defineProperty(this, "exportHooks", {
41
+ enumerable: true,
42
+ configurable: true,
43
+ writable: true,
44
+ value: void 0
45
+ });
46
+ Object.defineProperty(this, "fontSize", {
47
+ enumerable: true,
48
+ configurable: true,
49
+ writable: true,
50
+ value: void 0
51
+ });
52
+ Object.defineProperty(this, "fontWeight", {
53
+ enumerable: true,
54
+ configurable: true,
55
+ writable: true,
56
+ value: void 0
57
+ });
58
+ Object.defineProperty(this, "fontFamily", {
59
+ enumerable: true,
60
+ configurable: true,
61
+ writable: true,
62
+ value: void 0
63
+ });
64
+ Object.defineProperty(this, "color", {
65
+ enumerable: true,
66
+ configurable: true,
67
+ writable: true,
68
+ value: void 0
69
+ });
70
+ Object.defineProperty(this, "lineHeight", {
71
+ enumerable: true,
72
+ configurable: true,
73
+ writable: true,
74
+ value: void 0
75
+ });
76
+ Object.defineProperty(this, "marginTop", {
77
+ enumerable: true,
78
+ configurable: true,
79
+ writable: true,
80
+ value: void 0
81
+ });
82
+ Object.defineProperty(this, "marginBottom", {
83
+ enumerable: true,
84
+ configurable: true,
85
+ writable: true,
86
+ value: void 0
87
+ });
88
+ this.type = componentType;
89
+ this.display = config.display ?? true;
90
+ this.text = config.text;
91
+ this.position = config.position ?? 'top';
92
+ this.variant = config.variant ?? 'caption';
93
+ this.align = config.align ?? 'center';
94
+ this.fontSize = config.fontSize;
95
+ this.fontWeight = config.fontWeight;
96
+ this.fontFamily = config.fontFamily;
97
+ this.color = config.color;
98
+ this.lineHeight = config.lineHeight;
99
+ this.marginTop = config.marginTop;
100
+ this.marginBottom = config.marginBottom;
101
+ this.exportHooks = config.exportHooks;
102
+ }
103
+ getExportConfig() {
104
+ return {
105
+ display: this.display,
106
+ text: this.text,
107
+ position: this.position,
108
+ variant: this.variant,
109
+ align: this.align,
110
+ fontSize: this.fontSize,
111
+ fontWeight: this.fontWeight,
112
+ fontFamily: this.fontFamily,
113
+ color: this.color,
114
+ lineHeight: this.lineHeight,
115
+ marginTop: this.marginTop,
116
+ marginBottom: this.marginBottom,
117
+ };
118
+ }
119
+ createExportComponent(override) {
120
+ const merged = mergeDeep(this.getExportConfig(), override);
121
+ return new Text({
122
+ ...merged,
123
+ exportHooks: this.exportHooks,
124
+ }, this.type);
125
+ }
126
+ getRequiredSpace(theme) {
127
+ if (!this.display) {
128
+ return {
129
+ width: 0,
130
+ height: 0,
131
+ position: this.position,
132
+ };
133
+ }
134
+ const style = this.resolveStyle(theme);
135
+ const lineCount = this.getLines().length;
136
+ const textHeight = style.fontSize +
137
+ Math.max(0, lineCount - 1) * style.fontSize * style.lineHeight;
138
+ return {
139
+ width: 0,
140
+ height: style.marginTop + textHeight + style.marginBottom,
141
+ position: this.position,
142
+ };
143
+ }
144
+ render(svg, theme, width, x = 0, y = 0) {
145
+ if (!this.display) {
146
+ return;
147
+ }
148
+ const style = this.resolveStyle(theme);
149
+ const { textX, textAnchor } = this.resolveTextPosition(theme, width);
150
+ const textGroup = svg
151
+ .append('g')
152
+ .attr('class', this.resolveClassName())
153
+ .attr('transform', `translate(${x}, ${y})`);
154
+ const text = textGroup
155
+ .append('text')
156
+ .attr('x', textX)
157
+ .attr('text-anchor', textAnchor)
158
+ .attr('font-size', `${style.fontSize}px`)
159
+ .attr('font-weight', style.fontWeight)
160
+ .attr('font-family', style.fontFamily)
161
+ .attr('fill', style.color);
162
+ this.getLines().forEach((line, index) => {
163
+ const tspan = text.append('tspan').attr('x', textX).text(line);
164
+ if (index === 0) {
165
+ tspan.attr('y', style.marginTop + style.fontSize);
166
+ return;
167
+ }
168
+ tspan.attr('dy', `${style.lineHeight}em`);
169
+ });
170
+ }
171
+ resolveStyle(theme) {
172
+ const variant = theme?.text.variants[this.variant] ?? {};
173
+ return {
174
+ fontSize: this.resolveStyleValue(this.fontSize, variant.fontSize, 12),
175
+ fontWeight: this.resolveStyleValue(this.fontWeight, variant.fontWeight, 'normal'),
176
+ fontFamily: this.fontFamily ??
177
+ variant.fontFamily ??
178
+ theme?.fontFamily ??
179
+ '',
180
+ color: this.resolveStyleValue(this.color, variant.color, '#1f2a36'),
181
+ lineHeight: this.resolveStyleValue(this.lineHeight, variant.lineHeight, 1.2),
182
+ marginTop: this.resolveStyleValue(this.marginTop, variant.marginTop, 0),
183
+ marginBottom: this.resolveStyleValue(this.marginBottom, variant.marginBottom, 0),
184
+ };
185
+ }
186
+ resolveStyleValue(configured, variant, fallback) {
187
+ return configured ?? variant ?? fallback;
188
+ }
189
+ getLines() {
190
+ return this.text.split('\n');
191
+ }
192
+ resolveTextPosition(theme, width) {
193
+ if (this.align === 'left') {
194
+ return {
195
+ textX: theme.margins.left,
196
+ textAnchor: 'start',
197
+ };
198
+ }
199
+ if (this.align === 'right') {
200
+ return {
201
+ textX: width - theme.margins.right,
202
+ textAnchor: 'end',
203
+ };
204
+ }
205
+ return {
206
+ textX: width / 2,
207
+ textAnchor: 'middle',
208
+ };
209
+ }
210
+ resolveClassName() {
211
+ const classes = ['text', `text--${sanitizeForCSS(this.variant)}`];
212
+ if (this.type === 'title') {
213
+ classes.unshift('title');
214
+ }
215
+ return classes.join(' ');
216
+ }
217
+ }
package/dist/theme.js CHANGED
@@ -40,6 +40,34 @@ export const defaultTheme = {
40
40
  itemSpacingX: 20,
41
41
  itemSpacingY: 8,
42
42
  },
43
+ text: {
44
+ variants: {
45
+ title: {
46
+ fontSize: 18,
47
+ fontWeight: 'bold',
48
+ color: '#1f2a36',
49
+ lineHeight: 1.2,
50
+ marginTop: 10,
51
+ marginBottom: 15,
52
+ },
53
+ subtitle: {
54
+ fontSize: 14,
55
+ fontWeight: 'normal',
56
+ color: '#4b5563',
57
+ lineHeight: 1.35,
58
+ marginTop: 0,
59
+ marginBottom: 12,
60
+ },
61
+ caption: {
62
+ fontSize: 12,
63
+ fontWeight: 'normal',
64
+ color: '#6b7280',
65
+ lineHeight: 1.3,
66
+ marginTop: 8,
67
+ marginBottom: 0,
68
+ },
69
+ },
70
+ },
43
71
  tooltip: {
44
72
  background: '#ffffff',
45
73
  border: '#dddddd',
@@ -128,6 +156,34 @@ export const newspaperTheme = {
128
156
  itemSpacingX: 20,
129
157
  itemSpacingY: 8,
130
158
  },
159
+ text: {
160
+ variants: {
161
+ title: {
162
+ fontSize: 18,
163
+ fontWeight: 'bold',
164
+ color: '#1a1a1a',
165
+ lineHeight: 1.2,
166
+ marginTop: 10,
167
+ marginBottom: 15,
168
+ },
169
+ subtitle: {
170
+ fontSize: 14,
171
+ fontWeight: 'normal',
172
+ color: '#4a4a4a',
173
+ lineHeight: 1.35,
174
+ marginTop: 0,
175
+ marginBottom: 12,
176
+ },
177
+ caption: {
178
+ fontSize: 12,
179
+ fontWeight: 'normal',
180
+ color: '#6b6b6b',
181
+ lineHeight: 1.3,
182
+ marginTop: 8,
183
+ marginBottom: 0,
184
+ },
185
+ },
186
+ },
131
187
  tooltip: {
132
188
  background: '#111111',
133
189
  border: '#111111',
package/dist/title.d.ts CHANGED
@@ -1,23 +1,17 @@
1
1
  import { type Selection } from 'd3';
2
- import type { TitleConfig, ChartTheme, ExportHooks, TitleConfigBase } from './types.js';
3
- import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
2
+ import type { ChartTheme, ExportHooks, TitleConfig, TitleConfigBase } from './types.js';
3
+ import type { ComponentSpace, LayoutAwareComponent } from './chart-interface.js';
4
4
  export declare class Title implements LayoutAwareComponent<TitleConfigBase> {
5
5
  readonly type: "title";
6
6
  readonly display: boolean;
7
7
  readonly text: string;
8
+ readonly position: "top";
9
+ readonly variant = "title";
8
10
  readonly exportHooks?: ExportHooks<TitleConfigBase>;
9
- private readonly fontSize;
10
- private readonly fontWeight;
11
- private readonly fontFamily?;
12
- private readonly align;
13
- private readonly marginTop;
14
- private readonly marginBottom;
11
+ private readonly textComponent;
15
12
  constructor(config: TitleConfig);
16
13
  getExportConfig(): TitleConfigBase;
17
14
  createExportComponent(override?: Partial<TitleConfigBase>): LayoutAwareComponent<TitleConfigBase>;
18
- /**
19
- * Returns the space required by the title
20
- */
21
- getRequiredSpace(): ComponentSpace;
15
+ getRequiredSpace(theme?: ChartTheme): ComponentSpace;
22
16
  render(svg: Selection<SVGSVGElement, undefined, null, undefined>, theme: ChartTheme, width: number, x?: number, y?: number): void;
23
17
  }
package/dist/title.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { Text } from './text.js';
1
2
  import { mergeDeep } from './utils.js';
2
3
  export class Title {
3
4
  constructor(config) {
@@ -19,68 +20,53 @@ export class Title {
19
20
  writable: true,
20
21
  value: void 0
21
22
  });
22
- Object.defineProperty(this, "exportHooks", {
23
- enumerable: true,
24
- configurable: true,
25
- writable: true,
26
- value: void 0
27
- });
28
- Object.defineProperty(this, "fontSize", {
29
- enumerable: true,
30
- configurable: true,
31
- writable: true,
32
- value: void 0
33
- });
34
- Object.defineProperty(this, "fontWeight", {
35
- enumerable: true,
36
- configurable: true,
37
- writable: true,
38
- value: void 0
39
- });
40
- Object.defineProperty(this, "fontFamily", {
23
+ Object.defineProperty(this, "position", {
41
24
  enumerable: true,
42
25
  configurable: true,
43
26
  writable: true,
44
- value: void 0
27
+ value: 'top'
45
28
  });
46
- Object.defineProperty(this, "align", {
29
+ Object.defineProperty(this, "variant", {
47
30
  enumerable: true,
48
31
  configurable: true,
49
32
  writable: true,
50
- value: void 0
33
+ value: 'title'
51
34
  });
52
- Object.defineProperty(this, "marginTop", {
35
+ Object.defineProperty(this, "exportHooks", {
53
36
  enumerable: true,
54
37
  configurable: true,
55
38
  writable: true,
56
39
  value: void 0
57
40
  });
58
- Object.defineProperty(this, "marginBottom", {
41
+ Object.defineProperty(this, "textComponent", {
59
42
  enumerable: true,
60
43
  configurable: true,
61
44
  writable: true,
62
45
  value: void 0
63
46
  });
64
- this.display = config.display ?? true;
65
- this.text = config.text;
66
- this.fontSize = config.fontSize ?? 18;
67
- this.fontWeight = config.fontWeight ?? 'bold';
68
- this.fontFamily = config.fontFamily;
69
- this.align = config.align ?? 'center';
70
- this.marginTop = config.marginTop ?? 10;
71
- this.marginBottom = config.marginBottom ?? 15;
47
+ this.textComponent = new Text({
48
+ ...config,
49
+ position: 'top',
50
+ variant: 'title',
51
+ exportHooks: config.exportHooks,
52
+ }, 'title');
53
+ this.display = this.textComponent.display;
54
+ this.text = this.textComponent.text;
72
55
  this.exportHooks = config.exportHooks;
73
56
  }
74
57
  getExportConfig() {
58
+ const config = this.textComponent.getExportConfig();
75
59
  return {
76
- display: this.display,
77
- text: this.text,
78
- fontSize: this.fontSize,
79
- fontWeight: this.fontWeight,
80
- fontFamily: this.fontFamily,
81
- align: this.align,
82
- marginTop: this.marginTop,
83
- marginBottom: this.marginBottom,
60
+ display: config.display,
61
+ text: config.text,
62
+ fontSize: config.fontSize,
63
+ fontWeight: config.fontWeight,
64
+ fontFamily: config.fontFamily,
65
+ align: config.align,
66
+ color: config.color,
67
+ lineHeight: config.lineHeight,
68
+ marginTop: config.marginTop,
69
+ marginBottom: config.marginBottom,
84
70
  };
85
71
  }
86
72
  createExportComponent(override) {
@@ -90,49 +76,10 @@ export class Title {
90
76
  exportHooks: this.exportHooks,
91
77
  });
92
78
  }
93
- /**
94
- * Returns the space required by the title
95
- */
96
- getRequiredSpace() {
97
- if (!this.display) {
98
- return {
99
- width: 0,
100
- height: 0,
101
- position: 'top',
102
- };
103
- }
104
- return {
105
- width: 0, // Title spans full width
106
- height: this.marginTop + this.fontSize + this.marginBottom,
107
- position: 'top',
108
- };
79
+ getRequiredSpace(theme) {
80
+ return this.textComponent.getRequiredSpace(theme);
109
81
  }
110
82
  render(svg, theme, width, x = 0, y = 0) {
111
- if (!this.display) {
112
- return;
113
- }
114
- const titleGroup = svg
115
- .append('g')
116
- .attr('class', 'title')
117
- .attr('transform', `translate(${x}, ${y})`);
118
- let textX = width / 2;
119
- let textAnchor = 'middle';
120
- if (this.align === 'left') {
121
- textX = theme.margins.left;
122
- textAnchor = 'start';
123
- }
124
- else if (this.align === 'right') {
125
- textX = width - theme.margins.right;
126
- textAnchor = 'end';
127
- }
128
- titleGroup
129
- .append('text')
130
- .attr('x', textX)
131
- .attr('y', this.marginTop + this.fontSize)
132
- .attr('text-anchor', textAnchor)
133
- .attr('font-size', `${this.fontSize}px`)
134
- .attr('font-weight', this.fontWeight)
135
- .attr('font-family', this.fontFamily || theme.fontFamily)
136
- .text(this.text);
83
+ this.textComponent.render(svg, theme, width, x, y);
137
84
  }
138
85
  }
package/dist/tooltip.js CHANGED
@@ -269,7 +269,11 @@ export class Tooltip {
269
269
  .attr('aria-hidden', 'true')
270
270
  .style('fill', 'none')
271
271
  .style('pointer-events', 'all');
272
- const pointSeries = series.filter((currentSeries) => {
272
+ const focusCircleSeries = series.filter((currentSeries) => {
273
+ if (currentSeries.type === 'line' &&
274
+ currentSeries.points.show === 'never') {
275
+ return false;
276
+ }
273
277
  return (currentSeries.type === 'line' ||
274
278
  currentSeries.type === 'area' ||
275
279
  currentSeries.type === 'scatter');
@@ -278,7 +282,7 @@ export class Tooltip {
278
282
  return currentSeries.type === 'bar';
279
283
  });
280
284
  const hasBarSeries = barSeries.length > 0;
281
- const focusCircles = pointSeries.map((currentSeries) => {
285
+ const focusCircles = focusCircleSeries.map((currentSeries) => {
282
286
  const seriesColor = getSeriesColor(currentSeries);
283
287
  return svg
284
288
  .append('circle')
@@ -303,7 +307,7 @@ export class Tooltip {
303
307
  const updateVisualStateAtIndex = (closestIndex) => {
304
308
  const dataPoint = data[closestIndex];
305
309
  const dataPointPosition = dataPointPositions[closestIndex];
306
- pointSeries.forEach((currentSeries, seriesIndex) => {
310
+ focusCircleSeries.forEach((currentSeries, seriesIndex) => {
307
311
  const value = resolveSeriesValue(currentSeries, dataPoint, closestIndex);
308
312
  if (!Number.isFinite(value)) {
309
313
  focusCircles[seriesIndex].style('opacity', 0);