@internetstiftelsen/charts 0.0.7 → 0.0.8

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/bar.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Selection } from 'd3';
2
- import type { BarConfig, DataItem, ScaleType, Orientation, ChartTheme } from './types.js';
2
+ import type { BarConfig, DataItem, ScaleType, Orientation, ChartTheme, BarValueLabelConfig } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
4
  export declare class Bar implements ChartComponent {
5
5
  readonly type: "bar";
@@ -7,8 +7,13 @@ export declare class Bar implements ChartComponent {
7
7
  readonly fill: string;
8
8
  readonly colorAdapter?: (data: DataItem, index: number) => string;
9
9
  readonly orientation: Orientation;
10
+ readonly maxBarSize?: number;
11
+ readonly valueLabel?: BarValueLabelConfig;
10
12
  constructor(config: BarConfig);
11
- render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType?: ScaleType, _theme?: ChartTheme): void;
13
+ private getScaledPosition;
14
+ render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType?: ScaleType, theme?: ChartTheme): void;
12
15
  private renderVertical;
13
16
  private renderHorizontal;
17
+ private renderVerticalValueLabels;
18
+ private renderHorizontalValueLabels;
14
19
  }
package/bar.js CHANGED
@@ -30,40 +30,62 @@ export class Bar {
30
30
  writable: true,
31
31
  value: void 0
32
32
  });
33
+ Object.defineProperty(this, "maxBarSize", {
34
+ enumerable: true,
35
+ configurable: true,
36
+ writable: true,
37
+ value: void 0
38
+ });
39
+ Object.defineProperty(this, "valueLabel", {
40
+ enumerable: true,
41
+ configurable: true,
42
+ writable: true,
43
+ value: void 0
44
+ });
33
45
  this.dataKey = config.dataKey;
34
46
  this.fill = config.fill || '#8884d8';
35
47
  this.colorAdapter = config.colorAdapter;
36
48
  this.orientation = config.orientation || 'vertical';
49
+ this.maxBarSize = config.maxBarSize;
50
+ this.valueLabel = config.valueLabel;
51
+ }
52
+ getScaledPosition(data, key, scale, scaleType) {
53
+ const value = data[key];
54
+ let scaledValue;
55
+ switch (scaleType) {
56
+ case 'band':
57
+ scaledValue = value;
58
+ break;
59
+ case 'time':
60
+ scaledValue = value instanceof Date ? value : new Date(value);
61
+ break;
62
+ case 'linear':
63
+ case 'log':
64
+ scaledValue = typeof value === 'number' ? value : Number(value);
65
+ break;
66
+ }
67
+ return scale(scaledValue) || 0;
37
68
  }
38
- render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', _theme) {
69
+ render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
39
70
  if (this.orientation === 'vertical') {
40
71
  this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType);
41
72
  }
42
73
  else {
43
74
  this.renderHorizontal(plotGroup, data, xKey, x, y, parseValue, xScaleType);
44
75
  }
76
+ // Render value labels if enabled
77
+ if (this.valueLabel?.show && theme) {
78
+ if (this.orientation === 'vertical') {
79
+ this.renderVerticalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme);
80
+ }
81
+ else {
82
+ this.renderHorizontalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme);
83
+ }
84
+ }
45
85
  }
