Compare commits
6 Commits
e07afe37f2
...
38e42d10bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38e42d10bb | ||
|
|
ae420246c8 | ||
|
|
7d9643bd1b | ||
|
|
99ff461d4d | ||
|
|
ce93ba456c | ||
|
|
3fc5fb8267 |
@@ -54,8 +54,8 @@
|
|||||||
### "I need to display data"
|
### "I need to display data"
|
||||||
- Key metrics → **StatCard** (with optional sparkline/trend)
|
- Key metrics → **StatCard** (with optional sparkline/trend)
|
||||||
- Tabular data → **DataTable** (sortable, paginated)
|
- Tabular data → **DataTable** (sortable, paginated)
|
||||||
- Time series → **LineChart**, **AreaChart**
|
- Time series → **ThemedChart** with `<Line>` or `<Area>`
|
||||||
- Categorical comparison → **BarChart**
|
- Categorical comparison → **ThemedChart** with `<Bar>`
|
||||||
- Inline trend → **Sparkline**
|
- Inline trend → **Sparkline**
|
||||||
- Advanced charts (treemap, radar, heatmap, pie, etc.) → **Recharts** with `rechartsTheme` (see [Charting Strategy](#charting-strategy))
|
- Advanced charts (treemap, radar, heatmap, pie, etc.) → **Recharts** with `rechartsTheme` (see [Charting Strategy](#charting-strategy))
|
||||||
- Event log → **EventFeed**
|
- Event log → **EventFeed**
|
||||||
@@ -138,7 +138,7 @@ FormField wraps any input (Input, Textarea, Select, RadioGroup, etc.)
|
|||||||
### KPI dashboard
|
### KPI dashboard
|
||||||
```
|
```
|
||||||
Row of StatCard components (each with optional Sparkline and trend)
|
Row of StatCard components (each with optional Sparkline and trend)
|
||||||
Below: charts (AreaChart, LineChart, BarChart)
|
Below: charts (ThemedChart with Line, Area, or Bar)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Master/detail management pattern
|
### Master/detail management pattern
|
||||||
@@ -175,57 +175,54 @@ StatCard strip (top, recalculates per scope)
|
|||||||
→ GroupCard grid (2-col for all, full-width for single app)
|
→ GroupCard grid (2-col for all, full-width for single app)
|
||||||
Each GroupCard: header (app name + count) + meta (TPS, routes) + instance rows
|
Each GroupCard: header (app name + count) + meta (TPS, routes) + instance rows
|
||||||
Instance rows: StatusDot + name + Badge + metrics
|
Instance rows: StatusDot + name + Badge + metrics
|
||||||
Single instance: expanded with LineChart panels
|
Single instance: expanded with ThemedChart panels
|
||||||
→ EventFeed (bottom, filtered by scope)
|
→ EventFeed (bottom, filtered by scope)
|
||||||
URL-driven progressive filtering: /agents → /agents/:appId → /agents/:appId/:instanceId
|
URL-driven progressive filtering: /agents → /agents/:appId → /agents/:appId/:instanceId
|
||||||
```
|
```
|
||||||
|
|
||||||
## Charting Strategy
|
## Charting Strategy
|
||||||
|
|
||||||
The design system includes built-in **AreaChart**, **BarChart**, **LineChart**, and **Sparkline** components for standard use cases. For advanced chart types (treemap, radar, heatmap, pie, sankey, etc.), consuming apps should use **Recharts** directly with the design system's theme preset for visual consistency.
|
The design system provides a **ThemedChart** wrapper component that applies consistent styling to Recharts charts. Recharts is bundled as a dependency — consumers do not need to install it separately.
|
||||||
|
|
||||||
**Recharts is the app's dependency, not the design system's.** The design system only exports a theme config object.
|
### Usage
|
||||||
|
|
||||||
### Setup in consuming app
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install recharts
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage with theme preset
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { rechartsTheme, CHART_COLORS } from '@cameleer/design-system'
|
import { ThemedChart, Line, Area, ReferenceLine, CHART_COLORS } from '@cameleer/design-system'
|
||||||
import {
|
|
||||||
ResponsiveContainer, LineChart, Line,
|
|
||||||
CartesianGrid, XAxis, YAxis, Tooltip, Legend,
|
|
||||||
} from 'recharts'
|
|
||||||
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
const data = metrics.map(m => ({ time: m.timestamp, cpu: m.value * 100 }))
|
||||||
<LineChart data={data}>
|
|
||||||
<CartesianGrid {...rechartsTheme.cartesianGrid} />
|
<ThemedChart data={data} height={160} xDataKey="time" yLabel="%">
|
||||||
<XAxis dataKey="name" {...rechartsTheme.xAxis} />
|
<Area dataKey="cpu" stroke={CHART_COLORS[0]} fill={CHART_COLORS[0]} fillOpacity={0.1} />
|
||||||
<YAxis {...rechartsTheme.yAxis} />
|
<ReferenceLine y={85} stroke="var(--error)" strokeDasharray="5 3" label="Alert" />
|
||||||
<Tooltip {...rechartsTheme.tooltip} />
|
</ThemedChart>
|
||||||
<Legend {...rechartsTheme.legend} />
|
|
||||||
<Line dataKey="value" stroke={CHART_COLORS[0]} strokeWidth={2} dot={false} />
|
|
||||||
<Line dataKey="other" stroke={CHART_COLORS[1]} strokeWidth={2} dot={false} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Exports
|
### ThemedChart Props
|
||||||
|
|
||||||
| Export | Description |
|
| Prop | Type | Default | Description |
|
||||||
|--------|-------------|
|
|------|------|---------|-------------|
|
||||||
| `rechartsTheme.cartesianGrid` | Dashed gridlines, subtle stroke |
|
| `data` | `Record<string, any>[]` | required | Flat array of data objects |
|
||||||
| `rechartsTheme.xAxis` | Mono font axis ticks, subtle color |
|
| `height` | `number` | `200` | Chart height in pixels |
|
||||||
| `rechartsTheme.yAxis` | Mono font axis ticks, no axis line |
|
| `xDataKey` | `string` | `"time"` | Key for x-axis values |
|
||||||
| `rechartsTheme.tooltip` | Surface bg, border, shadow, monospace values |
|
| `xType` | `'number' \| 'category'` | `"category"` | X-axis scale type |
|
||||||
| `rechartsTheme.legend` | Matching text size and color |
|
| `xTickFormatter` | `(value: any) => string` | — | Custom x-axis label formatter |
|
||||||
| `rechartsTheme.colors` | The 8 `CHART_COLORS` (CSS variables with light/dark support) |
|
| `yTickFormatter` | `(value: any) => string` | — | Custom y-axis label formatter |
|
||||||
|
| `yLabel` | `string` | — | Y-axis label text |
|
||||||
|
| `children` | `ReactNode` | required | Recharts elements (Line, Area, Bar, etc.) |
|
||||||
|
| `className` | `string` | — | Container CSS class |
|
||||||
|
|
||||||
|
### Available Recharts Re-exports
|
||||||
|
|
||||||
|
`Line`, `Area`, `Bar`, `ReferenceLine`, `ReferenceArea`, `Legend`, `Brush`
|
||||||
|
|
||||||
|
For chart types not covered (treemap, radar, pie, sankey), import from `recharts` directly and use `rechartsTheme` for consistent styling.
|
||||||
|
|
||||||
|
### Theme Utilities
|
||||||
|
|
||||||
|
| Export | Purpose |
|
||||||
|
|--------|---------|
|
||||||
| `CHART_COLORS` | Array of `var(--chart-1)` through `var(--chart-8)` |
|
| `CHART_COLORS` | Array of `var(--chart-1)` through `var(--chart-8)` |
|
||||||
| `ChartSeries` / `DataPoint` | Type interfaces for chart data |
|
| `rechartsTheme` | Pre-configured prop objects for Recharts sub-components |
|
||||||
|
|
||||||
## Component Index
|
## Component Index
|
||||||
|
|
||||||
@@ -234,11 +231,9 @@ import {
|
|||||||
| Accordion | composite | Multiple collapsible sections, single or multi-open mode |
|
| Accordion | composite | Multiple collapsible sections, single or multi-open mode |
|
||||||
| Alert | primitive | Page-level attention banner with variant colors |
|
| Alert | primitive | Page-level attention banner with variant colors |
|
||||||
| AlertDialog | composite | Confirmation dialog for destructive/important actions |
|
| AlertDialog | composite | Confirmation dialog for destructive/important actions |
|
||||||
| AreaChart | composite | Time series visualization with filled area |
|
|
||||||
| Avatar | primitive | User representation with initials and color |
|
| Avatar | primitive | User representation with initials and color |
|
||||||
| AvatarGroup | composite | Stacked overlapping avatars with overflow count |
|
| AvatarGroup | composite | Stacked overlapping avatars with overflow count |
|
||||||
| Badge | primitive | Labeled status indicator with semantic colors |
|
| Badge | primitive | Labeled status indicator with semantic colors |
|
||||||
| BarChart | composite | Categorical data comparison, optional stacking |
|
|
||||||
| Breadcrumb | composite | Navigation path showing current location |
|
| Breadcrumb | composite | Navigation path showing current location |
|
||||||
| Button | primitive | Action trigger (primary, secondary, danger, ghost) |
|
| Button | primitive | Action trigger (primary, secondary, danger, ghost) |
|
||||||
| ButtonGroup | primitive | Multi-select toggle group with optional colored dot indicators. Props: items (value, label, color?), value (Set), onChange |
|
| ButtonGroup | primitive | Multi-select toggle group with optional colored dot indicators. Props: items (value, label, color?), value (Set), onChange |
|
||||||
@@ -266,7 +261,7 @@ import {
|
|||||||
| KeyboardHint | primitive | Keyboard shortcut display |
|
| KeyboardHint | primitive | Keyboard shortcut display |
|
||||||
| KpiStrip | composite | Horizontal row of KPI cards with colored left border, trend, subtitle, optional sparkline |
|
| KpiStrip | composite | Horizontal row of KPI cards with colored left border, trend, subtitle, optional sparkline |
|
||||||
| Label | primitive | Form label with optional required asterisk |
|
| Label | primitive | Form label with optional required asterisk |
|
||||||
| LineChart | composite | Time series line visualization |
|
| ThemedChart | composite | Recharts wrapper with themed axes, grid, and tooltip |
|
||||||
| LogViewer | composite | Scrollable log output with timestamped, severity-colored monospace entries |
|
| LogViewer | composite | Scrollable log output with timestamped, severity-colored monospace entries |
|
||||||
| MenuItem | composite | Sidebar navigation item with health/count |
|
| MenuItem | composite | Sidebar navigation item with health/count |
|
||||||
| Modal | composite | Generic dialog overlay with backdrop |
|
| Modal | composite | Generic dialog overlay with backdrop |
|
||||||
|
|||||||
411
package-lock.json
generated
411
package-lock.json
generated
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "@cameleer/design-system",
|
"name": "@cameleer/design-system",
|
||||||
"version": "0.1.44",
|
"version": "0.1.46",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@cameleer/design-system",
|
"name": "@cameleer/design-system",
|
||||||
"version": "0.1.44",
|
"version": "0.1.46",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.0.0"
|
"react-router-dom": "^7.0.0",
|
||||||
|
"recharts": "^3.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.58.2",
|
"@playwright/test": "^1.58.2",
|
||||||
@@ -1012,6 +1013,42 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^11.0.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit/node_modules/immer": {
|
||||||
|
"version": "11.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz",
|
||||||
|
"integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
@@ -1519,6 +1556,18 @@
|
|||||||
"string-argv": "~0.3.1"
|
"string-argv": "~0.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@testing-library/dom": {
|
"node_modules/@testing-library/dom": {
|
||||||
"version": "10.4.1",
|
"version": "10.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
@@ -1680,6 +1729,69 @@
|
|||||||
"assertion-error": "^2.0.1"
|
"assertion-error": "^2.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||||
|
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/deep-eql": {
|
"node_modules/@types/deep-eql": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
|
||||||
@@ -1708,7 +1820,7 @@
|
|||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -1724,6 +1836,12 @@
|
|||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/whatwg-mimetype": {
|
"node_modules/@types/whatwg-mimetype": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||||
@@ -2319,6 +2437,15 @@
|
|||||||
"node": ">= 16"
|
"node": ">= 16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -2399,9 +2526,130 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/de-indent": {
|
"node_modules/de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
@@ -2427,6 +2675,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-eql": {
|
"node_modules/deep-eql": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
|
||||||
@@ -2506,6 +2760,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/es-toolkit": {
|
||||||
|
"version": "1.45.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz",
|
||||||
|
"integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"docs",
|
||||||
|
"benchmarks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@@ -2568,6 +2832,12 @@
|
|||||||
"@types/estree": "^1.0.0"
|
"@types/estree": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/expect-type": {
|
"node_modules/expect-type": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
|
||||||
@@ -2814,6 +3084,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-lazy": {
|
"node_modules/import-lazy": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz",
|
||||||
@@ -2834,6 +3114,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-core-module": {
|
"node_modules/is-core-module": {
|
||||||
"version": "2.16.1",
|
"version": "2.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
@@ -3476,10 +3765,32 @@
|
|||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@@ -3528,6 +3839,36 @@
|
|||||||
"react-dom": ">=18"
|
"react-dom": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "3.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz",
|
||||||
|
"integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"www"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "^1.9.0 || 2.x.x",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"es-toolkit": "^1.39.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"react-redux": "8.x.x || 9.x.x",
|
||||||
|
"reselect": "5.1.1",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"use-sync-external-store": "^1.2.2",
|
||||||
|
"victory-vendor": "^37.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/redent": {
|
"node_modules/redent": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||||
@@ -3542,6 +3883,21 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-from-string": {
|
"node_modules/require-from-string": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
@@ -3552,6 +3908,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.11",
|
"version": "1.22.11",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||||
@@ -3921,6 +4283,12 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-invariant": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
@@ -4051,6 +4419,37 @@
|
|||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "37.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.4.1",
|
"version": "6.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@cameleer/design-system",
|
"name": "@cameleer/design-system",
|
||||||
"version": "0.1.46",
|
"version": "0.1.47",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.es.js",
|
"main": "./dist/index.es.js",
|
||||||
"module": "./dist/index.es.js",
|
"module": "./dist/index.es.js",
|
||||||
@@ -40,7 +40,8 @@
|
|||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.0.0"
|
"react-router-dom": "^7.0.0",
|
||||||
|
"recharts": "^3.8.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
@@ -1,167 +0,0 @@
|
|||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg {
|
|
||||||
display: block;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grid */
|
|
||||||
.gridLine {
|
|
||||||
stroke: var(--border-subtle);
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke-dasharray: 3 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.axisLabel {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 9px;
|
|
||||||
fill: var(--text-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Threshold line */
|
|
||||||
.thresholdLine {
|
|
||||||
stroke: var(--error);
|
|
||||||
stroke-width: 1.5;
|
|
||||||
stroke-dasharray: 5 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholdLabel {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 9px;
|
|
||||||
fill: var(--error);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Area + line */
|
|
||||||
.area {
|
|
||||||
opacity: 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Crosshair */
|
|
||||||
.crosshair {
|
|
||||||
stroke: var(--text-faint);
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke-dasharray: 3 3;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tooltip */
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 10;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipTime {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipRow {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipRow:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipDot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipLabel {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipValue {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Legend */
|
|
||||||
.legend {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
padding-left: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendDot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendLabel {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Axis labels */
|
|
||||||
.yLabel {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
writing-mode: vertical-lr;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
left: 4px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 28px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xLabel {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
padding-left: 48px;
|
|
||||||
}
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import styles from './AreaChart.module.css'
|
|
||||||
import {
|
|
||||||
computeYScale,
|
|
||||||
computeXScale,
|
|
||||||
seriesPoints,
|
|
||||||
seriesPath,
|
|
||||||
formatAxisLabel,
|
|
||||||
CHART_COLORS,
|
|
||||||
type ChartSeries,
|
|
||||||
} from '../_chart-utils'
|
|
||||||
|
|
||||||
interface Threshold {
|
|
||||||
value: number
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AreaChartProps {
|
|
||||||
series: ChartSeries[]
|
|
||||||
xLabel?: string
|
|
||||||
yLabel?: string
|
|
||||||
threshold?: Threshold
|
|
||||||
height?: number
|
|
||||||
width?: number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Y_TICK_COUNT = 4
|
|
||||||
const DIMS = {
|
|
||||||
paddingTop: 12,
|
|
||||||
paddingRight: 16,
|
|
||||||
paddingBottom: 28,
|
|
||||||
paddingLeft: 48,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AreaChart({
|
|
||||||
series,
|
|
||||||
xLabel,
|
|
||||||
yLabel,
|
|
||||||
threshold,
|
|
||||||
height = 200,
|
|
||||||
width = 400,
|
|
||||||
className,
|
|
||||||
}: AreaChartProps) {
|
|
||||||
const [tooltip, setTooltip] = useState<{
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
xLabel: string
|
|
||||||
values: { label: string; value: number; color: string }[]
|
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
const dims = { ...DIMS, width, height }
|
|
||||||
const allData = series.flatMap((s) => s.data)
|
|
||||||
|
|
||||||
if (allData.length === 0) {
|
|
||||||
return <div className={`${styles.empty} ${className ?? ''}`}>No data</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const { max, toY } = computeYScale(series, dims, threshold?.value)
|
|
||||||
const { toX } = computeXScale(series, dims)
|
|
||||||
const plotH = height - dims.paddingTop - dims.paddingBottom
|
|
||||||
const plotW = width - dims.paddingLeft - dims.paddingRight
|
|
||||||
const bottomY = dims.paddingTop + plotH
|
|
||||||
|
|
||||||
// Y-axis ticks
|
|
||||||
const yTicks = Array.from({ length: Y_TICK_COUNT + 1 }, (_, i) =>
|
|
||||||
Math.round((max / Y_TICK_COUNT) * i),
|
|
||||||
)
|
|
||||||
|
|
||||||
// X-axis ticks (first, middle, last)
|
|
||||||
const firstSeries = series[0]
|
|
||||||
const xSamples =
|
|
||||||
firstSeries && firstSeries.data.length > 0
|
|
||||||
? [
|
|
||||||
firstSeries.data[0].x,
|
|
||||||
firstSeries.data[Math.floor(firstSeries.data.length / 2)]?.x,
|
|
||||||
firstSeries.data[firstSeries.data.length - 1].x,
|
|
||||||
].filter(Boolean)
|
|
||||||
: []
|
|
||||||
|
|
||||||
function handleMouseMove(e: React.MouseEvent<SVGSVGElement>) {
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect()
|
|
||||||
const mx = e.clientX - rect.left
|
|
||||||
const my = e.clientY - rect.top
|
|
||||||
|
|
||||||
// Find closest x value
|
|
||||||
const pctX = (mx - dims.paddingLeft) / plotW
|
|
||||||
const firstS = series[0]
|
|
||||||
const idx0 = Math.max(0, Math.min(firstS.data.length - 1, Math.round(pctX * (firstS.data.length - 1))))
|
|
||||||
const xVal = firstS.data[idx0]?.x
|
|
||||||
const xLabel = xVal instanceof Date
|
|
||||||
? xVal.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
||||||
: typeof xVal === 'number' && xVal > 1e10
|
|
||||||
? new Date(xVal).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
||||||
: String(xVal ?? '')
|
|
||||||
|
|
||||||
const values = series.map((s, i) => {
|
|
||||||
const idx = Math.round(pctX * (s.data.length - 1))
|
|
||||||
const clamped = Math.max(0, Math.min(s.data.length - 1, idx))
|
|
||||||
const pt = s.data[clamped]
|
|
||||||
return {
|
|
||||||
label: s.label,
|
|
||||||
value: pt?.y ?? 0,
|
|
||||||
color: s.color ?? CHART_COLORS[i % CHART_COLORS.length],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setTooltip({ x: mx, y: my, xLabel, values })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${styles.wrapper} ${className ?? ''}`}>
|
|
||||||
{yLabel && <div className={styles.yLabel}>{yLabel}</div>}
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height={height}
|
|
||||||
viewBox={`0 0 ${width} ${height}`}
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
className={styles.svg}
|
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
onMouseLeave={() => setTooltip(null)}
|
|
||||||
aria-label="Area chart"
|
|
||||||
role="img"
|
|
||||||
>
|
|
||||||
{/* Grid lines */}
|
|
||||||
{yTicks.map((val) => {
|
|
||||||
const y = toY(val)
|
|
||||||
return (
|
|
||||||
<g key={val}>
|
|
||||||
<line
|
|
||||||
x1={dims.paddingLeft}
|
|
||||||
y1={y}
|
|
||||||
x2={width - dims.paddingRight}
|
|
||||||
y2={y}
|
|
||||||
className={styles.gridLine}
|
|
||||||
/>
|
|
||||||
<text x={dims.paddingLeft - 6} y={y + 4} className={styles.axisLabel} textAnchor="end">
|
|
||||||
{formatAxisLabel(val)}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* X-axis labels */}
|
|
||||||
{xSamples.map((xVal, i) => {
|
|
||||||
const xPos = toX(xVal)
|
|
||||||
const xv = xVal instanceof Date ? xVal : new Date(xVal as number)
|
|
||||||
const label =
|
|
||||||
xVal instanceof Date || (typeof xVal === 'number' && xVal > 1e10)
|
|
||||||
? xv.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
||||||
: formatAxisLabel(xVal as number)
|
|
||||||
const anchor = i === 0 ? 'start' : i === xSamples.length - 1 ? 'end' : 'middle'
|
|
||||||
return (
|
|
||||||
<text
|
|
||||||
key={i}
|
|
||||||
x={xPos}
|
|
||||||
y={height - dims.paddingBottom + 16}
|
|
||||||
className={styles.axisLabel}
|
|
||||||
textAnchor={anchor}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</text>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* SLA threshold line */}
|
|
||||||
{threshold && (
|
|
||||||
<g>
|
|
||||||
<line
|
|
||||||
x1={dims.paddingLeft}
|
|
||||||
y1={toY(threshold.value)}
|
|
||||||
x2={width - dims.paddingRight}
|
|
||||||
y2={toY(threshold.value)}
|
|
||||||
className={styles.thresholdLine}
|
|
||||||
/>
|
|
||||||
<text
|
|
||||||
x={width - dims.paddingRight - 4}
|
|
||||||
y={toY(threshold.value) - 4}
|
|
||||||
className={styles.thresholdLabel}
|
|
||||||
textAnchor="end"
|
|
||||||
>
|
|
||||||
{threshold.label}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Area fills */}
|
|
||||||
{series.map((s, i) => {
|
|
||||||
const color = s.color ?? CHART_COLORS[i % CHART_COLORS.length]
|
|
||||||
const areaD = seriesPath(s, toX, toY, bottomY)
|
|
||||||
return (
|
|
||||||
<path
|
|
||||||
key={`area-${i}`}
|
|
||||||
d={areaD}
|
|
||||||
fill={color}
|
|
||||||
className={styles.area}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Lines */}
|
|
||||||
{series.map((s, i) => {
|
|
||||||
const color = s.color ?? CHART_COLORS[i % CHART_COLORS.length]
|
|
||||||
const pts = seriesPoints(s, toX, toY)
|
|
||||||
return (
|
|
||||||
<polyline
|
|
||||||
key={`line-${i}`}
|
|
||||||
points={pts}
|
|
||||||
fill="none"
|
|
||||||
stroke={color}
|
|
||||||
className={styles.line}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Crosshair */}
|
|
||||||
{tooltip && (
|
|
||||||
<line
|
|
||||||
x1={tooltip.x}
|
|
||||||
y1={dims.paddingTop}
|
|
||||||
x2={tooltip.x}
|
|
||||||
y2={bottomY}
|
|
||||||
className={styles.crosshair}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{/* Tooltip */}
|
|
||||||
{tooltip && (
|
|
||||||
<div
|
|
||||||
className={styles.tooltip}
|
|
||||||
style={{
|
|
||||||
left: tooltip.x + 12,
|
|
||||||
top: tooltip.y,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tooltip.xLabel && (
|
|
||||||
<div className={styles.tooltipTime}>{tooltip.xLabel}</div>
|
|
||||||
)}
|
|
||||||
{tooltip.values.map((v) => (
|
|
||||||
<div key={v.label} className={styles.tooltipRow}>
|
|
||||||
<span className={styles.tooltipDot} style={{ background: v.color }} />
|
|
||||||
<span className={styles.tooltipLabel}>{v.label}:</span>
|
|
||||||
<span className={styles.tooltipValue}>{formatAxisLabel(v.value)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Legend */}
|
|
||||||
{series.length > 1 && (
|
|
||||||
<div className={styles.legend}>
|
|
||||||
{series.map((s, i) => (
|
|
||||||
<div key={s.label} className={styles.legendItem}>
|
|
||||||
<span
|
|
||||||
className={styles.legendDot}
|
|
||||||
style={{ background: s.color ?? CHART_COLORS[i % CHART_COLORS.length] }}
|
|
||||||
/>
|
|
||||||
<span className={styles.legendLabel}>{s.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{xLabel && <div className={styles.xLabel}>{xLabel}</div>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg {
|
|
||||||
display: block;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gridLine {
|
|
||||||
stroke: var(--border-subtle);
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke-dasharray: 3 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.axisLabel {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 9px;
|
|
||||||
fill: var(--text-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.catLabel {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 9px;
|
|
||||||
fill: var(--text-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar {
|
|
||||||
cursor: pointer;
|
|
||||||
transition: opacity 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bar:hover {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 10;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipTitle {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipRow {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipRow:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipDot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipLabel {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipValue {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
padding-left: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendDot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendLabel {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yLabel {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
writing-mode: vertical-lr;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
left: 4px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xLabel {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
padding-left: 48px;
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import styles from './BarChart.module.css'
|
|
||||||
import { formatAxisLabel, CHART_COLORS } from '../_chart-utils'
|
|
||||||
|
|
||||||
interface BarSeries {
|
|
||||||
label: string
|
|
||||||
data: { x: string; y: number }[]
|
|
||||||
color?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BarChartProps {
|
|
||||||
series: BarSeries[]
|
|
||||||
stacked?: boolean
|
|
||||||
height?: number
|
|
||||||
width?: number
|
|
||||||
xLabel?: string
|
|
||||||
yLabel?: string
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const PADDING = { top: 12, right: 16, bottom: 40, left: 48 }
|
|
||||||
const Y_TICK_COUNT = 4
|
|
||||||
const BAR_GAP = 0.2 // fraction of bar group width reserved for gaps
|
|
||||||
|
|
||||||
export function BarChart({
|
|
||||||
series,
|
|
||||||
stacked = false,
|
|
||||||
height = 200,
|
|
||||||
width = 400,
|
|
||||||
xLabel,
|
|
||||||
yLabel,
|
|
||||||
className,
|
|
||||||
}: BarChartProps) {
|
|
||||||
const [tooltip, setTooltip] = useState<{
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
label: string
|
|
||||||
values: { series: string; value: number; color: string }[]
|
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
if (series.length === 0 || series[0].data.length === 0) {
|
|
||||||
return <div className={`${styles.empty} ${className ?? ''}`}>No data</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all x categories (union across all series)
|
|
||||||
const categories = Array.from(new Set(series.flatMap((s) => s.data.map((d) => d.x))))
|
|
||||||
const numCats = categories.length
|
|
||||||
|
|
||||||
const plotW = width - PADDING.left - PADDING.right
|
|
||||||
const plotH = height - PADDING.top - PADDING.bottom
|
|
||||||
|
|
||||||
// Compute max Y
|
|
||||||
let maxY = 0
|
|
||||||
if (stacked) {
|
|
||||||
for (const cat of categories) {
|
|
||||||
const sum = series.reduce((acc, s) => {
|
|
||||||
const pt = s.data.find((d) => d.x === cat)
|
|
||||||
return acc + (pt?.y ?? 0)
|
|
||||||
}, 0)
|
|
||||||
maxY = Math.max(maxY, sum)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
maxY = Math.max(...series.flatMap((s) => s.data.map((d) => d.y)))
|
|
||||||
}
|
|
||||||
maxY = maxY || 1
|
|
||||||
|
|
||||||
const yTicks = Array.from({ length: Y_TICK_COUNT + 1 }, (_, i) =>
|
|
||||||
Math.round((maxY / Y_TICK_COUNT) * i),
|
|
||||||
)
|
|
||||||
const toY = (val: number) => PADDING.top + plotH - (val / maxY) * plotH
|
|
||||||
const bottomY = PADDING.top + plotH
|
|
||||||
|
|
||||||
const catWidth = plotW / numCats
|
|
||||||
const groupGap = catWidth * BAR_GAP
|
|
||||||
const groupW = catWidth - groupGap
|
|
||||||
|
|
||||||
function handleMouseEnter(
|
|
||||||
catLabel: string,
|
|
||||||
mx: number,
|
|
||||||
my: number,
|
|
||||||
values: { series: string; value: number; color: string }[],
|
|
||||||
) {
|
|
||||||
setTooltip({ x: mx, y: my, label: catLabel, values })
|
|
||||||
}
|
|
||||||
|
|
||||||
function showBarTooltip(e: React.MouseEvent<SVGRectElement>, cat: string) {
|
|
||||||
const rect = e.currentTarget.closest('svg')!.getBoundingClientRect()
|
|
||||||
handleMouseEnter(
|
|
||||||
cat,
|
|
||||||
e.clientX - rect.left,
|
|
||||||
e.clientY - rect.top,
|
|
||||||
series.map((ss, ssi) => ({
|
|
||||||
series: ss.label,
|
|
||||||
value: ss.data.find((d) => d.x === cat)?.y ?? 0,
|
|
||||||
color: ss.color ?? CHART_COLORS[ssi % CHART_COLORS.length],
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${styles.wrapper} ${className ?? ''}`}>
|
|
||||||
{yLabel && <div className={styles.yLabel}>{yLabel}</div>}
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height={height}
|
|
||||||
viewBox={`0 0 ${width} ${height}`}
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
className={styles.svg}
|
|
||||||
onMouseLeave={() => setTooltip(null)}
|
|
||||||
aria-label="Bar chart"
|
|
||||||
role="img"
|
|
||||||
>
|
|
||||||
{/* Grid lines */}
|
|
||||||
{yTicks.map((val) => {
|
|
||||||
const y = toY(val)
|
|
||||||
return (
|
|
||||||
<g key={val}>
|
|
||||||
<line
|
|
||||||
x1={PADDING.left}
|
|
||||||
y1={y}
|
|
||||||
x2={width - PADDING.right}
|
|
||||||
y2={y}
|
|
||||||
className={styles.gridLine}
|
|
||||||
/>
|
|
||||||
<text x={PADDING.left - 6} y={y + 4} className={styles.axisLabel} textAnchor="end">
|
|
||||||
{formatAxisLabel(val)}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Bars */}
|
|
||||||
{categories.map((cat, ci) => {
|
|
||||||
const groupX = PADDING.left + ci * catWidth + groupGap / 2
|
|
||||||
|
|
||||||
if (stacked) {
|
|
||||||
let stackY = bottomY
|
|
||||||
return (
|
|
||||||
<g key={cat}>
|
|
||||||
{series.map((s, si) => {
|
|
||||||
const pt = s.data.find((d) => d.x === cat)
|
|
||||||
const val = pt?.y ?? 0
|
|
||||||
const barH = (val / maxY) * plotH
|
|
||||||
const color = s.color ?? CHART_COLORS[si % CHART_COLORS.length]
|
|
||||||
const y = stackY - barH
|
|
||||||
stackY -= barH
|
|
||||||
return (
|
|
||||||
<rect
|
|
||||||
key={si}
|
|
||||||
x={groupX}
|
|
||||||
y={y}
|
|
||||||
width={groupW}
|
|
||||||
height={barH}
|
|
||||||
fill={color}
|
|
||||||
className={styles.bar}
|
|
||||||
onMouseEnter={(e) => showBarTooltip(e, cat)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<text
|
|
||||||
x={groupX + groupW / 2}
|
|
||||||
y={bottomY + 14}
|
|
||||||
className={styles.catLabel}
|
|
||||||
textAnchor="middle"
|
|
||||||
>
|
|
||||||
{cat}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grouped
|
|
||||||
const barW = groupW / series.length
|
|
||||||
return (
|
|
||||||
<g key={cat}>
|
|
||||||
{series.map((s, si) => {
|
|
||||||
const pt = s.data.find((d) => d.x === cat)
|
|
||||||
const val = pt?.y ?? 0
|
|
||||||
const barH = (val / maxY) * plotH
|
|
||||||
const color = s.color ?? CHART_COLORS[si % CHART_COLORS.length]
|
|
||||||
return (
|
|
||||||
<rect
|
|
||||||
key={si}
|
|
||||||
x={groupX + si * barW}
|
|
||||||
y={toY(val)}
|
|
||||||
width={barW - 1}
|
|
||||||
height={barH}
|
|
||||||
fill={color}
|
|
||||||
className={styles.bar}
|
|
||||||
onMouseEnter={(e) => showBarTooltip(e, cat)}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<text
|
|
||||||
x={groupX + groupW / 2}
|
|
||||||
y={bottomY + 14}
|
|
||||||
className={styles.catLabel}
|
|
||||||
textAnchor="middle"
|
|
||||||
>
|
|
||||||
{cat}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{/* Tooltip */}
|
|
||||||
{tooltip && (
|
|
||||||
<div
|
|
||||||
className={styles.tooltip}
|
|
||||||
style={{ left: tooltip.x + 12, top: tooltip.y }}
|
|
||||||
>
|
|
||||||
<div className={styles.tooltipTitle}>{tooltip.label}</div>
|
|
||||||
{tooltip.values.map((v) => (
|
|
||||||
<div key={v.series} className={styles.tooltipRow}>
|
|
||||||
<span className={styles.tooltipDot} style={{ background: v.color }} />
|
|
||||||
<span className={styles.tooltipLabel}>{v.series}:</span>
|
|
||||||
<span className={styles.tooltipValue}>{formatAxisLabel(v.value)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Legend */}
|
|
||||||
{series.length > 1 && (
|
|
||||||
<div className={styles.legend}>
|
|
||||||
{series.map((s, i) => (
|
|
||||||
<div key={s.label} className={styles.legendItem}>
|
|
||||||
<span
|
|
||||||
className={styles.legendDot}
|
|
||||||
style={{ background: s.color ?? CHART_COLORS[i % CHART_COLORS.length] }}
|
|
||||||
/>
|
|
||||||
<span className={styles.legendLabel}>{s.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{xLabel && <div className={styles.xLabel}>{xLabel}</div>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
.wrapper {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg {
|
|
||||||
display: block;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 12px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gridLine {
|
|
||||||
stroke: var(--border-subtle);
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke-dasharray: 3 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.axisLabel {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 9px;
|
|
||||||
fill: var(--text-faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholdLine {
|
|
||||||
stroke: var(--error);
|
|
||||||
stroke-width: 1.5;
|
|
||||||
stroke-dasharray: 5 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thresholdLabel {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 9px;
|
|
||||||
fill: var(--error);
|
|
||||||
}
|
|
||||||
|
|
||||||
.line {
|
|
||||||
stroke-width: 2;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
}
|
|
||||||
|
|
||||||
.crosshair {
|
|
||||||
stroke: var(--text-faint);
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke-dasharray: 3 3;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
background: var(--bg-surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
padding: 6px 10px;
|
|
||||||
font-size: 12px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 10;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipTime {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
padding-bottom: 3px;
|
|
||||||
border-bottom: 1px solid var(--border-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipRow {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipRow:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipDot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipLabel {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltipValue {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
padding-left: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendItem {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendDot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendLabel {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yLabel {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
writing-mode: vertical-lr;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
left: 4px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 28px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xLabel {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
padding-left: 48px;
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
import { useState } from 'react'
|
|
||||||
import styles from './LineChart.module.css'
|
|
||||||
import {
|
|
||||||
computeYScale,
|
|
||||||
computeXScale,
|
|
||||||
seriesPoints,
|
|
||||||
formatAxisLabel,
|
|
||||||
CHART_COLORS,
|
|
||||||
type ChartSeries,
|
|
||||||
} from '../_chart-utils'
|
|
||||||
|
|
||||||
interface Threshold {
|
|
||||||
value: number
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LineChartProps {
|
|
||||||
series: ChartSeries[]
|
|
||||||
xLabel?: string
|
|
||||||
yLabel?: string
|
|
||||||
threshold?: Threshold
|
|
||||||
height?: number
|
|
||||||
width?: number
|
|
||||||
className?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const Y_TICK_COUNT = 4
|
|
||||||
const DIMS = {
|
|
||||||
paddingTop: 12,
|
|
||||||
paddingRight: 16,
|
|
||||||
paddingBottom: 28,
|
|
||||||
paddingLeft: 48,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LineChart({
|
|
||||||
series,
|
|
||||||
xLabel,
|
|
||||||
yLabel,
|
|
||||||
threshold,
|
|
||||||
height = 200,
|
|
||||||
width = 400,
|
|
||||||
className,
|
|
||||||
}: LineChartProps) {
|
|
||||||
const [tooltip, setTooltip] = useState<{
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
xLabel: string
|
|
||||||
values: { label: string; value: number; color: string }[]
|
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
const dims = { ...DIMS, width, height }
|
|
||||||
const allData = series.flatMap((s) => s.data)
|
|
||||||
|
|
||||||
if (allData.length === 0) {
|
|
||||||
return <div className={`${styles.empty} ${className ?? ''}`}>No data</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const { max, toY } = computeYScale(series, dims, threshold?.value)
|
|
||||||
const { toX } = computeXScale(series, dims)
|
|
||||||
const plotH = height - dims.paddingTop - dims.paddingBottom
|
|
||||||
const plotW = width - dims.paddingLeft - dims.paddingRight
|
|
||||||
const bottomY = dims.paddingTop + plotH
|
|
||||||
|
|
||||||
const yTicks = Array.from({ length: Y_TICK_COUNT + 1 }, (_, i) =>
|
|
||||||
Math.round((max / Y_TICK_COUNT) * i),
|
|
||||||
)
|
|
||||||
|
|
||||||
const firstSeries = series[0]
|
|
||||||
const xSamples =
|
|
||||||
firstSeries && firstSeries.data.length > 0
|
|
||||||
? [
|
|
||||||
firstSeries.data[0].x,
|
|
||||||
firstSeries.data[Math.floor(firstSeries.data.length / 2)]?.x,
|
|
||||||
firstSeries.data[firstSeries.data.length - 1].x,
|
|
||||||
].filter(Boolean)
|
|
||||||
: []
|
|
||||||
|
|
||||||
function handleMouseMove(e: React.MouseEvent<SVGSVGElement>) {
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect()
|
|
||||||
const mx = e.clientX - rect.left
|
|
||||||
const my = e.clientY - rect.top
|
|
||||||
const pctX = (mx - dims.paddingLeft) / plotW
|
|
||||||
|
|
||||||
const firstS = series[0]
|
|
||||||
const idx0 = Math.max(0, Math.min(firstS.data.length - 1, Math.round(pctX * (firstS.data.length - 1))))
|
|
||||||
const xVal = firstS.data[idx0]?.x
|
|
||||||
const xLabel = xVal instanceof Date
|
|
||||||
? xVal.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
||||||
: typeof xVal === 'number' && xVal > 1e10
|
|
||||||
? new Date(xVal).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
||||||
: String(xVal ?? '')
|
|
||||||
|
|
||||||
const values = series.map((s, i) => {
|
|
||||||
const idx = Math.round(pctX * (s.data.length - 1))
|
|
||||||
const clamped = Math.max(0, Math.min(s.data.length - 1, idx))
|
|
||||||
const pt = s.data[clamped]
|
|
||||||
return {
|
|
||||||
label: s.label,
|
|
||||||
value: pt?.y ?? 0,
|
|
||||||
color: s.color ?? CHART_COLORS[i % CHART_COLORS.length],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setTooltip({ x: mx, y: my, xLabel, values })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${styles.wrapper} ${className ?? ''}`}>
|
|
||||||
{yLabel && <div className={styles.yLabel}>{yLabel}</div>}
|
|
||||||
<svg
|
|
||||||
width="100%"
|
|
||||||
height={height}
|
|
||||||
viewBox={`0 0 ${width} ${height}`}
|
|
||||||
preserveAspectRatio="none"
|
|
||||||
className={styles.svg}
|
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
onMouseLeave={() => setTooltip(null)}
|
|
||||||
aria-label="Line chart"
|
|
||||||
role="img"
|
|
||||||
>
|
|
||||||
{/* Grid lines */}
|
|
||||||
{yTicks.map((val) => {
|
|
||||||
const y = toY(val)
|
|
||||||
return (
|
|
||||||
<g key={val}>
|
|
||||||
<line
|
|
||||||
x1={dims.paddingLeft}
|
|
||||||
y1={y}
|
|
||||||
x2={width - dims.paddingRight}
|
|
||||||
y2={y}
|
|
||||||
className={styles.gridLine}
|
|
||||||
/>
|
|
||||||
<text x={dims.paddingLeft - 6} y={y + 4} className={styles.axisLabel} textAnchor="end">
|
|
||||||
{formatAxisLabel(val)}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* X-axis labels */}
|
|
||||||
{xSamples.map((xVal, i) => {
|
|
||||||
const xPos = toX(xVal)
|
|
||||||
const xv = xVal instanceof Date ? xVal : new Date(xVal as number)
|
|
||||||
const label =
|
|
||||||
xVal instanceof Date || (typeof xVal === 'number' && xVal > 1e10)
|
|
||||||
? xv.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
||||||
: formatAxisLabel(xVal as number)
|
|
||||||
const anchor = i === 0 ? 'start' : i === xSamples.length - 1 ? 'end' : 'middle'
|
|
||||||
return (
|
|
||||||
<text
|
|
||||||
key={i}
|
|
||||||
x={xPos}
|
|
||||||
y={height - dims.paddingBottom + 16}
|
|
||||||
className={styles.axisLabel}
|
|
||||||
textAnchor={anchor}
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
</text>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Threshold line */}
|
|
||||||
{threshold && (
|
|
||||||
<g>
|
|
||||||
<line
|
|
||||||
x1={dims.paddingLeft}
|
|
||||||
y1={toY(threshold.value)}
|
|
||||||
x2={width - dims.paddingRight}
|
|
||||||
y2={toY(threshold.value)}
|
|
||||||
className={styles.thresholdLine}
|
|
||||||
/>
|
|
||||||
<text
|
|
||||||
x={width - dims.paddingRight - 4}
|
|
||||||
y={toY(threshold.value) - 4}
|
|
||||||
className={styles.thresholdLabel}
|
|
||||||
textAnchor="end"
|
|
||||||
>
|
|
||||||
{threshold.label}
|
|
||||||
</text>
|
|
||||||
</g>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Lines only (no area fill) */}
|
|
||||||
{series.map((s, i) => {
|
|
||||||
const color = s.color ?? CHART_COLORS[i % CHART_COLORS.length]
|
|
||||||
const pts = seriesPoints(s, toX, toY)
|
|
||||||
return (
|
|
||||||
<polyline
|
|
||||||
key={`line-${i}`}
|
|
||||||
points={pts}
|
|
||||||
fill="none"
|
|
||||||
stroke={color}
|
|
||||||
className={styles.line}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Crosshair */}
|
|
||||||
{tooltip && (
|
|
||||||
<line
|
|
||||||
x1={tooltip.x}
|
|
||||||
y1={dims.paddingTop}
|
|
||||||
x2={tooltip.x}
|
|
||||||
y2={bottomY}
|
|
||||||
className={styles.crosshair}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{/* Tooltip */}
|
|
||||||
{tooltip && (
|
|
||||||
<div
|
|
||||||
className={styles.tooltip}
|
|
||||||
style={{ left: tooltip.x + 12, top: tooltip.y }}
|
|
||||||
>
|
|
||||||
{tooltip.xLabel && (
|
|
||||||
<div className={styles.tooltipTime}>{tooltip.xLabel}</div>
|
|
||||||
)}
|
|
||||||
{tooltip.values.map((v) => (
|
|
||||||
<div key={v.label} className={styles.tooltipRow}>
|
|
||||||
<span className={styles.tooltipDot} style={{ background: v.color }} />
|
|
||||||
<span className={styles.tooltipLabel}>{v.label}:</span>
|
|
||||||
<span className={styles.tooltipValue}>{formatAxisLabel(v.value)}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Legend */}
|
|
||||||
{series.length > 1 && (
|
|
||||||
<div className={styles.legend}>
|
|
||||||
{series.map((s, i) => (
|
|
||||||
<div key={s.label} className={styles.legendItem}>
|
|
||||||
<span
|
|
||||||
className={styles.legendDot}
|
|
||||||
style={{ background: s.color ?? CHART_COLORS[i % CHART_COLORS.length] }}
|
|
||||||
/>
|
|
||||||
<span className={styles.legendLabel}>{s.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{xLabel && <div className={styles.xLabel}>{xLabel}</div>}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.tooltip {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
41
src/design-system/composites/ThemedChart/ChartTooltip.tsx
Normal file
41
src/design-system/composites/ThemedChart/ChartTooltip.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { TooltipProps } from 'recharts'
|
||||||
|
import styles from './ChartTooltip.module.css'
|
||||||
|
|
||||||
|
function formatValue(val: number): string {
|
||||||
|
if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(1)}M`
|
||||||
|
if (val >= 1000) return `${(val / 1000).toFixed(1)}k`
|
||||||
|
if (Number.isInteger(val)) return String(val)
|
||||||
|
return val.toFixed(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(val: unknown): string | null {
|
||||||
|
if (val == null) return null
|
||||||
|
const str = String(val)
|
||||||
|
const ms = typeof val === 'number' && val > 1e12 ? val
|
||||||
|
: typeof val === 'number' && val > 1e9 ? val * 1000
|
||||||
|
: Date.parse(str)
|
||||||
|
if (isNaN(ms)) return str
|
||||||
|
return new Date(ms).toLocaleString([], {
|
||||||
|
month: 'short', day: 'numeric',
|
||||||
|
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChartTooltip({ active, payload, label }: TooltipProps<number, string>) {
|
||||||
|
if (!active || !payload?.length) return null
|
||||||
|
|
||||||
|
const timeLabel = formatTimestamp(label)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.tooltip}>
|
||||||
|
{timeLabel && <div className={styles.time}>{timeLabel}</div>}
|
||||||
|
{payload.map((entry) => (
|
||||||
|
<div key={entry.dataKey as string} className={styles.row}>
|
||||||
|
<span className={styles.dot} style={{ background: entry.color }} />
|
||||||
|
<span className={styles.label}>{entry.name}:</span>
|
||||||
|
<span className={styles.value}>{formatValue(entry.value as number)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import { ThemedChart } from './ThemedChart'
|
||||||
|
import { Line } from 'recharts'
|
||||||
|
|
||||||
|
// Recharts uses ResizeObserver internally
|
||||||
|
class ResizeObserverMock {
|
||||||
|
observe() {}
|
||||||
|
unobserve() {}
|
||||||
|
disconnect() {}
|
||||||
|
}
|
||||||
|
globalThis.ResizeObserver = ResizeObserverMock as any
|
||||||
|
|
||||||
|
describe('ThemedChart', () => {
|
||||||
|
it('renders nothing when data is empty', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<ThemedChart data={[]}>
|
||||||
|
<Line dataKey="value" />
|
||||||
|
</ThemedChart>,
|
||||||
|
)
|
||||||
|
expect(container.innerHTML).toBe('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a chart container when data is provided', () => {
|
||||||
|
const data = [
|
||||||
|
{ time: '10:00', value: 10 },
|
||||||
|
{ time: '10:01', value: 20 },
|
||||||
|
]
|
||||||
|
const { container } = render(
|
||||||
|
<ThemedChart data={data} height={160}>
|
||||||
|
<Line dataKey="value" />
|
||||||
|
</ThemedChart>,
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.recharts-responsive-container')).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies custom className', () => {
|
||||||
|
const data = [{ time: '10:00', value: 5 }]
|
||||||
|
const { container } = render(
|
||||||
|
<ThemedChart data={data} className="my-chart">
|
||||||
|
<Line dataKey="value" />
|
||||||
|
</ThemedChart>,
|
||||||
|
)
|
||||||
|
expect(container.querySelector('.my-chart')).toBeTruthy()
|
||||||
|
})
|
||||||
|
})
|
||||||
66
src/design-system/composites/ThemedChart/ThemedChart.tsx
Normal file
66
src/design-system/composites/ThemedChart/ThemedChart.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
ResponsiveContainer,
|
||||||
|
ComposedChart,
|
||||||
|
CartesianGrid,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
Tooltip,
|
||||||
|
} from 'recharts'
|
||||||
|
import { rechartsTheme } from '../../utils/rechartsTheme'
|
||||||
|
import { ChartTooltip } from './ChartTooltip'
|
||||||
|
|
||||||
|
interface ThemedChartProps {
|
||||||
|
data: Record<string, any>[]
|
||||||
|
height?: number
|
||||||
|
xDataKey?: string
|
||||||
|
xType?: 'number' | 'category'
|
||||||
|
xTickFormatter?: (value: any) => string
|
||||||
|
yTickFormatter?: (value: any) => string
|
||||||
|
yLabel?: string
|
||||||
|
children: React.ReactNode
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemedChart({
|
||||||
|
data,
|
||||||
|
height = 200,
|
||||||
|
xDataKey = 'time',
|
||||||
|
xType = 'category',
|
||||||
|
xTickFormatter,
|
||||||
|
yTickFormatter,
|
||||||
|
yLabel,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: ThemedChartProps) {
|
||||||
|
if (!data.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} style={{ width: '100%', height }}>
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<ComposedChart data={data} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>
|
||||||
|
<CartesianGrid {...rechartsTheme.cartesianGrid} />
|
||||||
|
<XAxis
|
||||||
|
dataKey={xDataKey}
|
||||||
|
type={xType}
|
||||||
|
{...rechartsTheme.xAxis}
|
||||||
|
tickFormatter={xTickFormatter}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
{...rechartsTheme.yAxis}
|
||||||
|
tickFormatter={yTickFormatter}
|
||||||
|
label={yLabel ? {
|
||||||
|
value: yLabel,
|
||||||
|
angle: -90,
|
||||||
|
position: 'insideLeft',
|
||||||
|
style: { fontSize: 11, fill: 'var(--text-muted)' },
|
||||||
|
} : undefined}
|
||||||
|
/>
|
||||||
|
<Tooltip content={<ChartTooltip />} cursor={rechartsTheme.tooltip.cursor} />
|
||||||
|
{children}
|
||||||
|
</ComposedChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
export interface DataPoint {
|
|
||||||
x: number | Date
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChartSeries {
|
|
||||||
label: string
|
|
||||||
data: DataPoint[]
|
|
||||||
color?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChartDimensions {
|
|
||||||
width: number
|
|
||||||
height: number
|
|
||||||
paddingTop: number
|
|
||||||
paddingRight: number
|
|
||||||
paddingBottom: number
|
|
||||||
paddingLeft: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeYScale(
|
|
||||||
series: ChartSeries[],
|
|
||||||
dims: ChartDimensions,
|
|
||||||
threshold?: number,
|
|
||||||
) {
|
|
||||||
const allY = series.flatMap((s) => s.data.map((d) => d.y))
|
|
||||||
if (threshold != null) allY.push(threshold)
|
|
||||||
const min = 0
|
|
||||||
const max = Math.max(...allY, 1)
|
|
||||||
const range = max - min
|
|
||||||
|
|
||||||
const plotH = dims.height - dims.paddingTop - dims.paddingBottom
|
|
||||||
const toY = (val: number) => dims.paddingTop + plotH - ((val - min) / range) * plotH
|
|
||||||
|
|
||||||
return { min, max, range, toY }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computeXScale(series: ChartSeries[], dims: ChartDimensions) {
|
|
||||||
const allX = series.flatMap((s) =>
|
|
||||||
s.data.map((d) => (d.x instanceof Date ? d.x.getTime() : d.x)),
|
|
||||||
)
|
|
||||||
const minX = Math.min(...allX)
|
|
||||||
const maxX = Math.max(...allX)
|
|
||||||
const rangeX = maxX - minX || 1
|
|
||||||
|
|
||||||
const plotW = dims.width - dims.paddingLeft - dims.paddingRight
|
|
||||||
const toX = (val: number | Date) => {
|
|
||||||
const v = val instanceof Date ? val.getTime() : val
|
|
||||||
return dims.paddingLeft + ((v - minX) / rangeX) * plotW
|
|
||||||
}
|
|
||||||
|
|
||||||
return { minX, maxX, rangeX, toX }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function seriesPoints(
|
|
||||||
series: ChartSeries,
|
|
||||||
toX: (v: number | Date) => number,
|
|
||||||
toY: (v: number) => number,
|
|
||||||
): string {
|
|
||||||
return series.data.map((d) => `${toX(d.x).toFixed(1)},${toY(d.y).toFixed(1)}`).join(' ')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function seriesPath(
|
|
||||||
series: ChartSeries,
|
|
||||||
toX: (v: number | Date) => number,
|
|
||||||
toY: (v: number) => number,
|
|
||||||
bottomY: number,
|
|
||||||
): string {
|
|
||||||
if (series.data.length === 0) return ''
|
|
||||||
const pts = series.data.map((d) => `${toX(d.x).toFixed(1)},${toY(d.y).toFixed(1)}`)
|
|
||||||
const firstX = toX(series.data[0].x).toFixed(1)
|
|
||||||
const lastX = toX(series.data[series.data.length - 1].x).toFixed(1)
|
|
||||||
return `M${pts.join(' L')} L${lastX},${bottomY} L${firstX},${bottomY} Z`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatAxisLabel(val: number): string {
|
|
||||||
if (val >= 1_000_000) return `${(val / 1_000_000).toFixed(1)}M`
|
|
||||||
if (val >= 1000) return `${(val / 1000).toFixed(1)}k`
|
|
||||||
if (Number.isInteger(val)) return String(val)
|
|
||||||
return val.toFixed(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatXLabel(val: number | Date, _totalPoints: number): string {
|
|
||||||
if (val instanceof Date || (typeof val === 'number' && val > 1e10)) {
|
|
||||||
const d = val instanceof Date ? val : new Date(val)
|
|
||||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
||||||
}
|
|
||||||
return formatAxisLabel(val as number)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default color tokens for series (fallback when no color given)
|
|
||||||
export const CHART_COLORS = [
|
|
||||||
'var(--chart-1)',
|
|
||||||
'var(--chart-2)',
|
|
||||||
'var(--chart-3)',
|
|
||||||
'var(--chart-4)',
|
|
||||||
'var(--chart-5)',
|
|
||||||
'var(--chart-6)',
|
|
||||||
'var(--chart-7)',
|
|
||||||
'var(--chart-8)',
|
|
||||||
]
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
export { Accordion } from './Accordion/Accordion'
|
export { Accordion } from './Accordion/Accordion'
|
||||||
export { AlertDialog } from './AlertDialog/AlertDialog'
|
export { AlertDialog } from './AlertDialog/AlertDialog'
|
||||||
export { AreaChart } from './AreaChart/AreaChart'
|
|
||||||
export { AvatarGroup } from './AvatarGroup/AvatarGroup'
|
export { AvatarGroup } from './AvatarGroup/AvatarGroup'
|
||||||
export { BarChart } from './BarChart/BarChart'
|
|
||||||
export { Breadcrumb } from './Breadcrumb/Breadcrumb'
|
export { Breadcrumb } from './Breadcrumb/Breadcrumb'
|
||||||
export { CommandPalette } from './CommandPalette/CommandPalette'
|
export { CommandPalette } from './CommandPalette/CommandPalette'
|
||||||
export type { SearchResult, SearchCategory, ScopeFilter } from './CommandPalette/types'
|
export type { SearchResult, SearchCategory, ScopeFilter } from './CommandPalette/types'
|
||||||
@@ -20,7 +18,6 @@ export { KpiStrip } from './KpiStrip/KpiStrip'
|
|||||||
export type { KpiItem, KpiStripProps } from './KpiStrip/KpiStrip'
|
export type { KpiItem, KpiStripProps } from './KpiStrip/KpiStrip'
|
||||||
export type { FeedEvent } from './EventFeed/EventFeed'
|
export type { FeedEvent } from './EventFeed/EventFeed'
|
||||||
export { FilterBar } from './FilterBar/FilterBar'
|
export { FilterBar } from './FilterBar/FilterBar'
|
||||||
export { LineChart } from './LineChart/LineChart'
|
|
||||||
export { LogViewer } from './LogViewer/LogViewer'
|
export { LogViewer } from './LogViewer/LogViewer'
|
||||||
export type { LogEntry, LogViewerProps } from './LogViewer/LogViewer'
|
export type { LogEntry, LogViewerProps } from './LogViewer/LogViewer'
|
||||||
export { LoginDialog } from './LoginForm/LoginDialog'
|
export { LoginDialog } from './LoginForm/LoginDialog'
|
||||||
@@ -43,6 +40,11 @@ export { Tabs } from './Tabs/Tabs'
|
|||||||
export { ToastProvider, useToast } from './Toast/Toast'
|
export { ToastProvider, useToast } from './Toast/Toast'
|
||||||
export { TreeView } from './TreeView/TreeView'
|
export { TreeView } from './TreeView/TreeView'
|
||||||
|
|
||||||
// Chart utilities for consumers using Recharts or custom charts
|
// Charts — ThemedChart wrapper + Recharts re-exports
|
||||||
export { CHART_COLORS } from './_chart-utils'
|
export { ThemedChart } from './ThemedChart/ThemedChart'
|
||||||
export type { ChartSeries, DataPoint } from './_chart-utils'
|
export { CHART_COLORS, rechartsTheme } from '../utils/rechartsTheme'
|
||||||
|
export {
|
||||||
|
Line, Area, Bar,
|
||||||
|
ReferenceLine, ReferenceArea,
|
||||||
|
Legend, Brush,
|
||||||
|
} from 'recharts'
|
||||||
|
|||||||
@@ -11,4 +11,4 @@ export { BreadcrumbProvider, useBreadcrumb } from './providers/BreadcrumbProvide
|
|||||||
export type { BreadcrumbItem } from './providers/BreadcrumbProvider'
|
export type { BreadcrumbItem } from './providers/BreadcrumbProvider'
|
||||||
export * from './utils/hashColor'
|
export * from './utils/hashColor'
|
||||||
export * from './utils/timePresets'
|
export * from './utils/timePresets'
|
||||||
export * from './utils/rechartsTheme'
|
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
import { CHART_COLORS } from '../composites/_chart-utils'
|
export const CHART_COLORS = [
|
||||||
|
'var(--chart-1)',
|
||||||
|
'var(--chart-2)',
|
||||||
|
'var(--chart-3)',
|
||||||
|
'var(--chart-4)',
|
||||||
|
'var(--chart-5)',
|
||||||
|
'var(--chart-6)',
|
||||||
|
'var(--chart-7)',
|
||||||
|
'var(--chart-8)',
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pre-configured Recharts prop objects that match the design system's
|
* Pre-configured Recharts prop objects that match the design system's
|
||||||
* chart styling. Spread these onto Recharts sub-components:
|
* chart styling. Used internally by ThemedChart and available for
|
||||||
*
|
* consumers composing Recharts directly.
|
||||||
* ```tsx
|
|
||||||
* import { rechartsTheme, CHART_COLORS } from '@cameleer/design-system'
|
|
||||||
* import { LineChart, Line, CartesianGrid, XAxis, YAxis, Tooltip, Legend } from 'recharts'
|
|
||||||
*
|
|
||||||
* <LineChart data={data}>
|
|
||||||
* <CartesianGrid {...rechartsTheme.cartesianGrid} />
|
|
||||||
* <XAxis dataKey="name" {...rechartsTheme.xAxis} />
|
|
||||||
* <YAxis {...rechartsTheme.yAxis} />
|
|
||||||
* <Tooltip {...rechartsTheme.tooltip} />
|
|
||||||
* <Legend {...rechartsTheme.legend} />
|
|
||||||
* <Line stroke={CHART_COLORS[0]} strokeWidth={2} dot={false} />
|
|
||||||
* </LineChart>
|
|
||||||
* ```
|
|
||||||
*/
|
*/
|
||||||
export const rechartsTheme = {
|
export const rechartsTheme = {
|
||||||
colors: CHART_COLORS,
|
colors: CHART_COLORS,
|
||||||
|
|||||||
Reference in New Issue
Block a user