@internetstiftelsen/charts 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bar.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import type { Selection } from 'd3';
2
+ import type { BarConfig, DataItem, ScaleType, Orientation } from './types';
3
+ import type { ChartComponent } from '@/lib/chart-interface';
4
+ export declare class Bar implements ChartComponent {
5
+ readonly type: "bar";
6
+ readonly dataKey: string;
7
+ readonly fill: string;
8
+ readonly orientation: Orientation;
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;
11
+ private renderVertical;
12
+ private renderHorizontal;
13
+ }
package/bar.js ADDED
@@ -0,0 +1,137 @@
1
+ export class Bar {
2
+ constructor(config) {
3
+ Object.defineProperty(this, "type", {
4
+ enumerable: true,
5
+ configurable: true,
6
+ writable: true,
7
+ value: 'bar'
8
+ });
9
+ Object.defineProperty(this, "dataKey", {
10
+ enumerable: true,
11
+ configurable: true,
12
+ writable: true,
13
+ value: void 0
14
+ });
15
+ Object.defineProperty(this, "fill", {
16
+ enumerable: true,
17
+ configurable: true,
18
+ writable: true,
19
+ value: void 0
20
+ });
21
+ Object.defineProperty(this, "orientation", {
22
+ enumerable: true,
23
+ configurable: true,
24
+ writable: true,
25
+ value: void 0
26
+ });
27
+ this.dataKey = config.dataKey;
28
+ this.fill = config.fill || '#8884d8';
29
+ this.orientation = config.orientation || 'vertical';
30
+ }
31
+ render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band') {
32
+ if (this.orientation === 'vertical') {
33
+ this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType);
34
+ }
35
+ else {
36
+ this.renderHorizontal(plotGroup, data, xKey, x, y, parseValue, xScaleType);
37
+ }
38
+ }
39
+ renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType) {
40
+ const getXPosition = (d) => {
41
+ const xValue = d[xKey];
42
+ let scaledValue;
43
+ switch (xScaleType) {
44
+ case 'band':
45
+ scaledValue = xValue;
46
+ break;
47
+ case 'time':
48
+ scaledValue =
49
+ xValue instanceof Date ? xValue : new Date(xValue);
50
+ break;
51
+ case 'linear':
52
+ case 'log':
53
+ scaledValue =
54
+ typeof xValue === 'number' ? xValue : Number(xValue);
55
+ break;
56
+ }
57
+ return x(scaledValue) || 0;
58
+ };
59
+ const bandwidth = x.bandwidth ? x.bandwidth() : 20;
60
+ // Get the baseline value from the Y scale's domain
61
+ // For linear scales, use 0 if it's in the domain, otherwise use domain max (bottom of chart)
62
+ // For log scales, use the minimum value from the domain
63
+ const yDomain = y.domain();
64
+ const baselineValue = yDomain[0] >= 0 ? Math.max(0, yDomain[0]) : yDomain[0];
65
+ const yBaseline = y(baselineValue) || 0;
66
+ // Add bar rectangles
67
+ plotGroup
68
+ .selectAll(`.bar-${this.dataKey.replace(/\s+/g, '-')}`)
69
+ .data(data)
70
+ .join('rect')
71
+ .attr('class', `bar-${this.dataKey.replace(/\s+/g, '-')}`)
72
+ .attr('x', (d) => {
73
+ const xPos = getXPosition(d);
74
+ // For non-band scales, center the bar
75
+ return xScaleType === 'band' ? xPos : xPos - bandwidth / 2;
76
+ })
77
+ .attr('y', (d) => {
78
+ const yPos = y(parseValue(d[this.dataKey])) || 0;
79
+ return Math.min(yBaseline, yPos);
80
+ })
81
+ .attr('width', bandwidth)
82
+ .attr('height', (d) => {
83
+ const yPos = y(parseValue(d[this.dataKey])) || 0;
84
+ return Math.abs(yBaseline - yPos);
85
+ })
86
+ .attr('fill', this.fill);
87
+ }
88
+ renderHorizontal(plotGroup, data, xKey, x, y, parseValue, yScaleType) {
89
+ const getYPosition = (d) => {
90
+ const yValue = d[xKey];
91
+ let scaledValue;
92
+ switch (yScaleType) {
93
+ case 'band':
94
+ scaledValue = yValue;
95
+ break;
96
+ case 'time':
97
+ scaledValue =
98
+ yValue instanceof Date ? yValue : new Date(yValue);
99
+ break;
100
+ case 'linear':
101
+ case 'log':
102
+ scaledValue =
103
+ typeof yValue === 'number' ? yValue : Number(yValue);
104
+ break;
105
+ }
106
+ return y(scaledValue) || 0;
107
+ };
108
+ const bandwidth = y.bandwidth ? y.bandwidth() : 20;
109
+ // Get the baseline value from the scale's domain
110
+ // For linear scales, use 0 if it's in the domain, otherwise use domain min
111
+ // For log scales, use the minimum value from the domain
112
+ const domain = x.domain();
113
+ const baselineValue = domain[0] >= 0 ? Math.max(0, domain[0]) : domain[0];
114
+ const xBaseline = x(baselineValue) || 0;
115
+ // Add bar rectangles (horizontal)
116
+ plotGroup
117
+ .selectAll(`.bar-${this.dataKey.replace(/\s+/g, '-')}`)
118
+ .data(data)
119
+ .join('rect')
120
+ .attr('class', `bar-${this.dataKey.replace(/\s+/g, '-')}`)
121
+ .attr('x', (d) => {
122
+ const xPos = x(parseValue(d[this.dataKey])) || 0;
123
+ return Math.min(xBaseline, xPos);
124
+ })
125
+ .attr('y', (d) => {
126
+ const yPos = getYPosition(d);
127
+ // For non-band scales, center the bar
128
+ return yScaleType === 'band' ? yPos : yPos - bandwidth / 2;
129
+ })
130
+ .attr('width', (d) => {
131
+ const xPos = x(parseValue(d[this.dataKey])) || 0;
132
+ return Math.abs(xPos - xBaseline);
133
+ })
134
+ .attr('height', bandwidth)
135
+ .attr('fill', this.fill);
136
+ }
137
+ }
@@ -0,0 +1,72 @@
1
+ import { type Selection } from 'd3';
2
+ import type { DataItem, ChartTheme, AxisScaleConfig } from './types';
3
+ import type { ChartComponent } from '@/lib/chart-interface';
4
+ import type { XAxis } from './x-axis';
5
+ import type { YAxis } from './y-axis';
6
+ import type { Grid } from './grid';
7
+ import type { Tooltip } from './tooltip';
8
+ import type { Legend } from './legend';
9
+ import type { Title } from './title';
10
+ import { LayoutManager, type PlotAreaBounds } from './layout-manager';
11
+ export type BaseChartConfig = {
12
+ data: DataItem[];
13
+ theme?: Partial<ChartTheme>;
14
+ scales?: AxisScaleConfig;
15
+ };
16
+ /**
17
+ * Base chart class that provides common functionality for all chart types
18
+ */
19
+ export declare abstract class BaseChart {
20
+ protected data: DataItem[];
21
+ protected readonly theme: ChartTheme;
22
+ protected readonly scaleConfig: AxisScaleConfig;
23
+ protected width: number;
24
+ protected xAxis: XAxis | null;
25
+ protected yAxis: YAxis | null;
26
+ protected grid: Grid | null;
27
+ protected tooltip: Tooltip | null;
28
+ protected legend: Legend | null;
29
+ protected title: Title | null;
30
+ protected svg: Selection<SVGSVGElement, undefined, null, undefined> | null;
31
+ protected plotGroup: Selection<SVGGElement, undefined, null, undefined> | null;
32
+ protected container: HTMLElement | null;
33
+ protected x: any;
34
+ protected y: any;
35
+ protected resizeObserver: ResizeObserver | null;
36
+ protected layoutManager: LayoutManager;
37
+ protected plotArea: PlotAreaBounds | null;
38
+ protected constructor(config: BaseChartConfig);
39
+ /**
40
+ * Adds a component (axis, grid, tooltip, etc.) to the chart
41
+ */
42
+ abstract addChild(component: ChartComponent): this;
43
+ /**
44
+ * Renders the chart to the specified target element
45
+ */
46
+ render(target: string): HTMLElement | null;
47
+ /**
48
+ * Performs the actual rendering logic
49
+ */
50
+ private performRender;
51
+ /**
52
+ * Get layout-aware components in order
53
+ */
54
+ private getLayoutComponents;
55
+ /**
56
+ * Setup ResizeObserver for automatic resize handling
57
+ */
58
+ private setupResizeObserver;
59
+ /**
60
+ * Subclasses must implement this method to define their rendering logic
61
+ */
62
+ protected abstract renderChart(): void;
63
+ /**
64
+ * Updates the chart with new data
65
+ */
66
+ update(data: DataItem[]): void;
67
+ /**
68
+ * Destroys the chart and cleans up resources
69
+ */
70
+ destroy(): void;
71
+ protected parseValue(value: any): number;
72
+ }
package/base-chart.js ADDED
@@ -0,0 +1,227 @@
1
+ import { create } from 'd3';
2
+ import { defaultTheme } from './theme';
3
+ import { ChartValidator } from './validation';
4
+ import { LayoutManager } from './layout-manager';
5
+ /**
6
+ * Base chart class that provides common functionality for all chart types
7
+ */
8
+ export class BaseChart {
9
+ constructor(config) {
10
+ Object.defineProperty(this, "data", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: void 0
15
+ });
16
+ Object.defineProperty(this, "theme", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: void 0
21
+ });
22
+ Object.defineProperty(this, "scaleConfig", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: void 0
27
+ });
28
+ Object.defineProperty(this, "width", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: void 0
33
+ }); // Current rendering width
34
+ Object.defineProperty(this, "xAxis", {
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ value: null
39
+ });
40
+ Object.defineProperty(this, "yAxis", {
41
+ enumerable: true,
42
+ configurable: true,
43
+ writable: true,
44
+ value: null
45
+ });
46
+ Object.defineProperty(this, "grid", {
47
+ enumerable: true,
48
+ configurable: true,
49
+ writable: true,
50
+ value: null
51
+ });
52
+ Object.defineProperty(this, "tooltip", {
53
+ enumerable: true,
54
+ configurable: true,
55
+ writable: true,
56
+ value: null
57
+ });
58
+ Object.defineProperty(this, "legend", {
59
+ enumerable: true,
60
+ configurable: true,
61
+ writable: true,
62
+ value: null
63
+ });
64
+ Object.defineProperty(this, "title", {
65
+ enumerable: true,
66
+ configurable: true,
67
+ writable: true,
68
+ value: null
69
+ });
70
+ Object.defineProperty(this, "svg", {
71
+ enumerable: true,
72
+ configurable: true,
73
+ writable: true,
74
+ value: null
75
+ });
76
+ Object.defineProperty(this, "plotGroup", {
77
+ enumerable: true,
78
+ configurable: true,
79
+ writable: true,
80
+ value: null
81
+ });
82
+ Object.defineProperty(this, "container", {
83
+ enumerable: true,
84
+ configurable: true,
85
+ writable: true,
86
+ value: null
87
+ });
88
+ Object.defineProperty(this, "x", {
89
+ enumerable: true,
90
+ configurable: true,
91
+ writable: true,
92
+ value: null
93
+ });
94
+ Object.defineProperty(this, "y", {
95
+ enumerable: true,
96
+ configurable: true,
97
+ writable: true,
98
+ value: null
99
+ });
100
+ Object.defineProperty(this, "resizeObserver", {
101
+ enumerable: true,
102
+ configurable: true,
103
+ writable: true,
104
+ value: null
105
+ });
106
+ Object.defineProperty(this, "layoutManager", {
107
+ enumerable: true,
108
+ configurable: true,
109
+ writable: true,
110
+ value: void 0
111
+ });
112
+ Object.defineProperty(this, "plotArea", {
113
+ enumerable: true,
114
+ configurable: true,
115
+ writable: true,
116
+ value: null
117
+ });
118
+ // Validate data
119
+ ChartValidator.validateData(config.data);
120
+ this.data = config.data;
121
+ this.theme = { ...defaultTheme, ...config.theme };
122
+ this.width = this.theme.width;
123
+ this.scaleConfig = config.scales || {};
124
+ this.layoutManager = new LayoutManager(this.theme);
125
+ }
126
+ /**
127
+ * Renders the chart to the specified target element
128
+ */
129
+ render(target) {
130
+ const container = document.querySelector(target);
131
+ if (!container) {
132
+ throw new Error(`Container "${target}" not found`);
133
+ }
134
+ this.container = container;
135
+ container.innerHTML = '';
136
+ // Perform initial render
137
+ this.performRender();
138
+ // Set up ResizeObserver for automatic resize handling
139
+ this.setupResizeObserver();
140
+ return container;
141
+ }
142
+ /**
143
+ * Performs the actual rendering logic
144
+ */
145
+ performRender() {
146
+ if (!this.container)
147
+ return;
148
+ // Calculate current width
149
+ this.width = Math.min(this.container.getBoundingClientRect().width || this.theme.width, this.theme.width);
150
+ // Clear and setup SVG
151
+ this.container.innerHTML = '';
152
+ this.svg = create('svg')
153
+ .attr('width', '100%')
154
+ .attr('height', this.theme.height)
155
+ .style('max-width', `${this.theme.width}px`)
156
+ .style('display', 'block');
157
+ this.container.appendChild(this.svg.node());
158
+ // Calculate layout
159
+ const layoutTheme = { ...this.theme, width: this.width };
160
+ this.layoutManager = new LayoutManager(layoutTheme);
161
+ const components = this.getLayoutComponents();
162
+ this.plotArea = this.layoutManager.calculateLayout(components);
163
+ // Create plot group
164
+ this.plotGroup = this.svg.append('g').attr('class', 'chart-plot');
165
+ // Render chart content
166
+ this.renderChart();
167
+ }
168
+ /**
169
+ * Get layout-aware components in order
170
+ */
171
+ getLayoutComponents() {
172
+ const components = [];
173
+ if (this.title)
174
+ components.push(this.title);
175
+ if (this.xAxis)
176
+ components.push(this.xAxis);
177
+ if (this.yAxis)
178
+ components.push(this.yAxis);
179
+ if (this.legend)
180
+ components.push(this.legend);
181
+ return components;
182
+ }
183
+ /**
184
+ * Setup ResizeObserver for automatic resize handling
185
+ */
186
+ setupResizeObserver() {
187
+ if (!this.container)
188
+ return;
189
+ if (this.resizeObserver) {
190
+ this.resizeObserver.disconnect();
191
+ }
192
+ this.resizeObserver = new ResizeObserver(() => this.performRender());
193
+ this.resizeObserver.observe(this.container);
194
+ }
195
+ /**
196
+ * Updates the chart with new data
197
+ */
198
+ update(data) {
199
+ ChartValidator.validateData(data);
200
+ this.data = data;
201
+ if (!this.container) {
202
+ throw new Error('Chart must be rendered before update()');
203
+ }
204
+ this.performRender();
205
+ }
206
+ /**
207
+ * Destroys the chart and cleans up resources
208
+ */
209
+ destroy() {
210
+ this.tooltip?.cleanup();
211
+ if (this.resizeObserver) {
212
+ this.resizeObserver.disconnect();
213
+ this.resizeObserver = null;
214
+ }
215
+ if (this.container) {
216
+ this.container.innerHTML = '';
217
+ }
218
+ this.svg = null;
219
+ this.plotGroup = null;
220
+ this.plotArea = null;
221
+ this.x = null;
222
+ this.y = null;
223
+ }
224
+ parseValue(value) {
225
+ return typeof value === 'string' ? parseFloat(value) : value;
226
+ }
227
+ }
@@ -0,0 +1,11 @@
1
+ export interface ChartComponent {
2
+ type: 'line' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title';
3
+ }
4
+ export type ComponentSpace = {
5
+ width: number;
6
+ height: number;
7
+ position: 'top' | 'bottom' | 'left' | 'right';
8
+ };
9
+ export interface LayoutAwareComponent extends ChartComponent {
10
+ getRequiredSpace(): ComponentSpace;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
package/grid.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { type Selection } from 'd3';
2
+ import type { GridConfig, ChartTheme } from './types';
3
+ import type { ChartComponent } from '@/lib/chart-interface';
4
+ export declare class Grid implements ChartComponent {
5
+ readonly type: "grid";
6
+ readonly horizontal: boolean;
7
+ readonly vertical: boolean;
8
+ constructor(config?: GridConfig);
9
+ render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, x: any, y: any, theme: ChartTheme): void;
10
+ }
package/grid.js ADDED
@@ -0,0 +1,63 @@
1
+ import { axisBottom, axisLeft } from 'd3';
2
+ export class Grid {
3
+ constructor(config) {
4
+ Object.defineProperty(this, "type", {
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true,
8
+ value: 'grid'
9
+ });
10
+ Object.defineProperty(this, "horizontal", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: void 0
15
+ });
16
+ Object.defineProperty(this, "vertical", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: void 0
21
+ });
22
+ this.horizontal = config?.horizontal ?? true;
23
+ this.vertical = config?.vertical ?? true;
24
+ }
25
+ render(plotGroup, x, y, theme) {
26
+ // Get plot area dimensions from the scale ranges
27
+ const xRange = x.range();
28
+ const yRange = y.range();
29
+ const plotWidth = xRange[1] - xRange[0];
30
+ const plotHeight = yRange[0] - yRange[1];
31
+ if (this.horizontal) {
32
+ plotGroup
33
+ .append('g')
34
+ .attr('class', 'grid-lines horizontal')
35
+ .attr('transform', `translate(${xRange[0]},0)`)
36
+ .call(axisLeft(y)
37
+ .ticks(5)
38
+ .tickSize(-plotWidth)
39
+ .tickFormat(() => ''))
40
+ .call((g) => g
41
+ .selectAll('.tick line')
42
+ .attr('stroke', theme.gridColor)
43
+ .attr('stroke-opacity', 0.5))
44
+ .selectAll('.domain')
45
+ .remove();
46
+ }
47
+ if (this.vertical) {
48
+ plotGroup
49
+ .append('g')
50
+ .attr('class', 'grid-lines vertical')
51
+ .attr('transform', `translate(0,${yRange[0]})`)
52
+ .call(axisBottom(x)
53
+ .tickSize(-plotHeight)
54
+ .tickFormat(() => ''))
55
+ .call((g) => g
56
+ .selectAll('.tick line')
57
+ .attr('stroke', theme.gridColor)
58
+ .attr('stroke-opacity', 0.5))
59
+ .selectAll('.domain')
60
+ .remove();
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,38 @@
1
+ import type { LayoutAwareComponent } from './chart-interface';
2
+ import type { ChartTheme } from './types';
3
+ export type PlotAreaBounds = {
4
+ left: number;
5
+ right: number;
6
+ top: number;
7
+ bottom: number;
8
+ width: number;
9
+ height: number;
10
+ };
11
+ export type ComponentPosition = {
12
+ x: number;
13
+ y: number;
14
+ };
15
+ /**
16
+ * Layout manager that calculates component positions and plot area bounds
17
+ * Follows D3's margin convention pattern
18
+ */
19
+ export declare class LayoutManager {
20
+ private theme;
21
+ private plotBounds;
22
+ private componentPositions;
23
+ constructor(theme: ChartTheme);
24
+ /**
25
+ * Calculate layout based on registered components
26
+ * Returns the plot area bounds
27
+ */
28
+ calculateLayout(components: LayoutAwareComponent[]): PlotAreaBounds;
29
+ /**
30
+ * Get the position for a specific component
31
+ */
32
+ getComponentPosition(component: LayoutAwareComponent): ComponentPosition;
33
+ /**
34
+ * Calculate positions for all components based on their space requirements
35
+ * Components are positioned in registration order, stacking outward from the plot area
36
+ */
37
+ private calculateComponentPositions;
38
+ }