46
86
  renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType) {
47
- const getXPosition = (d) => {
48
- const xValue = d[xKey];
49
- let scaledValue;
50
- switch (xScaleType) {
51
- case 'band':
52
- scaledValue = xValue;
53
- break;
54
- case 'time':
55
- scaledValue =
56
- xValue instanceof Date ? xValue : new Date(xValue);
57
- break;
58
- case 'linear':
59
- case 'log':
60
- scaledValue =
61
- typeof xValue === 'number' ? xValue : Number(xValue);
62
- break;
63
- }
64
- return x(scaledValue) || 0;
65
- };
66
87
  const bandwidth = x.bandwidth ? x.bandwidth() : 20;
88
+ const barWidth = this.maxBarSize ? Math.min(bandwidth, this.maxBarSize) : bandwidth;
67
89
  // Get the baseline value from the Y scale's domain
68
90
  // For linear scales, use 0 if it's in the domain, otherwise use domain max (bottom of chart)
69
91
  // For log scales, use the minimum value from the domain
@@ -77,15 +99,16 @@ export class Bar {
77
99
  .join('rect')
78
100
  .attr('class', `bar-${this.dataKey.replace(/\s+/g, '-')}`)
79
101
  .attr('x', (d) => {
80
- const xPos = getXPosition(d);
102
+ const xPos = this.getScaledPosition(d, xKey, x, xScaleType);
103
+ const offset = (bandwidth - barWidth) / 2;
81
104
  // For non-band scales, center the bar
82
- return xScaleType === 'band' ? xPos : xPos - bandwidth / 2;
105
+ return xScaleType === 'band' ? xPos + offset : xPos - barWidth / 2;
83
106
  })
84
107
  .attr('y', (d) => {
85
108
  const yPos = y(parseValue(d[this.dataKey])) || 0;
86
109
  return Math.min(yBaseline, yPos);
87
110
  })
88
- .attr('width', bandwidth)
111
+ .attr('width', barWidth)
89
112
  .attr('height', (d) => {
90
113
  const yPos = y(parseValue(d[this.dataKey])) || 0;
91
114
  return Math.abs(yBaseline - yPos);
@@ -93,26 +116,8 @@ export class Bar {
93
116
  .attr('fill', (d, i) => this.colorAdapter ? this.colorAdapter(d, i) : this.fill);
94
117
  }
95
118
  renderHorizontal(plotGroup, data, xKey, x, y, parseValue, yScaleType) {
96
- const getYPosition = (d) => {
97
- const yValue = d[xKey];
98
- let scaledValue;
99
- switch (yScaleType) {
100
- case 'band':
101
- scaledValue = yValue;
102
- break;
103
- case 'time':
104
- scaledValue =
105
- yValue instanceof Date ? yValue : new Date(yValue);
106
- break;
107
- case 'linear':
108
- case 'log':
109
- scaledValue =
110
- typeof yValue === 'number' ? yValue : Number(yValue);
111
- break;
112
- }
113
- return y(scaledValue) || 0;
114
- };
115
119
  const bandwidth = y.bandwidth ? y.bandwidth() : 20;
120
+ const barHeight = this.maxBarSize ? Math.min(bandwidth, this.maxBarSize) : bandwidth;
116
121
  // Get the baseline value from the scale's domain
117
122
  // For linear scales, use 0 if it's in the domain, otherwise use domain min
118
123
  // For log scales, use the minimum value from the domain
@@ -130,15 +135,220 @@ export class Bar {
130
135
  return Math.min(xBaseline, xPos);
131
136
  })
132
137
  .attr('y', (d) => {
133
- const yPos = getYPosition(d);
138
+ const yPos = this.getScaledPosition(d, xKey, y, yScaleType);
139
+ const offset = (bandwidth - barHeight) / 2;
134
140
  // For non-band scales, center the bar
135
- return yScaleType === 'band' ? yPos : yPos - bandwidth / 2;
141
+ return yScaleType === 'band' ? yPos + offset : yPos - barHeight / 2;
136
142
  })
137
143
  .attr('width', (d) => {
138
144
  const xPos = x(parseValue(d[this.dataKey])) || 0;
139
145
  return Math.abs(xPos - xBaseline);
140
146
  })
141
- .attr('height', bandwidth)
147
+ .attr('height', barHeight)
142
148
  .attr('fill', (d, i) => this.colorAdapter ? this.colorAdapter(d, i) : this.fill);
143
149
  }
150
+ renderVerticalValueLabels(plotGroup, data, xKey, x, y, parseValue, xScaleType, theme) {
151
+ const bandwidth = x.bandwidth ? x.bandwidth() : 20;
152
+ const barWidth = this.maxBarSize ? Math.min(bandwidth, this.maxBarSize) : bandwidth;
153
+ const yDomain = y.domain();
154
+ const baselineValue = yDomain[0] >= 0 ? Math.max(0, yDomain[0]) : yDomain[0];
155
+ const yBaseline = y(baselineValue) || 0;
156
+ const config = this.valueLabel;
157
+ const position = config.position || 'outside';
158
+ const insidePosition = config.insidePosition || 'top';
159
+ const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
160
+ const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
161
+ const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
162
+ const color = config.color ?? theme.valueLabel.color;
163
+ const background = config.background ?? theme.valueLabel.background;
164
+ const border = config.border ?? theme.valueLabel.border;
165
+ const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
166
+ const padding = config.padding ?? theme.valueLabel.padding;
167
+ const labelGroup = plotGroup
168
+ .append('g')
169
+ .attr('class', `bar-value-labels-${this.dataKey.replace(/\s+/g, '-')}`);
170
+ data.forEach((d) => {
171
+ const value = parseValue(d[this.dataKey]);
172
+ const valueText = String(value);
173
+ const xPos = this.getScaledPosition(d, xKey, x, xScaleType);
174
+ const yPos = y(value) || 0;
175
+ const barHeight = Math.abs(yBaseline - yPos);
176
+ const barTop = Math.min(yBaseline, yPos);
177
+ const barBottom = Math.max(yBaseline, yPos);
178
+ const barCenterX = xPos + (xScaleType === 'band' ? (bandwidth - barWidth) / 2 : -barWidth / 2) + barWidth / 2;
179
+ // Create temporary text to measure dimensions
180
+ const tempText = labelGroup
181
+ .append('text')
182
+ .style('font-size', `${fontSize}px`)
183
+ .style('font-family', fontFamily)
184
+ .style('font-weight', fontWeight)
185
+ .text(valueText);
186
+ const textBBox = tempText.node().getBBox();
187
+ const boxWidth = textBBox.width + padding * 2;
188
+ const boxHeight = textBBox.height + padding * 2;
189
+ let labelX = barCenterX;
190
+ let labelY;
191
+ let shouldRender = true;
192
+ if (position === 'outside') {
193
+ // Place above the bar
194
+ labelY = barTop - boxHeight / 2 - 4;
195
+ // Check if it fits (not going above plot area)
196
+ const plotTop = y.range()[1];
197
+ if (labelY - boxHeight / 2 < plotTop) {
198
+ shouldRender = false;
199
+ }
200
+ }
201
+ else {
202
+ // Inside the bar
203
+ switch (insidePosition) {
204
+ case 'top':
205
+ labelY = barTop + boxHeight / 2 + 4;
206
+ break;
207
+ case 'middle':
208
+ labelY = (barTop + barBottom) / 2;
209
+ break;
210
+ case 'bottom':
211
+ labelY = barBottom - boxHeight / 2 - 4;
212
+ break;
213
+ }
214
+ // Check if it fits inside the bar
215
+ if (boxHeight + 8 > barHeight) {
216
+ shouldRender = false;
217
+ }
218
+ }
219
+ tempText.remove();
220
+ if (shouldRender) {
221
+ const group = labelGroup.append('g');
222
+ if (position === 'outside') {
223
+ // Draw rounded rectangle background
224
+ group
225
+ .append('rect')
226
+ .attr('x', labelX - boxWidth / 2)
227
+ .attr('y', labelY - boxHeight / 2)
228
+ .attr('width', boxWidth)
229
+ .attr('height', boxHeight)
230
+ .attr('rx', borderRadius)
231
+ .attr('ry', borderRadius)
232
+ .attr('fill', background)
233
+ .attr('stroke', border)
234
+ .attr('stroke-width', 1);
235
+ }
236
+ // Draw text
237
+ group
238
+ .append('text')
239
+ .attr('x', labelX)
240
+ .attr('y', labelY)
241
+ .attr('text-anchor', 'middle')
242
+ .attr('dominant-baseline', 'central')
243
+ .style('font-size', `${fontSize}px`)
244
+ .style('font-family', fontFamily)
245
+ .style('font-weight', fontWeight)
246
+ .style('fill', color)
247
+ .style('pointer-events', 'none')
248
+ .text(valueText);
249
+ }
250
+ });
251
+ }
252
+ renderHorizontalValueLabels(plotGroup, data, xKey, x, y, parseValue, yScaleType, theme) {
253
+ const bandwidth = y.bandwidth ? y.bandwidth() : 20;
254
+ const barHeight = this.maxBarSize ? Math.min(bandwidth, this.maxBarSize) : bandwidth;
255
+ const domain = x.domain();
256
+ const baselineValue = domain[0] >= 0 ? Math.max(0, domain[0]) : domain[0];
257
+ const xBaseline = x(baselineValue) || 0;
258
+ const config = this.valueLabel;
259
+ const position = config.position || 'outside';
260
+ const insidePosition = config.insidePosition || 'top';
261
+ const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
262
+ const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
263
+ const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
264
+ const color = config.color ?? theme.valueLabel.color;
265
+ const background = config.background ?? theme.valueLabel.background;
266
+ const border = config.border ?? theme.valueLabel.border;
267
+ const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
268
+ const padding = config.padding ?? theme.valueLabel.padding;
269
+ const labelGroup = plotGroup
270
+ .append('g')
271
+ .attr('class', `bar-value-labels-${this.dataKey.replace(/\s+/g, '-')}`);
272
+ data.forEach((d) => {
273
+ const value = parseValue(d[this.dataKey]);
274
+ const valueText = String(value);
275
+ const yPos = this.getScaledPosition(d, xKey, y, yScaleType);
276
+ const xPos = x(value) || 0;
277
+ const barWidth = Math.abs(xPos - xBaseline);
278
+ const barLeft = Math.min(xBaseline, xPos);
279
+ const barRight = Math.max(xBaseline, xPos);
280
+ const barCenterY = yPos + (yScaleType === 'band' ? (bandwidth - barHeight) / 2 : -barHeight / 2) + barHeight / 2;
281
+ // Create temporary text to measure dimensions
282
+ const tempText = labelGroup
283
+ .append('text')
284
+ .style('font-size', `${fontSize}px`)
285
+ .style('font-family', fontFamily)
286
+ .style('font-weight', fontWeight)
287
+ .text(valueText);
288
+ const textBBox = tempText.node().getBBox();
289
+ const boxWidth = textBBox.width + padding * 2;
290
+ const boxHeight = textBBox.height + padding * 2;
291
+ let labelX;
292
+ let labelY = barCenterY;
293
+ let shouldRender = true;
294
+ if (position === 'outside') {
295
+ // Place to the right of the bar
296
+ labelX = barRight + boxWidth / 2 + 4;
297
+ // Check if it fits (not going beyond plot area)
298
+ const plotRight = x.range()[1];
299
+ if (labelX + boxWidth / 2 > plotRight) {
300
+ shouldRender = false;
301
+ }
302
+ }
303
+ else {
304
+ // Inside the bar - map top/middle/bottom to start/middle/end for horizontal
305
+ switch (insidePosition) {
306
+ case 'top': // start of bar (left side)
307
+ labelX = barLeft + boxWidth / 2 + 4;
308
+ break;
309
+ case 'middle':
310
+ labelX = (barLeft + barRight) / 2;
311
+ break;
312
+ case 'bottom': // end of bar (right side)
313
+ labelX = barRight - boxWidth / 2 - 4;
314
+ break;
315
+ }
316
+ // Check if it fits inside the bar
317
+ if (boxWidth + 8 > barWidth) {
318
+ shouldRender = false;
319
+ }
320
+ }
321
+ tempText.remove();
322
+ if (shouldRender) {
323
+ const group = labelGroup.append('g');
324
+ if (position === 'outside') {
325
+ // Draw rounded rectangle background
326
+ group
327
+ .append('rect')
328
+ .attr('x', labelX - boxWidth / 2)
329
+ .attr('y', labelY - boxHeight / 2)
330
+ .attr('width', boxWidth)
331
+ .attr('height', boxHeight)
332
+ .attr('rx', borderRadius)
333
+ .attr('ry', borderRadius)
334
+ .attr('fill', background)
335
+ .attr('stroke', border)
336
+ .attr('stroke-width', 1);
337
+ }
338
+ // Draw text
339
+ group
340
+ .append('text')
341
+ .attr('x', labelX)
342
+ .attr('y', labelY)
343
+ .attr('text-anchor', 'middle')
344
+ .attr('dominant-baseline', 'central')
345
+ .style('font-size', `${fontSize}px`)
346
+ .style('font-family', fontFamily)
347
+ .style('font-weight', fontWeight)
348
+ .style('fill', color)
349
+ .style('pointer-events', 'none')
350
+ .text(valueText);
351
+ }
352
+ });
353
+ }
144
354
  }
package/line.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { type Selection } from 'd3';
2
- import type { LineConfig, DataItem, ScaleType, ChartTheme } from './types.js';
2
+ import type { LineConfig, DataItem, ScaleType, ChartTheme, LineValueLabelConfig } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
4
  export declare class Line implements ChartComponent {
5
5
  readonly type: "line";
6
6
  readonly dataKey: string;
7
7
  readonly stroke: string;
8
8
  readonly strokeWidth?: number;
9
+ readonly valueLabel?: LineValueLabelConfig;
9
10
  constructor(config: LineConfig);
10
11
  render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType: ScaleType | undefined, theme: ChartTheme): void;
12
+ private renderValueLabels;
11
13
  }
package/line.js CHANGED
@@ -25,9 +25,16 @@ export class Line {
25
25
  writable: true,
26
26
  value: void 0
27
27
  });
