@internetstiftelsen/charts 0.0.2 → 0.0.4

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 ADDED
@@ -0,0 +1,417 @@
1
+ # Chart Library
2
+
3
+ A framework-agnostic, composable charting library built on D3.js with TypeScript.
4
+
5
+ ## Features
6
+
7
+ - **Framework Agnostic**: Core library has zero framework dependencies - works with vanilla JS, React, Vue, Svelte, or any other framework
8
+ - **Flexible Scale System**: Support for band, linear, time, and logarithmic scales
9
+ - **Composable Architecture**: Build charts by composing components (lines, axes, grids, tooltips, legends, titles)
10
+ - **Layout-Driven Design**: Components self-measure and automatically adjust chart dimensions
11
+ - **Automatic Resize**: Built-in ResizeObserver handles responsive behavior automatically
12
+ - **Type Safe**: Written in TypeScript with comprehensive type definitions
13
+ - **Data Validation**: Built-in validation with helpful error messages
14
+ - **Performance Optimized**: Data caching and minimized redundant calculations
15
+ - **Automatic Color Assignment**: Smart color palette system with sensible defaults
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @internetstiftelsen/charts
21
+ ```
22
+
23
+ ## Quick Start
24
+
25
+ ### Vanilla JavaScript
26
+
27
+ ```javascript
28
+ import { XYChart } from '@internetstiftelsen/charts/xy-chart';
29
+ import { Line } from '@internetstiftelsen/charts/line';
30
+ import { Bar } from '@internetstiftelsen/charts/bar';
31
+ import { XAxis } from '@internetstiftelsen/charts/x-axis';
32
+ import { YAxis } from '@internetstiftelsen/charts/y-axis';
33
+ import { Grid } from '@internetstiftelsen/charts/grid';
34
+ import { Tooltip } from '@internetstiftelsen/charts/tooltip';
35
+ import { Legend } from '@internetstiftelsen/charts/legend';
36
+
37
+ // Your data
38
+ const data = [
39
+ { date: '2010', revenue: 100, expenses: 80 },
40
+ { date: '2011', revenue: 150, expenses: 90 },
41
+ { date: '2012', revenue: 200, expenses: 110 },
42
+ { date: '2013', revenue: 250, expenses: 130 },
43
+ ];
44
+
45
+ // Create chart
46
+ const chart = new XYChart({ data });
47
+
48
+ // Add components
49
+ chart
50
+ .addChild(new Title({ text: 'Revenue vs Expenses' }))
51
+ .addChild(new Grid({ horizontal: true, vertical: false }))
52
+ .addChild(new XAxis({ dataKey: 'date' }))
53
+ .addChild(new YAxis())
54
+ .addChild(
55
+ new Tooltip({
56
+ formatter: (dataKey, value) => `<strong>${dataKey}</strong>: $${value}k`,
57
+ })
58
+ )
59
+ .addChild(new Legend({ position: 'bottom' }))
60
+ .addChild(new Line({ dataKey: 'revenue' })) // Auto-assigned color
61
+ .addChild(new Line({ dataKey: 'expenses' })); // Auto-assigned color
62
+
63
+ // Render to DOM (automatically resizes with container)
64
+ chart.render('#chart-container');
65
+
66
+ // Later: update with new data
67
+ chart.update(newData);
68
+
69
+ // Clean up when done
70
+ chart.destroy();
71
+ ```
72
+
73
+ ### With React (Demo Wrapper)
74
+
75
+ ```jsx
76
+ import { useRef, useEffect } from 'react';
77
+ import { XYChart, Line, Bar, XAxis, YAxis, Grid, Tooltip, Legend } from './charts';
78
+
79
+ function Chart({ data }) {
80
+ const containerRef = useRef(null);
81
+ const chartRef = useRef(null);
82
+
83
+ useEffect(() => {
84
+ if (containerRef.current) {
85
+ // Create chart
86
+ const chart = new XYChart({ data });
87
+
88
+ chart
89
+ .addChild(new Grid({ horizontal: true }))
90
+ .addChild(new XAxis({ dataKey: 'column' }))
91
+ .addChild(new YAxis())
92
+ .addChild(new Tooltip())
93
+ .addChild(new Legend({ position: 'bottom' }))
94
+ .addChild(new Line({ dataKey: 'value1' }))
95
+ .addChild(new Line({ dataKey: 'value2' }));
96
+
97
+ chart.render(containerRef.current);
98
+ chartRef.current = chart;
99
+
100
+ return () => {
101
+ chart.destroy();
102
+ };
103
+ }
104
+ }, []);
105
+
106
+ // Update when data changes
107
+ useEffect(() => {
108
+ if (chartRef.current && data) {
109
+ chartRef.current.update(data);
110
+ }
111
+ }, [data]);
112
+
113
+ return <div ref={containerRef} />;
114
+ }
115
+ ```
116
+
117
+ ## API Reference
118
+
119
+ ### XYChart
120
+
121
+ The main chart class for creating XY-coordinate charts (line, bar, or mixed).
122
+
123
+ #### Constructor
124
+
125
+ ```typescript
126
+ new XYChart(config: XYChartConfig)
127
+ ```
128
+
129
+ **Config Options:**
130
+
131
+ - `data: DataItem[]` - Array of data objects (required)
132
+ - `theme?: Partial<ChartTheme>` - Theme customization
133
+ - `width: number` - Chart max-width in pixels (default: 928)
134
+ - `height: number` - Chart height in pixels (default: 600)
135
+ - `margins: { top, right, bottom, left }` - Base margins around plot area (default: { top: 20, right: 20, bottom: 30, left: 40 })
136
+ - `colorPalette: string[]` - Array of colors for auto-assignment
137
+ - `gridColor: string` - Grid line color (default: '#e0e0e0')
138
+ - `axis: { fontFamily, fontSize }` - Axis text styling
139
+ - `scales?: AxisScaleConfig` - Scale configuration
140
+ - `x?: { type: 'band' | 'linear' | 'time' | 'log', domain?: any[], padding?: number, nice?: boolean }`
141
+ - `y?: { type: 'band' | 'linear' | 'time' | 'log', domain?: any[], padding?: number, nice?: boolean }`
142
+
143
+ #### Methods
144
+
145
+ **`addChild(component: ChartComponent): this`**
146
+ Adds a component to the chart (chainable).
147
+
148
+ **`render(target: string): HTMLElement`**
149
+ Renders the chart to a DOM element specified by CSS selector. Automatically sets up resize handling.
150
+
151
+ **`update(data: DataItem[]): void`**
152
+ Updates the chart with new data and re-renders.
153
+
154
+ **`destroy(): void`**
155
+ Cleans up all resources, removes resize observer, and clears the chart from the DOM.
156
+
157
+ ### Components
158
+
159
+ #### Line
160
+
161
+ Renders a line series on the chart.
162
+
163
+ ```typescript
164
+ new Line({
165
+ dataKey: string, // Key in data objects for Y values (required)
166
+ stroke? : string, // Line color (auto-assigned if omitted)
167
+ strokeWidth? : number, // Line width in pixels (default: 2)
168
+ })
169
+ ```
170
+
171
+ #### Bar
172
+
173
+ Renders a bar series on the chart.
174
+
175
+ ```typescript
176
+ new Bar({
177
+ dataKey: string, // Key in data objects for Y values (required)
178
+ fill? : string, // Bar color (auto-assigned if omitted)
179
+ })
180
+ ```
181
+
182
+ #### XAxis
183
+
184
+ Renders the X axis.
185
+
186
+ ```typescript
187
+ new XAxis({
188
+ dataKey? : string, // Key in data objects for X values (auto-detected if omitted)
189
+ })
190
+ ```
191
+
192
+ #### YAxis
193
+
194
+ Renders the Y axis.
195
+
196
+ ```typescript
197
+ new YAxis({
198
+ tickFormat?: string | null, // D3 format specifier (e.g., 's' for SI-prefix like "35k"). Default: null (no formatting)
199
+ })
200
+ ```
201
+
202
+ **Examples:**
203
+ ```javascript
204
+ new YAxis() // Shows raw numbers: 35000
205
+ new YAxis({ tickFormat: 's' }) // Shows SI-prefix: 35k
206
+ new YAxis({ tickFormat: '$,' }) // Shows formatted: $35,000
207
+ ```
208
+
209
+ #### Grid
210
+
211
+ Renders grid lines.
212
+
213
+ ```typescript
214
+ new Grid({
215
+ horizontal? : boolean, // Show horizontal lines (default: true)
216
+ vertical? : boolean, // Show vertical lines (default: true)
217
+ })
218
+ ```
219
+
220
+ #### Tooltip
221
+
222
+ Renders interactive tooltips on hover.
223
+
224
+ ```typescript
225
+ new Tooltip({
226
+ formatter? : (dataKey: string, value: any, data: DataItem) => string
227
+ })
228
+ ```
229
+
230
+ **Example formatter:**
231
+
232
+ ```javascript
233
+ new Tooltip({
234
+ formatter: (dataKey, value, data) =>
235
+ `<strong>${dataKey}</strong><br/>Value: ${value}<br/>Date: ${data.date}`
236
+ })
237
+ ```
238
+
239
+ #### Legend
240
+
241
+ Renders a legend for the chart.
242
+
243
+ ```typescript
244
+ new Legend({
245
+ position?: 'bottom', // Position (currently only 'bottom' supported)
246
+ marginTop?: number, // Space above legend (default: 20)
247
+ marginBottom?: number, // Space below legend (default: 10)
248
+ })
249
+ ```
250
+
251
+ #### Title
252
+
253
+ Renders a title for the chart.
254
+
255
+ ```typescript
256
+ new Title({
257
+ text: string, // Title text (required)
258
+ fontSize?: number, // Font size in pixels (default: 18)
259
+ fontWeight?: string, // Font weight (default: 'bold')
260
+ align?: 'left' | 'center' | 'right', // Alignment (default: 'center')
261
+ marginTop?: number, // Space above title (default: 10)
262
+ marginBottom?: number, // Space below title (default: 15)
263
+ })
264
+ ```
265
+
266
+ ## Advanced Usage
267
+
268
+ ### Custom Scale Types
269
+
270
+ Use time scales for temporal data:
271
+
272
+ ```javascript
273
+ const chart = new XYChart({
274
+ data: [
275
+ { date: new Date('2024-01-01'), value: 100 },
276
+ { date: new Date('2024-01-02'), value: 150 },
277
+ ],
278
+ scales: {
279
+ x: { type: 'time', nice: true },
280
+ y: { type: 'linear', nice: true },
281
+ },
282
+ });
283
+ ```
284
+
285
+ Use logarithmic scales for exponential data:
286
+
287
+ ```javascript
288
+ const chart = new XYChart({
289
+ data: [
290
+ { x: 1, y: 10 },
291
+ { x: 2, y: 100 },
292
+ { x: 3, y: 1000 },
293
+ ],
294
+ scales: {
295
+ y: { type: 'log', domain: [1, 10000] },
296
+ },
297
+ });
298
+ ```
299
+
300
+ ### Custom Theming
301
+
302
+ ```javascript
303
+ const chart = new XYChart({
304
+ data,
305
+ theme: {
306
+ width: 1200, // Max-width (chart won't exceed this)
307
+ height: 600,
308
+ margins: {
309
+ top: 30,
310
+ right: 30,
311
+ bottom: 40,
312
+ left: 60,
313
+ },
314
+ colorPalette: ['#ff6b6b', '#4ecdc4', '#45b7d1', '#f7b731'],
315
+ gridColor: '#333333',
316
+ axis: {
317
+ fontFamily: 'Inter, sans-serif',
318
+ fontSize: '12',
319
+ },
320
+ },
321
+ });
322
+ ```
323
+
324
+ ### Manual Color Assignment
325
+
326
+ ```javascript
327
+ chart
328
+ .addChild(new Line({ dataKey: 'revenue', stroke: '#00ff00' }))
329
+ .addChild(new Line({ dataKey: 'expenses', stroke: '#ff0000' }));
330
+ ```
331
+
332
+ ### Responsive Charts
333
+
334
+ Charts automatically resize with their container using ResizeObserver. The chart width adapts to the container up to the `theme.width` (which acts as max-width).
335
+
336
+ ```javascript
337
+ // Container width: 500px → Chart width: 500px
338
+ // Container width: 1200px → Chart width: 928px (theme default max-width)
339
+
340
+ // Custom max-width
341
+ const chart = new XYChart({
342
+ data,
343
+ theme: { width: 1200 }, // Chart won't exceed 1200px
344
+ });
345
+ ```
346
+
347
+ **No manual resize calls needed** - the chart automatically responds to container size changes!
348
+
349
+ ## Data Validation
350
+
351
+ The library includes built-in validation with helpful error messages:
352
+
353
+ ```javascript
354
+ // Empty data
355
+ new XYChart({ data: [] });
356
+ // Error: Data array cannot be empty
357
+
358
+ // Missing dataKey
359
+ new Line({ dataKey: 'nonexistent' });
360
+ // Error: Line: dataKey "nonexistent" not found in data items at indices: 0, 1, 2
361
+
362
+ // Invalid numeric data
363
+ new Line({ dataKey: 'textField' });
364
+ // Error: Line: No valid numeric values found for dataKey "textField"
365
+ ```
366
+
367
+ ## Browser Support
368
+
369
+ Modern browsers with ES6+ support. Uses D3.js v7.
370
+
371
+ ## TypeScript
372
+
373
+ Full TypeScript support included:
374
+
375
+ ```typescript
376
+ import type { DataItem, ChartTheme, LineConfig } from './charts/types';
377
+
378
+ const data: DataItem[] = [
379
+ { x: 1, y: 100 },
380
+ { x: 2, y: 200 },
381
+ ];
382
+
383
+ const config: LineConfig = {
384
+ dataKey: 'y',
385
+ stroke: '#8884d8',
386
+ strokeWidth: 2,
387
+ };
388
+ ```
389
+
390
+ ## Architecture
391
+
392
+ The library follows a composable, layout-driven design:
393
+
394
+ - **BaseChart**: Abstract base class providing common functionality (lifecycle, rendering, validation)
395
+ - **XYChart**: Concrete implementation for XY-coordinate charts (lines, bars, or mixed)
396
+ - **LayoutManager**: Calculates component positions and plot area dimensions (D3 margin convention)
397
+ - **LayoutAwareComponent**: Interface for self-measuring components (Title, Legend, Axes)
398
+ - **Components**: Modular components that implement `ChartComponent` or `LayoutAwareComponent`
399
+ - **Validation**: Centralized validation layer with `ChartValidator`
400
+ - **Scales**: Flexible scale factory supporting multiple D3 scale types
401
+
402
+ Key principles:
403
+ - **Layout-driven**: Components report their space requirements, plot area adjusts automatically
404
+ - **Separation of concerns**: Only the plot area (grid) scales; UI elements stay fixed size
405
+ - **D3 conventions**: Follows D3's margin convention pattern for clean, predictable layouts
406
+
407
+ This architecture makes it easy to add new chart types or series (Area, Scatter, etc.) by extending BaseChart or implementing new series components.
408
+
409
+ ## Performance
410
+
411
+ - **Data Caching**: Sorted data is cached to avoid redundant sorting operations
412
+ - **Smart Re-rendering**: Only re-renders when necessary (data updates or container resize)
413
+ - **Automatic Cleanup**: ResizeObserver and tooltips properly cleaned up on destroy
414
+ - **Minimal DOM Manipulation**: Uses D3's efficient data binding
415
+ - **SVG Optimization**: Clean SVG generation with proper cleanup
416
+ - **Small Bundle**: ~105 KB gzipped (including D3)
417
+ - **Small Bundle**: ~105 KB gzipped (including D3)
package/bar.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import type { Selection } from 'd3';
2
- import type { BarConfig, DataItem, ScaleType, Orientation } from './types.js';
3
- import type { ChartComponent } from '@/lib/chart-interface';
2
+ import type { BarConfig, DataItem, ScaleType, Orientation, ChartTheme } from './types.js';
3
+ import type { ChartComponent } from './chart-interface.js';
4
4
  export declare class Bar implements ChartComponent {
5
5
  readonly type: "bar";
6
6
  readonly dataKey: string;
7
7
  readonly fill: string;
8
8
  readonly orientation: Orientation;
9
9
  constructor(config: BarConfig);
10
- render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType?: ScaleType): void;
10
+ render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType?: ScaleType, _theme?: ChartTheme): void;
11
11
  private renderVertical;
12
12
  private renderHorizontal;
13
13
  }
package/bar.js CHANGED
@@ -28,7 +28,7 @@ export class Bar {
28
28
  this.fill = config.fill || '#8884d8';
29
29
  this.orientation = config.orientation || 'vertical';
30
30
  }
31
- render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band') {
31
+ render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', _theme) {
32
32
  if (this.orientation === 'vertical') {
33
33
  this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType);
34
34
  }
package/base-chart.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Selection } from 'd3';
2
2
  import type { DataItem, ChartTheme, AxisScaleConfig } from './types.js';
3
- import type { ChartComponent } from '@/lib/chart-interface';
3
+ import type { ChartComponent } from './chart-interface.js';
4
4
  import type { XAxis } from './x-axis.js';
5
5
  import type { YAxis } from './y-axis.js';
6
6
  import type { Grid } from './grid.js';
package/base-chart.js CHANGED
@@ -146,13 +146,13 @@ export class BaseChart {
146
146
  if (!this.container)
147
147
  return;
148
148
  // Calculate current width
149
- this.width = Math.min(this.container.getBoundingClientRect().width || this.theme.width, this.theme.width);
149
+ this.width =
150
+ this.container.getBoundingClientRect().width || this.theme.width;
150
151
  // Clear and setup SVG
151
152
  this.container.innerHTML = '';
152
153
  this.svg = create('svg')
153
154
  .attr('width', '100%')
154
155
  .attr('height', this.theme.height)
155
- .style('max-width', `${this.theme.width}px`)
156
156
  .style('display', 'block');
157
157
  this.container.appendChild(this.svg.node());
158
158
  // Calculate layout
package/grid.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Selection } from 'd3';
2
2
  import type { GridConfig, ChartTheme } from './types.js';
3
- import type { ChartComponent } from '@/lib/chart-interface';
3
+ import type { ChartComponent } from './chart-interface.js';
4
4
  export declare class Grid implements ChartComponent {
5
5
  readonly type: "grid";
6
6
  readonly horizontal: boolean;
package/grid.js CHANGED
@@ -39,8 +39,8 @@ export class Grid {
39
39
  .tickFormat(() => ''))
40
40
  .call((g) => g
41
41
  .selectAll('.tick line')
42
- .attr('stroke', theme.gridColor)
43
- .attr('stroke-opacity', 0.5))
42
+ .attr('stroke', theme.grid.color)
43
+ .attr('stroke-opacity', theme.grid.opacity))
44
44
  .selectAll('.domain')
45
45
  .remove();
46
46
  }
@@ -54,8 +54,8 @@ export class Grid {
54
54
  .tickFormat(() => ''))
55
55
  .call((g) => g
56
56
  .selectAll('.tick line')
57
- .attr('stroke', theme.gridColor)
58
- .attr('stroke-opacity', 0.5))
57
+ .attr('stroke', theme.grid.color)
58
+ .attr('stroke-opacity', theme.grid.opacity))
59
59
  .selectAll('.domain')
60
60
  .remove();
61
61
  }
package/legend.d.ts CHANGED
@@ -2,7 +2,7 @@ import { type Selection } from 'd3';
2
2
  import type { LegendConfig, ChartTheme } from './types.js';
3
3
  import type { Line } from './line.js';
4
4
  import type { Bar } from './bar.js';
5
- import type { LayoutAwareComponent, ComponentSpace } from '@/lib/chart-interface';
5
+ import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
6
6
  export declare class Legend implements LayoutAwareComponent {
7
7
  readonly type: "legend";
8
8
  readonly position: LegendConfig['position'];
@@ -15,6 +15,7 @@ export declare class Legend implements LayoutAwareComponent {
15
15
  setToggleCallback(callback: () => void): void;
16
16
  isSeriesVisible(dataKey: string): boolean;
17
17
  private getCheckmarkPath;
18
+ private parseColor;
18
19
  /**
19
20
  * Returns the space required by the legend
20
21
  */
package/legend.js CHANGED
@@ -1,4 +1,3 @@
1
- import {} from 'd3';
2
1
  import { getSeriesColor } from './types.js';
3
2
  export class Legend {
4
3
  constructor(config) {
@@ -58,6 +57,29 @@ export class Legend {
58
57
  const scale = size / 24;
59
58
  return `M ${4 * scale} ${12 * scale} L ${9 * scale} ${17 * scale} L ${20 * scale} ${6 * scale}`;
60
59
  }
60
+ parseColor(color) {
61
+ // Handle hex colors
62
+ if (color.startsWith('#')) {
63
+ const hex = color.slice(1);
64
+ const r = parseInt(hex.slice(0, 2), 16);
65
+ const g = parseInt(hex.slice(2, 4), 16);
66
+ const b = parseInt(hex.slice(4, 6), 16);
67
+ return { r, g, b };
68
+ }
69
+ // Handle rgb/rgba colors
70
+ if (color.startsWith('rgb')) {
71
+ const match = color.match(/\d+/g);
72
+ if (match) {
73
+ return {
74
+ r: parseInt(match[0]),
75
+ g: parseInt(match[1]),
76
+ b: parseInt(match[2]),
77
+ };
78
+ }
79
+ }
80
+ // Default to black if we can't parse
81
+ return { r: 0, g: 0, b: 0 };
82
+ }
61
83
  /**
62
84
  * Returns the space required by the legend
63
85
  */
@@ -137,7 +159,13 @@ export class Legend {
137
159
  .append('path')
138
160
  .attr('d', this.getCheckmarkPath(boxSize))
139
161
  .attr('fill', 'none')
140
- .attr('stroke', '#000')
162
+ .attr('stroke', (d) => {
163
+ // Calculate luminance to determine if we need black or white checkmark
164
+ const color = d.color;
165
+ const rgb = this.parseColor(color);
166
+ const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
167
+ return luminance > 0.5 ? '#000' : '#fff';
168
+ })
141
169
  .attr('stroke-width', 2)
142
170
  .attr('stroke-linecap', 'round')
143
171
  .attr('stroke-linejoin', 'round')
package/line.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { type Selection } from 'd3';
2
- import type { LineConfig, DataItem, ScaleType } from './types.js';
3
- import type { ChartComponent } from '@/lib/chart-interface';
2
+ import type { LineConfig, DataItem, ScaleType, ChartTheme } from './types.js';
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
- readonly strokeWidth: number;
8
+ readonly strokeWidth?: number;
9
9
  constructor(config: LineConfig);
10
- render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType?: ScaleType): void;
10
+ 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;
11
11
  }
package/line.js CHANGED
@@ -27,9 +27,9 @@ export class Line {
27
27
  });
28
28
  this.dataKey = config.dataKey;
29
29
  this.stroke = config.stroke || '#8884d8';
30
- this.strokeWidth = config.strokeWidth || 2;
30
+ this.strokeWidth = config.strokeWidth;
31
31
  }
32
- render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band') {
32
+ render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
33
33
  const getXPosition = (d) => {
34
34
  const xValue = d[xKey];
35
35
  // Handle different scale types appropriately
@@ -58,13 +58,18 @@ export class Line {
58
58
  const lineGenerator = line()
59
59
  .x(getXPosition)
60
60
  .y((d) => y(parseValue(d[this.dataKey])) || 0);
61
+ const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
62
+ const pointSize = theme.line.point.size;
63
+ const pointStrokeWidth = theme.line.point.strokeWidth;
64
+ const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
65
+ const pointColor = theme.line.point.color || this.stroke;
61
66
  // Add line path
62
67
  plotGroup
63
68
  .append('path')
64
69
  .datum(data)
65
70
  .attr('fill', 'none')
66
71
  .attr('stroke', this.stroke)
67
- .attr('stroke-width', this.strokeWidth)
72
+ .attr('stroke-width', lineStrokeWidth)
68
73
  .attr('d', lineGenerator);
69
74
  // Add data point circles
70
75
  plotGroup
@@ -74,9 +79,9 @@ export class Line {
74
79
  .attr('class', `circle-${this.dataKey.replace(/\s+/g, '-')}`)
75
80
  .attr('cx', getXPosition)
76
81
  .attr('cy', (d) => y(parseValue(d[this.dataKey])) || 0)
77
- .attr('r', 4)
78
- .attr('fill', this.stroke)
79
- .attr('stroke', 'white')
80
- .attr('stroke-width', 2);
82
+ .attr('r', pointSize)
83
+ .attr('fill', pointColor)
84
+ .attr('stroke', pointStrokeColor)
85
+ .attr('stroke-width', pointStrokeWidth);
81
86
  }
82
87
  }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
- "version": "0.0.2",
2
+ "version": "0.0.4",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
+ "sideEffects": false,
5
6
  "exports": {
6
7
  "./*": {
7
8
  "types": "./*.d.ts",
@@ -10,7 +11,8 @@
10
11
  },
11
12
  "files": [
12
13
  "*.js",
13
- "*.d.ts"
14
+ "*.d.ts",
15
+ "README.md"
14
16
  ],
15
17
  "scripts": {
16
18
  "dev": "vite",
@@ -18,7 +20,7 @@
18
20
  "lint": "eslint .",
19
21
  "preview": "vite preview",
20
22
  "build:lib": "tsc --project tsconfig.lib.json && tsc-alias --project tsconfig.lib.json",
21
- "prepub": "rm -rf dist && npm run build:lib && cp package.json dist",
23
+ "prepub": "rm -rf dist && npm run build:lib && cp package.json dist && cp README.md dist",
22
24
  "pub": "npm run prepub && cd dist && npm publish --access public"
23
25
  },
24
26
  "dependencies": {
package/theme.d.ts CHANGED
@@ -1,2 +1,7 @@
1
- import { type ChartTheme } from '@/lib/types';
1
+ import { type ChartTheme } from './types.js';
2
2
  export declare const defaultTheme: ChartTheme;
3
+ export declare const newspaperTheme: ChartTheme;
4
+ export declare const themes: {
5
+ default: ChartTheme;
6
+ newspaper: ChartTheme;
7
+ };
package/theme.js CHANGED
@@ -1,14 +1,17 @@
1
- import {} from '@/lib/types';
2
1
  export const defaultTheme = {
3
2
  width: 928,
4
3
  height: 600,
4
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
5
5
  margins: {
6
6
  top: 20,
7
7
  right: 20,
8
8
  bottom: 30,
9
9
  left: 40,
10
10
  },
11
- gridColor: '#e0e0e0',
11
+ grid: {
12
+ color: '#e0e0e0',
13
+ opacity: 0.5,
14
+ },
12
15
  colorPalette: [
13
16
  '#50b2fc', // ocean
14
17
  '#ff4069', // ruby
@@ -22,10 +25,67 @@ export const defaultTheme = {
22
25
  axis: {
23
26
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
24
27
  fontSize: '14',
28
+ fontWeight: 'normal',
25
29
  },
26
30
  legend: {
27
31
  boxSize: 20,
28
32
  uncheckedColor: '#d0d0d0',
29
33
  fontSize: 14,
30
34
  },
35
+ line: {
36
+ strokeWidth: 4,
37
+ point: {
38
+ strokeWidth: 3,
39
+ color: 'white',
40
+ size: 5,
41
+ },
42
+ },
43
+ };
44
+ export const newspaperTheme = {
45
+ width: 928,
46
+ height: 600,
47
+ fontFamily: 'Georgia, "Times New Roman", Times, serif',
48
+ margins: {
49
+ top: 20,
50
+ right: 20,
51
+ bottom: 30,
52
+ left: 40,
53
+ },
54
+ grid: {
55
+ color: '#2c2c2c',
56
+ opacity: 0.15,
57
+ },
58
+ colorPalette: [
59
+ '#1a1a1a', // ink black
60
+ '#8b0000', // dark red
61
+ '#4a4a4a', // charcoal
62
+ '#6b4423', // sepia brown
63
+ '#2f4f4f', // dark slate
64
+ '#556b2f', // dark olive
65
+ '#8b4513', // saddle brown
66
+ '#708090', // slate gray
67
+ ],
68
+ axis: {
69
+ fontFamily: 'Georgia, "Times New Roman", Times, serif',
70
+ fontSize: '13',
71
+ fontWeight: '600',
72
+ },
73
+ legend: {
74
+ boxSize: 18,
75
+ uncheckedColor: '#d3d3d3',
76
+ fontSize: 13,
77
+ },
78
+ line: {
79
+ strokeWidth: 2.5,
80
+ point: {
81
+ strokeWidth: 1.5,
82
+ strokeColor: '#1a1a1a',
83
+ color: '#f5f5dc',
84
+ size: 5,
85
+ },
86
+ },
87
+ };
88
+ export const themes = {
89
+ default: defaultTheme,
90
+ newspaper: newspaperTheme,
31
91
  };
package/title.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare class Title implements LayoutAwareComponent {
6
6
  readonly text: string;
7
7
  private readonly fontSize;
8
8
  private readonly fontWeight;
9
+ private readonly fontFamily?;
9
10
  private readonly align;
10
11
  private readonly marginTop;
11
12
  private readonly marginBottom;
package/title.js CHANGED
@@ -1,4 +1,3 @@
1
- import {} from 'd3';
2
1
  export class Title {
3
2
  constructor(config) {
4
3
  Object.defineProperty(this, "type", {
@@ -25,6 +24,12 @@ export class Title {
25
24
  writable: true,
26
25
  value: void 0
27
26
  });
27
+ Object.defineProperty(this, "fontFamily", {
28
+ enumerable: true,
29
+ configurable: true,
30
+ writable: true,
31
+ value: void 0
32
+ });
28
33
  Object.defineProperty(this, "align", {
29
34
  enumerable: true,
30
35
  configurable: true,
@@ -46,6 +51,7 @@ export class Title {
46
51
  this.text = config.text;
47
52
  this.fontSize = config.fontSize ?? 18;
48
53
  this.fontWeight = config.fontWeight ?? 'bold';
54
+ this.fontFamily = config.fontFamily;
49
55
  this.align = config.align ?? 'center';
50
56
  this.marginTop = config.marginTop ?? 10;
51
57
  this.marginBottom = config.marginBottom ?? 15;
@@ -82,7 +88,7 @@ export class Title {
82
88
  .attr('text-anchor', textAnchor)
83
89
  .attr('font-size', `${this.fontSize}px`)
84
90
  .attr('font-weight', this.fontWeight)
85
- .attr('font-family', theme.axis.fontFamily)
91
+ .attr('font-family', this.fontFamily || theme.fontFamily)
86
92
  .text(this.text);
87
93
  }
88
94
  }
package/tooltip.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Selection } from 'd3';
2
2
  import type { TooltipConfig, DataItem, ChartTheme } from './types.js';
3
- import type { ChartComponent } from '@/lib/chart-interface';
3
+ import type { ChartComponent } from './chart-interface.js';
4
4
  import type { Line } from './line.js';
5
5
  import type { Bar } from './bar.js';
6
6
  import type { PlotAreaBounds } from './layout-manager.js';
@@ -10,6 +10,6 @@ export declare class Tooltip implements ChartComponent {
10
10
  private tooltipDiv;
11
11
  constructor(config?: TooltipConfig);
12
12
  initialize(theme: ChartTheme): void;
13
- attachToArea(svg: Selection<SVGSVGElement, undefined, null, undefined>, data: DataItem[], series: (Line | Bar)[], xKey: string, x: any, y: any, _theme: ChartTheme, plotArea: PlotAreaBounds, parseValue: (value: any) => number): void;
13
+ attachToArea(svg: Selection<SVGSVGElement, undefined, null, undefined>, data: DataItem[], series: (Line | Bar)[], xKey: string, x: any, y: any, theme: ChartTheme, plotArea: PlotAreaBounds, parseValue: (value: any) => number): void;
14
14
  cleanup(): void;
15
15
  }
package/tooltip.js CHANGED
@@ -38,7 +38,7 @@ export class Tooltip {
38
38
  .style('pointer-events', 'none')
39
39
  .style('z-index', '1000');
40
40
  }
41
- attachToArea(svg, data, series, xKey, x, y, _theme, plotArea, parseValue) {
41
+ attachToArea(svg, data, series, xKey, x, y, theme, plotArea, parseValue) {
42
42
  if (!this.tooltipDiv)
43
43
  return;
44
44
  const tooltip = this.tooltipDiv;
@@ -65,13 +65,14 @@ export class Tooltip {
65
65
  .style('pointer-events', 'all');
66
66
  // Create focus circles for each series
67
67
  const focusCircles = series.map((s) => {
68
+ const seriesColor = getSeriesColor(s);
68
69
  return svg
69
70
  .append('circle')
70
71
  .attr('class', `focus-circle-${s.dataKey.replace(/\s+/g, '-')}`)
71
- .attr('r', 5)
72
- .attr('fill', getSeriesColor(s))
73
- .attr('stroke', 'white')
74
- .attr('stroke-width', 2)
72
+ .attr('r', theme.line.point.size + 1)
73
+ .attr('fill', theme.line.point.color || seriesColor)
74
+ .attr('stroke', theme.line.point.strokeColor || seriesColor)
75
+ .attr('stroke-width', theme.line.point.strokeWidth)
75
76
  .style('opacity', 0)
76
77
  .style('pointer-events', 'none');
77
78
  });
@@ -114,8 +115,9 @@ export class Tooltip {
114
115
  });
115
116
  // Position tooltip relative to the data point
116
117
  const svgRect = svg.node().getBoundingClientRect();
117
- const tooltipX = svgRect.left + xPos + 10;
118
+ const tooltipX = svgRect.left + window.scrollX + xPos + 10;
118
119
  const tooltipY = svgRect.top +
120
+ window.scrollY +
119
121
  y(parseValue(dataPoint[series[0].dataKey])) -
120
122
  10;
121
123
  tooltip
package/types.d.ts CHANGED
@@ -5,23 +5,37 @@ export type ColorPalette = string[];
5
5
  export type ChartTheme = {
6
6
  width: number;
7
7
  height: number;
8
+ fontFamily: string;
8
9
  margins: {
9
10
  top: number;
10
11
  right: number;
11
12
  bottom: number;
12
13
  left: number;
13
14
  };
14
- gridColor: string;
15
+ grid: {
16
+ color: string;
17
+ opacity: number;
18
+ };
15
19
  colorPalette: ColorPalette;
16
20
  axis: {
17
21
  fontFamily: string;
18
22
  fontSize: string;
23
+ fontWeight?: string;
19
24
  };
20
25
  legend: {
21
26
  boxSize: number;
22
27
  uncheckedColor: string;
23
28
  fontSize: number;
24
29
  };
30
+ line: {
31
+ strokeWidth: number;
32
+ point: {
33
+ strokeWidth: number;
34
+ strokeColor?: string;
35
+ color?: string;
36
+ size: number;
37
+ };
38
+ };
25
39
  };
26
40
  export type LineConfig = {
27
41
  dataKey: string;
@@ -60,6 +74,7 @@ export type TitleConfig = {
60
74
  text: string;
61
75
  fontSize?: number;
62
76
  fontWeight?: string;
77
+ fontFamily?: string;
63
78
  align?: 'left' | 'center' | 'right';
64
79
  marginTop?: number;
65
80
  marginBottom?: number;
@@ -75,6 +90,8 @@ export type ScaleConfig = {
75
90
  range?: any[];
76
91
  padding?: number;
77
92
  nice?: boolean;
93
+ min?: number;
94
+ max?: number;
78
95
  };
79
96
  export type AxisScaleConfig = {
80
97
  x?: Partial<ScaleConfig>;
package/x-axis.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Selection } from 'd3';
2
2
  import type { XAxisConfig, ChartTheme } from './types.js';
3
- import type { LayoutAwareComponent, ComponentSpace } from '@/lib/chart-interface';
3
+ import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
4
4
  export declare class XAxis implements LayoutAwareComponent {
5
5
  readonly type: "xAxis";
6
6
  readonly dataKey?: string;
package/x-axis.js CHANGED
@@ -58,6 +58,7 @@ export class XAxis {
58
58
  .tickPadding(this.tickPadding))
59
59
  .attr('font-size', theme.axis.fontSize)
60
60
  .attr('font-family', theme.axis.fontFamily)
61
+ .attr('font-weight', theme.axis.fontWeight || 'normal')
61
62
  .attr('stroke', 'none');
62
63
  // Apply rotation to labels if enabled
63
64
  if (this.rotatedLabels) {
package/xy-chart.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { BaseChart, type BaseChartConfig } from './base-chart.js';
2
- import type { ChartComponent } from '@/lib/chart-interface';
2
+ import type { ChartComponent } from './chart-interface.js';
3
3
  export type XYChartConfig = BaseChartConfig;
4
4
  export declare class XYChart extends BaseChart {
5
5
  private readonly series;
package/xy-chart.js CHANGED
@@ -189,8 +189,8 @@ export class XYChart extends BaseChart {
189
189
  else if ((scaleType === 'linear' || scaleType === 'log') && dataKey) {
190
190
  // For linear and log scales with a dataKey, calculate from that key
191
191
  const values = this.data.map((d) => this.parseValue(d[dataKey]));
192
- const minVal = min(values) ?? 0;
193
- const maxVal = max(values) ?? 100;
192
+ const minVal = config.min ?? (min(values) ?? 0);
193
+ const maxVal = config.max ?? (max(values) ?? 100);
194
194
  domain =
195
195
  scaleType === 'log' && minVal <= 0
196
196
  ? [1, maxVal]
@@ -199,8 +199,8 @@ export class XYChart extends BaseChart {
199
199
  else {
200
200
  // Calculate from series data (for value axes without explicit dataKey)
201
201
  const values = this.data.flatMap((d) => this.series.map((s) => this.parseValue(d[s.dataKey])));
202
- const minVal = min(values) ?? 0;
203
- const maxVal = max(values) ?? 100;
202
+ const minVal = config.min ?? (min(values) ?? 0);
203
+ const maxVal = config.max ?? (max(values) ?? 100);
204
204
  domain =
205
205
  scaleType === 'log' && minVal <= 0
206
206
  ? [1, maxVal]
@@ -266,7 +266,7 @@ export class XYChart extends BaseChart {
266
266
  ? this.series.filter((series) => this.legend.isSeriesVisible(series.dataKey))
267
267
  : this.series;
268
268
  visibleSeries.forEach((series) => {
269
- series.render(this.plotGroup, sortedData, xKey, this.x, this.y, this.parseValue, categoryScaleType);
269
+ series.render(this.plotGroup, sortedData, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.theme);
270
270
  });
271
271
  }
272
272
  }
package/y-axis.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Selection } from 'd3';
2
2
  import type { ChartTheme, YAxisConfig } from './types.js';
3
- import type { LayoutAwareComponent, ComponentSpace } from '@/lib/chart-interface';
3
+ import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
4
4
  export declare class YAxis implements LayoutAwareComponent {
5
5
  readonly type: "yAxis";
6
6
  private readonly tickPadding;
package/y-axis.js CHANGED
@@ -52,6 +52,7 @@ export class YAxis {
52
52
  .call(axis)
53
53
  .attr('font-size', theme.axis.fontSize)
54
54
  .attr('font-family', theme.axis.fontFamily)
55
+ .attr('font-weight', theme.axis.fontWeight || 'normal')
55
56
  .selectAll('.domain')
56
57
  .remove();
57
58
  }