@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 +13 -0
- package/bar.js +137 -0
- package/base-chart.d.ts +72 -0
- package/base-chart.js +227 -0
- package/chart-interface.d.ts +11 -0
- package/chart-interface.js +1 -0
- package/grid.d.ts +10 -0
- package/grid.js +63 -0
- package/layout-manager.d.ts +38 -0
- package/layout-manager.js +123 -0
- package/legend.d.ts +23 -0
- package/legend.js +157 -0
- package/line.d.ts +11 -0
- package/line.js +82 -0
- package/package.json +57 -0
- package/theme.d.ts +2 -0
- package/theme.js +31 -0
- package/title.d.ts +18 -0
- package/title.js +88 -0
- package/tooltip.d.ts +15 -0
- package/tooltip.js +138 -0
- package/types.d.ts +83 -0
- package/types.js +4 -0
- package/utils.d.ts +2 -0
- package/utils.js +5 -0
- package/validation.d.ts +29 -0
- package/validation.js +75 -0
- package/x-axis.d.ts +16 -0
- package/x-axis.js +70 -0
- package/xy-chart.d.ts +18 -0
- package/xy-chart.js +272 -0
- package/y-axis.d.ts +15 -0
- package/y-axis.js +58 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout manager that calculates component positions and plot area bounds
|
|
3
|
+
* Follows D3's margin convention pattern
|
|
4
|
+
*/
|
|
5
|
+
export class LayoutManager {
|
|
6
|
+
constructor(theme) {
|
|
7
|
+
Object.defineProperty(this, "theme", {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true,
|
|
11
|
+
value: void 0
|
|
12
|
+
});
|
|
13
|
+
Object.defineProperty(this, "plotBounds", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
value: null
|
|
18
|
+
});
|
|
19
|
+
Object.defineProperty(this, "componentPositions", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: new Map()
|
|
24
|
+
});
|
|
25
|
+
this.theme = theme;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Calculate layout based on registered components
|
|
29
|
+
* Returns the plot area bounds
|
|
30
|
+
*/
|
|
31
|
+
calculateLayout(components) {
|
|
32
|
+
// Start with base margins from theme
|
|
33
|
+
let marginTop = this.theme.margins.top;
|
|
34
|
+
let marginRight = this.theme.margins.right;
|
|
35
|
+
let marginBottom = this.theme.margins.bottom;
|
|
36
|
+
let marginLeft = this.theme.margins.left;
|
|
37
|
+
// Accumulate space requirements from components
|
|
38
|
+
for (const component of components) {
|
|
39
|
+
const space = component.getRequiredSpace();
|
|
40
|
+
switch (space.position) {
|
|
41
|
+
case 'top':
|
|
42
|
+
marginTop += space.height;
|
|
43
|
+
break;
|
|
44
|
+
case 'bottom':
|
|
45
|
+
marginBottom += space.height;
|
|
46
|
+
break;
|
|
47
|
+
case 'left':
|
|
48
|
+
marginLeft += space.width;
|
|
49
|
+
break;
|
|
50
|
+
case 'right':
|
|
51
|
+
marginRight += space.width;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Calculate plot area bounds
|
|
56
|
+
this.plotBounds = {
|
|
57
|
+
left: marginLeft,
|
|
58
|
+
right: this.theme.width - marginRight,
|
|
59
|
+
top: marginTop,
|
|
60
|
+
bottom: this.theme.height - marginBottom,
|
|
61
|
+
width: this.theme.width - marginLeft - marginRight,
|
|
62
|
+
height: this.theme.height - marginTop - marginBottom,
|
|
63
|
+
};
|
|
64
|
+
// Calculate component positions
|
|
65
|
+
this.calculateComponentPositions(components);
|
|
66
|
+
return this.plotBounds;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get the position for a specific component
|
|
70
|
+
*/
|
|
71
|
+
getComponentPosition(component) {
|
|
72
|
+
const position = this.componentPositions.get(component);
|
|
73
|
+
if (!position) {
|
|
74
|
+
throw new Error('Component not found in layout');
|
|
75
|
+
}
|
|
76
|
+
return position;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Calculate positions for all components based on their space requirements
|
|
80
|
+
* Components are positioned in registration order, stacking outward from the plot area
|
|
81
|
+
*/
|
|
82
|
+
calculateComponentPositions(components) {
|
|
83
|
+
if (!this.plotBounds)
|
|
84
|
+
return;
|
|
85
|
+
// Track cumulative offsets from plot area edges
|
|
86
|
+
let topOffset = 0;
|
|
87
|
+
let bottomOffset = 0;
|
|
88
|
+
let leftOffset = 0;
|
|
89
|
+
let rightOffset = 0;
|
|
90
|
+
for (const component of components) {
|
|
91
|
+
const space = component.getRequiredSpace();
|
|
92
|
+
let x = 0;
|
|
93
|
+
let y = 0;
|
|
94
|
+
switch (space.position) {
|
|
95
|
+
case 'top':
|
|
96
|
+
// Position above plot area, stacking upward
|
|
97
|
+
x = 0;
|
|
98
|
+
y = this.plotBounds.top - topOffset - space.height;
|
|
99
|
+
topOffset += space.height;
|
|
100
|
+
break;
|
|
101
|
+
case 'bottom':
|
|
102
|
+
// Position below plot area, stacking downward
|
|
103
|
+
x = 0;
|
|
104
|
+
y = this.plotBounds.bottom + bottomOffset;
|
|
105
|
+
bottomOffset += space.height;
|
|
106
|
+
break;
|
|
107
|
+
case 'left':
|
|
108
|
+
// Position left of plot area, stacking leftward
|
|
109
|
+
x = this.plotBounds.left - leftOffset - space.width;
|
|
110
|
+
y = 0;
|
|
111
|
+
leftOffset += space.width;
|
|
112
|
+
break;
|
|
113
|
+
case 'right':
|
|
114
|
+
// Position right of plot area, stacking rightward
|
|
115
|
+
x = this.plotBounds.right + rightOffset;
|
|
116
|
+
y = 0;
|
|
117
|
+
rightOffset += space.width;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
this.componentPositions.set(component, { x, y });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
package/legend.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Selection } from 'd3';
|
|
2
|
+
import type { LegendConfig, ChartTheme } from './types';
|
|
3
|
+
import type { Line } from './line';
|
|
4
|
+
import type { Bar } from './bar';
|
|
5
|
+
import type { LayoutAwareComponent, ComponentSpace } from '@/lib/chart-interface';
|
|
6
|
+
export declare class Legend implements LayoutAwareComponent {
|
|
7
|
+
readonly type: "legend";
|
|
8
|
+
readonly position: LegendConfig['position'];
|
|
9
|
+
private readonly marginTop;
|
|
10
|
+
private readonly marginBottom;
|
|
11
|
+
private readonly itemHeight;
|
|
12
|
+
private visibilityState;
|
|
13
|
+
private onToggleCallback?;
|
|
14
|
+
constructor(config?: LegendConfig);
|
|
15
|
+
setToggleCallback(callback: () => void): void;
|
|
16
|
+
isSeriesVisible(dataKey: string): boolean;
|
|
17
|
+
private getCheckmarkPath;
|
|
18
|
+
/**
|
|
19
|
+
* Returns the space required by the legend
|
|
20
|
+
*/
|
|
21
|
+
getRequiredSpace(): ComponentSpace;
|
|
22
|
+
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, series: (Line | Bar)[], theme: ChartTheme, width: number, _x?: number, y?: number): void;
|
|
23
|
+
}
|
package/legend.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {} from 'd3';
|
|
2
|
+
import { getSeriesColor } from './types';
|
|
3
|
+
export class Legend {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
Object.defineProperty(this, "type", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: 'legend'
|
|
10
|
+
});
|
|
11
|
+
Object.defineProperty(this, "position", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, "marginTop", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: void 0
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "marginBottom", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: void 0
|
|
28
|
+
});
|
|
29
|
+
Object.defineProperty(this, "itemHeight", {
|
|
30
|
+
enumerable: true,
|
|
31
|
+
configurable: true,
|
|
32
|
+
writable: true,
|
|
33
|
+
value: 15
|
|
34
|
+
});
|
|
35
|
+
Object.defineProperty(this, "visibilityState", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: new Map()
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(this, "onToggleCallback", {
|
|
42
|
+
enumerable: true,
|
|
43
|
+
configurable: true,
|
|
44
|
+
writable: true,
|
|
45
|
+
value: void 0
|
|
46
|
+
});
|
|
47
|
+
this.position = config?.position || 'bottom';
|
|
48
|
+
this.marginTop = config?.marginTop ?? 20;
|
|
49
|
+
this.marginBottom = config?.marginBottom ?? 10;
|
|
50
|
+
}
|
|
51
|
+
setToggleCallback(callback) {
|
|
52
|
+
this.onToggleCallback = callback;
|
|
53
|
+
}
|
|
54
|
+
isSeriesVisible(dataKey) {
|
|
55
|
+
return this.visibilityState.get(dataKey) ?? true;
|
|
56
|
+
}
|
|
57
|
+
getCheckmarkPath(size) {
|
|
58
|
+
const scale = size / 24;
|
|
59
|
+
return `M ${4 * scale} ${12 * scale} L ${9 * scale} ${17 * scale} L ${20 * scale} ${6 * scale}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns the space required by the legend
|
|
63
|
+
*/
|
|
64
|
+
getRequiredSpace() {
|
|
65
|
+
return {
|
|
66
|
+
width: 0, // Legend spans full width
|
|
67
|
+
height: this.marginTop + this.itemHeight + this.marginBottom,
|
|
68
|
+
position: 'bottom',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
render(svg, series, theme, width, _x = 0, y = 0) {
|
|
72
|
+
const boxSize = theme.legend.boxSize;
|
|
73
|
+
const gapBetweenBoxAndText = 8;
|
|
74
|
+
const itemSpacing = 20; // Space between legend items
|
|
75
|
+
const legendItems = series.map((s) => ({
|
|
76
|
+
label: s.dataKey,
|
|
77
|
+
color: getSeriesColor(s),
|
|
78
|
+
dataKey: s.dataKey,
|
|
79
|
+
}));
|
|
80
|
+
legendItems.forEach((item) => {
|
|
81
|
+
if (!this.visibilityState.has(item.dataKey)) {
|
|
82
|
+
this.visibilityState.set(item.dataKey, true);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Create temporary text elements to measure widths
|
|
86
|
+
const tempSvg = svg.append('g').style('visibility', 'hidden');
|
|
87
|
+
const itemWidths = legendItems.map((item) => {
|
|
88
|
+
const textElem = tempSvg
|
|
89
|
+
.append('text')
|
|
90
|
+
.attr('font-size', `${theme.legend.fontSize}px`)
|
|
91
|
+
.attr('font-family', theme.axis.fontFamily)
|
|
92
|
+
.text(item.label);
|
|
93
|
+
const textWidth = textElem.node()?.getBBox().width || 0;
|
|
94
|
+
textElem.remove();
|
|
95
|
+
return boxSize + gapBetweenBoxAndText + textWidth;
|
|
96
|
+
});
|
|
97
|
+
tempSvg.remove();
|
|
98
|
+
// Calculate positions for each item
|
|
99
|
+
const itemPositions = [];
|
|
100
|
+
let currentX = 0;
|
|
101
|
+
itemWidths.forEach((itemWidth) => {
|
|
102
|
+
itemPositions.push(currentX);
|
|
103
|
+
currentX += itemWidth + itemSpacing;
|
|
104
|
+
});
|
|
105
|
+
const totalLegendWidth = currentX - itemSpacing;
|
|
106
|
+
const legendX = (width - totalLegendWidth) / 2;
|
|
107
|
+
const legendY = y + this.marginTop;
|
|
108
|
+
const legend = svg
|
|
109
|
+
.append('g')
|
|
110
|
+
.attr('class', 'legend')
|
|
111
|
+
.attr('transform', `translate(${legendX}, ${legendY})`);
|
|
112
|
+
const legendGroups = legend
|
|
113
|
+
.selectAll('g')
|
|
114
|
+
.data(legendItems)
|
|
115
|
+
.join('g')
|
|
116
|
+
.attr('transform', (_d, i) => `translate(${itemPositions[i]}, 0)`)
|
|
117
|
+
.style('cursor', 'pointer')
|
|
118
|
+
.on('click', (_event, d) => {
|
|
119
|
+
const currentState = this.visibilityState.get(d.dataKey) ?? true;
|
|
120
|
+
this.visibilityState.set(d.dataKey, !currentState);
|
|
121
|
+
if (this.onToggleCallback) {
|
|
122
|
+
this.onToggleCallback();
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
// Add checkbox rect
|
|
126
|
+
legendGroups
|
|
127
|
+
.append('rect')
|
|
128
|
+
.attr('width', boxSize)
|
|
129
|
+
.attr('height', boxSize)
|
|
130
|
+
.attr('fill', (d) => {
|
|
131
|
+
const isVisible = this.visibilityState.get(d.dataKey) ?? true;
|
|
132
|
+
return isVisible ? d.color : theme.legend.uncheckedColor;
|
|
133
|
+
})
|
|
134
|
+
.attr('rx', 2);
|
|
135
|
+
// Add checkmark when visible
|
|
136
|
+
legendGroups
|
|
137
|
+
.append('path')
|
|
138
|
+
.attr('d', this.getCheckmarkPath(boxSize))
|
|
139
|
+
.attr('fill', 'none')
|
|
140
|
+
.attr('stroke', '#000')
|
|
141
|
+
.attr('stroke-width', 2)
|
|
142
|
+
.attr('stroke-linecap', 'round')
|
|
143
|
+
.attr('stroke-linejoin', 'round')
|
|
144
|
+
.style('display', (d) => {
|
|
145
|
+
const isVisible = this.visibilityState.get(d.dataKey) ?? true;
|
|
146
|
+
return isVisible ? 'block' : 'none';
|
|
147
|
+
});
|
|
148
|
+
// Add label text
|
|
149
|
+
legendGroups
|
|
150
|
+
.append('text')
|
|
151
|
+
.attr('x', boxSize + gapBetweenBoxAndText)
|
|
152
|
+
.attr('y', boxSize / 2 + 4)
|
|
153
|
+
.attr('font-size', `${theme.legend.fontSize}px`)
|
|
154
|
+
.attr('font-family', theme.axis.fontFamily)
|
|
155
|
+
.text((d) => d.label);
|
|
156
|
+
}
|
|
157
|
+
}
|
package/line.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Selection } from 'd3';
|
|
2
|
+
import type { LineConfig, DataItem, ScaleType } from './types';
|
|
3
|
+
import type { ChartComponent } from '@/lib/chart-interface';
|
|
4
|
+
export declare class Line implements ChartComponent {
|
|
5
|
+
readonly type: "line";
|
|
6
|
+
readonly dataKey: string;
|
|
7
|
+
readonly stroke: string;
|
|
8
|
+
readonly strokeWidth: number;
|
|
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?: ScaleType): void;
|
|
11
|
+
}
|
package/line.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { line } from 'd3';
|
|
2
|
+
export class Line {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
Object.defineProperty(this, "type", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: 'line'
|
|
9
|
+
});
|
|
10
|
+
Object.defineProperty(this, "dataKey", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: void 0
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "stroke", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: void 0
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(this, "strokeWidth", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: void 0
|
|
27
|
+
});
|
|
28
|
+
this.dataKey = config.dataKey;
|
|
29
|
+
this.stroke = config.stroke || '#8884d8';
|
|
30
|
+
this.strokeWidth = config.strokeWidth || 2;
|
|
31
|
+
}
|
|
32
|
+
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band') {
|
|
33
|
+
const getXPosition = (d) => {
|
|
34
|
+
const xValue = d[xKey];
|
|
35
|
+
// Handle different scale types appropriately
|
|
36
|
+
let scaledValue;
|
|
37
|
+
switch (xScaleType) {
|
|
38
|
+
case 'band':
|
|
39
|
+
// Band scale - use string or value as-is
|
|
40
|
+
scaledValue = xValue;
|
|
41
|
+
break;
|
|
42
|
+
case 'time':
|
|
43
|
+
// Time scale - convert to Date
|
|
44
|
+
scaledValue =
|
|
45
|
+
xValue instanceof Date ? xValue : new Date(xValue);
|
|
46
|
+
break;
|
|
47
|
+
case 'linear':
|
|
48
|
+
case 'log':
|
|
49
|
+
// Linear/log scale - convert to number
|
|
50
|
+
scaledValue =
|
|
51
|
+
typeof xValue === 'number' ? xValue : Number(xValue);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
const scaled = x(scaledValue);
|
|
55
|
+
// Handle band scales with bandwidth
|
|
56
|
+
return (scaled || 0) + (x.bandwidth ? x.bandwidth() / 2 : 0);
|
|
57
|
+
};
|
|
58
|
+
const lineGenerator = line()
|
|
59
|
+
.x(getXPosition)
|
|
60
|
+
.y((d) => y(parseValue(d[this.dataKey])) || 0);
|
|
61
|
+
// Add line path
|
|
62
|
+
plotGroup
|
|
63
|
+
.append('path')
|
|
64
|
+
.datum(data)
|
|
65
|
+
.attr('fill', 'none')
|
|
66
|
+
.attr('stroke', this.stroke)
|
|
67
|
+
.attr('stroke-width', this.strokeWidth)
|
|
68
|
+
.attr('d', lineGenerator);
|
|
69
|
+
// Add data point circles
|
|
70
|
+
plotGroup
|
|
71
|
+
.selectAll(`.circle-${this.dataKey.replace(/\s+/g, '-')}`)
|
|
72
|
+
.data(data)
|
|
73
|
+
.join('circle')
|
|
74
|
+
.attr('class', `circle-${this.dataKey.replace(/\s+/g, '-')}`)
|
|
75
|
+
.attr('cx', getXPosition)
|
|
76
|
+
.attr('cy', (d) => y(parseValue(d[this.dataKey])) || 0)
|
|
77
|
+
.attr('r', 4)
|
|
78
|
+
.attr('fill', this.stroke)
|
|
79
|
+
.attr('stroke', 'white')
|
|
80
|
+
.attr('stroke-width', 2);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.0.1",
|
|
3
|
+
"name": "@internetstiftelsen/charts",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
"./*": {
|
|
7
|
+
"types": "./*.d.ts",
|
|
8
|
+
"import": "./*.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"*.js",
|
|
13
|
+
"*.d.ts"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "vite",
|
|
17
|
+
"build": "tsc -b && vite build",
|
|
18
|
+
"lint": "eslint .",
|
|
19
|
+
"preview": "vite preview",
|
|
20
|
+
"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",
|
|
22
|
+
"pub": "npm run prepub && cd dist && npm publish --access public"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@radix-ui/react-label": "^2.1.8",
|
|
26
|
+
"@radix-ui/react-select": "^2.2.6",
|
|
27
|
+
"@radix-ui/react-switch": "^1.2.6",
|
|
28
|
+
"@radix-ui/react-tabs": "^1.1.13",
|
|
29
|
+
"@tailwindcss/vite": "^4.1.16",
|
|
30
|
+
"@types/d3": "^7.4.3",
|
|
31
|
+
"class-variance-authority": "^0.7.1",
|
|
32
|
+
"clsx": "^2.1.1",
|
|
33
|
+
"d3": "^7.9.0",
|
|
34
|
+
"lucide-react": "^0.548.0",
|
|
35
|
+
"react": "^19.2.0",
|
|
36
|
+
"react-dom": "^19.2.0",
|
|
37
|
+
"tailwind-merge": "^3.3.1",
|
|
38
|
+
"tailwindcss": "^4.1.16"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@eslint/js": "^9.38.0",
|
|
42
|
+
"@types/node": "^24.9.2",
|
|
43
|
+
"@types/react": "^19.2.2",
|
|
44
|
+
"@types/react-dom": "^19.2.2",
|
|
45
|
+
"@vitejs/plugin-react-swc": "^4.2.0",
|
|
46
|
+
"eslint": "^9.38.0",
|
|
47
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
48
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
49
|
+
"globals": "^16.4.0",
|
|
50
|
+
"prettier": "3.6.2",
|
|
51
|
+
"tsc-alias": "^1.8.16",
|
|
52
|
+
"tw-animate-css": "^1.4.0",
|
|
53
|
+
"typescript": "~5.9.3",
|
|
54
|
+
"typescript-eslint": "^8.46.2",
|
|
55
|
+
"vite": "^7.1.12"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/theme.d.ts
ADDED
package/theme.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {} from '@/lib/types';
|
|
2
|
+
export const defaultTheme = {
|
|
3
|
+
width: 928,
|
|
4
|
+
height: 600,
|
|
5
|
+
margins: {
|
|
6
|
+
top: 20,
|
|
7
|
+
right: 20,
|
|
8
|
+
bottom: 30,
|
|
9
|
+
left: 40,
|
|
10
|
+
},
|
|
11
|
+
gridColor: '#e0e0e0',
|
|
12
|
+
colorPalette: [
|
|
13
|
+
'#50b2fc', // ocean
|
|
14
|
+
'#ff4069', // ruby
|
|
15
|
+
'#55c7b4', // jade
|
|
16
|
+
'#ffce2e', // lemon
|
|
17
|
+
'#c27fec', // peacock
|
|
18
|
+
'#f99963', // sandstone
|
|
19
|
+
'#ff9fb4', // ruby-light
|
|
20
|
+
'#1f2a36', // cyberspace
|
|
21
|
+
],
|
|
22
|
+
axis: {
|
|
23
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
|
|
24
|
+
fontSize: '14',
|
|
25
|
+
},
|
|
26
|
+
legend: {
|
|
27
|
+
boxSize: 20,
|
|
28
|
+
uncheckedColor: '#d0d0d0',
|
|
29
|
+
fontSize: 14,
|
|
30
|
+
},
|
|
31
|
+
};
|
package/title.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Selection } from 'd3';
|
|
2
|
+
import type { TitleConfig, ChartTheme } from './types';
|
|
3
|
+
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface';
|
|
4
|
+
export declare class Title implements LayoutAwareComponent {
|
|
5
|
+
readonly type: "title";
|
|
6
|
+
readonly text: string;
|
|
7
|
+
private readonly fontSize;
|
|
8
|
+
private readonly fontWeight;
|
|
9
|
+
private readonly align;
|
|
10
|
+
private readonly marginTop;
|
|
11
|
+
private readonly marginBottom;
|
|
12
|
+
constructor(config: TitleConfig);
|
|
13
|
+
/**
|
|
14
|
+
* Returns the space required by the title
|
|
15
|
+
*/
|
|
16
|
+
getRequiredSpace(): ComponentSpace;
|
|
17
|
+
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, theme: ChartTheme, width: number, x?: number, y?: number): void;
|
|
18
|
+
}
|
package/title.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {} from 'd3';
|
|
2
|
+
export class Title {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
Object.defineProperty(this, "type", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: 'title'
|
|
9
|
+
});
|
|
10
|
+
Object.defineProperty(this, "text", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: void 0
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "fontSize", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: void 0
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(this, "fontWeight", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: void 0
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "align", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: void 0
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "marginTop", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: void 0
|
|
39
|
+
});
|
|
40
|
+
Object.defineProperty(this, "marginBottom", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
value: void 0
|
|
45
|
+
});
|
|
46
|
+
this.text = config.text;
|
|
47
|
+
this.fontSize = config.fontSize ?? 18;
|
|
48
|
+
this.fontWeight = config.fontWeight ?? 'bold';
|
|
49
|
+
this.align = config.align ?? 'center';
|
|
50
|
+
this.marginTop = config.marginTop ?? 10;
|
|
51
|
+
this.marginBottom = config.marginBottom ?? 15;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Returns the space required by the title
|
|
55
|
+
*/
|
|
56
|
+
getRequiredSpace() {
|
|
57
|
+
return {
|
|
58
|
+
width: 0, // Title spans full width
|
|
59
|
+
height: this.marginTop + this.fontSize + this.marginBottom,
|
|
60
|
+
position: 'top',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
render(svg, theme, width, x = 0, y = 0) {
|
|
64
|
+
const titleGroup = svg
|
|
65
|
+
.append('g')
|
|
66
|
+
.attr('class', 'title')
|
|
67
|
+
.attr('transform', `translate(${x}, ${y})`);
|
|
68
|
+
let textX = width / 2;
|
|
69
|
+
let textAnchor = 'middle';
|
|
70
|
+
if (this.align === 'left') {
|
|
71
|
+
textX = theme.margins.left;
|
|
72
|
+
textAnchor = 'start';
|
|
73
|
+
}
|
|
74
|
+
else if (this.align === 'right') {
|
|
75
|
+
textX = width - theme.margins.right;
|
|
76
|
+
textAnchor = 'end';
|
|
77
|
+
}
|
|
78
|
+
titleGroup
|
|
79
|
+
.append('text')
|
|
80
|
+
.attr('x', textX)
|
|
81
|
+
.attr('y', this.marginTop + this.fontSize)
|
|
82
|
+
.attr('text-anchor', textAnchor)
|
|
83
|
+
.attr('font-size', `${this.fontSize}px`)
|
|
84
|
+
.attr('font-weight', this.fontWeight)
|
|
85
|
+
.attr('font-family', theme.axis.fontFamily)
|
|
86
|
+
.text(this.text);
|
|
87
|
+
}
|
|
88
|
+
}
|
package/tooltip.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Selection } from 'd3';
|
|
2
|
+
import type { TooltipConfig, DataItem, ChartTheme } from './types';
|
|
3
|
+
import type { ChartComponent } from '@/lib/chart-interface';
|
|
4
|
+
import type { Line } from './line';
|
|
5
|
+
import type { Bar } from './bar';
|
|
6
|
+
import type { PlotAreaBounds } from './layout-manager';
|
|
7
|
+
export declare class Tooltip implements ChartComponent {
|
|
8
|
+
readonly type: "tooltip";
|
|
9
|
+
readonly formatter?: (dataKey: string, value: any, data: DataItem) => string;
|
|
10
|
+
private tooltipDiv;
|
|
11
|
+
constructor(config?: TooltipConfig);
|
|
12
|
+
initialize(theme: ChartTheme): void;
|
|
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
|
+
cleanup(): void;
|
|
15
|
+
}
|