28
+ Object.defineProperty(this, "valueLabel", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: void 0
33
+ });
28
34
  this.dataKey = config.dataKey;
29
35
  this.stroke = config.stroke || '#8884d8';
30
36
  this.strokeWidth = config.strokeWidth;
37
+ this.valueLabel = config.valueLabel;
31
38
  }
32
39
  render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
33
40
  const getXPosition = (d) => {
@@ -83,5 +90,83 @@ export class Line {
83
90
  .attr('fill', pointColor)
84
91
  .attr('stroke', pointStrokeColor)
85
92
  .attr('stroke-width', pointStrokeWidth);
93
+ // Render value labels if enabled
94
+ if (this.valueLabel?.show) {
95
+ this.renderValueLabels(plotGroup, data, y, parseValue, theme, getXPosition);
96
+ }
97
+ }
98
+ renderValueLabels(plotGroup, data, y, parseValue, theme, getXPosition) {
99
+ const config = this.valueLabel;
100
+ const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
101
+ const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
102
+ const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
103
+ const color = config.color ?? theme.valueLabel.color;
104
+ const background = config.background ?? theme.valueLabel.background;
105
+ const border = config.border ?? theme.valueLabel.border;
106
+ const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
107
+ const padding = config.padding ?? theme.valueLabel.padding;
108
+ const labelGroup = plotGroup
109
+ .append('g')
110
+ .attr('class', `line-value-labels-${this.dataKey.replace(/\s+/g, '-')}`);
111
+ const plotTop = y.range()[1];
112
+ const plotBottom = y.range()[0];
113
+ data.forEach((d) => {
114
+ const value = parseValue(d[this.dataKey]);
115
+ const valueText = String(value);
116
+ const xPos = getXPosition(d);
117
+ const yPos = y(value) || 0;
118
+ // Create temporary text to measure dimensions
119
+ const tempText = labelGroup
120
+ .append('text')
121
+ .style('font-size', `${fontSize}px`)
122
+ .style('font-family', fontFamily)
123
+ .style('font-weight', fontWeight)
124
+ .text(valueText);
125
+ const textBBox = tempText.node().getBBox();
126
+ const boxWidth = textBBox.width + padding * 2;
127
+ const boxHeight = textBBox.height + padding * 2;
128
+ let labelX = xPos;
129
+ let labelY;
130
+ let shouldRender = true;
131
+ // Default: place above the point
132
+ labelY = yPos - boxHeight / 2 - theme.line.point.size - 4;
133
+ // If too close to top, place below instead
134
+ if (labelY - boxHeight / 2 < plotTop + 4) {
135
+ labelY = yPos + boxHeight / 2 + theme.line.point.size + 4;
136
+ // Check if it fits below
137
+ if (labelY + boxHeight / 2 > plotBottom - 4) {
138
+ shouldRender = false;
139
+ }
140
+ }
141
+ tempText.remove();
142
+ if (shouldRender) {
143
+ const group = labelGroup.append('g');
144
+ // Draw rounded rectangle background
145
+ group
146
+ .append('rect')
147
+ .attr('x', labelX - boxWidth / 2)
148
+ .attr('y', labelY - boxHeight / 2)
149
+ .attr('width', boxWidth)
150
+ .attr('height', boxHeight)
151
+ .attr('rx', borderRadius)
152
+ .attr('ry', borderRadius)
153
+ .attr('fill', background)
154
+ .attr('stroke', border)
155
+ .attr('stroke-width', 1);
156
+ // Draw text
157
+ group
158
+ .append('text')
159
+ .attr('x', labelX)
160
+ .attr('y', labelY)
161
+ .attr('text-anchor', 'middle')
162
+ .attr('dominant-baseline', 'central')
163
+ .style('font-size', `${fontSize}px`)
164
+ .style('font-family', fontFamily)
165
+ .style('font-weight', fontWeight)
166
+ .style('fill', color)
167
+ .style('pointer-events', 'none')
168
+ .text(valueText);
169
+ }
170
+ });
86
171
  }
87
172
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.7",
2
+ "version": "0.0.8",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
5
  "sideEffects": false,
package/theme.js CHANGED
@@ -40,6 +40,16 @@ export const defaultTheme = {
40
40
  size: 5,
41
41
  },
42
42
  },
