@internetstiftelsen/charts 0.1.1 → 0.3.0
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 +21 -377
- package/bar.js +142 -30
- package/base-chart.d.ts +3 -2
- package/base-chart.js +3 -6
- package/chart-interface.d.ts +1 -1
- package/donut-center-content.d.ts +27 -0
- package/donut-center-content.js +98 -0
- package/donut-chart.d.ts +32 -0
- package/donut-chart.js +240 -0
- package/legend.d.ts +2 -4
- package/line.js +3 -1
- package/package.json +1 -1
- package/theme.js +44 -0
- package/tooltip.js +58 -19
- package/types.d.ts +33 -0
- package/x-axis.d.ts +4 -0
- package/x-axis.js +87 -2
- package/xy-chart.js +17 -16
- package/y-axis.js +3 -1
package/README.md
CHANGED
|
@@ -4,15 +4,14 @@ A framework-agnostic, composable charting library built on D3.js with TypeScript
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- **Framework Agnostic
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **Type Safe
|
|
13
|
-
- **Data Validation
|
|
14
|
-
- **
|
|
15
|
-
- **Automatic Color Assignment**: Smart color palette system with sensible defaults
|
|
7
|
+
- **Framework Agnostic** - Works with vanilla JS, React, Vue, Svelte, or any framework
|
|
8
|
+
- **Composable Architecture** - Build charts by composing components
|
|
9
|
+
- **Multiple Chart Types** - XYChart (lines, bars) and DonutChart
|
|
10
|
+
- **Flexible Scales** - Band, linear, time, and logarithmic scales
|
|
11
|
+
- **Auto Resize** - Built-in ResizeObserver handles responsive behavior
|
|
12
|
+
- **Type Safe** - Written in TypeScript with full type definitions
|
|
13
|
+
- **Data Validation** - Built-in validation with helpful error messages
|
|
14
|
+
- **Auto Colors** - Smart color palette with sensible defaults
|
|
16
15
|
|
|
17
16
|
## Installation
|
|
18
17
|
|
|
@@ -22,396 +21,41 @@ npm install @internetstiftelsen/charts
|
|
|
22
21
|
|
|
23
22
|
## Quick Start
|
|
24
23
|
|
|
25
|
-
### Vanilla JavaScript
|
|
26
|
-
|
|
27
24
|
```javascript
|
|
28
25
|
import { XYChart } from '@internetstiftelsen/charts/xy-chart';
|
|
29
26
|
import { Line } from '@internetstiftelsen/charts/line';
|
|
30
|
-
import { Bar } from '@internetstiftelsen/charts/bar';
|
|
31
27
|
import { XAxis } from '@internetstiftelsen/charts/x-axis';
|
|
32
28
|
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
29
|
|
|
37
|
-
// Your data
|
|
38
30
|
const data = [
|
|
39
|
-
{ date: '
|
|
40
|
-
{ date: '
|
|
41
|
-
{ date: '2012', revenue: 200, expenses: 110 },
|
|
42
|
-
{ date: '2013', revenue: 250, expenses: 130 },
|
|
31
|
+
{ date: '2023', revenue: 100, expenses: 80 },
|
|
32
|
+
{ date: '2024', revenue: 150, expenses: 90 },
|
|
43
33
|
];
|
|
44
34
|
|
|
45
|
-
// Create chart
|
|
46
35
|
const chart = new XYChart({ data });
|
|
47
36
|
|
|
48
|
-
// Add components
|
|
49
37
|
chart
|
|
50
|
-
.addChild(new Title({ text: 'Revenue vs Expenses' }))
|
|
51
|
-
.addChild(new Grid({ horizontal: true, vertical: false }))
|
|
52
38
|
.addChild(new XAxis({ dataKey: 'date' }))
|
|
53
39
|
.addChild(new YAxis())
|
|
54
|
-
.addChild(
|
|
55
|
-
|
|
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
|
|
40
|
+
.addChild(new Line({ dataKey: 'revenue' }))
|
|
41
|
+
.addChild(new Line({ dataKey: 'expenses' }));
|
|
62
42
|
|
|
63
|
-
// Render to DOM (automatically resizes with container)
|
|
64
43
|
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
44
|
```
|
|
128
45
|
|
|
129
|
-
|
|
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
|
|
46
|
+
## Documentation
|
|
144
47
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
```
|
|
48
|
+
- [Getting Started](./docs/getting-started.md) - Installation, Vanilla JS, React integration
|
|
49
|
+
- [XYChart](./docs/xy-chart.md) - Line and bar charts API
|
|
50
|
+
- [DonutChart](./docs/donut-chart.md) - Donut/pie charts API
|
|
51
|
+
- [Components](./docs/components.md) - Axes, Grid, Tooltip, Legend, Title
|
|
52
|
+
- [Theming](./docs/theming.md) - Colors, fonts, and styling
|
|
53
|
+
- [Advanced](./docs/advanced.md) - Scales, TypeScript, architecture, performance
|
|
366
54
|
|
|
367
55
|
## Browser Support
|
|
368
56
|
|
|
369
57
|
Modern browsers with ES6+ support. Uses D3.js v7.
|
|
370
58
|
|
|
371
|
-
##
|
|
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
|
|
59
|
+
## License
|
|
410
60
|
|
|
411
|
-
|
|
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)
|
|
61
|
+
MIT
|
package/bar.js
CHANGED
|
@@ -387,7 +387,7 @@ export class Bar {
|
|
|
387
387
|
const boxWidth = textBBox.width + padding * 2;
|
|
388
388
|
const boxHeight = textBBox.height + padding * 2;
|
|
389
389
|
const labelX = barCenterX;
|
|
390
|
-
let labelY;
|
|
390
|
+
let labelY = (barTop + barBottom) / 2; // Default to middle
|
|
391
391
|
let shouldRender = true;
|
|
392
392
|
if (position === 'outside') {
|
|
393
393
|
// Place above the bar
|
|
@@ -399,21 +399,76 @@ export class Bar {
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
else {
|
|
402
|
-
// Inside the bar
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
402
|
+
// Inside the bar - with special handling for layer mode
|
|
403
|
+
if (mode === 'layer') {
|
|
404
|
+
const totalSeries = stackingContext?.totalSeries ?? 1;
|
|
405
|
+
const seriesIndex = stackingContext?.seriesIndex ?? 0;
|
|
406
|
+
const isTopLayer = seriesIndex === totalSeries - 1;
|
|
407
|
+
switch (insidePosition) {
|
|
408
|
+
case 'top':
|
|
409
|
+
// For layer mode + inside + top: check if there's enough space in the gap
|
|
410
|
+
if (seriesIndex < totalSeries - 1) {
|
|
411
|
+
// Calculate the gap to the next layer
|
|
412
|
+
const nextLayerScaleFactor = 1 - ((seriesIndex + 1) / totalSeries) * 0.7;
|
|
413
|
+
const nextLayerWidth = (this.maxBarSize
|
|
414
|
+
? Math.min(bandwidth, this.maxBarSize)
|
|
415
|
+
: bandwidth) * nextLayerScaleFactor;
|
|
416
|
+
const gap = (barWidth - nextLayerWidth) / 2;
|
|
417
|
+
const marginBelow = 4; // Minimum margin below text
|
|
418
|
+
if (boxHeight + marginBelow <= gap) {
|
|
419
|
+
labelY =
|
|
420
|
+
barTop + boxHeight / 2 + marginBelow;
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
shouldRender = false;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// Top layer - use normal top position if it fits
|
|
428
|
+
labelY = barTop + boxHeight / 2 + 4;
|
|
429
|
+
if (boxHeight + 8 > barHeight) {
|
|
430
|
+
shouldRender = false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
case 'middle':
|
|
435
|
+
// For layer mode + inside + middle: only show what fits
|
|
436
|
+
labelY = (barTop + barBottom) / 2;
|
|
437
|
+
if (boxHeight + 8 > barHeight) {
|
|
438
|
+
shouldRender = false;
|
|
439
|
+
}
|
|
440
|
+
break;
|
|
441
|
+
case 'bottom':
|
|
442
|
+
// For layer mode + inside + bottom: only show for top layer if it fits
|
|
443
|
+
if (isTopLayer) {
|
|
444
|
+
labelY = barBottom - boxHeight / 2 - 4;
|
|
445
|
+
if (boxHeight + 8 > barHeight) {
|
|
446
|
+
shouldRender = false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
shouldRender = false;
|
|
451
|
+
}
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
413
454
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
455
|
+
else {
|
|
456
|
+
// Non-layer modes - use existing logic
|
|
457
|
+
switch (insidePosition) {
|
|
458
|
+
case 'top':
|
|
459
|
+
labelY = barTop + boxHeight / 2 + 4;
|
|
460
|
+
break;
|
|
461
|
+
case 'middle':
|
|
462
|
+
labelY = (barTop + barBottom) / 2;
|
|
463
|
+
break;
|
|
464
|
+
case 'bottom':
|
|
465
|
+
labelY = barBottom - boxHeight / 2 - 4;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
// Check if it fits inside the bar
|
|
469
|
+
if (boxHeight + 8 > barHeight) {
|
|
470
|
+
shouldRender = false;
|
|
471
|
+
}
|
|
417
472
|
}
|
|
418
473
|
}
|
|
419
474
|
tempText.remove();
|
|
@@ -543,7 +598,7 @@ export class Bar {
|
|
|
543
598
|
const textBBox = tempText.node().getBBox();
|
|
544
599
|
const boxWidth = textBBox.width + padding * 2;
|
|
545
600
|
const boxHeight = textBBox.height + padding * 2;
|
|
546
|
-
let labelX;
|
|
601
|
+
let labelX = (barLeft + barRight) / 2; // Default to middle
|
|
547
602
|
const labelY = barCenterY;
|
|
548
603
|
let shouldRender = true;
|
|
549
604
|
if (position === 'outside') {
|
|
@@ -556,21 +611,78 @@ export class Bar {
|
|
|
556
611
|
}
|
|
557
612
|
}
|
|
558
613
|
else {
|
|
559
|
-
// Inside the bar -
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
614
|
+
// Inside the bar - with special handling for layer mode
|
|
615
|
+
if (mode === 'layer') {
|
|
616
|
+
const totalSeries = stackingContext?.totalSeries ?? 1;
|
|
617
|
+
const seriesIndex = stackingContext?.seriesIndex ?? 0;
|
|
618
|
+
const isTopLayer = seriesIndex === totalSeries - 1;
|
|
619
|
+
// Map top/middle/bottom to start/middle/end for horizontal
|
|
620
|
+
switch (insidePosition) {
|
|
621
|
+
case 'top': // start of bar (left side)
|
|
622
|
+
// For layer mode + inside + top(left): check if there's enough space in the gap
|
|
623
|
+
if (seriesIndex < totalSeries - 1) {
|
|
624
|
+
// Calculate the gap to the next layer
|
|
625
|
+
const nextLayerScaleFactor = 1 - ((seriesIndex + 1) / totalSeries) * 0.7;
|
|
626
|
+
const nextLayerHeight = (this.maxBarSize
|
|
627
|
+
? Math.min(bandwidth, this.maxBarSize)
|
|
628
|
+
: bandwidth) * nextLayerScaleFactor;
|
|
629
|
+
const gap = (barHeight - nextLayerHeight) / 2;
|
|
630
|
+
const marginRight = 4; // Minimum margin to the right of text
|
|
631
|
+
if (boxWidth + marginRight <= gap) {
|
|
632
|
+
labelX =
|
|
633
|
+
barLeft + boxWidth / 2 + marginRight;
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
shouldRender = false;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// Top layer - use normal left position if it fits
|
|
641
|
+
labelX = barLeft + boxWidth / 2 + 4;
|
|
642
|
+
if (boxWidth + 8 > barWidth) {
|
|
643
|
+
shouldRender = false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
break;
|
|
647
|
+
case 'middle':
|
|
648
|
+
// For layer mode + inside + middle: only show what fits
|
|
649
|
+
labelX = (barLeft + barRight) / 2;
|
|
650
|
+
if (boxWidth + 8 > barWidth) {
|
|
651
|
+
shouldRender = false;
|
|
652
|
+
}
|
|
653
|
+
break;
|
|
654
|
+
case 'bottom': // end of bar (right side)
|
|
655
|
+
// For layer mode + inside + bottom(right): only show for top layer if it fits
|
|
656
|
+
if (isTopLayer) {
|
|
657
|
+
labelX = barRight - boxWidth / 2 - 4;
|
|
658
|
+
if (boxWidth + 8 > barWidth) {
|
|
659
|
+
shouldRender = false;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
shouldRender = false;
|
|
664
|
+
}
|
|
665
|
+
break;
|
|
666
|
+
}
|
|
570
667
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
668
|
+
else {
|
|
669
|
+
// Non-layer modes - use existing logic
|
|
670
|
+
// Map top/middle/bottom to start/middle/end for horizontal
|
|
671
|
+
switch (insidePosition) {
|
|
672
|
+
case 'top': // start of bar (left side)
|
|
673
|
+
labelX = barLeft + boxWidth / 2 + 4;
|
|
674
|
+
break;
|
|
675
|
+
case 'middle':
|
|
676
|
+
labelX = (barLeft + barRight) / 2;
|
|
677
|
+
break;
|
|
678
|
+
case 'bottom': // end of bar (right side)
|
|
679
|
+
labelX = barRight - boxWidth / 2 - 4;
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
// Check if it fits inside the bar
|
|
683
|
+
if (boxWidth + 8 > barWidth) {
|
|
684
|
+
shouldRender = false;
|
|
685
|
+
}
|
|
574
686
|
}
|
|
575
687
|
}
|
|
576
688
|
tempText.remove();
|
package/base-chart.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
2
|
import type { DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale } from './types.js';
|
|
3
|
-
import type { ChartComponent } from './chart-interface.js';
|
|
3
|
+
import type { ChartComponent, LayoutAwareComponent } 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';
|
|
@@ -50,8 +50,9 @@ export declare abstract class BaseChart {
|
|
|
50
50
|
private performRender;
|
|
51
51
|
/**
|
|
52
52
|
* Get layout-aware components in order
|
|
53
|
+
* Override in subclasses to provide chart-specific components
|
|
53
54
|
*/
|
|
54
|
-
|
|
55
|
+
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
55
56
|
/**
|
|
56
57
|
* Setup ResizeObserver for automatic resize handling
|
|
57
58
|
*/
|
package/base-chart.js
CHANGED
|
@@ -167,6 +167,7 @@ export class BaseChart {
|
|
|
167
167
|
}
|
|
168
168
|
/**
|
|
169
169
|
* Get layout-aware components in order
|
|
170
|
+
* Override in subclasses to provide chart-specific components
|
|
170
171
|
*/
|
|
171
172
|
getLayoutComponents() {
|
|
172
173
|
const components = [];
|
|
@@ -237,9 +238,7 @@ export class BaseChart {
|
|
|
237
238
|
* @returns The exported content as a string if download is false/undefined, void if download is true
|
|
238
239
|
*/
|
|
239
240
|
export(format, options) {
|
|
240
|
-
const content = format === 'svg'
|
|
241
|
-
? this.exportSVG()
|
|
242
|
-
: this.exportJSON();
|
|
241
|
+
const content = format === 'svg' ? this.exportSVG() : this.exportJSON();
|
|
243
242
|
if (options?.download) {
|
|
244
243
|
this.downloadContent(content, format, options);
|
|
245
244
|
return;
|
|
@@ -250,9 +249,7 @@ export class BaseChart {
|
|
|
250
249
|
* Downloads the exported content as a file
|
|
251
250
|
*/
|
|
252
251
|
downloadContent(content, format, options) {
|
|
253
|
-
const mimeType = format === 'svg'
|
|
254
|
-
? 'image/svg+xml'
|
|
255
|
-
: 'application/json';
|
|
252
|
+
const mimeType = format === 'svg' ? 'image/svg+xml' : 'application/json';
|
|
256
253
|
const blob = new Blob([content], { type: mimeType });
|
|
257
254
|
const url = URL.createObjectURL(blob);
|
|
258
255
|
const link = document.createElement('a');
|