chartifypdf 0.1.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/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/components/Axes.d.ts +15 -0
- package/dist/components/GridLines.d.ts +15 -0
- package/dist/components/LineChart.d.ts +3 -0
- package/dist/components/LinePath.d.ts +12 -0
- package/dist/components/Tooltip.d.ts +15 -0
- package/dist/components/ZoomControls.d.ts +19 -0
- package/dist/hooks/useChartDimensions.d.ts +5 -0
- package/dist/hooks/useScales.d.ts +10 -0
- package/dist/hooks/useTooltip.d.ts +12 -0
- package/dist/hooks/useZoom.d.ts +15 -0
- package/dist/index.d.ts +123 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types/chart.types.d.ts +76 -0
- package/dist/utils/color.d.ts +2 -0
- package/dist/utils/math.d.ts +11 -0
- package/dist/utils/path.d.ts +4 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 thuankg1752
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# ChartifyPDF
|
|
2
|
+
|
|
3
|
+
A zero-dependency React line chart component built with pure SVG. Supports zoom, tooltips, responsive sizing, and customizable styling.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Pure SVG** — no external charting libraries
|
|
8
|
+
- **TypeScript** — fully typed props and exports
|
|
9
|
+
- **Responsive** — auto-resizes via ResizeObserver, or set fixed dimensions
|
|
10
|
+
- **Zoom & Pan** — scroll to zoom, drag to pan, with configurable controls
|
|
11
|
+
- **Tooltips** — hover to see data values, with custom formatters and renderers
|
|
12
|
+
- **Multi-series** — plot multiple data lines on one chart
|
|
13
|
+
- **Customizable** — colors, axes, grid lines, fonts, margins, and more
|
|
14
|
+
- **Animation** — line drawing animation on mount
|
|
15
|
+
- **Tree-shakeable** — `sideEffects: false`, ESM + CJS dual output
|
|
16
|
+
- **Accessible** — `role="img"` with configurable `aria-label`
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install chartifypdf
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Peer dependencies:** React >= 19.0.0, React DOM >= 19.0.0
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { LineChart } from "chartifypdf";
|
|
30
|
+
|
|
31
|
+
const data = [
|
|
32
|
+
{
|
|
33
|
+
id: "revenue",
|
|
34
|
+
name: "Revenue",
|
|
35
|
+
data: [
|
|
36
|
+
{ x: 1, y: 100 },
|
|
37
|
+
{ x: 2, y: 200 },
|
|
38
|
+
{ x: 3, y: 150 },
|
|
39
|
+
{ x: 4, y: 300 },
|
|
40
|
+
{ x: 5, y: 280 },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function App() {
|
|
46
|
+
return <LineChart data={data} width={600} height={400} />;
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Props Reference
|
|
51
|
+
|
|
52
|
+
### `LineChartProps`
|
|
53
|
+
|
|
54
|
+
| Prop | Type | Default | Description |
|
|
55
|
+
|------|------|---------|-------------|
|
|
56
|
+
| `data` | `DataSeries[]` | *required* | Array of data series to plot |
|
|
57
|
+
| `width` | `number` | auto (responsive) | Fixed width in pixels |
|
|
58
|
+
| `height` | `number` | `400` | Fixed height in pixels |
|
|
59
|
+
| `margin` | `Partial<ChartMargin>` | `{ top: 20, right: 20, bottom: 50, left: 60 }` | Chart margins |
|
|
60
|
+
| `xAxis` | `AxisConfig` | — | X-axis configuration |
|
|
61
|
+
| `yAxis` | `AxisConfig` | — | Y-axis configuration |
|
|
62
|
+
| `tooltip` | `TooltipConfig` | `{ enabled: true }` | Tooltip configuration |
|
|
63
|
+
| `zoom` | `ZoomConfig` | `{ enabled: false }` | Zoom & pan configuration |
|
|
64
|
+
| `style` | `ChartStyle` | — | Visual style overrides |
|
|
65
|
+
| `animation` | `AnimationConfig` | — | Line drawing animation |
|
|
66
|
+
| `colorPalette` | `string[]` | 10-color palette | Custom color palette |
|
|
67
|
+
| `className` | `string` | — | CSS class for container div |
|
|
68
|
+
| `onPointClick` | `(point, series) => void` | — | Click handler for data points |
|
|
69
|
+
| `ariaLabel` | `string` | `"Line chart"` | Accessibility label |
|
|
70
|
+
|
|
71
|
+
### `DataSeries`
|
|
72
|
+
|
|
73
|
+
| Field | Type | Default | Description |
|
|
74
|
+
|-------|------|---------|-------------|
|
|
75
|
+
| `id` | `string` | *required* | Unique identifier |
|
|
76
|
+
| `name` | `string` | *required* | Display name (shown in tooltip) |
|
|
77
|
+
| `data` | `DataPoint[]` | *required* | Array of `{ x, y, label? }` |
|
|
78
|
+
| `color` | `string` | auto | Line color (from palette if not set) |
|
|
79
|
+
| `strokeWidth` | `number` | `2` | Line thickness |
|
|
80
|
+
| `strokeDasharray` | `string` | — | SVG dash pattern (e.g. `"5 3"`) |
|
|
81
|
+
| `showDots` | `boolean` | `false` | Show circles at data points |
|
|
82
|
+
| `dotRadius` | `number` | `3.5` | Dot circle radius |
|
|
83
|
+
|
|
84
|
+
### `AxisConfig`
|
|
85
|
+
|
|
86
|
+
| Field | Type | Default | Description |
|
|
87
|
+
|-------|------|---------|-------------|
|
|
88
|
+
| `label` | `string` | — | Axis label text |
|
|
89
|
+
| `tickCount` | `number` | `6` | Approximate number of ticks |
|
|
90
|
+
| `tickFormat` | `(value) => string` | auto | Custom tick label formatter |
|
|
91
|
+
| `min` | `number` | auto | Override axis minimum |
|
|
92
|
+
| `max` | `number` | auto | Override axis maximum |
|
|
93
|
+
| `gridLines` | `boolean` | Y: `true`, X: `false` | Show grid lines |
|
|
94
|
+
| `gridLineColor` | `string` | `"#e0e0e0"` | Grid line color |
|
|
95
|
+
|
|
96
|
+
### `TooltipConfig`
|
|
97
|
+
|
|
98
|
+
| Field | Type | Default | Description |
|
|
99
|
+
|-------|------|---------|-------------|
|
|
100
|
+
| `enabled` | `boolean` | `true` | Enable/disable tooltip |
|
|
101
|
+
| `backgroundColor` | `string` | `"rgba(0,0,0,0.8)"` | Tooltip background |
|
|
102
|
+
| `textColor` | `string` | `"#fff"` | Tooltip text color |
|
|
103
|
+
| `formatter` | `(point, series) => string` | auto | Custom text formatter |
|
|
104
|
+
| `renderCustom` | `(point, series) => ReactNode` | — | Full custom render |
|
|
105
|
+
|
|
106
|
+
### `ZoomConfig`
|
|
107
|
+
|
|
108
|
+
| Field | Type | Default | Description |
|
|
109
|
+
|-------|------|---------|-------------|
|
|
110
|
+
| `enabled` | `boolean` | `false` | Enable zoom & pan |
|
|
111
|
+
| `minScale` | `number` | `1` | Minimum zoom level |
|
|
112
|
+
| `maxScale` | `number` | `10` | Maximum zoom level |
|
|
113
|
+
| `step` | `number` | `0.5` | Zoom step per action |
|
|
114
|
+
| `showControls` | `boolean` | `true` | Show +/-/reset buttons |
|
|
115
|
+
| `controlsPosition` | `string` | `"top-right"` | `"top-left"` \| `"top-right"` \| `"bottom-left"` \| `"bottom-right"` |
|
|
116
|
+
| `enableWheel` | `boolean` | `true` | Zoom with mouse wheel |
|
|
117
|
+
| `enablePan` | `boolean` | `true` | Pan by dragging |
|
|
118
|
+
|
|
119
|
+
### `ChartStyle`
|
|
120
|
+
|
|
121
|
+
| Field | Type | Default | Description |
|
|
122
|
+
|-------|------|---------|-------------|
|
|
123
|
+
| `backgroundColor` | `string` | — | Chart background color |
|
|
124
|
+
| `fontFamily` | `string` | `"sans-serif"` | Font for labels/ticks |
|
|
125
|
+
| `fontSize` | `number` | `11` | Base font size (px) |
|
|
126
|
+
| `axisColor` | `string` | `"#333"` | Axis line color |
|
|
127
|
+
| `tickColor` | `string` | `"#666"` | Tick label color |
|
|
128
|
+
|
|
129
|
+
### `AnimationConfig`
|
|
130
|
+
|
|
131
|
+
| Field | Type | Default | Description |
|
|
132
|
+
|-------|------|---------|-------------|
|
|
133
|
+
| `enabled` | `boolean` | `false` | Enable line draw animation |
|
|
134
|
+
| `duration` | `number` | `800` | Animation duration (ms) |
|
|
135
|
+
| `easing` | `string` | `"ease-in-out"` | CSS easing function |
|
|
136
|
+
|
|
137
|
+
## Examples
|
|
138
|
+
|
|
139
|
+
### Responsive Chart (auto width)
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<LineChart data={data} height={300} />
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The chart fills its parent container's width and updates on resize.
|
|
146
|
+
|
|
147
|
+
### Multi-series with Custom Colors
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
const data = [
|
|
151
|
+
{
|
|
152
|
+
id: "sales",
|
|
153
|
+
name: "Sales",
|
|
154
|
+
data: [{ x: 1, y: 30 }, { x: 2, y: 50 }, { x: 3, y: 45 }],
|
|
155
|
+
color: "#ff6384",
|
|
156
|
+
showDots: true,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "expenses",
|
|
160
|
+
name: "Expenses",
|
|
161
|
+
data: [{ x: 1, y: 20 }, { x: 2, y: 35 }, { x: 3, y: 40 }],
|
|
162
|
+
color: "#36a2eb",
|
|
163
|
+
strokeDasharray: "5 3",
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
<LineChart data={data} width={800} height={400} />
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### With Zoom and Custom Axes
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<LineChart
|
|
174
|
+
data={data}
|
|
175
|
+
width={800}
|
|
176
|
+
height={400}
|
|
177
|
+
xAxis={{ label: "Month", tickCount: 12, gridLines: true }}
|
|
178
|
+
yAxis={{ label: "Revenue ($)", tickFormat: (v) => `$${v}` }}
|
|
179
|
+
zoom={{ enabled: true, maxScale: 5, controlsPosition: "top-left" }}
|
|
180
|
+
/>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Custom Tooltip
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
<LineChart
|
|
187
|
+
data={data}
|
|
188
|
+
tooltip={{
|
|
189
|
+
formatter: (point, series) =>
|
|
190
|
+
`${series.name}: ${point.y.toFixed(2)} at x=${point.x}`,
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Custom Tooltip with React Node
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
<LineChart
|
|
199
|
+
data={data}
|
|
200
|
+
tooltip={{
|
|
201
|
+
renderCustom: (point, series) => (
|
|
202
|
+
<div>
|
|
203
|
+
<strong>{series.name}</strong>
|
|
204
|
+
<br />
|
|
205
|
+
X: {point.x} | Y: {point.y}
|
|
206
|
+
</div>
|
|
207
|
+
),
|
|
208
|
+
}}
|
|
209
|
+
/>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### With Animation
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
<LineChart
|
|
216
|
+
data={data}
|
|
217
|
+
animation={{ enabled: true, duration: 1200, easing: "ease-out" }}
|
|
218
|
+
/>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Point Click Handler
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<LineChart
|
|
225
|
+
data={data}
|
|
226
|
+
onPointClick={(point, series) => {
|
|
227
|
+
console.log(`Clicked ${series.name}: (${point.x}, ${point.y})`);
|
|
228
|
+
}}
|
|
229
|
+
/>
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Note: Set `showDots: true` on the series to make points clickable.
|
|
233
|
+
|
|
234
|
+
## Exported Hooks
|
|
235
|
+
|
|
236
|
+
For advanced usage, you can import the hooks directly:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
import { useChartDimensions, useScales, useZoom, useTooltip } from "chartifypdf";
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| Hook | Purpose |
|
|
243
|
+
|------|---------|
|
|
244
|
+
| `useChartDimensions` | Responsive container measurement via ResizeObserver |
|
|
245
|
+
| `useScales` | Compute linear scales, ticks, and domains from data |
|
|
246
|
+
| `useZoom` | Manage zoom/pan state and event handlers |
|
|
247
|
+
| `useTooltip` | Track mouse position and find nearest data point |
|
|
248
|
+
|
|
249
|
+
## Browser Support
|
|
250
|
+
|
|
251
|
+
Works in all modern browsers that support:
|
|
252
|
+
- SVG
|
|
253
|
+
- ResizeObserver
|
|
254
|
+
- CSS transitions (for animation)
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { AxisConfig, ChartStyle } from "../types/chart.types";
|
|
3
|
+
interface AxesProps {
|
|
4
|
+
xTicks: number[];
|
|
5
|
+
yTicks: number[];
|
|
6
|
+
xScale: (v: number) => number;
|
|
7
|
+
yScale: (v: number) => number;
|
|
8
|
+
plotWidth: number;
|
|
9
|
+
plotHeight: number;
|
|
10
|
+
xAxisConfig?: AxisConfig;
|
|
11
|
+
yAxisConfig?: AxisConfig;
|
|
12
|
+
style?: ChartStyle;
|
|
13
|
+
}
|
|
14
|
+
export declare const Axes: React.FC<AxesProps>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface GridLinesProps {
|
|
3
|
+
xTicks: number[];
|
|
4
|
+
yTicks: number[];
|
|
5
|
+
xScale: (v: number) => number;
|
|
6
|
+
yScale: (v: number) => number;
|
|
7
|
+
plotWidth: number;
|
|
8
|
+
plotHeight: number;
|
|
9
|
+
showXGrid: boolean;
|
|
10
|
+
showYGrid: boolean;
|
|
11
|
+
xGridColor?: string;
|
|
12
|
+
yGridColor?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const GridLines: React.FC<GridLinesProps>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { DataSeries, DataPoint, AnimationConfig } from "../types/chart.types";
|
|
3
|
+
interface LinePathProps {
|
|
4
|
+
series: DataSeries;
|
|
5
|
+
xScale: (v: number) => number;
|
|
6
|
+
yScale: (v: number) => number;
|
|
7
|
+
color: string;
|
|
8
|
+
animation?: AnimationConfig;
|
|
9
|
+
onPointClick?: (point: DataPoint, series: DataSeries) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare const LinePath: React.FC<LinePathProps>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { DataPoint, DataSeries, TooltipConfig } from "../types/chart.types";
|
|
3
|
+
interface TooltipProps {
|
|
4
|
+
visible: boolean;
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
point: DataPoint | null;
|
|
8
|
+
series: DataSeries | null;
|
|
9
|
+
plotHeight: number;
|
|
10
|
+
plotWidth: number;
|
|
11
|
+
config?: TooltipConfig;
|
|
12
|
+
seriesColor: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const Tooltip: React.FC<TooltipProps>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ZoomConfig } from "../types/chart.types";
|
|
3
|
+
interface ZoomControlsProps {
|
|
4
|
+
config?: ZoomConfig;
|
|
5
|
+
svgWidth: number;
|
|
6
|
+
svgHeight: number;
|
|
7
|
+
margin: {
|
|
8
|
+
top: number;
|
|
9
|
+
right: number;
|
|
10
|
+
bottom: number;
|
|
11
|
+
left: number;
|
|
12
|
+
};
|
|
13
|
+
scale: number;
|
|
14
|
+
onZoomIn: () => void;
|
|
15
|
+
onZoomOut: () => void;
|
|
16
|
+
onReset: () => void;
|
|
17
|
+
}
|
|
18
|
+
export declare const ZoomControls: React.FC<ZoomControlsProps>;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DataSeries, AxisConfig } from "../types/chart.types";
|
|
2
|
+
export interface ScalesResult {
|
|
3
|
+
xScale: (value: number) => number;
|
|
4
|
+
yScale: (value: number) => number;
|
|
5
|
+
xTicks: number[];
|
|
6
|
+
yTicks: number[];
|
|
7
|
+
xDomain: [number, number];
|
|
8
|
+
yDomain: [number, number];
|
|
9
|
+
}
|
|
10
|
+
export declare function useScales(data: DataSeries[], plotWidth: number, plotHeight: number, xAxisConfig?: AxisConfig, yAxisConfig?: AxisConfig): ScalesResult;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
import type { DataSeries, DataPoint, TooltipConfig } from "../types/chart.types";
|
|
3
|
+
export interface TooltipState {
|
|
4
|
+
tooltipVisible: boolean;
|
|
5
|
+
tooltipX: number;
|
|
6
|
+
tooltipY: number;
|
|
7
|
+
activePoint: DataPoint | null;
|
|
8
|
+
activeSeries: DataSeries | null;
|
|
9
|
+
handleMouseMove: (e: React.MouseEvent) => void;
|
|
10
|
+
handleMouseLeave: () => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function useTooltip(svgRef: RefObject<SVGSVGElement | null>, data: DataSeries[], xScale: (v: number) => number, yScale: (v: number) => number, marginLeft: number, marginTop: number, plotWidth: number, _config?: TooltipConfig): TooltipState;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ZoomConfig } from "../types/chart.types";
|
|
2
|
+
export interface ZoomState {
|
|
3
|
+
scale: number;
|
|
4
|
+
offsetX: number;
|
|
5
|
+
offsetY: number;
|
|
6
|
+
isPanning: boolean;
|
|
7
|
+
zoomIn: () => void;
|
|
8
|
+
zoomOut: () => void;
|
|
9
|
+
resetZoom: () => void;
|
|
10
|
+
handleWheel: (e: React.WheelEvent) => void;
|
|
11
|
+
handlePanStart: (e: React.MouseEvent) => void;
|
|
12
|
+
handlePanMove: (e: React.MouseEvent) => void;
|
|
13
|
+
handlePanEnd: () => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function useZoom(config: ZoomConfig | undefined, plotWidth: number, plotHeight: number): ZoomState;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React$1, { ReactNode, RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
interface DataPoint {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
interface DataSeries {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
data: DataPoint[];
|
|
12
|
+
color?: string;
|
|
13
|
+
strokeWidth?: number;
|
|
14
|
+
strokeDasharray?: string;
|
|
15
|
+
showDots?: boolean;
|
|
16
|
+
dotRadius?: number;
|
|
17
|
+
}
|
|
18
|
+
interface AxisConfig {
|
|
19
|
+
label?: string;
|
|
20
|
+
tickCount?: number;
|
|
21
|
+
tickFormat?: (value: number) => string;
|
|
22
|
+
min?: number;
|
|
23
|
+
max?: number;
|
|
24
|
+
gridLines?: boolean;
|
|
25
|
+
gridLineColor?: string;
|
|
26
|
+
}
|
|
27
|
+
interface TooltipConfig {
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
backgroundColor?: string;
|
|
30
|
+
textColor?: string;
|
|
31
|
+
formatter?: (point: DataPoint, series: DataSeries) => string;
|
|
32
|
+
renderCustom?: (point: DataPoint, series: DataSeries) => ReactNode;
|
|
33
|
+
}
|
|
34
|
+
interface ZoomConfig {
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
minScale?: number;
|
|
37
|
+
maxScale?: number;
|
|
38
|
+
step?: number;
|
|
39
|
+
showControls?: boolean;
|
|
40
|
+
controlsPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
41
|
+
enableWheel?: boolean;
|
|
42
|
+
enablePan?: boolean;
|
|
43
|
+
}
|
|
44
|
+
interface ChartMargin {
|
|
45
|
+
top: number;
|
|
46
|
+
right: number;
|
|
47
|
+
bottom: number;
|
|
48
|
+
left: number;
|
|
49
|
+
}
|
|
50
|
+
interface ChartStyle {
|
|
51
|
+
backgroundColor?: string;
|
|
52
|
+
fontFamily?: string;
|
|
53
|
+
fontSize?: number;
|
|
54
|
+
axisColor?: string;
|
|
55
|
+
tickColor?: string;
|
|
56
|
+
}
|
|
57
|
+
interface AnimationConfig {
|
|
58
|
+
enabled?: boolean;
|
|
59
|
+
duration?: number;
|
|
60
|
+
easing?: string;
|
|
61
|
+
}
|
|
62
|
+
interface LineChartProps {
|
|
63
|
+
data: DataSeries[];
|
|
64
|
+
width?: number;
|
|
65
|
+
height?: number;
|
|
66
|
+
margin?: Partial<ChartMargin>;
|
|
67
|
+
xAxis?: AxisConfig;
|
|
68
|
+
yAxis?: AxisConfig;
|
|
69
|
+
tooltip?: TooltipConfig;
|
|
70
|
+
zoom?: ZoomConfig;
|
|
71
|
+
style?: ChartStyle;
|
|
72
|
+
animation?: AnimationConfig;
|
|
73
|
+
colorPalette?: string[];
|
|
74
|
+
className?: string;
|
|
75
|
+
onPointClick?: (point: DataPoint, series: DataSeries) => void;
|
|
76
|
+
ariaLabel?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
declare const LineChart: React$1.FC<LineChartProps>;
|
|
80
|
+
|
|
81
|
+
declare function useChartDimensions(containerRef: RefObject<HTMLDivElement | null>, providedWidth?: number, providedHeight?: number): {
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
interface ScalesResult {
|
|
87
|
+
xScale: (value: number) => number;
|
|
88
|
+
yScale: (value: number) => number;
|
|
89
|
+
xTicks: number[];
|
|
90
|
+
yTicks: number[];
|
|
91
|
+
xDomain: [number, number];
|
|
92
|
+
yDomain: [number, number];
|
|
93
|
+
}
|
|
94
|
+
declare function useScales(data: DataSeries[], plotWidth: number, plotHeight: number, xAxisConfig?: AxisConfig, yAxisConfig?: AxisConfig): ScalesResult;
|
|
95
|
+
|
|
96
|
+
interface ZoomState {
|
|
97
|
+
scale: number;
|
|
98
|
+
offsetX: number;
|
|
99
|
+
offsetY: number;
|
|
100
|
+
isPanning: boolean;
|
|
101
|
+
zoomIn: () => void;
|
|
102
|
+
zoomOut: () => void;
|
|
103
|
+
resetZoom: () => void;
|
|
104
|
+
handleWheel: (e: React.WheelEvent) => void;
|
|
105
|
+
handlePanStart: (e: React.MouseEvent) => void;
|
|
106
|
+
handlePanMove: (e: React.MouseEvent) => void;
|
|
107
|
+
handlePanEnd: () => void;
|
|
108
|
+
}
|
|
109
|
+
declare function useZoom(config: ZoomConfig | undefined, plotWidth: number, plotHeight: number): ZoomState;
|
|
110
|
+
|
|
111
|
+
interface TooltipState {
|
|
112
|
+
tooltipVisible: boolean;
|
|
113
|
+
tooltipX: number;
|
|
114
|
+
tooltipY: number;
|
|
115
|
+
activePoint: DataPoint | null;
|
|
116
|
+
activeSeries: DataSeries | null;
|
|
117
|
+
handleMouseMove: (e: React.MouseEvent) => void;
|
|
118
|
+
handleMouseLeave: () => void;
|
|
119
|
+
}
|
|
120
|
+
declare function useTooltip(svgRef: RefObject<SVGSVGElement | null>, data: DataSeries[], xScale: (v: number) => number, yScale: (v: number) => number, marginLeft: number, marginTop: number, plotWidth: number, _config?: TooltipConfig): TooltipState;
|
|
121
|
+
|
|
122
|
+
export { LineChart, useChartDimensions, useScales, useTooltip, useZoom };
|
|
123
|
+
export type { AnimationConfig, AxisConfig, ChartMargin, ChartStyle, DataPoint, DataSeries, LineChartProps, TooltipConfig, ZoomConfig };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var e=require("react/jsx-runtime"),t=require("react");const o=["#4e79a7","#f28e2b","#e15759","#76b7b2","#59a14f","#edc948","#b07aa1","#ff9da7","#9c755f","#bab0ac"];function n(e,t){const n=t??o;return n[e%n.length]}function i(e,o,n){const[i,s]=t.useState({width:o??0,height:n??0}),r=void 0!==o&&void 0!==n;return t.useLayoutEffect(()=>{if(r)return void s({width:o,height:n});const t=e.current;if(!t)return;const i=()=>{const e=t.getBoundingClientRect();s({width:e.width,height:e.height})};i();const a=new ResizeObserver(()=>{i()});return a.observe(t),()=>a.disconnect()},[e,r,o,n]),i}function s(e,t,o){return Math.min(Math.max(e,t),o)}function r(e,t){const o=Math.floor(Math.log10(e)),n=e/Math.pow(10,o);let i;return i=t?n<1.5?1:n<3?2:n<7?5:10:n<=1?1:n<=2?2:n<=5?5:10,i*Math.pow(10,o)}function a(e,t,o){if(e===t){const o=0===e?1:.1*Math.abs(e);e-=o,t+=o}const n=r(t-e,!1),i=r(n/(o-1),!0);return{niceMin:Math.floor(e/i)*i,niceMax:Math.ceil(t/i)*i,tickStep:i}}function l(e,t,o,n){return i=>{if(t===e)return(o+n)/2;const s=function(e,t,o){return e===t?0:(o-e)/(t-e)}(e,t,i);return function(e,t,o){return e+(t-e)*o}(o,n,s)}}function c(e,t,o){const n=[];for(let i=e;i<=t+.5*o;i+=o)n.push(parseFloat(i.toPrecision(12)));return n}function d(e,o,n,i,s){return t.useMemo(()=>{let t=1/0,r=-1/0,d=1/0,h=-1/0;for(const o of e)for(const e of o.data)e.x<t&&(t=e.x),e.x>r&&(r=e.x),e.y<d&&(d=e.y),e.y>h&&(h=e.y);isFinite(t)||(t=0,r=1,d=0,h=1),void 0!==i?.min&&(t=i.min),void 0!==i?.max&&(r=i.max),void 0!==s?.min&&(d=s.min),void 0!==s?.max&&(h=s.max);const u=i?.tickCount??6,f=s?.tickCount??6,x=a(t,r,u),m=a(d,h,f);return{xScale:l(x.niceMin,x.niceMax,0,o),yScale:l(m.niceMin,m.niceMax,n,0),xTicks:c(x.niceMin,x.niceMax,x.tickStep),yTicks:c(m.niceMin,m.niceMax,m.tickStep),xDomain:[x.niceMin,x.niceMax],yDomain:[m.niceMin,m.niceMax]}},[e,o,n,i,s])}function h(e,o,n){const i=e?.enabled??!1,r=e?.minScale??1,a=e?.maxScale??10,l=e?.step??.5,c=e?.enableWheel??!0,d=e?.enablePan??!0,[h,u]=t.useState(1),[f,x]=t.useState(0),[m,y]=t.useState(0),[g,p]=t.useState(!1),b=t.useRef({x:0,y:0,offsetX:0,offsetY:0}),k=t.useCallback((e,t,i)=>{const r=n*(i-1);return{x:s(e,-(o*(i-1)),0),y:s(t,-r,0)}},[o,n]),C=t.useCallback(()=>{i&&u(e=>{const t=s(e+l,r,a);return x(e=>k(e-o*l/2,0,t).x),y(e=>k(0,e-n*l/2,t).y),t})},[i,l,r,a,o,n,k]),S=t.useCallback(()=>{i&&u(e=>{const t=s(e-l,r,a);return x(e=>k(e+o*l/2,0,t).x),y(e=>k(0,e+n*l/2,t).y),t})},[i,l,r,a,o,n,k]),v=t.useCallback(()=>{u(1),x(0),y(0)},[]),M=t.useCallback(e=>{if(!i||!c)return;e.preventDefault();const t=e.currentTarget.getBoundingClientRect(),o=e.clientX-t.left,n=e.clientY-t.top,d=e.deltaY<0?l:-l;u(e=>{const t=s(e+d,r,a),i=t/e;return x(e=>k(o-i*(o-e),0,t).x),y(e=>k(0,n-i*(n-e),t).y),t})},[i,c,l,r,a,k]),j=t.useCallback(e=>{!i||!d||h<=1||0===e.button&&(p(!0),b.current={x:e.clientX,y:e.clientY,offsetX:f,offsetY:m})},[i,d,h,f,m]),w=t.useCallback(e=>{if(!g)return;const t=e.clientX-b.current.x,o=e.clientY-b.current.y,n=k(b.current.offsetX+t,b.current.offsetY+o,h);x(n.x),y(n.y)},[g,h,k]),$=t.useCallback(()=>{p(!1)},[]);return{scale:h,offsetX:f,offsetY:m,isPanning:g,zoomIn:C,zoomOut:S,resetZoom:v,handleWheel:M,handlePanStart:j,handlePanMove:w,handlePanEnd:$}}function u(e,t,o){if(0===e.length)return null;let n=0,i=e.length-1;for(;n<i;){const s=Math.floor((n+i)/2);o(e[s].x)<t?n=s+1:i=s}let s=e[n],r=Math.abs(o(s.x)-t);if(n>0){const i=Math.abs(o(e[n-1].x)-t);i<r&&(r=i,s=e[n-1])}return s}function f(e,o,n,i,s,r,a,l){const[c,d]=t.useState(!1),[h,f]=t.useState(0),[x,m]=t.useState(0),[y,g]=t.useState(null),[p,b]=t.useState(null);return{tooltipVisible:c,tooltipX:h,tooltipY:x,activePoint:y,activeSeries:p,handleMouseMove:t.useCallback(t=>{const r=e.current;if(!r)return;const l=r.getBoundingClientRect(),c=t.clientX-l.left-s;if(c<0||c>a)return void d(!1);let h=null,x=null,y=1/0;for(const e of o){const t=u(e.data,c,n);if(!t)continue;const o=Math.abs(n(t.x)-c);o<y&&(y=o,h=t,x=e)}h&&x&&(f(n(h.x)),m(i(h.y)),g(h),b(x),d(!0))},[e,o,n,i,s,a]),handleMouseLeave:t.useCallback(()=>{d(!1),g(null),b(null)},[])}}const x=e=>Math.abs(e)>=1e6?`${(e/1e6).toFixed(1)}M`:Math.abs(e)>=1e3?`${(e/1e3).toFixed(1)}K`:Number.isInteger(e)?e.toString():e.toFixed(1),m=t.memo(({xTicks:t,yTicks:o,xScale:n,yScale:i,plotWidth:s,plotHeight:r,xAxisConfig:a,yAxisConfig:l,style:c})=>{const d=c?.axisColor??"#333",h=c?.tickColor??"#666",u=c?.fontFamily??"sans-serif",f=c?.fontSize??11,m=a?.tickFormat??x,y=l?.tickFormat??x;return e.jsxs("g",{className:"chartifypdf-axes",children:[e.jsx("line",{x1:0,y1:r,x2:s,y2:r,stroke:d,strokeWidth:1}),t.map(t=>{const o=n(t);return e.jsxs("g",{children:[e.jsx("line",{x1:o,y1:r,x2:o,y2:r+6,stroke:d,strokeWidth:1}),e.jsx("text",{x:o,y:r+18,textAnchor:"middle",fill:h,fontFamily:u,fontSize:f,children:m(t)})]},`x-tick-${t}`)}),a?.label&&e.jsx("text",{x:s/2,y:r+38,textAnchor:"middle",fill:d,fontFamily:u,fontSize:f+1,fontWeight:"bold",children:a.label}),e.jsx("line",{x1:0,y1:0,x2:0,y2:r,stroke:d,strokeWidth:1}),o.map(t=>{const o=i(t);return e.jsxs("g",{children:[e.jsx("line",{x1:-6,y1:o,x2:0,y2:o,stroke:d,strokeWidth:1}),e.jsx("text",{x:-10,y:o,textAnchor:"end",dominantBaseline:"middle",fill:h,fontFamily:u,fontSize:f,children:y(t)})]},`y-tick-${t}`)}),l?.label&&e.jsx("text",{x:0,y:0,textAnchor:"middle",fill:d,fontFamily:u,fontSize:f+1,fontWeight:"bold",transform:`translate(-40, ${r/2}) rotate(-90)`,children:l.label})]})});m.displayName="Axes";const y=t.memo(({xTicks:t,yTicks:o,xScale:n,yScale:i,plotWidth:s,plotHeight:r,showXGrid:a,showYGrid:l,xGridColor:c="#e0e0e0",yGridColor:d="#e0e0e0"})=>e.jsxs("g",{className:"chartifypdf-grid",children:[l&&o.map(t=>{const o=i(t);return e.jsx("line",{x1:0,y1:o,x2:s,y2:o,stroke:d,strokeDasharray:"4 4",strokeOpacity:.5},`y-grid-${t}`)}),a&&t.map(t=>{const o=n(t);return e.jsx("line",{x1:o,y1:0,x2:o,y2:r,stroke:c,strokeDasharray:"4 4",strokeOpacity:.5},`x-grid-${t}`)})]}));y.displayName="GridLines";const g=t.memo(({series:o,xScale:n,yScale:i,color:s,animation:r,onPointClick:a})=>{const l=t.useRef(null),[c,d]=t.useState(0),[h,u]=t.useState(!r?.enabled),f=o.strokeWidth??2,x=o.showDots??!1,m=o.dotRadius??3.5,y=function(e){if(0===e.length)return"";const[t,...o]=e;return`M${t.x},${t.y}`+o.map(e=>`L${e.x},${e.y}`).join("")}(o.data.map(e=>({x:n(e.x),y:i(e.y)})));t.useEffect(()=>{if(r?.enabled&&l.current){const e=l.current.getTotalLength();d(e),u(!1);const t=setTimeout(()=>{u(!0)},r.duration??800);return()=>clearTimeout(t)}},[y,r?.enabled,r?.duration]);const g=r?.enabled&&c>0?{strokeDasharray:c,strokeDashoffset:h?0:c,transition:`stroke-dashoffset ${r.duration??800}ms ${r.easing??"ease-in-out"}`}:{};return e.jsxs("g",{className:"chartifypdf-line-path",children:[e.jsx("path",{ref:l,d:y,fill:"none",stroke:s,strokeWidth:f,strokeDasharray:o.strokeDasharray,strokeLinecap:"round",strokeLinejoin:"round",style:g}),x&&o.data.map((t,r)=>e.jsx("circle",{cx:n(t.x),cy:i(t.y),r:m,fill:s,stroke:"#fff",strokeWidth:1.5,style:{cursor:a?"pointer":"default"},onClick:a?()=>a(t,o):void 0},`dot-${o.id}-${r}`))]})});g.displayName="LinePath";const p=t.memo(({visible:t,x:o,y:n,point:i,series:s,plotHeight:r,plotWidth:a,config:l,seriesColor:c})=>{if(!t||!i||!s)return null;const d=l?.backgroundColor??"rgba(0, 0, 0, 0.8)",h=l?.textColor??"#fff";let u;if(l?.renderCustom)u=l.renderCustom(i,s);else{u=l?.formatter?l.formatter(i,s):`${s.name}: (${i.x}, ${i.y})`}let f=o+12,x=n-40-8;return f+140>a&&(f=o-140-12),x<0&&(x=n+12),e.jsxs("g",{className:"chartifypdf-tooltip",pointerEvents:"none",children:[e.jsx("line",{x1:o,y1:0,x2:o,y2:r,stroke:"#999",strokeWidth:1,strokeDasharray:"4 4",opacity:.6}),e.jsx("circle",{cx:o,cy:n,r:5,fill:c,stroke:"#fff",strokeWidth:2}),e.jsx("foreignObject",{x:f,y:x,width:140,height:60,overflow:"visible",children:e.jsx("div",{style:{backgroundColor:d,color:h,padding:"6px 10px",borderRadius:"4px",fontSize:"12px",fontFamily:"sans-serif",whiteSpace:"nowrap",lineHeight:1.4,boxShadow:"0 2px 6px rgba(0,0,0,0.2)"},children:u})})]})});p.displayName="Tooltip";const b=t.memo(({config:t,svgWidth:o,svgHeight:n,margin:i,scale:s,onZoomIn:r,onZoomOut:a,onReset:l})=>{if(!t?.enabled||!1===t.showControls)return null;const c=t.controlsPosition??"top-right",d=24;let h,u;switch(c){case"top-left":h=i.left+8,u=i.top+8;break;case"bottom-left":h=i.left+8,u=n-i.bottom-24-8;break;case"bottom-right":h=o-i.right-80-8,u=n-i.bottom-24-8;break;default:h=o-i.right-80-8,u=i.top+8}const f=s>1?[{label:"+",onClick:r},{label:"−",onClick:a},{label:"↺",onClick:l}]:[{label:"+",onClick:r},{label:"−",onClick:a}];return e.jsx("g",{className:"chartifypdf-zoom-controls",transform:`translate(${h}, ${u})`,children:f.map((t,o)=>e.jsxs("g",{transform:`translate(${28*o}, 0)`,onClick:t.onClick,style:{cursor:"pointer"},children:[e.jsx("rect",{width:d,height:d,rx:4,fill:"rgba(255,255,255,0.9)",stroke:"#ccc",strokeWidth:1}),e.jsx("text",{x:12,y:12,textAnchor:"middle",dominantBaseline:"central",fontSize:14,fontFamily:"sans-serif",fill:"#333",fontWeight:"bold",children:t.label})]},t.label))})});b.displayName="ZoomControls";const k={top:20,right:20,bottom:50,left:60},C=({data:o,width:s,height:r,margin:a,xAxis:l,yAxis:c,tooltip:u,zoom:x,style:C,animation:S,colorPalette:v,className:M,onPointClick:j,ariaLabel:w})=>{const $=t.useRef(null),W=t.useRef(null),P=t.useId(),L={...k,...a},T=i($,s,r??400),{width:N,height:z}=T,F=Math.max(0,N-L.left-L.right),D=Math.max(0,z-L.top-L.bottom),{xScale:X,yScale:Y,xTicks:A,yTicks:R}=d(o,F,D,l,c),G=h(x,F,D),H=!1!==u?.enabled,O=f(W,o,X,Y,L.left,L.top,F),Z=l?.gridLines??!1,I=c?.gridLines??!0;return 0===N||0===z?e.jsx("div",{ref:$,className:M,style:{width:s??"100%",height:r??400,backgroundColor:C?.backgroundColor}}):e.jsx("div",{ref:$,className:M,style:{width:s??"100%",height:r??400,backgroundColor:C?.backgroundColor},children:e.jsxs("svg",{ref:W,width:N,height:z,role:"img","aria-label":w??"Line chart",onMouseMove:H&&!G.isPanning?O.handleMouseMove:void 0,onMouseLeave:H?O.handleMouseLeave:void 0,onWheel:x?.enabled?G.handleWheel:void 0,onMouseDown:x?.enabled?G.handlePanStart:void 0,onMouseUp:x?.enabled?G.handlePanEnd:void 0,style:{userSelect:G.isPanning?"none":void 0,cursor:G.isPanning?"grabbing":x?.enabled&&G.scale>1?"grab":void 0},children:[e.jsx("defs",{children:e.jsx("clipPath",{id:P,children:e.jsx("rect",{width:F,height:D})})}),e.jsxs("g",{transform:`translate(${L.left}, ${L.top})`,children:[e.jsx(m,{xTicks:A,yTicks:R,xScale:X,yScale:Y,plotWidth:F,plotHeight:D,xAxisConfig:l,yAxisConfig:c,style:C}),e.jsx(y,{xTicks:A,yTicks:R,xScale:X,yScale:Y,plotWidth:F,plotHeight:D,showXGrid:Z,showYGrid:I,xGridColor:l?.gridLineColor,yGridColor:c?.gridLineColor}),e.jsx("g",{clipPath:`url(#${P})`,children:e.jsx("g",{transform:`translate(${G.offsetX}, ${G.offsetY}) scale(${G.scale})`,onMouseMove:x?.enabled?G.handlePanMove:void 0,children:o.map((t,o)=>e.jsx(g,{series:t,xScale:X,yScale:Y,color:t.color??n(o,v),animation:S,onPointClick:j},t.id))})}),H&&e.jsx(p,{visible:O.tooltipVisible,x:O.tooltipX,y:O.tooltipY,point:O.activePoint,series:O.activeSeries,plotHeight:D,plotWidth:F,config:u,seriesColor:O.activeSeries?.color??n(o.findIndex(e=>e.id===O.activeSeries?.id),v)})]}),e.jsx(b,{config:x,svgWidth:N,svgHeight:z,margin:L,scale:G.scale,onZoomIn:G.zoomIn,onZoomOut:G.zoomOut,onReset:G.resetZoom})]})})};C.displayName="LineChart",exports.LineChart=C,exports.useChartDimensions=i,exports.useScales=d,exports.useTooltip=f,exports.useZoom=h;
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/utils/color.ts","../src/hooks/useChartDimensions.ts","../src/utils/math.ts","../src/hooks/useScales.ts","../src/hooks/useZoom.ts","../src/hooks/useTooltip.ts","../src/components/Axes.tsx","../src/components/GridLines.tsx","../src/components/LinePath.tsx","../src/utils/path.ts","../src/components/Tooltip.tsx","../src/components/ZoomControls.tsx","../src/components/LineChart.tsx"],"sourcesContent":["export const DEFAULT_PALETTE = [\n \"#4e79a7\",\n \"#f28e2b\",\n \"#e15759\",\n \"#76b7b2\",\n \"#59a14f\",\n \"#edc948\",\n \"#b07aa1\",\n \"#ff9da7\",\n \"#9c755f\",\n \"#bab0ac\",\n];\n\nexport function getSeriesColor(index: number, palette?: string[]): string {\n const colors = palette ?? DEFAULT_PALETTE;\n return colors[index % colors.length];\n}\n","import { useLayoutEffect, useState, type RefObject } from \"react\";\n\nexport function useChartDimensions(\n containerRef: RefObject<HTMLDivElement | null>,\n providedWidth?: number,\n providedHeight?: number\n): { width: number; height: number } {\n const [dimensions, setDimensions] = useState<{\n width: number;\n height: number;\n }>({\n width: providedWidth ?? 0,\n height: providedHeight ?? 0,\n });\n\n const isFixed =\n providedWidth !== undefined && providedHeight !== undefined;\n\n useLayoutEffect(() => {\n if (isFixed) {\n setDimensions({ width: providedWidth!, height: providedHeight! });\n return;\n }\n\n const node = containerRef.current;\n if (!node) return;\n\n const measure = () => {\n const rect = node.getBoundingClientRect();\n setDimensions({ width: rect.width, height: rect.height });\n };\n\n measure();\n\n const observer = new ResizeObserver(() => {\n measure();\n });\n observer.observe(node);\n\n return () => observer.disconnect();\n }, [containerRef, isFixed, providedWidth, providedHeight]);\n\n return dimensions;\n}\n","export function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nexport function lerp(a: number, b: number, t: number): number {\n return a + (b - a) * t;\n}\n\nexport function inverseLerp(a: number, b: number, value: number): number {\n if (a === b) return 0;\n return (value - a) / (b - a);\n}\n\nexport function niceNumber(range: number, round: boolean): number {\n const exponent = Math.floor(Math.log10(range));\n const fraction = range / Math.pow(10, exponent);\n\n let niceFraction: number;\n if (round) {\n if (fraction < 1.5) niceFraction = 1;\n else if (fraction < 3) niceFraction = 2;\n else if (fraction < 7) niceFraction = 5;\n else niceFraction = 10;\n } else {\n if (fraction <= 1) niceFraction = 1;\n else if (fraction <= 2) niceFraction = 2;\n else if (fraction <= 5) niceFraction = 5;\n else niceFraction = 10;\n }\n\n return niceFraction * Math.pow(10, exponent);\n}\n\nexport function niceRange(\n min: number,\n max: number,\n tickCount: number\n): { niceMin: number; niceMax: number; tickStep: number } {\n if (min === max) {\n const offset = min === 0 ? 1 : Math.abs(min) * 0.1;\n min = min - offset;\n max = max + offset;\n }\n\n const range = niceNumber(max - min, false);\n const tickStep = niceNumber(range / (tickCount - 1), true);\n const niceMin = Math.floor(min / tickStep) * tickStep;\n const niceMax = Math.ceil(max / tickStep) * tickStep;\n\n return { niceMin, niceMax, tickStep };\n}\n\nexport function linearScale(\n domainMin: number,\n domainMax: number,\n rangeMin: number,\n rangeMax: number\n): (value: number) => number {\n return (value: number) => {\n if (domainMax === domainMin) return (rangeMin + rangeMax) / 2;\n const t = inverseLerp(domainMin, domainMax, value);\n return lerp(rangeMin, rangeMax, t);\n };\n}\n\nexport function generateTicks(\n niceMin: number,\n niceMax: number,\n tickStep: number\n): number[] {\n const ticks: number[] = [];\n for (let v = niceMin; v <= niceMax + tickStep * 0.5; v += tickStep) {\n ticks.push(parseFloat(v.toPrecision(12)));\n }\n return ticks;\n}\n","import { useMemo } from \"react\";\nimport type { DataSeries, AxisConfig } from \"../types/chart.types\";\nimport { niceRange, linearScale, generateTicks } from \"../utils/math\";\n\nexport interface ScalesResult {\n xScale: (value: number) => number;\n yScale: (value: number) => number;\n xTicks: number[];\n yTicks: number[];\n xDomain: [number, number];\n yDomain: [number, number];\n}\n\nexport function useScales(\n data: DataSeries[],\n plotWidth: number,\n plotHeight: number,\n xAxisConfig?: AxisConfig,\n yAxisConfig?: AxisConfig\n): ScalesResult {\n return useMemo(() => {\n let xMin = Infinity;\n let xMax = -Infinity;\n let yMin = Infinity;\n let yMax = -Infinity;\n\n for (const series of data) {\n for (const point of series.data) {\n if (point.x < xMin) xMin = point.x;\n if (point.x > xMax) xMax = point.x;\n if (point.y < yMin) yMin = point.y;\n if (point.y > yMax) yMax = point.y;\n }\n }\n\n if (!isFinite(xMin)) {\n xMin = 0;\n xMax = 1;\n yMin = 0;\n yMax = 1;\n }\n\n if (xAxisConfig?.min !== undefined) xMin = xAxisConfig.min;\n if (xAxisConfig?.max !== undefined) xMax = xAxisConfig.max;\n if (yAxisConfig?.min !== undefined) yMin = yAxisConfig.min;\n if (yAxisConfig?.max !== undefined) yMax = yAxisConfig.max;\n\n const xTickCount = xAxisConfig?.tickCount ?? 6;\n const yTickCount = yAxisConfig?.tickCount ?? 6;\n\n const xNice = niceRange(xMin, xMax, xTickCount);\n const yNice = niceRange(yMin, yMax, yTickCount);\n\n const xScale = linearScale(xNice.niceMin, xNice.niceMax, 0, plotWidth);\n // Y inverted: SVG Y increases downward\n const yScale = linearScale(yNice.niceMin, yNice.niceMax, plotHeight, 0);\n\n const xTicks = generateTicks(xNice.niceMin, xNice.niceMax, xNice.tickStep);\n const yTicks = generateTicks(yNice.niceMin, yNice.niceMax, yNice.tickStep);\n\n return {\n xScale,\n yScale,\n xTicks,\n yTicks,\n xDomain: [xNice.niceMin, xNice.niceMax] as [number, number],\n yDomain: [yNice.niceMin, yNice.niceMax] as [number, number],\n };\n }, [data, plotWidth, plotHeight, xAxisConfig, yAxisConfig]);\n}\n","import { useState, useCallback, useRef } from \"react\";\nimport type { ZoomConfig } from \"../types/chart.types\";\nimport { clamp } from \"../utils/math\";\n\nexport interface ZoomState {\n scale: number;\n offsetX: number;\n offsetY: number;\n isPanning: boolean;\n zoomIn: () => void;\n zoomOut: () => void;\n resetZoom: () => void;\n handleWheel: (e: React.WheelEvent) => void;\n handlePanStart: (e: React.MouseEvent) => void;\n handlePanMove: (e: React.MouseEvent) => void;\n handlePanEnd: () => void;\n}\n\nexport function useZoom(\n config: ZoomConfig | undefined,\n plotWidth: number,\n plotHeight: number\n): ZoomState {\n const enabled = config?.enabled ?? false;\n const minScale = config?.minScale ?? 1;\n const maxScale = config?.maxScale ?? 10;\n const step = config?.step ?? 0.5;\n const enableWheel = config?.enableWheel ?? true;\n const enablePan = config?.enablePan ?? true;\n\n const [scale, setScale] = useState(1);\n const [offsetX, setOffsetX] = useState(0);\n const [offsetY, setOffsetY] = useState(0);\n const [isPanning, setIsPanning] = useState(false);\n const panStart = useRef({ x: 0, y: 0, offsetX: 0, offsetY: 0 });\n\n const clampOffset = useCallback(\n (ox: number, oy: number, s: number) => {\n const maxOffsetX = plotWidth * (s - 1);\n const maxOffsetY = plotHeight * (s - 1);\n return {\n x: clamp(ox, -maxOffsetX, 0),\n y: clamp(oy, -maxOffsetY, 0),\n };\n },\n [plotWidth, plotHeight]\n );\n\n const zoomIn = useCallback(() => {\n if (!enabled) return;\n setScale((prev) => {\n const next = clamp(prev + step, minScale, maxScale);\n // Center zoom\n setOffsetX((ox) => {\n const newOx = ox - (plotWidth * step) / 2;\n return clampOffset(newOx, 0, next).x;\n });\n setOffsetY((oy) => {\n const newOy = oy - (plotHeight * step) / 2;\n return clampOffset(0, newOy, next).y;\n });\n return next;\n });\n }, [enabled, step, minScale, maxScale, plotWidth, plotHeight, clampOffset]);\n\n const zoomOut = useCallback(() => {\n if (!enabled) return;\n setScale((prev) => {\n const next = clamp(prev - step, minScale, maxScale);\n setOffsetX((ox) => {\n const newOx = ox + (plotWidth * step) / 2;\n return clampOffset(newOx, 0, next).x;\n });\n setOffsetY((oy) => {\n const newOy = oy + (plotHeight * step) / 2;\n return clampOffset(0, newOy, next).y;\n });\n return next;\n });\n }, [enabled, step, minScale, maxScale, plotWidth, plotHeight, clampOffset]);\n\n const resetZoom = useCallback(() => {\n setScale(1);\n setOffsetX(0);\n setOffsetY(0);\n }, []);\n\n const handleWheel = useCallback(\n (e: React.WheelEvent) => {\n if (!enabled || !enableWheel) return;\n e.preventDefault();\n\n const rect = (e.currentTarget as SVGElement).getBoundingClientRect();\n const mouseX = e.clientX - rect.left;\n const mouseY = e.clientY - rect.top;\n\n const delta = e.deltaY < 0 ? step : -step;\n\n setScale((prev) => {\n const next = clamp(prev + delta, minScale, maxScale);\n const ratio = next / prev;\n\n setOffsetX((ox) => {\n const newOx = mouseX - ratio * (mouseX - ox);\n return clampOffset(newOx, 0, next).x;\n });\n setOffsetY((oy) => {\n const newOy = mouseY - ratio * (mouseY - oy);\n return clampOffset(0, newOy, next).y;\n });\n\n return next;\n });\n },\n [enabled, enableWheel, step, minScale, maxScale, clampOffset]\n );\n\n const handlePanStart = useCallback(\n (e: React.MouseEvent) => {\n if (!enabled || !enablePan || scale <= 1) return;\n if (e.button !== 0) return; // left-click only\n setIsPanning(true);\n panStart.current = {\n x: e.clientX,\n y: e.clientY,\n offsetX,\n offsetY,\n };\n },\n [enabled, enablePan, scale, offsetX, offsetY]\n );\n\n const handlePanMove = useCallback(\n (e: React.MouseEvent) => {\n if (!isPanning) return;\n const dx = e.clientX - panStart.current.x;\n const dy = e.clientY - panStart.current.y;\n const clamped = clampOffset(\n panStart.current.offsetX + dx,\n panStart.current.offsetY + dy,\n scale\n );\n setOffsetX(clamped.x);\n setOffsetY(clamped.y);\n },\n [isPanning, scale, clampOffset]\n );\n\n const handlePanEnd = useCallback(() => {\n setIsPanning(false);\n }, []);\n\n return {\n scale,\n offsetX,\n offsetY,\n isPanning,\n zoomIn,\n zoomOut,\n resetZoom,\n handleWheel,\n handlePanStart,\n handlePanMove,\n handlePanEnd,\n };\n}\n","import { useState, useCallback, type RefObject } from \"react\";\nimport type { DataSeries, DataPoint, TooltipConfig } from \"../types/chart.types\";\n\nexport interface TooltipState {\n tooltipVisible: boolean;\n tooltipX: number;\n tooltipY: number;\n activePoint: DataPoint | null;\n activeSeries: DataSeries | null;\n handleMouseMove: (e: React.MouseEvent) => void;\n handleMouseLeave: () => void;\n}\n\nfunction findNearestByPixelX(\n sorted: DataPoint[],\n mouseX: number,\n xScale: (v: number) => number\n): DataPoint | null {\n if (sorted.length === 0) return null;\n\n let low = 0;\n let high = sorted.length - 1;\n\n // Binary search for nearest pixel X\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (xScale(sorted[mid].x) < mouseX) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n\n let nearest = sorted[low];\n let minDist = Math.abs(xScale(nearest.x) - mouseX);\n\n if (low > 0) {\n const dist = Math.abs(xScale(sorted[low - 1].x) - mouseX);\n if (dist < minDist) {\n minDist = dist;\n nearest = sorted[low - 1];\n }\n }\n\n return nearest;\n}\n\nexport function useTooltip(\n svgRef: RefObject<SVGSVGElement | null>,\n data: DataSeries[],\n xScale: (v: number) => number,\n yScale: (v: number) => number,\n marginLeft: number,\n marginTop: number,\n plotWidth: number,\n _config?: TooltipConfig\n): TooltipState {\n const [tooltipVisible, setTooltipVisible] = useState(false);\n const [tooltipX, setTooltipX] = useState(0);\n const [tooltipY, setTooltipY] = useState(0);\n const [activePoint, setActivePoint] = useState<DataPoint | null>(null);\n const [activeSeries, setActiveSeries] = useState<DataSeries | null>(null);\n\n const handleMouseMove = useCallback(\n (e: React.MouseEvent) => {\n const svg = svgRef.current;\n if (!svg) return;\n\n const rect = svg.getBoundingClientRect();\n const mouseX = e.clientX - rect.left - marginLeft;\n\n if (mouseX < 0 || mouseX > plotWidth) {\n setTooltipVisible(false);\n return;\n }\n\n let closestPoint: DataPoint | null = null;\n let closestSeries: DataSeries | null = null;\n let closestDist = Infinity;\n\n for (const series of data) {\n const nearest = findNearestByPixelX(series.data, mouseX, xScale);\n if (!nearest) continue;\n\n const dist = Math.abs(xScale(nearest.x) - mouseX);\n if (dist < closestDist) {\n closestDist = dist;\n closestPoint = nearest;\n closestSeries = series;\n }\n }\n\n if (closestPoint && closestSeries) {\n setTooltipX(xScale(closestPoint.x));\n setTooltipY(yScale(closestPoint.y));\n setActivePoint(closestPoint);\n setActiveSeries(closestSeries);\n setTooltipVisible(true);\n }\n },\n [svgRef, data, xScale, yScale, marginLeft, plotWidth]\n );\n\n const handleMouseLeave = useCallback(() => {\n setTooltipVisible(false);\n setActivePoint(null);\n setActiveSeries(null);\n }, []);\n\n return {\n tooltipVisible,\n tooltipX,\n tooltipY,\n activePoint,\n activeSeries,\n handleMouseMove,\n handleMouseLeave,\n };\n}\n","import React from \"react\";\nimport type { AxisConfig, ChartStyle } from \"../types/chart.types\";\n\ninterface AxesProps {\n xTicks: number[];\n yTicks: number[];\n xScale: (v: number) => number;\n yScale: (v: number) => number;\n plotWidth: number;\n plotHeight: number;\n xAxisConfig?: AxisConfig;\n yAxisConfig?: AxisConfig;\n style?: ChartStyle;\n}\n\nconst defaultTickFormat = (v: number): string => {\n if (Math.abs(v) >= 1e6) return `${(v / 1e6).toFixed(1)}M`;\n if (Math.abs(v) >= 1e3) return `${(v / 1e3).toFixed(1)}K`;\n return Number.isInteger(v) ? v.toString() : v.toFixed(1);\n};\n\nexport const Axes: React.FC<AxesProps> = React.memo(\n ({\n xTicks,\n yTicks,\n xScale,\n yScale,\n plotWidth,\n plotHeight,\n xAxisConfig,\n yAxisConfig,\n style,\n }) => {\n const axisColor = style?.axisColor ?? \"#333\";\n const tickColor = style?.tickColor ?? \"#666\";\n const fontFamily = style?.fontFamily ?? \"sans-serif\";\n const fontSize = style?.fontSize ?? 11;\n const xTickFormat = xAxisConfig?.tickFormat ?? defaultTickFormat;\n const yTickFormat = yAxisConfig?.tickFormat ?? defaultTickFormat;\n\n return (\n <g className=\"chartifypdf-axes\">\n {/* X Axis */}\n <line\n x1={0}\n y1={plotHeight}\n x2={plotWidth}\n y2={plotHeight}\n stroke={axisColor}\n strokeWidth={1}\n />\n {xTicks.map((tick) => {\n const x = xScale(tick);\n return (\n <g key={`x-tick-${tick}`}>\n <line\n x1={x}\n y1={plotHeight}\n x2={x}\n y2={plotHeight + 6}\n stroke={axisColor}\n strokeWidth={1}\n />\n <text\n x={x}\n y={plotHeight + 18}\n textAnchor=\"middle\"\n fill={tickColor}\n fontFamily={fontFamily}\n fontSize={fontSize}\n >\n {xTickFormat(tick)}\n </text>\n </g>\n );\n })}\n {xAxisConfig?.label && (\n <text\n x={plotWidth / 2}\n y={plotHeight + 38}\n textAnchor=\"middle\"\n fill={axisColor}\n fontFamily={fontFamily}\n fontSize={fontSize + 1}\n fontWeight=\"bold\"\n >\n {xAxisConfig.label}\n </text>\n )}\n\n {/* Y Axis */}\n <line\n x1={0}\n y1={0}\n x2={0}\n y2={plotHeight}\n stroke={axisColor}\n strokeWidth={1}\n />\n {yTicks.map((tick) => {\n const y = yScale(tick);\n return (\n <g key={`y-tick-${tick}`}>\n <line\n x1={-6}\n y1={y}\n x2={0}\n y2={y}\n stroke={axisColor}\n strokeWidth={1}\n />\n <text\n x={-10}\n y={y}\n textAnchor=\"end\"\n dominantBaseline=\"middle\"\n fill={tickColor}\n fontFamily={fontFamily}\n fontSize={fontSize}\n >\n {yTickFormat(tick)}\n </text>\n </g>\n );\n })}\n {yAxisConfig?.label && (\n <text\n x={0}\n y={0}\n textAnchor=\"middle\"\n fill={axisColor}\n fontFamily={fontFamily}\n fontSize={fontSize + 1}\n fontWeight=\"bold\"\n transform={`translate(${-40}, ${plotHeight / 2}) rotate(-90)`}\n >\n {yAxisConfig.label}\n </text>\n )}\n </g>\n );\n }\n);\n\nAxes.displayName = \"Axes\";\n","import React from \"react\";\n\ninterface GridLinesProps {\n xTicks: number[];\n yTicks: number[];\n xScale: (v: number) => number;\n yScale: (v: number) => number;\n plotWidth: number;\n plotHeight: number;\n showXGrid: boolean;\n showYGrid: boolean;\n xGridColor?: string;\n yGridColor?: string;\n}\n\nexport const GridLines: React.FC<GridLinesProps> = React.memo(\n ({\n xTicks,\n yTicks,\n xScale,\n yScale,\n plotWidth,\n plotHeight,\n showXGrid,\n showYGrid,\n xGridColor = \"#e0e0e0\",\n yGridColor = \"#e0e0e0\",\n }) => {\n return (\n <g className=\"chartifypdf-grid\">\n {showYGrid &&\n yTicks.map((tick) => {\n const y = yScale(tick);\n return (\n <line\n key={`y-grid-${tick}`}\n x1={0}\n y1={y}\n x2={plotWidth}\n y2={y}\n stroke={yGridColor}\n strokeDasharray=\"4 4\"\n strokeOpacity={0.5}\n />\n );\n })}\n {showXGrid &&\n xTicks.map((tick) => {\n const x = xScale(tick);\n return (\n <line\n key={`x-grid-${tick}`}\n x1={x}\n y1={0}\n x2={x}\n y2={plotHeight}\n stroke={xGridColor}\n strokeDasharray=\"4 4\"\n strokeOpacity={0.5}\n />\n );\n })}\n </g>\n );\n }\n);\n\nGridLines.displayName = \"GridLines\";\n","import React, { useRef, useEffect, useState } from \"react\";\nimport type { DataSeries, DataPoint, AnimationConfig } from \"../types/chart.types\";\nimport { buildLinePath } from \"../utils/path\";\n\ninterface LinePathProps {\n series: DataSeries;\n xScale: (v: number) => number;\n yScale: (v: number) => number;\n color: string;\n animation?: AnimationConfig;\n onPointClick?: (point: DataPoint, series: DataSeries) => void;\n}\n\nexport const LinePath: React.FC<LinePathProps> = React.memo(\n ({ series, xScale, yScale, color, animation, onPointClick }) => {\n const pathRef = useRef<SVGPathElement>(null);\n const [pathLength, setPathLength] = useState(0);\n const [animationDone, setAnimationDone] = useState(!animation?.enabled);\n\n const strokeWidth = series.strokeWidth ?? 2;\n const showDots = series.showDots ?? false;\n const dotRadius = series.dotRadius ?? 3.5;\n\n const points = series.data.map((pt) => ({\n x: xScale(pt.x),\n y: yScale(pt.y),\n }));\n\n const d = buildLinePath(points);\n\n useEffect(() => {\n if (animation?.enabled && pathRef.current) {\n const length = pathRef.current.getTotalLength();\n setPathLength(length);\n setAnimationDone(false);\n\n const timer = setTimeout(() => {\n setAnimationDone(true);\n }, animation.duration ?? 800);\n\n return () => clearTimeout(timer);\n }\n }, [d, animation?.enabled, animation?.duration]);\n\n const animStyle: React.CSSProperties =\n animation?.enabled && pathLength > 0\n ? {\n strokeDasharray: pathLength,\n strokeDashoffset: animationDone ? 0 : pathLength,\n transition: `stroke-dashoffset ${animation.duration ?? 800}ms ${animation.easing ?? \"ease-in-out\"}`,\n }\n : {};\n\n return (\n <g className=\"chartifypdf-line-path\">\n <path\n ref={pathRef}\n d={d}\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeDasharray={series.strokeDasharray}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n style={animStyle}\n />\n {showDots &&\n series.data.map((pt, i) => (\n <circle\n key={`dot-${series.id}-${i}`}\n cx={xScale(pt.x)}\n cy={yScale(pt.y)}\n r={dotRadius}\n fill={color}\n stroke=\"#fff\"\n strokeWidth={1.5}\n style={{ cursor: onPointClick ? \"pointer\" : \"default\" }}\n onClick={\n onPointClick\n ? () => onPointClick(pt, series)\n : undefined\n }\n />\n ))}\n </g>\n );\n }\n);\n\nLinePath.displayName = \"LinePath\";\n","export function buildLinePath(\n points: { x: number; y: number }[]\n): string {\n if (points.length === 0) return \"\";\n const [first, ...rest] = points;\n return `M${first.x},${first.y}` + rest.map((p) => `L${p.x},${p.y}`).join(\"\");\n}\n","import React from \"react\";\nimport type { DataPoint, DataSeries, TooltipConfig } from \"../types/chart.types\";\n\ninterface TooltipProps {\n visible: boolean;\n x: number;\n y: number;\n point: DataPoint | null;\n series: DataSeries | null;\n plotHeight: number;\n plotWidth: number;\n config?: TooltipConfig;\n seriesColor: string;\n}\n\nexport const Tooltip: React.FC<TooltipProps> = React.memo(\n ({ visible, x, y, point, series, plotHeight, plotWidth, config, seriesColor }) => {\n if (!visible || !point || !series) return null;\n\n const bgColor = config?.backgroundColor ?? \"rgba(0, 0, 0, 0.8)\";\n const textColor = config?.textColor ?? \"#fff\";\n\n // Determine tooltip text\n let content: React.ReactNode;\n if (config?.renderCustom) {\n content = config.renderCustom(point, series);\n } else {\n const text = config?.formatter\n ? config.formatter(point, series)\n : `${series.name}: (${point.x}, ${point.y})`;\n content = text;\n }\n\n // Smart positioning: flip when near edges\n const tooltipWidth = 140;\n const tooltipHeight = 40;\n let tx = x + 12;\n let ty = y - tooltipHeight - 8;\n\n if (tx + tooltipWidth > plotWidth) {\n tx = x - tooltipWidth - 12;\n }\n if (ty < 0) {\n ty = y + 12;\n }\n\n return (\n <g className=\"chartifypdf-tooltip\" pointerEvents=\"none\">\n {/* Vertical indicator line */}\n <line\n x1={x}\n y1={0}\n x2={x}\n y2={plotHeight}\n stroke=\"#999\"\n strokeWidth={1}\n strokeDasharray=\"4 4\"\n opacity={0.6}\n />\n {/* Highlight circle */}\n <circle\n cx={x}\n cy={y}\n r={5}\n fill={seriesColor}\n stroke=\"#fff\"\n strokeWidth={2}\n />\n {/* Tooltip box using foreignObject */}\n <foreignObject\n x={tx}\n y={ty}\n width={tooltipWidth}\n height={tooltipHeight + 20}\n overflow=\"visible\"\n >\n <div\n style={{\n backgroundColor: bgColor,\n color: textColor,\n padding: \"6px 10px\",\n borderRadius: \"4px\",\n fontSize: \"12px\",\n fontFamily: \"sans-serif\",\n whiteSpace: \"nowrap\",\n lineHeight: 1.4,\n boxShadow: \"0 2px 6px rgba(0,0,0,0.2)\",\n }}\n >\n {content}\n </div>\n </foreignObject>\n </g>\n );\n }\n);\n\nTooltip.displayName = \"Tooltip\";\n","import React from \"react\";\nimport type { ZoomConfig } from \"../types/chart.types\";\n\ninterface ZoomControlsProps {\n config?: ZoomConfig;\n svgWidth: number;\n svgHeight: number;\n margin: { top: number; right: number; bottom: number; left: number };\n scale: number;\n onZoomIn: () => void;\n onZoomOut: () => void;\n onReset: () => void;\n}\n\nexport const ZoomControls: React.FC<ZoomControlsProps> = React.memo(\n ({ config, svgWidth, svgHeight, margin, scale, onZoomIn, onZoomOut, onReset }) => {\n if (!config?.enabled || config.showControls === false) return null;\n\n const position = config.controlsPosition ?? \"top-right\";\n const btnSize = 24;\n const gap = 4;\n const groupWidth = btnSize * 3 + gap * 2;\n const groupHeight = btnSize;\n const padding = 8;\n\n let gx: number;\n let gy: number;\n\n switch (position) {\n case \"top-left\":\n gx = margin.left + padding;\n gy = margin.top + padding;\n break;\n case \"bottom-left\":\n gx = margin.left + padding;\n gy = svgHeight - margin.bottom - groupHeight - padding;\n break;\n case \"bottom-right\":\n gx = svgWidth - margin.right - groupWidth - padding;\n gy = svgHeight - margin.bottom - groupHeight - padding;\n break;\n case \"top-right\":\n default:\n gx = svgWidth - margin.right - groupWidth - padding;\n gy = margin.top + padding;\n break;\n }\n\n const buttons = [\n { label: \"+\", onClick: onZoomIn },\n { label: \"\\u2013\", onClick: onReset }, // en-dash for reset\n { label: \"\\u2212\", onClick: onZoomOut }, // minus sign\n ];\n\n // Replace middle button with reset when zoomed, or show as \"-\"\n const displayButtons =\n scale > 1\n ? [\n { label: \"+\", onClick: onZoomIn },\n { label: \"\\u2212\", onClick: onZoomOut },\n { label: \"\\u21BA\", onClick: onReset },\n ]\n : [\n { label: \"+\", onClick: onZoomIn },\n { label: \"\\u2212\", onClick: onZoomOut },\n ];\n\n return (\n <g\n className=\"chartifypdf-zoom-controls\"\n transform={`translate(${gx}, ${gy})`}\n >\n {displayButtons.map((btn, i) => (\n <g\n key={btn.label}\n transform={`translate(${i * (btnSize + gap)}, 0)`}\n onClick={btn.onClick}\n style={{ cursor: \"pointer\" }}\n >\n <rect\n width={btnSize}\n height={btnSize}\n rx={4}\n fill=\"rgba(255,255,255,0.9)\"\n stroke=\"#ccc\"\n strokeWidth={1}\n />\n <text\n x={btnSize / 2}\n y={btnSize / 2}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n fontSize={14}\n fontFamily=\"sans-serif\"\n fill=\"#333\"\n fontWeight=\"bold\"\n >\n {btn.label}\n </text>\n </g>\n ))}\n </g>\n );\n }\n);\n\nZoomControls.displayName = \"ZoomControls\";\n","import React, { useRef, useId } from \"react\";\nimport type {\n LineChartProps,\n ChartMargin,\n} from \"../types/chart.types\";\nimport { getSeriesColor } from \"../utils/color\";\nimport { useChartDimensions } from \"../hooks/useChartDimensions\";\nimport { useScales } from \"../hooks/useScales\";\nimport { useZoom } from \"../hooks/useZoom\";\nimport { useTooltip } from \"../hooks/useTooltip\";\nimport { Axes } from \"./Axes\";\nimport { GridLines } from \"./GridLines\";\nimport { LinePath } from \"./LinePath\";\nimport { Tooltip } from \"./Tooltip\";\nimport { ZoomControls } from \"./ZoomControls\";\n\nconst DEFAULT_MARGIN: ChartMargin = {\n top: 20,\n right: 20,\n bottom: 50,\n left: 60,\n};\n\nconst DEFAULT_HEIGHT = 400;\n\nexport const LineChart: React.FC<LineChartProps> = ({\n data,\n width: providedWidth,\n height: providedHeight,\n margin: marginOverride,\n xAxis: xAxisConfig,\n yAxis: yAxisConfig,\n tooltip: tooltipConfig,\n zoom: zoomConfig,\n style: chartStyle,\n animation,\n colorPalette,\n className,\n onPointClick,\n ariaLabel,\n}) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const svgRef = useRef<SVGSVGElement>(null);\n const clipId = useId();\n\n const margin: ChartMargin = {\n ...DEFAULT_MARGIN,\n ...marginOverride,\n };\n\n const dimensions = useChartDimensions(\n containerRef,\n providedWidth,\n providedHeight ?? DEFAULT_HEIGHT\n );\n const { width: svgWidth, height: svgHeight } = dimensions;\n\n const plotWidth = Math.max(0, svgWidth - margin.left - margin.right);\n const plotHeight = Math.max(0, svgHeight - margin.top - margin.bottom);\n\n const { xScale, yScale, xTicks, yTicks } = useScales(\n data,\n plotWidth,\n plotHeight,\n xAxisConfig,\n yAxisConfig\n );\n\n const zoomState = useZoom(zoomConfig, plotWidth, plotHeight);\n\n const tooltipEnabled = tooltipConfig?.enabled !== false;\n const tooltipState = useTooltip(\n svgRef,\n data,\n xScale,\n yScale,\n margin.left,\n margin.top,\n plotWidth,\n tooltipConfig\n );\n\n const showXGrid = xAxisConfig?.gridLines ?? false;\n const showYGrid = yAxisConfig?.gridLines ?? true;\n\n // Don't render until we have dimensions\n if (svgWidth === 0 || svgHeight === 0) {\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n width: providedWidth ?? \"100%\",\n height: providedHeight ?? DEFAULT_HEIGHT,\n backgroundColor: chartStyle?.backgroundColor,\n }}\n />\n );\n }\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n width: providedWidth ?? \"100%\",\n height: providedHeight ?? DEFAULT_HEIGHT,\n backgroundColor: chartStyle?.backgroundColor,\n }}\n >\n <svg\n ref={svgRef}\n width={svgWidth}\n height={svgHeight}\n role=\"img\"\n aria-label={ariaLabel ?? \"Line chart\"}\n onMouseMove={\n tooltipEnabled && !zoomState.isPanning\n ? tooltipState.handleMouseMove\n : undefined\n }\n onMouseLeave={tooltipEnabled ? tooltipState.handleMouseLeave : undefined}\n onWheel={zoomConfig?.enabled ? zoomState.handleWheel : undefined}\n onMouseDown={zoomConfig?.enabled ? zoomState.handlePanStart : undefined}\n onMouseUp={zoomConfig?.enabled ? zoomState.handlePanEnd : undefined}\n style={{\n userSelect: zoomState.isPanning ? \"none\" : undefined,\n cursor: zoomState.isPanning\n ? \"grabbing\"\n : zoomConfig?.enabled && zoomState.scale > 1\n ? \"grab\"\n : undefined,\n }}\n >\n <defs>\n <clipPath id={clipId}>\n <rect width={plotWidth} height={plotHeight} />\n </clipPath>\n </defs>\n\n <g transform={`translate(${margin.left}, ${margin.top})`}>\n {/* Axes render outside zoom transform — always crisp */}\n <Axes\n xTicks={xTicks}\n yTicks={yTicks}\n xScale={xScale}\n yScale={yScale}\n plotWidth={plotWidth}\n plotHeight={plotHeight}\n xAxisConfig={xAxisConfig}\n yAxisConfig={yAxisConfig}\n style={chartStyle}\n />\n\n {/* Grid lines */}\n <GridLines\n xTicks={xTicks}\n yTicks={yTicks}\n xScale={xScale}\n yScale={yScale}\n plotWidth={plotWidth}\n plotHeight={plotHeight}\n showXGrid={showXGrid}\n showYGrid={showYGrid}\n xGridColor={xAxisConfig?.gridLineColor}\n yGridColor={yAxisConfig?.gridLineColor}\n />\n\n {/* Clipped + zoomed data area */}\n <g clipPath={`url(#${clipId})`}>\n <g\n transform={`translate(${zoomState.offsetX}, ${zoomState.offsetY}) scale(${zoomState.scale})`}\n onMouseMove={\n zoomConfig?.enabled ? zoomState.handlePanMove : undefined\n }\n >\n {data.map((series, i) => (\n <LinePath\n key={series.id}\n series={series}\n xScale={xScale}\n yScale={yScale}\n color={\n series.color ?? getSeriesColor(i, colorPalette)\n }\n animation={animation}\n onPointClick={onPointClick}\n />\n ))}\n </g>\n </g>\n\n {/* Tooltip renders above everything in plot area */}\n {tooltipEnabled && (\n <Tooltip\n visible={tooltipState.tooltipVisible}\n x={tooltipState.tooltipX}\n y={tooltipState.tooltipY}\n point={tooltipState.activePoint}\n series={tooltipState.activeSeries}\n plotHeight={plotHeight}\n plotWidth={plotWidth}\n config={tooltipConfig}\n seriesColor={\n tooltipState.activeSeries?.color ??\n getSeriesColor(\n data.findIndex(\n (s) => s.id === tooltipState.activeSeries?.id\n ),\n colorPalette\n )\n }\n />\n )}\n </g>\n\n {/* Zoom controls render at SVG root level */}\n <ZoomControls\n config={zoomConfig}\n svgWidth={svgWidth}\n svgHeight={svgHeight}\n margin={margin}\n scale={zoomState.scale}\n onZoomIn={zoomState.zoomIn}\n onZoomOut={zoomState.zoomOut}\n onReset={zoomState.resetZoom}\n />\n </svg>\n </div>\n );\n};\n\nLineChart.displayName = \"LineChart\";\n"],"names":["DEFAULT_PALETTE","getSeriesColor","index","palette","colors","length","useChartDimensions","containerRef","providedWidth","providedHeight","dimensions","setDimensions","useState","width","height","isFixed","undefined","useLayoutEffect","node","current","measure","rect","getBoundingClientRect","observer","ResizeObserver","observe","disconnect","clamp","value","min","max","Math","niceNumber","range","round","exponent","floor","log10","fraction","pow","niceFraction","niceRange","tickCount","offset","abs","tickStep","niceMin","niceMax","ceil","linearScale","domainMin","domainMax","rangeMin","rangeMax","t","a","b","inverseLerp","lerp","generateTicks","ticks","v","push","parseFloat","toPrecision","useScales","data","plotWidth","plotHeight","xAxisConfig","yAxisConfig","useMemo","xMin","Infinity","xMax","yMin","yMax","series","point","x","y","isFinite","xTickCount","yTickCount","xNice","yNice","xScale","yScale","xTicks","yTicks","xDomain","yDomain","useZoom","config","enabled","minScale","maxScale","step","enableWheel","enablePan","scale","setScale","offsetX","setOffsetX","offsetY","setOffsetY","isPanning","setIsPanning","panStart","useRef","clampOffset","useCallback","ox","oy","s","maxOffsetY","zoomIn","prev","next","zoomOut","resetZoom","handleWheel","e","preventDefault","currentTarget","mouseX","clientX","left","mouseY","clientY","top","delta","deltaY","ratio","handlePanStart","button","handlePanMove","dx","dy","clamped","handlePanEnd","findNearestByPixelX","sorted","low","high","mid","nearest","minDist","dist","useTooltip","svgRef","marginLeft","marginTop","_config","tooltipVisible","setTooltipVisible","tooltipX","setTooltipX","tooltipY","setTooltipY","activePoint","setActivePoint","activeSeries","setActiveSeries","handleMouseMove","svg","closestPoint","closestSeries","closestDist","handleMouseLeave","defaultTickFormat","toFixed","Number","isInteger","toString","Axes","React","memo","style","axisColor","tickColor","fontFamily","fontSize","xTickFormat","tickFormat","yTickFormat","_jsxs","className","_jsx","x1","y1","x2","y2","stroke","strokeWidth","map","tick","children","textAnchor","fill","label","fontWeight","dominantBaseline","transform","displayName","GridLines","showXGrid","showYGrid","xGridColor","yGridColor","strokeDasharray","strokeOpacity","LinePath","color","animation","onPointClick","pathRef","pathLength","setPathLength","animationDone","setAnimationDone","showDots","dotRadius","d","points","first","rest","p","join","buildLinePath","pt","useEffect","getTotalLength","timer","setTimeout","duration","clearTimeout","animStyle","strokeDashoffset","transition","easing","ref","strokeLinecap","strokeLinejoin","i","cx","cy","r","cursor","onClick","id","Tooltip","visible","seriesColor","bgColor","backgroundColor","textColor","content","renderCustom","formatter","name","tx","ty","pointerEvents","opacity","tooltipHeight","overflow","padding","borderRadius","whiteSpace","lineHeight","boxShadow","ZoomControls","svgWidth","svgHeight","margin","onZoomIn","onZoomOut","onReset","showControls","position","controlsPosition","btnSize","gx","gy","bottom","right","displayButtons","btn","rx","DEFAULT_MARGIN","LineChart","marginOverride","xAxis","yAxis","tooltip","tooltipConfig","zoom","zoomConfig","chartStyle","colorPalette","ariaLabel","clipId","useId","zoomState","tooltipEnabled","tooltipState","gridLines","role","onMouseMove","onMouseLeave","onWheel","onMouseDown","onMouseUp","userSelect","gridLineColor","clipPath","findIndex"],"mappings":"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"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{jsxs as e,jsx as t}from"react/jsx-runtime";import o,{useState as n,useLayoutEffect as i,useMemo as r,useRef as l,useCallback as a,useEffect as s,useId as c}from"react";const d=["#4e79a7","#f28e2b","#e15759","#76b7b2","#59a14f","#edc948","#b07aa1","#ff9da7","#9c755f","#bab0ac"];function h(e,t){const o=t??d;return o[e%o.length]}function f(e,t,o){const[r,l]=n({width:t??0,height:o??0}),a=void 0!==t&&void 0!==o;return i(()=>{if(a)return void l({width:t,height:o});const n=e.current;if(!n)return;const i=()=>{const e=n.getBoundingClientRect();l({width:e.width,height:e.height})};i();const r=new ResizeObserver(()=>{i()});return r.observe(n),()=>r.disconnect()},[e,a,t,o]),r}function u(e,t,o){return Math.min(Math.max(e,t),o)}function x(e,t){const o=Math.floor(Math.log10(e)),n=e/Math.pow(10,o);let i;return i=t?n<1.5?1:n<3?2:n<7?5:10:n<=1?1:n<=2?2:n<=5?5:10,i*Math.pow(10,o)}function m(e,t,o){if(e===t){const o=0===e?1:.1*Math.abs(e);e-=o,t+=o}const n=x(t-e,!1),i=x(n/(o-1),!0);return{niceMin:Math.floor(e/i)*i,niceMax:Math.ceil(t/i)*i,tickStep:i}}function y(e,t,o,n){return i=>{if(t===e)return(o+n)/2;const r=function(e,t,o){return e===t?0:(o-e)/(t-e)}(e,t,i);return function(e,t,o){return e+(t-e)*o}(o,n,r)}}function g(e,t,o){const n=[];for(let i=e;i<=t+.5*o;i+=o)n.push(parseFloat(i.toPrecision(12)));return n}function p(e,t,o,n,i){return r(()=>{let r=1/0,l=-1/0,a=1/0,s=-1/0;for(const t of e)for(const e of t.data)e.x<r&&(r=e.x),e.x>l&&(l=e.x),e.y<a&&(a=e.y),e.y>s&&(s=e.y);isFinite(r)||(r=0,l=1,a=0,s=1),void 0!==n?.min&&(r=n.min),void 0!==n?.max&&(l=n.max),void 0!==i?.min&&(a=i.min),void 0!==i?.max&&(s=i.max);const c=n?.tickCount??6,d=i?.tickCount??6,h=m(r,l,c),f=m(a,s,d);return{xScale:y(h.niceMin,h.niceMax,0,t),yScale:y(f.niceMin,f.niceMax,o,0),xTicks:g(h.niceMin,h.niceMax,h.tickStep),yTicks:g(f.niceMin,f.niceMax,f.tickStep),xDomain:[h.niceMin,h.niceMax],yDomain:[f.niceMin,f.niceMax]}},[e,t,o,n,i])}function b(e,t,o){const i=e?.enabled??!1,r=e?.minScale??1,s=e?.maxScale??10,c=e?.step??.5,d=e?.enableWheel??!0,h=e?.enablePan??!0,[f,x]=n(1),[m,y]=n(0),[g,p]=n(0),[b,k]=n(!1),M=l({x:0,y:0,offsetX:0,offsetY:0}),v=a((e,n,i)=>{const r=o*(i-1);return{x:u(e,-(t*(i-1)),0),y:u(n,-r,0)}},[t,o]),C=a(()=>{i&&x(e=>{const n=u(e+c,r,s);return y(e=>v(e-t*c/2,0,n).x),p(e=>v(0,e-o*c/2,n).y),n})},[i,c,r,s,t,o,v]),S=a(()=>{i&&x(e=>{const n=u(e-c,r,s);return y(e=>v(e+t*c/2,0,n).x),p(e=>v(0,e+o*c/2,n).y),n})},[i,c,r,s,t,o,v]),w=a(()=>{x(1),y(0),p(0)},[]),$=a(e=>{if(!i||!d)return;e.preventDefault();const t=e.currentTarget.getBoundingClientRect(),o=e.clientX-t.left,n=e.clientY-t.top,l=e.deltaY<0?c:-c;x(e=>{const t=u(e+l,r,s),i=t/e;return y(e=>v(o-i*(o-e),0,t).x),p(e=>v(0,n-i*(n-e),t).y),t})},[i,d,c,r,s,v]),W=a(e=>{!i||!h||f<=1||0===e.button&&(k(!0),M.current={x:e.clientX,y:e.clientY,offsetX:m,offsetY:g})},[i,h,f,m,g]),P=a(e=>{if(!b)return;const t=e.clientX-M.current.x,o=e.clientY-M.current.y,n=v(M.current.offsetX+t,M.current.offsetY+o,f);y(n.x),p(n.y)},[b,f,v]),T=a(()=>{k(!1)},[]);return{scale:f,offsetX:m,offsetY:g,isPanning:b,zoomIn:C,zoomOut:S,resetZoom:w,handleWheel:$,handlePanStart:W,handlePanMove:P,handlePanEnd:T}}function k(e,t,o){if(0===e.length)return null;let n=0,i=e.length-1;for(;n<i;){const r=Math.floor((n+i)/2);o(e[r].x)<t?n=r+1:i=r}let r=e[n],l=Math.abs(o(r.x)-t);if(n>0){const i=Math.abs(o(e[n-1].x)-t);i<l&&(l=i,r=e[n-1])}return r}function M(e,t,o,i,r,l,s,c){const[d,h]=n(!1),[f,u]=n(0),[x,m]=n(0),[y,g]=n(null),[p,b]=n(null);return{tooltipVisible:d,tooltipX:f,tooltipY:x,activePoint:y,activeSeries:p,handleMouseMove:a(n=>{const l=e.current;if(!l)return;const a=l.getBoundingClientRect(),c=n.clientX-a.left-r;if(c<0||c>s)return void h(!1);let d=null,f=null,x=1/0;for(const e of t){const t=k(e.data,c,o);if(!t)continue;const n=Math.abs(o(t.x)-c);n<x&&(x=n,d=t,f=e)}d&&f&&(u(o(d.x)),m(i(d.y)),g(d),b(f),h(!0))},[e,t,o,i,r,s]),handleMouseLeave:a(()=>{h(!1),g(null),b(null)},[])}}const v=e=>Math.abs(e)>=1e6?`${(e/1e6).toFixed(1)}M`:Math.abs(e)>=1e3?`${(e/1e3).toFixed(1)}K`:Number.isInteger(e)?e.toString():e.toFixed(1),C=o.memo(({xTicks:o,yTicks:n,xScale:i,yScale:r,plotWidth:l,plotHeight:a,xAxisConfig:s,yAxisConfig:c,style:d})=>{const h=d?.axisColor??"#333",f=d?.tickColor??"#666",u=d?.fontFamily??"sans-serif",x=d?.fontSize??11,m=s?.tickFormat??v,y=c?.tickFormat??v;return e("g",{className:"chartifypdf-axes",children:[t("line",{x1:0,y1:a,x2:l,y2:a,stroke:h,strokeWidth:1}),o.map(o=>{const n=i(o);return e("g",{children:[t("line",{x1:n,y1:a,x2:n,y2:a+6,stroke:h,strokeWidth:1}),t("text",{x:n,y:a+18,textAnchor:"middle",fill:f,fontFamily:u,fontSize:x,children:m(o)})]},`x-tick-${o}`)}),s?.label&&t("text",{x:l/2,y:a+38,textAnchor:"middle",fill:h,fontFamily:u,fontSize:x+1,fontWeight:"bold",children:s.label}),t("line",{x1:0,y1:0,x2:0,y2:a,stroke:h,strokeWidth:1}),n.map(o=>{const n=r(o);return e("g",{children:[t("line",{x1:-6,y1:n,x2:0,y2:n,stroke:h,strokeWidth:1}),t("text",{x:-10,y:n,textAnchor:"end",dominantBaseline:"middle",fill:f,fontFamily:u,fontSize:x,children:y(o)})]},`y-tick-${o}`)}),c?.label&&t("text",{x:0,y:0,textAnchor:"middle",fill:h,fontFamily:u,fontSize:x+1,fontWeight:"bold",transform:`translate(-40, ${a/2}) rotate(-90)`,children:c.label})]})});C.displayName="Axes";const S=o.memo(({xTicks:o,yTicks:n,xScale:i,yScale:r,plotWidth:l,plotHeight:a,showXGrid:s,showYGrid:c,xGridColor:d="#e0e0e0",yGridColor:h="#e0e0e0"})=>e("g",{className:"chartifypdf-grid",children:[c&&n.map(e=>{const o=r(e);return t("line",{x1:0,y1:o,x2:l,y2:o,stroke:h,strokeDasharray:"4 4",strokeOpacity:.5},`y-grid-${e}`)}),s&&o.map(e=>{const o=i(e);return t("line",{x1:o,y1:0,x2:o,y2:a,stroke:d,strokeDasharray:"4 4",strokeOpacity:.5},`x-grid-${e}`)})]}));S.displayName="GridLines";const w=o.memo(({series:o,xScale:i,yScale:r,color:a,animation:c,onPointClick:d})=>{const h=l(null),[f,u]=n(0),[x,m]=n(!c?.enabled),y=o.strokeWidth??2,g=o.showDots??!1,p=o.dotRadius??3.5,b=function(e){if(0===e.length)return"";const[t,...o]=e;return`M${t.x},${t.y}`+o.map(e=>`L${e.x},${e.y}`).join("")}(o.data.map(e=>({x:i(e.x),y:r(e.y)})));s(()=>{if(c?.enabled&&h.current){const e=h.current.getTotalLength();u(e),m(!1);const t=setTimeout(()=>{m(!0)},c.duration??800);return()=>clearTimeout(t)}},[b,c?.enabled,c?.duration]);const k=c?.enabled&&f>0?{strokeDasharray:f,strokeDashoffset:x?0:f,transition:`stroke-dashoffset ${c.duration??800}ms ${c.easing??"ease-in-out"}`}:{};return e("g",{className:"chartifypdf-line-path",children:[t("path",{ref:h,d:b,fill:"none",stroke:a,strokeWidth:y,strokeDasharray:o.strokeDasharray,strokeLinecap:"round",strokeLinejoin:"round",style:k}),g&&o.data.map((e,n)=>t("circle",{cx:i(e.x),cy:r(e.y),r:p,fill:a,stroke:"#fff",strokeWidth:1.5,style:{cursor:d?"pointer":"default"},onClick:d?()=>d(e,o):void 0},`dot-${o.id}-${n}`))]})});w.displayName="LinePath";const $=o.memo(({visible:o,x:n,y:i,point:r,series:l,plotHeight:a,plotWidth:s,config:c,seriesColor:d})=>{if(!o||!r||!l)return null;const h=c?.backgroundColor??"rgba(0, 0, 0, 0.8)",f=c?.textColor??"#fff";let u;if(c?.renderCustom)u=c.renderCustom(r,l);else{u=c?.formatter?c.formatter(r,l):`${l.name}: (${r.x}, ${r.y})`}let x=n+12,m=i-40-8;return x+140>s&&(x=n-140-12),m<0&&(m=i+12),e("g",{className:"chartifypdf-tooltip",pointerEvents:"none",children:[t("line",{x1:n,y1:0,x2:n,y2:a,stroke:"#999",strokeWidth:1,strokeDasharray:"4 4",opacity:.6}),t("circle",{cx:n,cy:i,r:5,fill:d,stroke:"#fff",strokeWidth:2}),t("foreignObject",{x:x,y:m,width:140,height:60,overflow:"visible",children:t("div",{style:{backgroundColor:h,color:f,padding:"6px 10px",borderRadius:"4px",fontSize:"12px",fontFamily:"sans-serif",whiteSpace:"nowrap",lineHeight:1.4,boxShadow:"0 2px 6px rgba(0,0,0,0.2)"},children:u})})]})});$.displayName="Tooltip";const W=o.memo(({config:o,svgWidth:n,svgHeight:i,margin:r,scale:l,onZoomIn:a,onZoomOut:s,onReset:c})=>{if(!o?.enabled||!1===o.showControls)return null;const d=o.controlsPosition??"top-right",h=24;let f,u;switch(d){case"top-left":f=r.left+8,u=r.top+8;break;case"bottom-left":f=r.left+8,u=i-r.bottom-24-8;break;case"bottom-right":f=n-r.right-80-8,u=i-r.bottom-24-8;break;default:f=n-r.right-80-8,u=r.top+8}return t("g",{className:"chartifypdf-zoom-controls",transform:`translate(${f}, ${u})`,children:(l>1?[{label:"+",onClick:a},{label:"−",onClick:s},{label:"↺",onClick:c}]:[{label:"+",onClick:a},{label:"−",onClick:s}]).map((o,n)=>e("g",{transform:`translate(${28*n}, 0)`,onClick:o.onClick,style:{cursor:"pointer"},children:[t("rect",{width:h,height:h,rx:4,fill:"rgba(255,255,255,0.9)",stroke:"#ccc",strokeWidth:1}),t("text",{x:12,y:12,textAnchor:"middle",dominantBaseline:"central",fontSize:14,fontFamily:"sans-serif",fill:"#333",fontWeight:"bold",children:o.label})]},o.label))})});W.displayName="ZoomControls";const P={top:20,right:20,bottom:50,left:60},T=({data:o,width:n,height:i,margin:r,xAxis:a,yAxis:s,tooltip:d,zoom:u,style:x,animation:m,colorPalette:y,className:g,onPointClick:k,ariaLabel:v})=>{const T=l(null),L=l(null),N=c(),z={...P,...r},F=f(T,n,i??400),{width:X,height:Y}=F,A=Math.max(0,X-z.left-z.right),D=Math.max(0,Y-z.top-z.bottom),{xScale:G,yScale:H,xTicks:O,yTicks:R}=p(o,A,D,a,s),Z=b(u,A,D),I=!1!==d?.enabled,B=M(L,o,G,H,z.left,z.top,A),j=a?.gridLines??!1,E=s?.gridLines??!0;return t("div",0===X||0===Y?{ref:T,className:g,style:{width:n??"100%",height:i??400,backgroundColor:x?.backgroundColor}}:{ref:T,className:g,style:{width:n??"100%",height:i??400,backgroundColor:x?.backgroundColor},children:e("svg",{ref:L,width:X,height:Y,role:"img","aria-label":v??"Line chart",onMouseMove:I&&!Z.isPanning?B.handleMouseMove:void 0,onMouseLeave:I?B.handleMouseLeave:void 0,onWheel:u?.enabled?Z.handleWheel:void 0,onMouseDown:u?.enabled?Z.handlePanStart:void 0,onMouseUp:u?.enabled?Z.handlePanEnd:void 0,style:{userSelect:Z.isPanning?"none":void 0,cursor:Z.isPanning?"grabbing":u?.enabled&&Z.scale>1?"grab":void 0},children:[t("defs",{children:t("clipPath",{id:N,children:t("rect",{width:A,height:D})})}),e("g",{transform:`translate(${z.left}, ${z.top})`,children:[t(C,{xTicks:O,yTicks:R,xScale:G,yScale:H,plotWidth:A,plotHeight:D,xAxisConfig:a,yAxisConfig:s,style:x}),t(S,{xTicks:O,yTicks:R,xScale:G,yScale:H,plotWidth:A,plotHeight:D,showXGrid:j,showYGrid:E,xGridColor:a?.gridLineColor,yGridColor:s?.gridLineColor}),t("g",{clipPath:`url(#${N})`,children:t("g",{transform:`translate(${Z.offsetX}, ${Z.offsetY}) scale(${Z.scale})`,onMouseMove:u?.enabled?Z.handlePanMove:void 0,children:o.map((e,o)=>t(w,{series:e,xScale:G,yScale:H,color:e.color??h(o,y),animation:m,onPointClick:k},e.id))})}),I&&t($,{visible:B.tooltipVisible,x:B.tooltipX,y:B.tooltipY,point:B.activePoint,series:B.activeSeries,plotHeight:D,plotWidth:A,config:d,seriesColor:B.activeSeries?.color??h(o.findIndex(e=>e.id===B.activeSeries?.id),y)})]}),t(W,{config:u,svgWidth:X,svgHeight:Y,margin:z,scale:Z.scale,onZoomIn:Z.zoomIn,onZoomOut:Z.zoomOut,onReset:Z.resetZoom})]})})};T.displayName="LineChart";export{T as LineChart,f as useChartDimensions,p as useScales,M as useTooltip,b as useZoom};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../src/utils/color.ts","../src/hooks/useChartDimensions.ts","../src/utils/math.ts","../src/hooks/useScales.ts","../src/hooks/useZoom.ts","../src/hooks/useTooltip.ts","../src/components/Axes.tsx","../src/components/GridLines.tsx","../src/components/LinePath.tsx","../src/utils/path.ts","../src/components/Tooltip.tsx","../src/components/ZoomControls.tsx","../src/components/LineChart.tsx"],"sourcesContent":["export const DEFAULT_PALETTE = [\n \"#4e79a7\",\n \"#f28e2b\",\n \"#e15759\",\n \"#76b7b2\",\n \"#59a14f\",\n \"#edc948\",\n \"#b07aa1\",\n \"#ff9da7\",\n \"#9c755f\",\n \"#bab0ac\",\n];\n\nexport function getSeriesColor(index: number, palette?: string[]): string {\n const colors = palette ?? DEFAULT_PALETTE;\n return colors[index % colors.length];\n}\n","import { useLayoutEffect, useState, type RefObject } from \"react\";\n\nexport function useChartDimensions(\n containerRef: RefObject<HTMLDivElement | null>,\n providedWidth?: number,\n providedHeight?: number\n): { width: number; height: number } {\n const [dimensions, setDimensions] = useState<{\n width: number;\n height: number;\n }>({\n width: providedWidth ?? 0,\n height: providedHeight ?? 0,\n });\n\n const isFixed =\n providedWidth !== undefined && providedHeight !== undefined;\n\n useLayoutEffect(() => {\n if (isFixed) {\n setDimensions({ width: providedWidth!, height: providedHeight! });\n return;\n }\n\n const node = containerRef.current;\n if (!node) return;\n\n const measure = () => {\n const rect = node.getBoundingClientRect();\n setDimensions({ width: rect.width, height: rect.height });\n };\n\n measure();\n\n const observer = new ResizeObserver(() => {\n measure();\n });\n observer.observe(node);\n\n return () => observer.disconnect();\n }, [containerRef, isFixed, providedWidth, providedHeight]);\n\n return dimensions;\n}\n","export function clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n\nexport function lerp(a: number, b: number, t: number): number {\n return a + (b - a) * t;\n}\n\nexport function inverseLerp(a: number, b: number, value: number): number {\n if (a === b) return 0;\n return (value - a) / (b - a);\n}\n\nexport function niceNumber(range: number, round: boolean): number {\n const exponent = Math.floor(Math.log10(range));\n const fraction = range / Math.pow(10, exponent);\n\n let niceFraction: number;\n if (round) {\n if (fraction < 1.5) niceFraction = 1;\n else if (fraction < 3) niceFraction = 2;\n else if (fraction < 7) niceFraction = 5;\n else niceFraction = 10;\n } else {\n if (fraction <= 1) niceFraction = 1;\n else if (fraction <= 2) niceFraction = 2;\n else if (fraction <= 5) niceFraction = 5;\n else niceFraction = 10;\n }\n\n return niceFraction * Math.pow(10, exponent);\n}\n\nexport function niceRange(\n min: number,\n max: number,\n tickCount: number\n): { niceMin: number; niceMax: number; tickStep: number } {\n if (min === max) {\n const offset = min === 0 ? 1 : Math.abs(min) * 0.1;\n min = min - offset;\n max = max + offset;\n }\n\n const range = niceNumber(max - min, false);\n const tickStep = niceNumber(range / (tickCount - 1), true);\n const niceMin = Math.floor(min / tickStep) * tickStep;\n const niceMax = Math.ceil(max / tickStep) * tickStep;\n\n return { niceMin, niceMax, tickStep };\n}\n\nexport function linearScale(\n domainMin: number,\n domainMax: number,\n rangeMin: number,\n rangeMax: number\n): (value: number) => number {\n return (value: number) => {\n if (domainMax === domainMin) return (rangeMin + rangeMax) / 2;\n const t = inverseLerp(domainMin, domainMax, value);\n return lerp(rangeMin, rangeMax, t);\n };\n}\n\nexport function generateTicks(\n niceMin: number,\n niceMax: number,\n tickStep: number\n): number[] {\n const ticks: number[] = [];\n for (let v = niceMin; v <= niceMax + tickStep * 0.5; v += tickStep) {\n ticks.push(parseFloat(v.toPrecision(12)));\n }\n return ticks;\n}\n","import { useMemo } from \"react\";\nimport type { DataSeries, AxisConfig } from \"../types/chart.types\";\nimport { niceRange, linearScale, generateTicks } from \"../utils/math\";\n\nexport interface ScalesResult {\n xScale: (value: number) => number;\n yScale: (value: number) => number;\n xTicks: number[];\n yTicks: number[];\n xDomain: [number, number];\n yDomain: [number, number];\n}\n\nexport function useScales(\n data: DataSeries[],\n plotWidth: number,\n plotHeight: number,\n xAxisConfig?: AxisConfig,\n yAxisConfig?: AxisConfig\n): ScalesResult {\n return useMemo(() => {\n let xMin = Infinity;\n let xMax = -Infinity;\n let yMin = Infinity;\n let yMax = -Infinity;\n\n for (const series of data) {\n for (const point of series.data) {\n if (point.x < xMin) xMin = point.x;\n if (point.x > xMax) xMax = point.x;\n if (point.y < yMin) yMin = point.y;\n if (point.y > yMax) yMax = point.y;\n }\n }\n\n if (!isFinite(xMin)) {\n xMin = 0;\n xMax = 1;\n yMin = 0;\n yMax = 1;\n }\n\n if (xAxisConfig?.min !== undefined) xMin = xAxisConfig.min;\n if (xAxisConfig?.max !== undefined) xMax = xAxisConfig.max;\n if (yAxisConfig?.min !== undefined) yMin = yAxisConfig.min;\n if (yAxisConfig?.max !== undefined) yMax = yAxisConfig.max;\n\n const xTickCount = xAxisConfig?.tickCount ?? 6;\n const yTickCount = yAxisConfig?.tickCount ?? 6;\n\n const xNice = niceRange(xMin, xMax, xTickCount);\n const yNice = niceRange(yMin, yMax, yTickCount);\n\n const xScale = linearScale(xNice.niceMin, xNice.niceMax, 0, plotWidth);\n // Y inverted: SVG Y increases downward\n const yScale = linearScale(yNice.niceMin, yNice.niceMax, plotHeight, 0);\n\n const xTicks = generateTicks(xNice.niceMin, xNice.niceMax, xNice.tickStep);\n const yTicks = generateTicks(yNice.niceMin, yNice.niceMax, yNice.tickStep);\n\n return {\n xScale,\n yScale,\n xTicks,\n yTicks,\n xDomain: [xNice.niceMin, xNice.niceMax] as [number, number],\n yDomain: [yNice.niceMin, yNice.niceMax] as [number, number],\n };\n }, [data, plotWidth, plotHeight, xAxisConfig, yAxisConfig]);\n}\n","import { useState, useCallback, useRef } from \"react\";\nimport type { ZoomConfig } from \"../types/chart.types\";\nimport { clamp } from \"../utils/math\";\n\nexport interface ZoomState {\n scale: number;\n offsetX: number;\n offsetY: number;\n isPanning: boolean;\n zoomIn: () => void;\n zoomOut: () => void;\n resetZoom: () => void;\n handleWheel: (e: React.WheelEvent) => void;\n handlePanStart: (e: React.MouseEvent) => void;\n handlePanMove: (e: React.MouseEvent) => void;\n handlePanEnd: () => void;\n}\n\nexport function useZoom(\n config: ZoomConfig | undefined,\n plotWidth: number,\n plotHeight: number\n): ZoomState {\n const enabled = config?.enabled ?? false;\n const minScale = config?.minScale ?? 1;\n const maxScale = config?.maxScale ?? 10;\n const step = config?.step ?? 0.5;\n const enableWheel = config?.enableWheel ?? true;\n const enablePan = config?.enablePan ?? true;\n\n const [scale, setScale] = useState(1);\n const [offsetX, setOffsetX] = useState(0);\n const [offsetY, setOffsetY] = useState(0);\n const [isPanning, setIsPanning] = useState(false);\n const panStart = useRef({ x: 0, y: 0, offsetX: 0, offsetY: 0 });\n\n const clampOffset = useCallback(\n (ox: number, oy: number, s: number) => {\n const maxOffsetX = plotWidth * (s - 1);\n const maxOffsetY = plotHeight * (s - 1);\n return {\n x: clamp(ox, -maxOffsetX, 0),\n y: clamp(oy, -maxOffsetY, 0),\n };\n },\n [plotWidth, plotHeight]\n );\n\n const zoomIn = useCallback(() => {\n if (!enabled) return;\n setScale((prev) => {\n const next = clamp(prev + step, minScale, maxScale);\n // Center zoom\n setOffsetX((ox) => {\n const newOx = ox - (plotWidth * step) / 2;\n return clampOffset(newOx, 0, next).x;\n });\n setOffsetY((oy) => {\n const newOy = oy - (plotHeight * step) / 2;\n return clampOffset(0, newOy, next).y;\n });\n return next;\n });\n }, [enabled, step, minScale, maxScale, plotWidth, plotHeight, clampOffset]);\n\n const zoomOut = useCallback(() => {\n if (!enabled) return;\n setScale((prev) => {\n const next = clamp(prev - step, minScale, maxScale);\n setOffsetX((ox) => {\n const newOx = ox + (plotWidth * step) / 2;\n return clampOffset(newOx, 0, next).x;\n });\n setOffsetY((oy) => {\n const newOy = oy + (plotHeight * step) / 2;\n return clampOffset(0, newOy, next).y;\n });\n return next;\n });\n }, [enabled, step, minScale, maxScale, plotWidth, plotHeight, clampOffset]);\n\n const resetZoom = useCallback(() => {\n setScale(1);\n setOffsetX(0);\n setOffsetY(0);\n }, []);\n\n const handleWheel = useCallback(\n (e: React.WheelEvent) => {\n if (!enabled || !enableWheel) return;\n e.preventDefault();\n\n const rect = (e.currentTarget as SVGElement).getBoundingClientRect();\n const mouseX = e.clientX - rect.left;\n const mouseY = e.clientY - rect.top;\n\n const delta = e.deltaY < 0 ? step : -step;\n\n setScale((prev) => {\n const next = clamp(prev + delta, minScale, maxScale);\n const ratio = next / prev;\n\n setOffsetX((ox) => {\n const newOx = mouseX - ratio * (mouseX - ox);\n return clampOffset(newOx, 0, next).x;\n });\n setOffsetY((oy) => {\n const newOy = mouseY - ratio * (mouseY - oy);\n return clampOffset(0, newOy, next).y;\n });\n\n return next;\n });\n },\n [enabled, enableWheel, step, minScale, maxScale, clampOffset]\n );\n\n const handlePanStart = useCallback(\n (e: React.MouseEvent) => {\n if (!enabled || !enablePan || scale <= 1) return;\n if (e.button !== 0) return; // left-click only\n setIsPanning(true);\n panStart.current = {\n x: e.clientX,\n y: e.clientY,\n offsetX,\n offsetY,\n };\n },\n [enabled, enablePan, scale, offsetX, offsetY]\n );\n\n const handlePanMove = useCallback(\n (e: React.MouseEvent) => {\n if (!isPanning) return;\n const dx = e.clientX - panStart.current.x;\n const dy = e.clientY - panStart.current.y;\n const clamped = clampOffset(\n panStart.current.offsetX + dx,\n panStart.current.offsetY + dy,\n scale\n );\n setOffsetX(clamped.x);\n setOffsetY(clamped.y);\n },\n [isPanning, scale, clampOffset]\n );\n\n const handlePanEnd = useCallback(() => {\n setIsPanning(false);\n }, []);\n\n return {\n scale,\n offsetX,\n offsetY,\n isPanning,\n zoomIn,\n zoomOut,\n resetZoom,\n handleWheel,\n handlePanStart,\n handlePanMove,\n handlePanEnd,\n };\n}\n","import { useState, useCallback, type RefObject } from \"react\";\nimport type { DataSeries, DataPoint, TooltipConfig } from \"../types/chart.types\";\n\nexport interface TooltipState {\n tooltipVisible: boolean;\n tooltipX: number;\n tooltipY: number;\n activePoint: DataPoint | null;\n activeSeries: DataSeries | null;\n handleMouseMove: (e: React.MouseEvent) => void;\n handleMouseLeave: () => void;\n}\n\nfunction findNearestByPixelX(\n sorted: DataPoint[],\n mouseX: number,\n xScale: (v: number) => number\n): DataPoint | null {\n if (sorted.length === 0) return null;\n\n let low = 0;\n let high = sorted.length - 1;\n\n // Binary search for nearest pixel X\n while (low < high) {\n const mid = Math.floor((low + high) / 2);\n if (xScale(sorted[mid].x) < mouseX) {\n low = mid + 1;\n } else {\n high = mid;\n }\n }\n\n let nearest = sorted[low];\n let minDist = Math.abs(xScale(nearest.x) - mouseX);\n\n if (low > 0) {\n const dist = Math.abs(xScale(sorted[low - 1].x) - mouseX);\n if (dist < minDist) {\n minDist = dist;\n nearest = sorted[low - 1];\n }\n }\n\n return nearest;\n}\n\nexport function useTooltip(\n svgRef: RefObject<SVGSVGElement | null>,\n data: DataSeries[],\n xScale: (v: number) => number,\n yScale: (v: number) => number,\n marginLeft: number,\n marginTop: number,\n plotWidth: number,\n _config?: TooltipConfig\n): TooltipState {\n const [tooltipVisible, setTooltipVisible] = useState(false);\n const [tooltipX, setTooltipX] = useState(0);\n const [tooltipY, setTooltipY] = useState(0);\n const [activePoint, setActivePoint] = useState<DataPoint | null>(null);\n const [activeSeries, setActiveSeries] = useState<DataSeries | null>(null);\n\n const handleMouseMove = useCallback(\n (e: React.MouseEvent) => {\n const svg = svgRef.current;\n if (!svg) return;\n\n const rect = svg.getBoundingClientRect();\n const mouseX = e.clientX - rect.left - marginLeft;\n\n if (mouseX < 0 || mouseX > plotWidth) {\n setTooltipVisible(false);\n return;\n }\n\n let closestPoint: DataPoint | null = null;\n let closestSeries: DataSeries | null = null;\n let closestDist = Infinity;\n\n for (const series of data) {\n const nearest = findNearestByPixelX(series.data, mouseX, xScale);\n if (!nearest) continue;\n\n const dist = Math.abs(xScale(nearest.x) - mouseX);\n if (dist < closestDist) {\n closestDist = dist;\n closestPoint = nearest;\n closestSeries = series;\n }\n }\n\n if (closestPoint && closestSeries) {\n setTooltipX(xScale(closestPoint.x));\n setTooltipY(yScale(closestPoint.y));\n setActivePoint(closestPoint);\n setActiveSeries(closestSeries);\n setTooltipVisible(true);\n }\n },\n [svgRef, data, xScale, yScale, marginLeft, plotWidth]\n );\n\n const handleMouseLeave = useCallback(() => {\n setTooltipVisible(false);\n setActivePoint(null);\n setActiveSeries(null);\n }, []);\n\n return {\n tooltipVisible,\n tooltipX,\n tooltipY,\n activePoint,\n activeSeries,\n handleMouseMove,\n handleMouseLeave,\n };\n}\n","import React from \"react\";\nimport type { AxisConfig, ChartStyle } from \"../types/chart.types\";\n\ninterface AxesProps {\n xTicks: number[];\n yTicks: number[];\n xScale: (v: number) => number;\n yScale: (v: number) => number;\n plotWidth: number;\n plotHeight: number;\n xAxisConfig?: AxisConfig;\n yAxisConfig?: AxisConfig;\n style?: ChartStyle;\n}\n\nconst defaultTickFormat = (v: number): string => {\n if (Math.abs(v) >= 1e6) return `${(v / 1e6).toFixed(1)}M`;\n if (Math.abs(v) >= 1e3) return `${(v / 1e3).toFixed(1)}K`;\n return Number.isInteger(v) ? v.toString() : v.toFixed(1);\n};\n\nexport const Axes: React.FC<AxesProps> = React.memo(\n ({\n xTicks,\n yTicks,\n xScale,\n yScale,\n plotWidth,\n plotHeight,\n xAxisConfig,\n yAxisConfig,\n style,\n }) => {\n const axisColor = style?.axisColor ?? \"#333\";\n const tickColor = style?.tickColor ?? \"#666\";\n const fontFamily = style?.fontFamily ?? \"sans-serif\";\n const fontSize = style?.fontSize ?? 11;\n const xTickFormat = xAxisConfig?.tickFormat ?? defaultTickFormat;\n const yTickFormat = yAxisConfig?.tickFormat ?? defaultTickFormat;\n\n return (\n <g className=\"chartifypdf-axes\">\n {/* X Axis */}\n <line\n x1={0}\n y1={plotHeight}\n x2={plotWidth}\n y2={plotHeight}\n stroke={axisColor}\n strokeWidth={1}\n />\n {xTicks.map((tick) => {\n const x = xScale(tick);\n return (\n <g key={`x-tick-${tick}`}>\n <line\n x1={x}\n y1={plotHeight}\n x2={x}\n y2={plotHeight + 6}\n stroke={axisColor}\n strokeWidth={1}\n />\n <text\n x={x}\n y={plotHeight + 18}\n textAnchor=\"middle\"\n fill={tickColor}\n fontFamily={fontFamily}\n fontSize={fontSize}\n >\n {xTickFormat(tick)}\n </text>\n </g>\n );\n })}\n {xAxisConfig?.label && (\n <text\n x={plotWidth / 2}\n y={plotHeight + 38}\n textAnchor=\"middle\"\n fill={axisColor}\n fontFamily={fontFamily}\n fontSize={fontSize + 1}\n fontWeight=\"bold\"\n >\n {xAxisConfig.label}\n </text>\n )}\n\n {/* Y Axis */}\n <line\n x1={0}\n y1={0}\n x2={0}\n y2={plotHeight}\n stroke={axisColor}\n strokeWidth={1}\n />\n {yTicks.map((tick) => {\n const y = yScale(tick);\n return (\n <g key={`y-tick-${tick}`}>\n <line\n x1={-6}\n y1={y}\n x2={0}\n y2={y}\n stroke={axisColor}\n strokeWidth={1}\n />\n <text\n x={-10}\n y={y}\n textAnchor=\"end\"\n dominantBaseline=\"middle\"\n fill={tickColor}\n fontFamily={fontFamily}\n fontSize={fontSize}\n >\n {yTickFormat(tick)}\n </text>\n </g>\n );\n })}\n {yAxisConfig?.label && (\n <text\n x={0}\n y={0}\n textAnchor=\"middle\"\n fill={axisColor}\n fontFamily={fontFamily}\n fontSize={fontSize + 1}\n fontWeight=\"bold\"\n transform={`translate(${-40}, ${plotHeight / 2}) rotate(-90)`}\n >\n {yAxisConfig.label}\n </text>\n )}\n </g>\n );\n }\n);\n\nAxes.displayName = \"Axes\";\n","import React from \"react\";\n\ninterface GridLinesProps {\n xTicks: number[];\n yTicks: number[];\n xScale: (v: number) => number;\n yScale: (v: number) => number;\n plotWidth: number;\n plotHeight: number;\n showXGrid: boolean;\n showYGrid: boolean;\n xGridColor?: string;\n yGridColor?: string;\n}\n\nexport const GridLines: React.FC<GridLinesProps> = React.memo(\n ({\n xTicks,\n yTicks,\n xScale,\n yScale,\n plotWidth,\n plotHeight,\n showXGrid,\n showYGrid,\n xGridColor = \"#e0e0e0\",\n yGridColor = \"#e0e0e0\",\n }) => {\n return (\n <g className=\"chartifypdf-grid\">\n {showYGrid &&\n yTicks.map((tick) => {\n const y = yScale(tick);\n return (\n <line\n key={`y-grid-${tick}`}\n x1={0}\n y1={y}\n x2={plotWidth}\n y2={y}\n stroke={yGridColor}\n strokeDasharray=\"4 4\"\n strokeOpacity={0.5}\n />\n );\n })}\n {showXGrid &&\n xTicks.map((tick) => {\n const x = xScale(tick);\n return (\n <line\n key={`x-grid-${tick}`}\n x1={x}\n y1={0}\n x2={x}\n y2={plotHeight}\n stroke={xGridColor}\n strokeDasharray=\"4 4\"\n strokeOpacity={0.5}\n />\n );\n })}\n </g>\n );\n }\n);\n\nGridLines.displayName = \"GridLines\";\n","import React, { useRef, useEffect, useState } from \"react\";\nimport type { DataSeries, DataPoint, AnimationConfig } from \"../types/chart.types\";\nimport { buildLinePath } from \"../utils/path\";\n\ninterface LinePathProps {\n series: DataSeries;\n xScale: (v: number) => number;\n yScale: (v: number) => number;\n color: string;\n animation?: AnimationConfig;\n onPointClick?: (point: DataPoint, series: DataSeries) => void;\n}\n\nexport const LinePath: React.FC<LinePathProps> = React.memo(\n ({ series, xScale, yScale, color, animation, onPointClick }) => {\n const pathRef = useRef<SVGPathElement>(null);\n const [pathLength, setPathLength] = useState(0);\n const [animationDone, setAnimationDone] = useState(!animation?.enabled);\n\n const strokeWidth = series.strokeWidth ?? 2;\n const showDots = series.showDots ?? false;\n const dotRadius = series.dotRadius ?? 3.5;\n\n const points = series.data.map((pt) => ({\n x: xScale(pt.x),\n y: yScale(pt.y),\n }));\n\n const d = buildLinePath(points);\n\n useEffect(() => {\n if (animation?.enabled && pathRef.current) {\n const length = pathRef.current.getTotalLength();\n setPathLength(length);\n setAnimationDone(false);\n\n const timer = setTimeout(() => {\n setAnimationDone(true);\n }, animation.duration ?? 800);\n\n return () => clearTimeout(timer);\n }\n }, [d, animation?.enabled, animation?.duration]);\n\n const animStyle: React.CSSProperties =\n animation?.enabled && pathLength > 0\n ? {\n strokeDasharray: pathLength,\n strokeDashoffset: animationDone ? 0 : pathLength,\n transition: `stroke-dashoffset ${animation.duration ?? 800}ms ${animation.easing ?? \"ease-in-out\"}`,\n }\n : {};\n\n return (\n <g className=\"chartifypdf-line-path\">\n <path\n ref={pathRef}\n d={d}\n fill=\"none\"\n stroke={color}\n strokeWidth={strokeWidth}\n strokeDasharray={series.strokeDasharray}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n style={animStyle}\n />\n {showDots &&\n series.data.map((pt, i) => (\n <circle\n key={`dot-${series.id}-${i}`}\n cx={xScale(pt.x)}\n cy={yScale(pt.y)}\n r={dotRadius}\n fill={color}\n stroke=\"#fff\"\n strokeWidth={1.5}\n style={{ cursor: onPointClick ? \"pointer\" : \"default\" }}\n onClick={\n onPointClick\n ? () => onPointClick(pt, series)\n : undefined\n }\n />\n ))}\n </g>\n );\n }\n);\n\nLinePath.displayName = \"LinePath\";\n","export function buildLinePath(\n points: { x: number; y: number }[]\n): string {\n if (points.length === 0) return \"\";\n const [first, ...rest] = points;\n return `M${first.x},${first.y}` + rest.map((p) => `L${p.x},${p.y}`).join(\"\");\n}\n","import React from \"react\";\nimport type { DataPoint, DataSeries, TooltipConfig } from \"../types/chart.types\";\n\ninterface TooltipProps {\n visible: boolean;\n x: number;\n y: number;\n point: DataPoint | null;\n series: DataSeries | null;\n plotHeight: number;\n plotWidth: number;\n config?: TooltipConfig;\n seriesColor: string;\n}\n\nexport const Tooltip: React.FC<TooltipProps> = React.memo(\n ({ visible, x, y, point, series, plotHeight, plotWidth, config, seriesColor }) => {\n if (!visible || !point || !series) return null;\n\n const bgColor = config?.backgroundColor ?? \"rgba(0, 0, 0, 0.8)\";\n const textColor = config?.textColor ?? \"#fff\";\n\n // Determine tooltip text\n let content: React.ReactNode;\n if (config?.renderCustom) {\n content = config.renderCustom(point, series);\n } else {\n const text = config?.formatter\n ? config.formatter(point, series)\n : `${series.name}: (${point.x}, ${point.y})`;\n content = text;\n }\n\n // Smart positioning: flip when near edges\n const tooltipWidth = 140;\n const tooltipHeight = 40;\n let tx = x + 12;\n let ty = y - tooltipHeight - 8;\n\n if (tx + tooltipWidth > plotWidth) {\n tx = x - tooltipWidth - 12;\n }\n if (ty < 0) {\n ty = y + 12;\n }\n\n return (\n <g className=\"chartifypdf-tooltip\" pointerEvents=\"none\">\n {/* Vertical indicator line */}\n <line\n x1={x}\n y1={0}\n x2={x}\n y2={plotHeight}\n stroke=\"#999\"\n strokeWidth={1}\n strokeDasharray=\"4 4\"\n opacity={0.6}\n />\n {/* Highlight circle */}\n <circle\n cx={x}\n cy={y}\n r={5}\n fill={seriesColor}\n stroke=\"#fff\"\n strokeWidth={2}\n />\n {/* Tooltip box using foreignObject */}\n <foreignObject\n x={tx}\n y={ty}\n width={tooltipWidth}\n height={tooltipHeight + 20}\n overflow=\"visible\"\n >\n <div\n style={{\n backgroundColor: bgColor,\n color: textColor,\n padding: \"6px 10px\",\n borderRadius: \"4px\",\n fontSize: \"12px\",\n fontFamily: \"sans-serif\",\n whiteSpace: \"nowrap\",\n lineHeight: 1.4,\n boxShadow: \"0 2px 6px rgba(0,0,0,0.2)\",\n }}\n >\n {content}\n </div>\n </foreignObject>\n </g>\n );\n }\n);\n\nTooltip.displayName = \"Tooltip\";\n","import React from \"react\";\nimport type { ZoomConfig } from \"../types/chart.types\";\n\ninterface ZoomControlsProps {\n config?: ZoomConfig;\n svgWidth: number;\n svgHeight: number;\n margin: { top: number; right: number; bottom: number; left: number };\n scale: number;\n onZoomIn: () => void;\n onZoomOut: () => void;\n onReset: () => void;\n}\n\nexport const ZoomControls: React.FC<ZoomControlsProps> = React.memo(\n ({ config, svgWidth, svgHeight, margin, scale, onZoomIn, onZoomOut, onReset }) => {\n if (!config?.enabled || config.showControls === false) return null;\n\n const position = config.controlsPosition ?? \"top-right\";\n const btnSize = 24;\n const gap = 4;\n const groupWidth = btnSize * 3 + gap * 2;\n const groupHeight = btnSize;\n const padding = 8;\n\n let gx: number;\n let gy: number;\n\n switch (position) {\n case \"top-left\":\n gx = margin.left + padding;\n gy = margin.top + padding;\n break;\n case \"bottom-left\":\n gx = margin.left + padding;\n gy = svgHeight - margin.bottom - groupHeight - padding;\n break;\n case \"bottom-right\":\n gx = svgWidth - margin.right - groupWidth - padding;\n gy = svgHeight - margin.bottom - groupHeight - padding;\n break;\n case \"top-right\":\n default:\n gx = svgWidth - margin.right - groupWidth - padding;\n gy = margin.top + padding;\n break;\n }\n\n const buttons = [\n { label: \"+\", onClick: onZoomIn },\n { label: \"\\u2013\", onClick: onReset }, // en-dash for reset\n { label: \"\\u2212\", onClick: onZoomOut }, // minus sign\n ];\n\n // Replace middle button with reset when zoomed, or show as \"-\"\n const displayButtons =\n scale > 1\n ? [\n { label: \"+\", onClick: onZoomIn },\n { label: \"\\u2212\", onClick: onZoomOut },\n { label: \"\\u21BA\", onClick: onReset },\n ]\n : [\n { label: \"+\", onClick: onZoomIn },\n { label: \"\\u2212\", onClick: onZoomOut },\n ];\n\n return (\n <g\n className=\"chartifypdf-zoom-controls\"\n transform={`translate(${gx}, ${gy})`}\n >\n {displayButtons.map((btn, i) => (\n <g\n key={btn.label}\n transform={`translate(${i * (btnSize + gap)}, 0)`}\n onClick={btn.onClick}\n style={{ cursor: \"pointer\" }}\n >\n <rect\n width={btnSize}\n height={btnSize}\n rx={4}\n fill=\"rgba(255,255,255,0.9)\"\n stroke=\"#ccc\"\n strokeWidth={1}\n />\n <text\n x={btnSize / 2}\n y={btnSize / 2}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n fontSize={14}\n fontFamily=\"sans-serif\"\n fill=\"#333\"\n fontWeight=\"bold\"\n >\n {btn.label}\n </text>\n </g>\n ))}\n </g>\n );\n }\n);\n\nZoomControls.displayName = \"ZoomControls\";\n","import React, { useRef, useId } from \"react\";\nimport type {\n LineChartProps,\n ChartMargin,\n} from \"../types/chart.types\";\nimport { getSeriesColor } from \"../utils/color\";\nimport { useChartDimensions } from \"../hooks/useChartDimensions\";\nimport { useScales } from \"../hooks/useScales\";\nimport { useZoom } from \"../hooks/useZoom\";\nimport { useTooltip } from \"../hooks/useTooltip\";\nimport { Axes } from \"./Axes\";\nimport { GridLines } from \"./GridLines\";\nimport { LinePath } from \"./LinePath\";\nimport { Tooltip } from \"./Tooltip\";\nimport { ZoomControls } from \"./ZoomControls\";\n\nconst DEFAULT_MARGIN: ChartMargin = {\n top: 20,\n right: 20,\n bottom: 50,\n left: 60,\n};\n\nconst DEFAULT_HEIGHT = 400;\n\nexport const LineChart: React.FC<LineChartProps> = ({\n data,\n width: providedWidth,\n height: providedHeight,\n margin: marginOverride,\n xAxis: xAxisConfig,\n yAxis: yAxisConfig,\n tooltip: tooltipConfig,\n zoom: zoomConfig,\n style: chartStyle,\n animation,\n colorPalette,\n className,\n onPointClick,\n ariaLabel,\n}) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const svgRef = useRef<SVGSVGElement>(null);\n const clipId = useId();\n\n const margin: ChartMargin = {\n ...DEFAULT_MARGIN,\n ...marginOverride,\n };\n\n const dimensions = useChartDimensions(\n containerRef,\n providedWidth,\n providedHeight ?? DEFAULT_HEIGHT\n );\n const { width: svgWidth, height: svgHeight } = dimensions;\n\n const plotWidth = Math.max(0, svgWidth - margin.left - margin.right);\n const plotHeight = Math.max(0, svgHeight - margin.top - margin.bottom);\n\n const { xScale, yScale, xTicks, yTicks } = useScales(\n data,\n plotWidth,\n plotHeight,\n xAxisConfig,\n yAxisConfig\n );\n\n const zoomState = useZoom(zoomConfig, plotWidth, plotHeight);\n\n const tooltipEnabled = tooltipConfig?.enabled !== false;\n const tooltipState = useTooltip(\n svgRef,\n data,\n xScale,\n yScale,\n margin.left,\n margin.top,\n plotWidth,\n tooltipConfig\n );\n\n const showXGrid = xAxisConfig?.gridLines ?? false;\n const showYGrid = yAxisConfig?.gridLines ?? true;\n\n // Don't render until we have dimensions\n if (svgWidth === 0 || svgHeight === 0) {\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n width: providedWidth ?? \"100%\",\n height: providedHeight ?? DEFAULT_HEIGHT,\n backgroundColor: chartStyle?.backgroundColor,\n }}\n />\n );\n }\n\n return (\n <div\n ref={containerRef}\n className={className}\n style={{\n width: providedWidth ?? \"100%\",\n height: providedHeight ?? DEFAULT_HEIGHT,\n backgroundColor: chartStyle?.backgroundColor,\n }}\n >\n <svg\n ref={svgRef}\n width={svgWidth}\n height={svgHeight}\n role=\"img\"\n aria-label={ariaLabel ?? \"Line chart\"}\n onMouseMove={\n tooltipEnabled && !zoomState.isPanning\n ? tooltipState.handleMouseMove\n : undefined\n }\n onMouseLeave={tooltipEnabled ? tooltipState.handleMouseLeave : undefined}\n onWheel={zoomConfig?.enabled ? zoomState.handleWheel : undefined}\n onMouseDown={zoomConfig?.enabled ? zoomState.handlePanStart : undefined}\n onMouseUp={zoomConfig?.enabled ? zoomState.handlePanEnd : undefined}\n style={{\n userSelect: zoomState.isPanning ? \"none\" : undefined,\n cursor: zoomState.isPanning\n ? \"grabbing\"\n : zoomConfig?.enabled && zoomState.scale > 1\n ? \"grab\"\n : undefined,\n }}\n >\n <defs>\n <clipPath id={clipId}>\n <rect width={plotWidth} height={plotHeight} />\n </clipPath>\n </defs>\n\n <g transform={`translate(${margin.left}, ${margin.top})`}>\n {/* Axes render outside zoom transform — always crisp */}\n <Axes\n xTicks={xTicks}\n yTicks={yTicks}\n xScale={xScale}\n yScale={yScale}\n plotWidth={plotWidth}\n plotHeight={plotHeight}\n xAxisConfig={xAxisConfig}\n yAxisConfig={yAxisConfig}\n style={chartStyle}\n />\n\n {/* Grid lines */}\n <GridLines\n xTicks={xTicks}\n yTicks={yTicks}\n xScale={xScale}\n yScale={yScale}\n plotWidth={plotWidth}\n plotHeight={plotHeight}\n showXGrid={showXGrid}\n showYGrid={showYGrid}\n xGridColor={xAxisConfig?.gridLineColor}\n yGridColor={yAxisConfig?.gridLineColor}\n />\n\n {/* Clipped + zoomed data area */}\n <g clipPath={`url(#${clipId})`}>\n <g\n transform={`translate(${zoomState.offsetX}, ${zoomState.offsetY}) scale(${zoomState.scale})`}\n onMouseMove={\n zoomConfig?.enabled ? zoomState.handlePanMove : undefined\n }\n >\n {data.map((series, i) => (\n <LinePath\n key={series.id}\n series={series}\n xScale={xScale}\n yScale={yScale}\n color={\n series.color ?? getSeriesColor(i, colorPalette)\n }\n animation={animation}\n onPointClick={onPointClick}\n />\n ))}\n </g>\n </g>\n\n {/* Tooltip renders above everything in plot area */}\n {tooltipEnabled && (\n <Tooltip\n visible={tooltipState.tooltipVisible}\n x={tooltipState.tooltipX}\n y={tooltipState.tooltipY}\n point={tooltipState.activePoint}\n series={tooltipState.activeSeries}\n plotHeight={plotHeight}\n plotWidth={plotWidth}\n config={tooltipConfig}\n seriesColor={\n tooltipState.activeSeries?.color ??\n getSeriesColor(\n data.findIndex(\n (s) => s.id === tooltipState.activeSeries?.id\n ),\n colorPalette\n )\n }\n />\n )}\n </g>\n\n {/* Zoom controls render at SVG root level */}\n <ZoomControls\n config={zoomConfig}\n svgWidth={svgWidth}\n svgHeight={svgHeight}\n margin={margin}\n scale={zoomState.scale}\n onZoomIn={zoomState.zoomIn}\n onZoomOut={zoomState.zoomOut}\n onReset={zoomState.resetZoom}\n />\n </svg>\n </div>\n );\n};\n\nLineChart.displayName = \"LineChart\";\n"],"names":["DEFAULT_PALETTE","getSeriesColor","index","palette","colors","length","useChartDimensions","containerRef","providedWidth","providedHeight","dimensions","setDimensions","useState","width","height","isFixed","undefined","useLayoutEffect","node","current","measure","rect","getBoundingClientRect","observer","ResizeObserver","observe","disconnect","clamp","value","min","max","Math","niceNumber","range","round","exponent","floor","log10","fraction","pow","niceFraction","niceRange","tickCount","offset","abs","tickStep","niceMin","niceMax","ceil","linearScale","domainMin","domainMax","rangeMin","rangeMax","t","a","b","inverseLerp","lerp","generateTicks","ticks","v","push","parseFloat","toPrecision","useScales","data","plotWidth","plotHeight","xAxisConfig","yAxisConfig","useMemo","xMin","Infinity","xMax","yMin","yMax","series","point","x","y","isFinite","xTickCount","yTickCount","xNice","yNice","xScale","yScale","xTicks","yTicks","xDomain","yDomain","useZoom","config","enabled","minScale","maxScale","step","enableWheel","enablePan","scale","setScale","offsetX","setOffsetX","offsetY","setOffsetY","isPanning","setIsPanning","panStart","useRef","clampOffset","useCallback","ox","oy","s","maxOffsetY","zoomIn","prev","next","zoomOut","resetZoom","handleWheel","e","preventDefault","currentTarget","mouseX","clientX","left","mouseY","clientY","top","delta","deltaY","ratio","handlePanStart","button","handlePanMove","dx","dy","clamped","handlePanEnd","findNearestByPixelX","sorted","low","high","mid","nearest","minDist","dist","useTooltip","svgRef","marginLeft","marginTop","_config","tooltipVisible","setTooltipVisible","tooltipX","setTooltipX","tooltipY","setTooltipY","activePoint","setActivePoint","activeSeries","setActiveSeries","handleMouseMove","svg","closestPoint","closestSeries","closestDist","handleMouseLeave","defaultTickFormat","toFixed","Number","isInteger","toString","Axes","React","memo","style","axisColor","tickColor","fontFamily","fontSize","xTickFormat","tickFormat","yTickFormat","_jsxs","className","_jsx","x1","y1","x2","y2","stroke","strokeWidth","map","tick","children","textAnchor","fill","label","fontWeight","dominantBaseline","transform","displayName","GridLines","showXGrid","showYGrid","xGridColor","yGridColor","strokeDasharray","strokeOpacity","LinePath","color","animation","onPointClick","pathRef","pathLength","setPathLength","animationDone","setAnimationDone","showDots","dotRadius","d","points","first","rest","p","join","buildLinePath","pt","useEffect","getTotalLength","timer","setTimeout","duration","clearTimeout","animStyle","strokeDashoffset","transition","easing","ref","strokeLinecap","strokeLinejoin","i","cx","cy","r","cursor","onClick","id","Tooltip","visible","seriesColor","bgColor","backgroundColor","textColor","content","renderCustom","formatter","name","tx","ty","pointerEvents","opacity","tooltipHeight","overflow","padding","borderRadius","whiteSpace","lineHeight","boxShadow","ZoomControls","svgWidth","svgHeight","margin","onZoomIn","onZoomOut","onReset","showControls","position","controlsPosition","btnSize","gx","gy","bottom","right","btn","rx","DEFAULT_MARGIN","LineChart","marginOverride","xAxis","yAxis","tooltip","tooltipConfig","zoom","zoomConfig","chartStyle","colorPalette","ariaLabel","clipId","useId","zoomState","tooltipEnabled","tooltipState","gridLines","role","onMouseMove","onMouseLeave","onWheel","onMouseDown","onMouseUp","userSelect","gridLineColor","clipPath","findIndex"],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface DataPoint {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DataSeries {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
data: DataPoint[];
|
|
11
|
+
color?: string;
|
|
12
|
+
strokeWidth?: number;
|
|
13
|
+
strokeDasharray?: string;
|
|
14
|
+
showDots?: boolean;
|
|
15
|
+
dotRadius?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface AxisConfig {
|
|
18
|
+
label?: string;
|
|
19
|
+
tickCount?: number;
|
|
20
|
+
tickFormat?: (value: number) => string;
|
|
21
|
+
min?: number;
|
|
22
|
+
max?: number;
|
|
23
|
+
gridLines?: boolean;
|
|
24
|
+
gridLineColor?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface TooltipConfig {
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
backgroundColor?: string;
|
|
29
|
+
textColor?: string;
|
|
30
|
+
formatter?: (point: DataPoint, series: DataSeries) => string;
|
|
31
|
+
renderCustom?: (point: DataPoint, series: DataSeries) => ReactNode;
|
|
32
|
+
}
|
|
33
|
+
export interface ZoomConfig {
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
minScale?: number;
|
|
36
|
+
maxScale?: number;
|
|
37
|
+
step?: number;
|
|
38
|
+
showControls?: boolean;
|
|
39
|
+
controlsPosition?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
40
|
+
enableWheel?: boolean;
|
|
41
|
+
enablePan?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface ChartMargin {
|
|
44
|
+
top: number;
|
|
45
|
+
right: number;
|
|
46
|
+
bottom: number;
|
|
47
|
+
left: number;
|
|
48
|
+
}
|
|
49
|
+
export interface ChartStyle {
|
|
50
|
+
backgroundColor?: string;
|
|
51
|
+
fontFamily?: string;
|
|
52
|
+
fontSize?: number;
|
|
53
|
+
axisColor?: string;
|
|
54
|
+
tickColor?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface AnimationConfig {
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
duration?: number;
|
|
59
|
+
easing?: string;
|
|
60
|
+
}
|
|
61
|
+
export interface LineChartProps {
|
|
62
|
+
data: DataSeries[];
|
|
63
|
+
width?: number;
|
|
64
|
+
height?: number;
|
|
65
|
+
margin?: Partial<ChartMargin>;
|
|
66
|
+
xAxis?: AxisConfig;
|
|
67
|
+
yAxis?: AxisConfig;
|
|
68
|
+
tooltip?: TooltipConfig;
|
|
69
|
+
zoom?: ZoomConfig;
|
|
70
|
+
style?: ChartStyle;
|
|
71
|
+
animation?: AnimationConfig;
|
|
72
|
+
colorPalette?: string[];
|
|
73
|
+
className?: string;
|
|
74
|
+
onPointClick?: (point: DataPoint, series: DataSeries) => void;
|
|
75
|
+
ariaLabel?: string;
|
|
76
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function clamp(value: number, min: number, max: number): number;
|
|
2
|
+
export declare function lerp(a: number, b: number, t: number): number;
|
|
3
|
+
export declare function inverseLerp(a: number, b: number, value: number): number;
|
|
4
|
+
export declare function niceNumber(range: number, round: boolean): number;
|
|
5
|
+
export declare function niceRange(min: number, max: number, tickCount: number): {
|
|
6
|
+
niceMin: number;
|
|
7
|
+
niceMax: number;
|
|
8
|
+
tickStep: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function linearScale(domainMin: number, domainMax: number, rangeMin: number, rangeMax: number): (value: number) => number;
|
|
11
|
+
export declare function generateTicks(niceMin: number, niceMax: number, tickStep: number): number[];
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chartifypdf",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A zero-dependency React line chart component using pure SVG. Supports zoom, tooltip, responsive sizing, and customizable styling.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "rollup -c rollup.config.mjs",
|
|
23
|
+
"typecheck": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=19.0.0",
|
|
27
|
+
"react-dom": ">=19.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.7.0",
|
|
31
|
+
"rollup": "^4.29.0",
|
|
32
|
+
"@rollup/plugin-typescript": "^12.1.0",
|
|
33
|
+
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
34
|
+
"@rollup/plugin-commonjs": "^28.0.0",
|
|
35
|
+
"@rollup/plugin-terser": "^0.4.0",
|
|
36
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
37
|
+
"rollup-plugin-dts": "^6.1.0",
|
|
38
|
+
"tslib": "^2.8.0",
|
|
39
|
+
"@types/react": "^19.0.0",
|
|
40
|
+
"@types/react-dom": "^19.0.0",
|
|
41
|
+
"react": "^19.0.0",
|
|
42
|
+
"react-dom": "^19.0.0"
|
|
43
|
+
},
|
|
44
|
+
"keywords": [
|
|
45
|
+
"react",
|
|
46
|
+
"chart",
|
|
47
|
+
"line-chart",
|
|
48
|
+
"svg",
|
|
49
|
+
"typescript",
|
|
50
|
+
"responsive",
|
|
51
|
+
"zoom",
|
|
52
|
+
"tooltip"
|
|
53
|
+
],
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/thuankg1752/ChartifyPDF.git"
|
|
58
|
+
},
|
|
59
|
+
"author": "thuankg1752"
|
|
60
|
+
}
|