43
+ valueLabel: {
44
+ fontSize: 12,
45
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
46
+ fontWeight: '600',
47
+ color: '#1f2a36',
48
+ background: 'rgba(255, 255, 255, 0.95)',
49
+ border: '#e0e0e0',
50
+ borderRadius: 4,
51
+ padding: 4,
52
+ },
43
53
  };
44
54
  export const newspaperTheme = {
45
55
  width: 928,
@@ -84,6 +94,16 @@ export const newspaperTheme = {
84
94
  size: 5,
85
95
  },
86
96
  },
97
+ valueLabel: {
98
+ fontSize: 11,
99
+ fontFamily: 'Georgia, "Times New Roman", Times, serif',
100
+ fontWeight: '600',
101
+ color: '#1a1a1a',
102
+ background: 'rgba(245, 245, 220, 0.95)',
103
+ border: '#2c2c2c',
104
+ borderRadius: 2,
105
+ padding: 3,
106
+ },
87
107
  };
88
108
  export const themes = {
89
109
  default: defaultTheme,
package/tooltip.d.ts CHANGED
@@ -5,8 +5,14 @@ import type { Line } from './line.js';
5
5
  import type { Bar } from './bar.js';
6
6
  import type { PlotAreaBounds } from './layout-manager.js';
7
7
  export declare class Tooltip implements ChartComponent {
8
+ readonly id = "iisChartTooltip";
8
9
  readonly type: "tooltip";
9
10
  readonly formatter?: (dataKey: string, value: any, data: DataItem) => string;
11
+ readonly labelFormatter?: (label: string, data: DataItem) => string;
12
+ readonly customFormatter?: (data: DataItem, series: {
13
+ dataKey: string;
14
+ [key: string]: any;
15
+ }[]) => string;
10
16
  private tooltipDiv;
11
17
  constructor(config?: TooltipConfig);
12
18
  initialize(theme: ChartTheme): void;
package/tooltip.js CHANGED
@@ -2,6 +2,12 @@ import { pointer, select } from 'd3';
2
2
  import { getSeriesColor } from './types.js';
3
3
  export class Tooltip {
4
4
  constructor(config) {
5
+ Object.defineProperty(this, "id", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: 'iisChartTooltip'
10
+ });
5
11
  Object.defineProperty(this, "type", {
6
12
  enumerable: true,
7
13
  configurable: true,
@@ -14,6 +20,18 @@ export class Tooltip {
14
20
  writable: true,
15
21
  value: void 0
16
22
  });
23
+ Object.defineProperty(this, "labelFormatter", {
24
+ enumerable: true,
25
+ configurable: true,
26
+ writable: true,
27
+ value: void 0
28
+ });
29
+ Object.defineProperty(this, "customFormatter", {
30
+ enumerable: true,
31
+ configurable: true,
32
+ writable: true,
33
+ value: void 0
34
+ });
17
35
  Object.defineProperty(this, "tooltipDiv", {
18
36
  enumerable: true,
19
37
  configurable: true,
@@ -21,11 +39,15 @@ export class Tooltip {
21
39
  value: null
22
40
  });
23
41
  this.formatter = config?.formatter;
42
+ this.labelFormatter = config?.labelFormatter;
43
+ this.customFormatter = config?.customFormatter;
24
44
  }
25
45
  initialize(theme) {
46
+ this.cleanup();
26
47
  this.tooltipDiv = select('body')
27
48
  .append('div')
28
49
  .attr('class', 'chart-tooltip')
50
+ .attr('id', this.id)
29
51
  .style('position', 'absolute')
30
52
  .style('visibility', 'hidden')
31
53
  .style('background-color', 'white')
@@ -43,6 +65,8 @@ export class Tooltip {
43
65
  return;
44
66
  const tooltip = this.tooltipDiv;
45
67
  const formatter = this.formatter;
68
+ const labelFormatter = this.labelFormatter;
69
+ const customFormatter = this.customFormatter;
46
70
  // Helper to get x position for any scale type
47
71
  const getXPosition = (dataPoint) => {
48
72
  const xValue = dataPoint[xKey];
@@ -102,17 +126,27 @@ export class Tooltip {
102
126
  .style('opacity', 1);
103
127
  });
104
128
  // Build tooltip content
105
- let content = `<strong>${dataPoint[xKey]}</strong><br/>`;
106
- series.forEach((s) => {
107
- const value = dataPoint[s.dataKey];
108
- if (formatter) {
109
- content +=
110
- formatter(s.dataKey, value, dataPoint) + '<br/>';
111
- }
112
- else {
113
- content += `${s.dataKey}: ${value}<br/>`;
114
- }
115
- });
129
+ let content;
130
+ if (customFormatter) {
131
+ content = customFormatter(dataPoint, series);
132
+ }
133
+ else {
134
+ const label = labelFormatter
135
+ ? labelFormatter(dataPoint[xKey], dataPoint)
136
+ : dataPoint[xKey];
137
+ content = `<strong>${label}</strong><br/>`;
138
+ series.forEach((s) => {
139
+ const value = dataPoint[s.dataKey];
140
+ if (formatter) {
141
+ content +=
142
+ formatter(s.dataKey, value, dataPoint) +
143
+ '<br/>';
144
+ }
145
+ else {
146
+ content += `${s.dataKey}: ${value}<br/>`;
147
+ }
148
+ });
149
+ }
116
150
  // Position tooltip relative to the data point
117
151
  const svgRect = svg.node().getBoundingClientRect();
118
152
  const tooltipX = svgRect.left + window.scrollX + xPos + 10;
@@ -132,9 +166,11 @@ export class Tooltip {
132
166
  });
133
167
  }
134
168
  cleanup() {
135
- if (this.tooltipDiv) {
136
- this.tooltipDiv.remove();
137
- this.tooltipDiv = null;
169
+ const tooltip = select(`#${this.id}`);
170
+ if (tooltip.empty()) {
171
+ return;
138
172
  }
173
+ tooltip.remove();
174
+ this.tooltipDiv = null;
139
175
  }
140
176
  }
package/types.d.ts CHANGED
@@ -36,17 +36,48 @@ export type ChartTheme = {
36
36
  size: number;
37
37
  };
38
38
  };
39
+ valueLabel: {
40
+ fontSize: number;
41
+ fontFamily: string;
42
+ fontWeight: string;
43
+ color: string;
44
+ background: string;
45
+ border: string;
46
+ borderRadius: number;
47
+ padding: number;
48
+ };
49
+ };
50
+ export type ValueLabelConfig = {
51
+ fontSize?: number;
52
+ fontFamily?: string;
53
+ fontWeight?: string;
54
+ color?: string;
55
+ background?: string;
56
+ border?: string;
57
+ borderRadius?: number;
58
+ padding?: number;
59
+ };
60
+ export type LineValueLabelConfig = ValueLabelConfig & {
61
+ show?: boolean;
62
+ };
63
+ export type BarValueLabelConfig = ValueLabelConfig & {
64
+ show?: boolean;
65
+ position?: 'inside' | 'outside';
66
+ insidePosition?: 'top' | 'middle' | 'bottom';
39
67
  };
40
68
  export type LineConfig = {
41
69
  dataKey: string;
42
70
  stroke?: string;
43
71
  strokeWidth?: number;
72
+ valueLabel?: LineValueLabelConfig;
44
73
  };
45
74
  export type BarConfig = {
46
75
  dataKey: string;
47
76
  fill?: string;
48
77
  colorAdapter?: (data: DataItem, index: number) => string;
49
78
  orientation?: 'vertical' | 'horizontal';
79
+ maxBarSize?: number;
80
+ valueLabel?: BarValueLabelConfig;
50
81
  };
51
82
  export declare function getSeriesColor(series: {
52
83
  stroke?: string;
@@ -65,6 +96,11 @@ export type GridConfig = {
65
96
  };
66
97
  export type TooltipConfig = {
67
98
  formatter?: (dataKey: string, value: any, data: DataItem) => string;
99
+ labelFormatter?: (label: string, data: DataItem) => string;
100
+ customFormatter?: (data: DataItem, series: {
101
+ dataKey: string;
102
+ [key: string]: any;
103
+ }[]) => string;
68
104
  };
69
105
  export type LegendConfig = {
70
106
  position?: 'bottom';