@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 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**: 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
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: '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 },
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
- 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
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
- **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
46
+ ## Documentation
144
47
 
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
- ```
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
- ## 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
59
+ ## License
410
60
 
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)
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
- switch (insidePosition) {
404
- case 'top':
405
- labelY = barTop + boxHeight / 2 + 4;
406
- break;
407
- case 'middle':
408
- labelY = (barTop + barBottom) / 2;
409
- break;
410
- case 'bottom':
411
- labelY = barBottom - boxHeight / 2 - 4;
412
- break;
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
- // Check if it fits inside the bar
415
- if (boxHeight + 8 > barHeight) {
416
- shouldRender = false;
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 - map top/middle/bottom to start/middle/end for horizontal
560
- switch (insidePosition) {
561
- case 'top': // start of bar (left side)
562
- labelX = barLeft + boxWidth / 2 + 4;
563
- break;
564
- case 'middle':
565
- labelX = (barLeft + barRight) / 2;
566
- break;
567
- case 'bottom': // end of bar (right side)
568
- labelX = barRight - boxWidth / 2 - 4;
569
- break;
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
- // Check if it fits inside the bar
572
- if (boxWidth + 8 > barWidth) {
573
- shouldRender = false;
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
- private getLayoutComponents;
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');