@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 +417 -0
- package/bar.d.ts +3 -3
- package/bar.js +1 -1
- package/base-chart.d.ts +1 -1
- package/base-chart.js +2 -2
- package/grid.d.ts +1 -1
- package/grid.js +4 -4
- package/legend.d.ts +2 -1
- package/legend.js +30 -2
- package/line.d.ts +4 -4
- package/line.js +12 -7
- package/package.json +5 -3
- package/theme.d.ts +6 -1
- package/theme.js +62 -2
- package/title.d.ts +1 -0
- package/title.js +8 -2
- package/tooltip.d.ts +2 -2
- package/tooltip.js +8 -6
- package/types.d.ts +18 -1
- package/x-axis.d.ts +1 -1
- package/x-axis.js +1 -0
- package/xy-chart.d.ts +1 -1
- package/xy-chart.js +5 -5
- package/y-axis.d.ts +1 -1
- package/y-axis.js +1 -0
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 '
|
|
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 '
|
|
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 =
|
|
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 '
|
|
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.
|
|
43
|
-
.attr('stroke-opacity',
|
|
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.
|
|
58
|
-
.attr('stroke-opacity',
|
|
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 '
|
|
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',
|
|
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 '
|
|
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
|
|
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
|
|
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
|
|
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',
|
|
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',
|
|
78
|
-
.attr('fill',
|
|
79
|
-
.attr('stroke',
|
|
80
|
-
.attr('stroke-width',
|
|
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
|
+
"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 '
|
|
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
|
-
|
|
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.
|
|
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 '
|
|
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,
|
|
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,
|
|
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',
|
|
72
|
-
.attr('fill',
|
|
73
|
-
.attr('stroke',
|
|
74
|
-
.attr('stroke-width',
|
|
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
|
-
|
|
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 '
|
|
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 '
|
|
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 '
|
|
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;
|