@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/tooltip.js ADDED
@@ -0,0 +1,138 @@
1
+ import { pointer, select } from 'd3';
2
+ import { getSeriesColor } from './types';
3
+ export class Tooltip {
4
+ constructor(config) {
5
+ Object.defineProperty(this, "type", {
6
+ enumerable: true,
7
+ configurable: true,
8
+ writable: true,
9
+ value: 'tooltip'
10
+ });
11
+ Object.defineProperty(this, "formatter", {
12
+ enumerable: true,
13
+ configurable: true,
14
+ writable: true,
15
+ value: void 0
16
+ });
17
+ Object.defineProperty(this, "tooltipDiv", {
18
+ enumerable: true,
19
+ configurable: true,
20
+ writable: true,
21
+ value: null
22
+ });
23
+ this.formatter = config?.formatter;
24
+ }
25
+ initialize(theme) {
26
+ this.tooltipDiv = select('body')
27
+ .append('div')
28
+ .attr('class', 'chart-tooltip')
29
+ .style('position', 'absolute')
30
+ .style('visibility', 'hidden')
31
+ .style('background-color', 'white')
32
+ .style('border', '1px solid #ddd')
33
+ .style('border-radius', '4px')
34
+ .style('padding', '8px')
35
+ .style('box-shadow', '0 2px 4px rgba(0,0,0,0.1)')
36
+ .style('font-family', theme.axis.fontFamily)
37
+ .style('font-size', '12px')
38
+ .style('pointer-events', 'none')
39
+ .style('z-index', '1000');
40
+ }
41
+ attachToArea(svg, data, series, xKey, x, y, _theme, plotArea, parseValue) {
42
+ if (!this.tooltipDiv)
43
+ return;
44
+ const tooltip = this.tooltipDiv;
45
+ const formatter = this.formatter;
46
+ // Helper to get x position for any scale type
47
+ const getXPosition = (dataPoint) => {
48
+ const xValue = dataPoint[xKey];
49
+ const scaled = x(xValue instanceof Date
50
+ ? xValue
51
+ : typeof xValue === 'string'
52
+ ? xValue
53
+ : new Date(xValue));
54
+ return (scaled || 0) + (x.bandwidth ? x.bandwidth() / 2 : 0);
55
+ };
56
+ // Create overlay rect for mouse tracking using plot area bounds
57
+ const overlay = svg
58
+ .append('rect')
59
+ .attr('class', 'tooltip-overlay')
60
+ .attr('x', plotArea.left)
61
+ .attr('y', plotArea.top)
62
+ .attr('width', plotArea.width)
63
+ .attr('height', plotArea.height)
64
+ .style('fill', 'none')
65
+ .style('pointer-events', 'all');
66
+ // Create focus circles for each series
67
+ const focusCircles = series.map((s) => {
68
+ return svg
69
+ .append('circle')
70
+ .attr('class', `focus-circle-${s.dataKey.replace(/\s+/g, '-')}`)
71
+ .attr('r', 5)
72
+ .attr('fill', getSeriesColor(s))
73
+ .attr('stroke', 'white')
74
+ .attr('stroke-width', 2)
75
+ .style('opacity', 0)
76
+ .style('pointer-events', 'none');
77
+ });
78
+ overlay
79
+ .on('mousemove', (event) => {
80
+ const [mouseX] = pointer(event, svg.node());
81
+ // Find closest x value
82
+ const xPositions = data.map((d) => getXPosition(d));
83
+ let closestIndex = 0;
84
+ let minDistance = Math.abs(mouseX - xPositions[0]);
85
+ for (let i = 1; i < xPositions.length; i++) {
86
+ const distance = Math.abs(mouseX - xPositions[i]);
87
+ if (distance < minDistance) {
88
+ minDistance = distance;
89
+ closestIndex = i;
90
+ }
91
+ }
92
+ const dataPoint = data[closestIndex];
93
+ const xPos = xPositions[closestIndex];
94
+ // Update focus circles
95
+ series.forEach((s, i) => {
96
+ const yValue = parseValue(dataPoint[s.dataKey]);
97
+ const yPos = y(yValue);
98
+ focusCircles[i]
99
+ .attr('cx', xPos)
100
+ .attr('cy', yPos)
101
+ .style('opacity', 1);
102
+ });
103
+ // Build tooltip content
104
+ let content = `<strong>${dataPoint[xKey]}</strong><br/>`;
105
+ series.forEach((s) => {
106
+ const value = dataPoint[s.dataKey];
107
+ if (formatter) {
108
+ content +=
109
+ formatter(s.dataKey, value, dataPoint) + '<br/>';
110
+ }
111
+ else {
112
+ content += `${s.dataKey}: ${value}<br/>`;
113
+ }
114
+ });
115
+ // Position tooltip relative to the data point
116
+ const svgRect = svg.node().getBoundingClientRect();
117
+ const tooltipX = svgRect.left + xPos + 10;
118
+ const tooltipY = svgRect.top +
119
+ y(parseValue(dataPoint[series[0].dataKey])) -
120
+ 10;
121
+ tooltip
122
+ .style('visibility', 'visible')
123
+ .html(content)
124
+ .style('left', `${tooltipX}px`)
125
+ .style('top', `${tooltipY}px`);
126
+ })
127
+ .on('mouseout', () => {
128
+ tooltip.style('visibility', 'hidden');
129
+ focusCircles.forEach((circle) => circle.style('opacity', 0));
130
+ });
131
+ }
132
+ cleanup() {
133
+ if (this.tooltipDiv) {
134
+ this.tooltipDiv.remove();
135
+ this.tooltipDiv = null;
136
+ }
137
+ }
138
+ }
package/types.d.ts ADDED
@@ -0,0 +1,83 @@
1
+ export type DataItem = {
2
+ [key: string]: any;
3
+ };
4
+ export type ColorPalette = string[];
5
+ export type ChartTheme = {
6
+ width: number;
7
+ height: number;
8
+ margins: {
9
+ top: number;
10
+ right: number;
11
+ bottom: number;
12
+ left: number;
13
+ };
14
+ gridColor: string;
15
+ colorPalette: ColorPalette;
16
+ axis: {
17
+ fontFamily: string;
18
+ fontSize: string;
19
+ };
20
+ legend: {
21
+ boxSize: number;
22
+ uncheckedColor: string;
23
+ fontSize: number;
24
+ };
25
+ };
26
+ export type LineConfig = {
27
+ dataKey: string;
28
+ stroke?: string;
29
+ strokeWidth?: number;
30
+ };
31
+ export type BarConfig = {
32
+ dataKey: string;
33
+ fill?: string;
34
+ orientation?: 'vertical' | 'horizontal';
35
+ };
36
+ export declare function getSeriesColor(series: {
37
+ stroke?: string;
38
+ fill?: string;
39
+ }): string;
40
+ export type XAxisConfig = {
41
+ dataKey?: string;
42
+ rotatedLabels?: boolean;
43
+ };
44
+ export type YAxisConfig = {
45
+ tickFormat?: string | null;
46
+ };
47
+ export type GridConfig = {
48
+ horizontal?: boolean;
49
+ vertical?: boolean;
50
+ };
51
+ export type TooltipConfig = {
52
+ formatter?: (dataKey: string, value: any, data: DataItem) => string;
53
+ };
54
+ export type LegendConfig = {
55
+ position?: 'bottom';
56
+ marginTop?: number;
57
+ marginBottom?: number;
58
+ };
59
+ export type TitleConfig = {
60
+ text: string;
61
+ fontSize?: number;
62
+ fontWeight?: string;
63
+ align?: 'left' | 'center' | 'right';
64
+ marginTop?: number;
65
+ marginBottom?: number;
66
+ };
67
+ export type ChartStyle = {
68
+ maxHeight?: string;
69
+ aspectRatio?: number;
70
+ };
71
+ export type ScaleType = 'band' | 'linear' | 'time' | 'log';
72
+ export type ScaleConfig = {
73
+ type: ScaleType;
74
+ domain?: any[];
75
+ range?: any[];
76
+ padding?: number;
77
+ nice?: boolean;
78
+ };
79
+ export type AxisScaleConfig = {
80
+ x?: Partial<ScaleConfig>;
81
+ y?: Partial<ScaleConfig>;
82
+ };
83
+ export type Orientation = 'vertical' | 'horizontal';
package/types.js ADDED
@@ -0,0 +1,4 @@
1
+ // Helper to get color from any series type
2
+ export function getSeriesColor(series) {
3
+ return series.stroke || series.fill || '#8884d8';
4
+ }
package/utils.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { type ClassValue } from 'clsx';
2
+ export declare function cn(...inputs: ClassValue[]): string;
package/utils.js ADDED
@@ -0,0 +1,5 @@
1
+ import { clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }
@@ -0,0 +1,29 @@
1
+ import type { DataItem } from './types';
2
+ export declare class ChartValidationError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /**
6
+ * Validates chart data and configuration
7
+ */
8
+ export declare class ChartValidator {
9
+ /**
10
+ * Validates that data is an array and not empty
11
+ */
12
+ static validateData(data: any): asserts data is DataItem[];
13
+ /**
14
+ * Validates that the specified dataKey exists in all data items
15
+ */
16
+ static validateDataKey(data: DataItem[], dataKey: string, componentName: string): void;
17
+ /**
18
+ * Validates that data contains at least one valid numeric value for the specified key
19
+ */
20
+ static validateNumericData(data: DataItem[], dataKey: string, componentName: string): void;
21
+ /**
22
+ * Validates scale configuration
23
+ */
24
+ static validateScaleConfig(scaleType: string, domain: any[] | undefined): void;
25
+ /**
26
+ * Warns about potential issues without throwing errors
27
+ */
28
+ static warn(message: string): void;
29
+ }
package/validation.js ADDED
@@ -0,0 +1,75 @@
1
+ export class ChartValidationError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'ChartValidationError';
5
+ }
6
+ }
7
+ /**
8
+ * Validates chart data and configuration
9
+ */
10
+ export class ChartValidator {
11
+ /**
12
+ * Validates that data is an array and not empty
13
+ */
14
+ static validateData(data) {
15
+ if (!Array.isArray(data)) {
16
+ throw new ChartValidationError(`Data must be an array, received ${typeof data}`);
17
+ }
18
+ if (data.length === 0) {
19
+ throw new ChartValidationError('Data array cannot be empty');
20
+ }
21
+ // Validate that all items are objects
22
+ data.forEach((item, index) => {
23
+ if (typeof item !== 'object' || item === null) {
24
+ throw new ChartValidationError(`Data item at index ${index} must be an object, received ${typeof item}`);
25
+ }
26
+ });
27
+ }
28
+ /**
29
+ * Validates that the specified dataKey exists in all data items
30
+ */
31
+ static validateDataKey(data, dataKey, componentName) {
32
+ const missingKeys = [];
33
+ data.forEach((item, index) => {
34
+ if (!(dataKey in item)) {
35
+ missingKeys.push(index);
36
+ }
37
+ });
38
+ if (missingKeys.length > 0) {
39
+ const indices = missingKeys.slice(0, 3).join(', ');
40
+ const more = missingKeys.length > 3
41
+ ? ` and ${missingKeys.length - 3} more`
42
+ : '';
43
+ throw new ChartValidationError(`${componentName}: dataKey "${dataKey}" not found in data items at indices: ${indices}${more}`);
44
+ }
45
+ }
46
+ /**
47
+ * Validates that data contains at least one valid numeric value for the specified key
48
+ */
49
+ static validateNumericData(data, dataKey, componentName) {
50
+ const hasNumericValue = data.some((item) => {
51
+ const value = item[dataKey];
52
+ return (typeof value === 'number' || !isNaN(parseFloat(String(value))));
53
+ });
54
+ if (!hasNumericValue) {
55
+ throw new ChartValidationError(`${componentName}: No valid numeric values found for dataKey "${dataKey}"`);
56
+ }
57
+ }
58
+ /**
59
+ * Validates scale configuration
60
+ */
61
+ static validateScaleConfig(scaleType, domain) {
62
+ if (scaleType === 'log' && domain) {
63
+ const hasZeroOrNegative = domain.some((v) => typeof v === 'number' && v <= 0);
64
+ if (hasZeroOrNegative) {
65
+ throw new ChartValidationError('Logarithmic scale cannot include zero or negative values in domain');
66
+ }
67
+ }
68
+ }
69
+ /**
70
+ * Warns about potential issues without throwing errors
71
+ */
72
+ static warn(message) {
73
+ console.warn(`[Chart Warning] ${message}`);
74
+ }
75
+ }
package/x-axis.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type Selection } from 'd3';
2
+ import type { XAxisConfig, ChartTheme } from './types';
3
+ import type { LayoutAwareComponent, ComponentSpace } from '@/lib/chart-interface';
4
+ export declare class XAxis implements LayoutAwareComponent {
5
+ readonly type: "xAxis";
6
+ readonly dataKey?: string;
7
+ private readonly rotatedLabels;
8
+ private readonly tickPadding;
9
+ private readonly fontSize;
10
+ constructor(config?: XAxisConfig);
11
+ /**
12
+ * Returns the space required by the x-axis
13
+ */
14
+ getRequiredSpace(): ComponentSpace;
15
+ render(svg: Selection<SVGSVGElement, undefined, null, undefined>, x: any, theme: ChartTheme, yPosition: number): void;
16
+ }
package/x-axis.js ADDED
@@ -0,0 +1,70 @@
1
+ import { axisBottom } from 'd3';
2
+ export class XAxis {
3
+ constructor(config) {
4
+ Object.defineProperty(this, "type", {
5
+ enumerable: true,
6
+ configurable: true,
7
+ writable: true,
8
+ value: 'xAxis'
9
+ });
10
+ Object.defineProperty(this, "dataKey", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: void 0
15
+ });
16
+ Object.defineProperty(this, "rotatedLabels", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: void 0
21
+ });
22
+ Object.defineProperty(this, "tickPadding", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: 10
27
+ });
28
+ Object.defineProperty(this, "fontSize", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: 14
33
+ });
34
+ this.dataKey = config?.dataKey;
35
+ this.rotatedLabels = config?.rotatedLabels ?? false;
36
+ }
37
+ /**
38
+ * Returns the space required by the x-axis
39
+ */
40
+ getRequiredSpace() {
41
+ // Height = tick padding + font size + some extra space for descenders
42
+ // Rotated labels need more vertical space (roughly 2.5x for -45deg rotation)
43
+ const baseHeight = this.tickPadding + this.fontSize + 5;
44
+ const height = this.rotatedLabels ? baseHeight * 2.5 : baseHeight;
45
+ return {
46
+ width: 0, // X-axis spans full width
47
+ height,
48
+ position: 'bottom',
49
+ };
50
+ }
51
+ render(svg, x, theme, yPosition) {
52
+ const axis = svg
53
+ .append('g')
54
+ .attr('transform', `translate(0,${yPosition})`)
55
+ .call(axisBottom(x)
56
+ .tickSizeOuter(0)
57
+ .tickSize(0)
58
+ .tickPadding(this.tickPadding))
59
+ .attr('font-size', theme.axis.fontSize)
60
+ .attr('font-family', theme.axis.fontFamily)
61
+ .attr('stroke', 'none');
62
+ // Apply rotation to labels if enabled
63
+ if (this.rotatedLabels) {
64
+ axis.selectAll('text')
65
+ .style('text-anchor', 'end')
66
+ .attr('transform', 'rotate(-45)');
67
+ }
68
+ axis.selectAll('.domain').remove();
69
+ }
70
+ }
package/xy-chart.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { BaseChart, type BaseChartConfig } from './base-chart';
2
+ import type { ChartComponent } from '@/lib/chart-interface';
3
+ export type XYChartConfig = BaseChartConfig;
4
+ export declare class XYChart extends BaseChart {
5
+ private readonly series;
6
+ private sortedDataCache;
7
+ private xKeyCache;
8
+ constructor(config: XYChartConfig);
9
+ addChild(component: ChartComponent): this;
10
+ private rerender;
11
+ protected renderChart(): void;
12
+ private getXKey;
13
+ private getSortedData;
14
+ private setupScales;
15
+ private isHorizontalOrientation;
16
+ private createScale;
17
+ private renderSeries;
18
+ }