@djangocfg/ui-tools 2.1.430 → 2.1.432
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/dist/tree/index.cjs +43 -8
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +31 -1
- package/dist/tree/index.d.ts +31 -1
- package/dist/tree/index.mjs +43 -8
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +28 -14
- package/src/common/CodePanelHeader/CodePanelHeader.tsx +121 -0
- package/src/common/CodePanelHeader/index.ts +1 -0
- package/src/common/blocks/BlockError.tsx +46 -0
- package/src/common/blocks/index.ts +34 -0
- package/src/common/blocks/schemas.ts +143 -0
- package/src/common/blocks/types.ts +76 -0
- package/src/common/chips/README.md +67 -0
- package/src/common/chips/UrlChip.tsx +105 -0
- package/src/common/chips/index.ts +20 -0
- package/src/common/link-preview/LinkPreviewCard.tsx +178 -0
- package/src/common/link-preview/index.ts +12 -0
- package/src/common/link-preview/types.ts +44 -0
- package/src/common/tiptap/index.ts +10 -0
- package/src/common/tiptap/useNodeAttrs.ts +128 -0
- package/src/lib/page-snapshot/react/PageSnapshotChip.tsx +1 -1
- package/src/tools/chat/README.md +109 -2
- package/src/tools/chat/composer/Composer.tsx +45 -12
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +16 -4
- package/src/tools/chat/composer/PastedTextDialog.tsx +88 -0
- package/src/tools/chat/composer/index.ts +9 -0
- package/src/tools/chat/composer/textToAttachment.ts +69 -0
- package/src/tools/chat/composer/types.ts +13 -0
- package/src/tools/chat/composer/useComposerAttach.tsx +47 -0
- package/src/tools/chat/constants.ts +9 -2
- package/src/tools/chat/core/extractFirstUrl.ts +45 -0
- package/src/tools/chat/core/reducer.ts +32 -8
- package/src/tools/chat/core/transport/mappers/pydantic-ai.ts +1 -1
- package/src/tools/chat/core/transport/mock.ts +4 -1
- package/src/tools/chat/hooks/useChat.ts +20 -20
- package/src/tools/chat/lazy.tsx +10 -0
- package/src/tools/chat/messages/Attachments.tsx +335 -46
- package/src/tools/chat/messages/BubbleContextMenu.tsx +203 -0
- package/src/tools/chat/messages/MessageBubble.tsx +175 -31
- package/src/tools/chat/messages/MessageList.tsx +107 -21
- package/src/tools/chat/messages/ToolCalls.tsx +28 -13
- package/src/tools/chat/messages/blocks/MessageBlocks.tsx +70 -17
- package/src/tools/chat/messages/blocks/builtin.tsx +6 -3
- package/src/tools/chat/messages/blocks/renderers/AudioBlock.tsx +6 -0
- package/src/tools/chat/messages/blocks/renderers/ChatImageAlbum.tsx +154 -0
- package/src/tools/chat/messages/blocks/renderers/CodeBlock.tsx +94 -6
- package/src/tools/chat/messages/blocks/renderers/DiffBlock.tsx +29 -0
- package/src/tools/chat/messages/blocks/renderers/GalleryBlock.tsx +17 -10
- package/src/tools/chat/messages/blocks/renderers/ImageBlock.tsx +37 -16
- package/src/tools/chat/messages/blocks/renderers/JsonBlock.tsx +11 -6
- package/src/tools/chat/messages/blocks/renderers/LinkBlock.tsx +32 -0
- package/src/tools/chat/messages/blocks/renderers/MapBlock.tsx +181 -19
- package/src/tools/chat/messages/blocks/renderers/VideoBlock.tsx +2 -5
- package/src/tools/chat/messages/index.ts +13 -0
- package/src/tools/chat/public.ts +5 -1
- package/src/tools/chat/shell/ChatRoot.tsx +29 -0
- package/src/tools/chat/styles/bubbleTokens.ts +18 -0
- package/src/tools/chat/styles/index.ts +1 -0
- package/src/tools/chat/types/attachment.ts +38 -3
- package/src/tools/chat/types/block.ts +56 -9
- package/src/tools/chat/types/blockSchemas.ts +197 -0
- package/src/tools/chat/types/config.ts +33 -0
- package/src/tools/chat/types/events.ts +14 -7
- package/src/tools/chat/types/index.ts +4 -2
- package/src/tools/chat/types/message.ts +7 -0
- package/src/tools/data/JsonTree/JsonViewer.tsx +7 -7
- package/src/tools/data/Tree/README.md +30 -0
- package/src/tools/data/Tree/components/TreeRow.tsx +11 -2
- package/src/tools/data/Tree/context/menu/render.tsx +56 -27
- package/src/tools/data/Tree/data/appearance.ts +65 -3
- package/src/tools/data/Tree/data/index.ts +2 -0
- package/src/tools/data/Tree/index.tsx +2 -0
- package/src/tools/dev/Map/README.md +30 -345
- package/src/tools/dev/Map/cards/index.ts +6 -0
- package/src/tools/dev/Map/cards/types.ts +67 -0
- package/src/tools/dev/Map/components/CompassChip.tsx +67 -0
- package/src/tools/dev/Map/components/DraggableMarkers.tsx +79 -0
- package/src/tools/dev/Map/components/GeocoderControl.tsx +148 -63
- package/src/tools/dev/Map/components/GeocoderInput.tsx +181 -0
- package/src/tools/dev/Map/components/MapContainer/FullscreenDialog.tsx +140 -0
- package/src/tools/dev/Map/components/MapContainer/GeolocateHandle.tsx +70 -0
- package/src/tools/dev/Map/components/MapContainer/MapInner.tsx +326 -0
- package/src/tools/dev/Map/components/MapContainer/MapToolbar.tsx +286 -0
- package/src/tools/dev/Map/components/MapContainer/RotateHint.tsx +25 -0
- package/src/tools/dev/Map/components/MapContainer/ScrollLockOverlay.tsx +52 -0
- package/src/tools/dev/Map/components/MapContainer/constants.ts +15 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useAutoReset.ts +51 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useBasemapState.ts +48 -0
- package/src/tools/dev/Map/components/MapContainer/hooks/useExternalMapsUrl.ts +29 -0
- package/src/tools/dev/Map/components/MapContainer/index.tsx +29 -0
- package/src/tools/dev/Map/components/MapContainer/types.ts +198 -0
- package/src/tools/dev/Map/components/MapControls.tsx +27 -0
- package/src/tools/dev/Map/components/MapMarkers.tsx +94 -0
- package/src/tools/dev/Map/components/MarkerCard.tsx +102 -0
- package/src/tools/dev/Map/components/MeasureControl.tsx +219 -0
- package/src/tools/dev/Map/components/index.ts +35 -1
- package/src/tools/dev/Map/geocode/index.ts +2 -0
- package/src/tools/dev/Map/geocode/photon.ts +265 -0
- package/src/tools/dev/Map/hooks/index.ts +37 -0
- package/src/tools/dev/Map/hooks/useCompass.ts +44 -0
- package/src/tools/dev/Map/hooks/useDraggableMarkers.ts +112 -0
- package/src/tools/dev/Map/hooks/useGeocoder.ts +155 -0
- package/src/tools/dev/Map/hooks/useGeolocation.ts +174 -0
- package/src/tools/dev/Map/hooks/useMapControl.ts +5 -1
- package/src/tools/dev/Map/hooks/useMapImages.ts +112 -0
- package/src/tools/dev/Map/hooks/useMapScrollProtection.ts +155 -0
- package/src/tools/dev/Map/hooks/useMapTouchMode.ts +21 -0
- package/src/tools/dev/Map/hooks/useMarkerCard.ts +39 -0
- package/src/tools/dev/Map/hooks/useMarkers.ts +9 -3
- package/src/tools/dev/Map/hooks/useMeasure.ts +169 -0
- package/src/tools/dev/Map/hooks/useTerrain.ts +123 -0
- package/src/tools/dev/Map/index.ts +59 -2
- package/src/tools/dev/Map/layers/index.ts +1 -0
- package/src/tools/dev/Map/layers/symbol.ts +93 -0
- package/src/tools/dev/Map/lazy.tsx +120 -0
- package/src/tools/dev/Map/protocols/pmtiles.ts +73 -0
- package/src/tools/dev/Map/styles/index.ts +62 -0
- package/src/tools/dev/Map/styles/terrain.ts +29 -0
- package/src/tools/dev/Map/types.ts +31 -3
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +5 -12
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/SendButton.tsx +1 -1
- package/src/tools/dev/api/OpenapiViewer/components/shared/http-colors.ts +61 -0
- package/src/tools/dev/api/OpenapiViewer/components/shared/ui.tsx +4 -21
- package/src/tools/dev/code/MarkdownMessage/MarkdownMessage.tsx +150 -58
- package/src/tools/dev/code/MarkdownMessage/README.md +38 -0
- package/src/tools/dev/code/MarkdownMessage/builtinRules.tsx +92 -0
- package/src/tools/dev/code/MarkdownMessage/index.ts +3 -0
- package/src/tools/dev/code/MarkdownMessage/streamingBlocks.ts +164 -0
- package/src/tools/dev/code/MarkdownMessage/types.ts +20 -0
- package/src/tools/dev/code/PrettyCode/PrettyCode.client.tsx +27 -30
- package/src/tools/dev/code/PrettyCode/index.tsx +4 -1
- package/src/tools/dev/code/PrettyCode/lazy.tsx +3 -1
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +168 -3
- package/src/tools/forms/MarkdownEditor/README.md +84 -0
- package/src/tools/forms/MarkdownEditor/chip/ChipNode.ts +454 -0
- package/src/tools/forms/MarkdownEditor/chip/syncChips.ts +80 -0
- package/src/tools/forms/MarkdownEditor/filePath/FilePathChip.tsx +68 -0
- package/src/tools/forms/MarkdownEditor/filePath/__tests__/detect.test.ts +161 -0
- package/src/tools/forms/MarkdownEditor/filePath/detect.ts +289 -0
- package/src/tools/forms/MarkdownEditor/filePath/index.ts +15 -0
- package/src/tools/forms/MarkdownEditor/index.ts +19 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +10 -0
- package/src/tools/forms/MarkdownEditor/styles.css +134 -2
- package/src/tools/forms/MarkdownEditor/url/__tests__/detect.test.ts +121 -0
- package/src/tools/forms/MarkdownEditor/url/detect.ts +227 -0
- package/src/tools/forms/NotionEditor/BookmarkNode.ts +158 -0
- package/src/tools/forms/NotionEditor/BookmarkView.tsx +45 -0
- package/src/tools/forms/NotionEditor/MapEditLayer.tsx +199 -0
- package/src/tools/forms/NotionEditor/MapNode.ts +218 -0
- package/src/tools/forms/NotionEditor/MapView.tsx +103 -0
- package/src/tools/forms/NotionEditor/NotionEditor.tsx +7 -2
- package/src/tools/forms/NotionEditor/README.md +109 -188
- package/src/tools/forms/NotionEditor/extensions.ts +15 -0
- package/src/tools/forms/NotionEditor/slashItems.ts +19 -0
- package/src/tools/forms/NotionEditor/styles.css +25 -0
- package/src/tools/forms/NotionEditor/types.ts +9 -0
- package/src/tools/input/SpeechRecognition/core/engine/external.ts +5 -0
- package/src/tools/input/SpeechRecognition/types.ts +10 -0
- package/src/tools/media/Gallery/components/lightbox/GalleryLightbox.tsx +39 -22
- package/src/tools/media/ImageViewer/README.md +9 -0
- package/src/tools/media/ImageViewer/components/ImageViewer.tsx +165 -79
- package/src/tools/media/ImageViewer/components/LightboxChrome.tsx +137 -0
- package/src/tools/media/ImageViewer/components/MiddleEllipsis.tsx +46 -0
- package/src/tools/media/ImageViewer/components/index.ts +2 -0
- package/src/tools/media/ImageViewer/hooks/index.ts +3 -0
- package/src/tools/media/ImageViewer/hooks/useIdleChrome.ts +73 -0
- package/src/tools/media/ImageViewer/types.ts +10 -0
- package/src/tools/media/ImageViewer/utils/constants.ts +28 -0
- package/src/tools/media/ImageViewer/utils/index.ts +4 -0
- package/src/tools/media/VideoPlayer/VideoPlayer.tsx +34 -40
- package/src/tools/media/VideoPlayer/canvas/youtube-canvas.tsx +13 -13
- package/src/tools/media/VideoPlayer/parts/center-play.tsx +26 -0
- package/src/tools/media/VideoPlayer/parts/index.ts +2 -0
- package/src/tools/media/VideoPlayer/styles/video-player.css +102 -53
- package/src/tools/visual/charts/SmoothLine/README.md +97 -0
- package/src/tools/visual/charts/SmoothLine/SmoothLine.tsx +177 -0
- package/src/tools/visual/charts/SmoothLine/index.ts +21 -0
- package/src/tools/visual/charts/SmoothLine/lazy.tsx +23 -0
- package/src/tools/visual/charts/SmoothLine/smoothPath.ts +209 -0
- package/src/tools/visual/charts/SmoothLine/types.ts +98 -0
- package/src/tools/visual/charts/SmoothLine/useSmoothSeries.ts +429 -0
- package/src/tools/visual/indicators/Fps/types.ts +1 -1
- package/src/tools/chat/messages/blocks/renderers/LottieBlock.tsx +0 -11
- package/src/tools/dev/Map/components/MapContainer.tsx +0 -198
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SmoothLine
|
|
2
|
+
|
|
3
|
+
Animated sibling of [`Sparkline`](../Sparkline/README.md). Same pure-SVG family
|
|
4
|
+
(fixed viewBox, `preserveAspectRatio="none"`, `vectorEffect="non-scaling-stroke"`),
|
|
5
|
+
but with **two** kinds of smoothness:
|
|
6
|
+
|
|
7
|
+
1. **Curve smoothness (the line SHAPE).** The line is drawn as a flowing
|
|
8
|
+
**Catmull-Rom → cubic-bezier curve** through the points (`M … C …`), not a
|
|
9
|
+
jagged `M … L …` polyline — so it flows in gentle curves instead of kinking
|
|
10
|
+
at each data point. Controlled by `tension` (default `0.5`; `0` degenerates
|
|
11
|
+
to straight `L` segments — the disable path for charts that want SHARP
|
|
12
|
+
geometry like an ECG QRS spike). This is pure geometry: the curve is rebuilt
|
|
13
|
+
from the points each frame, so it both flows AND glides.
|
|
14
|
+
2. **Streaming smoothness (the MOTION).** With `scroll` on, the trace slides
|
|
15
|
+
left at **constant velocity** like a hospital monitor (the professional
|
|
16
|
+
D3/Smoothie model) instead of snapping the `d` each tick. We animate exactly
|
|
17
|
+
ONE thing — a `translateX` on the scroll group — and the position is a pure
|
|
18
|
+
function of the **wall clock**, so it is frame-rate independent and never
|
|
19
|
+
eased. The path `d` is rebuilt **once per data change** from the true values
|
|
20
|
+
(no per-frame Y-tween), and the newest point is appended **off-screen** behind
|
|
21
|
+
a one-interval `delay` buffer + `clipPath`, so the visible window only ever
|
|
22
|
+
scrolls into view — it never changes shape instantly.
|
|
23
|
+
|
|
24
|
+
**The velocity is driven by the MEASURED data cadence, not a hardcoded
|
|
25
|
+
`tweenMs`.** The engine records each datum's arrival time and EMAs the deltas
|
|
26
|
+
into a smoothed interval; the slide velocity is `slotWidth / measuredInterval`.
|
|
27
|
+
That is what makes a **jittery 1 Hz feed** (the Go stats ticker + Wails event
|
|
28
|
+
bus add ±100-200 ms) still glide at a steady speed: a late sample lets the
|
|
29
|
+
phase glide *past* one slot into the buffer (no freeze-then-snap), and the
|
|
30
|
+
leftover phase is carried across the commit seam (no per-tick teleport).
|
|
31
|
+
`tweenMs` is now only the **seed/clamp** for the very first slides before any
|
|
32
|
+
delta is measured — the engine adapts to the true rate within a couple ticks.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
import { SmoothLine } from '@djangocfg/ui-tools/smooth-line';
|
|
36
|
+
|
|
37
|
+
// stats charts tick ≈ 1 Hz
|
|
38
|
+
<SmoothLine values={rtt} color="info" tweenMs={1000} area />
|
|
39
|
+
|
|
40
|
+
// token bridge — inherit a text-* className tint (cmdop's text-chart-3)
|
|
41
|
+
<span className="text-chart-3">
|
|
42
|
+
<SmoothLine values={rtt} color="currentColor" />
|
|
43
|
+
</span>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Props
|
|
47
|
+
|
|
48
|
+
| Prop | Type | Default | Description |
|
|
49
|
+
|---|---|---|---|
|
|
50
|
+
| `values` | `readonly number[]` | — | Series, oldest-first. Empty → flat baseline. |
|
|
51
|
+
| `color` | `SmoothLineColor` | `'primary'` | Semantic token, or `'currentColor'` to inherit a `text-*` tint. |
|
|
52
|
+
| `width` | `number` | `240` | viewBox width (SVG stretches to its container). |
|
|
53
|
+
| `height` | `number` | `40` | viewBox height (px). |
|
|
54
|
+
| `strokeWidth` | `number` | `1.5` | Line stroke. |
|
|
55
|
+
| `area` | `boolean` | `false` | Alpha area fill under the line. |
|
|
56
|
+
| `tweenMs` | `number` | `1000` | **Seed** cadence (ms) for the scroll velocity + the clamp band — NOT a fixed slide speed. The engine measures the real inter-sample interval and drives the velocity off that. Pass the rough expected cadence (stats ≈ 1000, quality ≈ 30000). |
|
|
57
|
+
| `scroll` | `boolean` | `false` | Continuous constant-velocity left-scroll for live windowed charts. |
|
|
58
|
+
| `delay` | `boolean` | `true` | Hold the right edge back one interval (buffer the newest point off-screen) so it scrolls in cleanly instead of "grow then snap". Requires `scroll`. |
|
|
59
|
+
| `tension` | `number` | `0.5` | Curve smoothness (the line SHAPE). `0` → straight `L` segments; `0.5` → flowing curve; `1` → loosest. |
|
|
60
|
+
| `ariaLabel` | `string` | — | Accessible label. |
|
|
61
|
+
|
|
62
|
+
### Curve smoothing — for host-bespoke charts
|
|
63
|
+
|
|
64
|
+
The Catmull-Rom → bezier converter is exported so host charts that build their
|
|
65
|
+
own geometry (cmdop's TrafficChart mirror areas) can get the same flowing curve:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { smoothLinePath, smoothSegments } from '@djangocfg/ui-tools/smooth-line';
|
|
69
|
+
|
|
70
|
+
// full path through points (line stroke)
|
|
71
|
+
const d = smoothLinePath(points, 0.5, { width, height });
|
|
72
|
+
// just the C-segments (so an area fill can reuse the exact same curve)
|
|
73
|
+
const body = smoothSegments(points, 0.5, { width, height });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The **ECG keeps its bespoke SHARP builder** (sharp QRS pulses) and does NOT use
|
|
77
|
+
this — a cardiogram needs sharp pulses, not rounded blobs. It reuses only the
|
|
78
|
+
glide+scroll engine (`useSmoothSeries`), not the curve smoothing.
|
|
79
|
+
|
|
80
|
+
## `useSmoothSeries`
|
|
81
|
+
|
|
82
|
+
The rAF engine is exported as a hook so host-bespoke charts (e.g. cmdop's ECG,
|
|
83
|
+
which keeps its own QRS geometry) can reuse the same glide + scroll driver:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
const { pathRef, scrollRef, staticPath } = useSmoothSeries({
|
|
87
|
+
values, tweenMs: 1000, scroll: true, width: 240, buildPath: buildMyGeometry,
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`buildPath` receives the true values **once per data change** (not per frame) and
|
|
92
|
+
returns the `d` string; only the scroll transform updates per frame. The velocity
|
|
93
|
+
is self-measured from the data cadence (see above), so hosts don't need an exact
|
|
94
|
+
`tweenMs`. Guards (reduced-motion fallback to instant, pause on `document.hidden`)
|
|
95
|
+
are built in; `staticPath` is the initial / reduced-motion render value.
|
|
96
|
+
|
|
97
|
+
Storybook: `apps/storybook/stories/ui-tools/visual/SmoothLine.stories.tsx`
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// SmoothLine — the streaming sibling of Sparkline, now drawn on a CANVAS.
|
|
2
|
+
//
|
|
3
|
+
// The trace SCROLLS left at constant velocity via the `useSmoothSeries`
|
|
4
|
+
// wall-clock canvas engine: every rAF frame the whole monotone-cubic curve is
|
|
5
|
+
// redrawn at its scrolled X (one writer, one clock), so there is no SVG-era
|
|
6
|
+
// path/transform desync — the jump-then-catch-up is gone by construction.
|
|
7
|
+
//
|
|
8
|
+
// Geometry stays ours: the draw callback strokes a monotone-cubic (Fritsch–
|
|
9
|
+
// Carlson / d3 `curveMonotoneX`) polyline through the data points, with an
|
|
10
|
+
// optional area fill closed to the baseline. `tension={0}` degenerates to sharp
|
|
11
|
+
// straight segments.
|
|
12
|
+
//
|
|
13
|
+
// Color via the token bridge, resolved ONCE per theme change (not per frame):
|
|
14
|
+
// • a semantic token (`'primary' | 'success' | …`) → `useThemeColor()` → hex.
|
|
15
|
+
// • `'currentColor'` → the canvas inherits an ancestor's CSS `color` (a `text-*`
|
|
16
|
+
// className tint, e.g. cmdop's `text-chart-*`); we read the resolved color off
|
|
17
|
+
// the canvas element via `getComputedStyle(canvas).color` and pass it to
|
|
18
|
+
// `ctx.strokeStyle`. Re-resolved on theme change (the resolved-theme dep on
|
|
19
|
+
// `useThemeColor`'s own re-render path drives a redraw).
|
|
20
|
+
//
|
|
21
|
+
// Requires <UiProviders> mounted by the host app (no nested providers here).
|
|
22
|
+
|
|
23
|
+
'use client';
|
|
24
|
+
|
|
25
|
+
import * as React from 'react';
|
|
26
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
27
|
+
import { useThemeColor } from '@djangocfg/ui-core/styles/palette';
|
|
28
|
+
import { useResolvedTheme } from '@djangocfg/ui-core/hooks';
|
|
29
|
+
import type { SmoothLineProps } from './types';
|
|
30
|
+
import { useSmoothSeries, type DrawFn } from './useSmoothSeries';
|
|
31
|
+
import { DEFAULT_TENSION, type Pt, traceSmoothSegments } from './smoothPath';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Map oldest-first values to logical points (shared by the line + area draws).
|
|
35
|
+
*
|
|
36
|
+
* The Y-domain (min/max) is computed from the drawn values. Because the engine
|
|
37
|
+
* draws from a FIXED committed set between ticks (no per-frame Y-tween), the
|
|
38
|
+
* domain is stable across the whole scroll — the curve never rubber-bands.
|
|
39
|
+
*/
|
|
40
|
+
function valuePoints(
|
|
41
|
+
values: readonly number[],
|
|
42
|
+
width: number,
|
|
43
|
+
height: number,
|
|
44
|
+
): Pt[] {
|
|
45
|
+
const max = Math.max(...values, 1);
|
|
46
|
+
const min = Math.min(...values, 0);
|
|
47
|
+
const span = max - min || 1;
|
|
48
|
+
const stepX = values.length > 1 ? width / (values.length - 1) : width;
|
|
49
|
+
return values.map((v, i) => ({
|
|
50
|
+
x: i * stepX,
|
|
51
|
+
y: height - ((v - min) / span) * (height - 2) - 1,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function SmoothLine({
|
|
56
|
+
values,
|
|
57
|
+
color = 'primary',
|
|
58
|
+
width = 240,
|
|
59
|
+
height = 40,
|
|
60
|
+
strokeWidth = 1.5,
|
|
61
|
+
area = false,
|
|
62
|
+
tweenMs = 1000,
|
|
63
|
+
scroll = false,
|
|
64
|
+
delay = true,
|
|
65
|
+
tension = DEFAULT_TENSION,
|
|
66
|
+
ariaLabel,
|
|
67
|
+
className,
|
|
68
|
+
...rest
|
|
69
|
+
}: SmoothLineProps) {
|
|
70
|
+
const isBridge = color === 'currentColor';
|
|
71
|
+
// Semantic token → hex; bridge mode reads the canvas's inherited CSS color.
|
|
72
|
+
const themeColor = useThemeColor(isBridge ? 'primary' : color);
|
|
73
|
+
// Re-resolve the bridge color when the theme flips (dark/light).
|
|
74
|
+
const resolvedTheme = useResolvedTheme();
|
|
75
|
+
|
|
76
|
+
// With the right-edge lag, the drawn set drops the freshest point so the right
|
|
77
|
+
// edge is always a known point; the engine's scroll reveals the off-edge value.
|
|
78
|
+
const useLag = scroll && delay && values.length > 2;
|
|
79
|
+
const drawn = React.useMemo(
|
|
80
|
+
() => (useLag ? [...values].slice(0, -1) : [...values]),
|
|
81
|
+
// values identity changes per tick; the joined key gates recompute.
|
|
82
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
83
|
+
[values.join(','), useLag],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// The per-frame canvas draw: monotone-cubic stroke (+ optional area), at the
|
|
87
|
+
// scrolled X. Rebuilt when the committed values / geometry / color change — so
|
|
88
|
+
// there is no per-frame Y-tween, only the scroll moves.
|
|
89
|
+
const draw = React.useCallback<DrawFn>(
|
|
90
|
+
(ctx, frame) => {
|
|
91
|
+
// Resolve the stroke/fill color ONCE per draw (not a per-point cost). In
|
|
92
|
+
// bridge mode read the inherited CSS color off the canvas; else the token.
|
|
93
|
+
let stroke = themeColor;
|
|
94
|
+
if (isBridge && ctx.canvas) {
|
|
95
|
+
const c = getComputedStyle(ctx.canvas).color;
|
|
96
|
+
if (c) stroke = c;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const w = frame.width;
|
|
100
|
+
const h = frame.height;
|
|
101
|
+
const sx = frame.scrollX;
|
|
102
|
+
|
|
103
|
+
if (drawn.length === 0) {
|
|
104
|
+
const mid = h / 2;
|
|
105
|
+
ctx.beginPath();
|
|
106
|
+
ctx.moveTo(0, mid);
|
|
107
|
+
ctx.lineTo(w, mid);
|
|
108
|
+
ctx.strokeStyle = stroke;
|
|
109
|
+
ctx.lineWidth = strokeWidth;
|
|
110
|
+
ctx.stroke();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const pts = valuePoints(drawn, w, h).map((p) => ({
|
|
115
|
+
x: p.x + sx,
|
|
116
|
+
y: p.y,
|
|
117
|
+
}));
|
|
118
|
+
|
|
119
|
+
// Area fill first (under the stroke), hugging the same curve down to the
|
|
120
|
+
// baseline. Opacity 0.12 matches the old SVG fill.
|
|
121
|
+
if (area) {
|
|
122
|
+
ctx.beginPath();
|
|
123
|
+
ctx.moveTo(pts[0].x, h);
|
|
124
|
+
ctx.lineTo(pts[0].x, pts[0].y);
|
|
125
|
+
traceSmoothSegments(ctx, pts, tension);
|
|
126
|
+
ctx.lineTo(pts[pts.length - 1].x, h);
|
|
127
|
+
ctx.closePath();
|
|
128
|
+
ctx.globalAlpha = 0.12;
|
|
129
|
+
ctx.fillStyle = stroke;
|
|
130
|
+
ctx.fill();
|
|
131
|
+
ctx.globalAlpha = 1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Stroke the line over the fill.
|
|
135
|
+
ctx.beginPath();
|
|
136
|
+
ctx.moveTo(pts[0].x, pts[0].y);
|
|
137
|
+
traceSmoothSegments(ctx, pts, tension);
|
|
138
|
+
ctx.strokeStyle = stroke;
|
|
139
|
+
ctx.lineWidth = strokeWidth;
|
|
140
|
+
ctx.lineJoin = 'round';
|
|
141
|
+
ctx.lineCap = 'round';
|
|
142
|
+
ctx.stroke();
|
|
143
|
+
},
|
|
144
|
+
// resolvedTheme re-runs the bridge color read on a theme flip.
|
|
145
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
146
|
+
[drawn, area, tension, strokeWidth, themeColor, isBridge, resolvedTheme],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const { canvasRef } = useSmoothSeries({
|
|
150
|
+
values,
|
|
151
|
+
tweenMs,
|
|
152
|
+
scroll,
|
|
153
|
+
delay,
|
|
154
|
+
width,
|
|
155
|
+
height,
|
|
156
|
+
draw,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
data-slot="smooth-line"
|
|
162
|
+
data-color={color}
|
|
163
|
+
className={cn('block w-full', className)}
|
|
164
|
+
style={isBridge ? undefined : { color: themeColor }}
|
|
165
|
+
{...rest}
|
|
166
|
+
>
|
|
167
|
+
<canvas
|
|
168
|
+
ref={canvasRef}
|
|
169
|
+
className="block w-full"
|
|
170
|
+
style={{ height, color: isBridge ? undefined : themeColor }}
|
|
171
|
+
role="img"
|
|
172
|
+
aria-label={ariaLabel}
|
|
173
|
+
aria-hidden={ariaLabel ? undefined : true}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export { SmoothLine } from './SmoothLine';
|
|
2
|
+
export {
|
|
3
|
+
useSmoothSeries,
|
|
4
|
+
useReducedMotion,
|
|
5
|
+
type DrawFn,
|
|
6
|
+
type DrawContext,
|
|
7
|
+
type SmoothSeriesConfig,
|
|
8
|
+
type SmoothSeriesHandles,
|
|
9
|
+
} from './useSmoothSeries';
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_TENSION,
|
|
12
|
+
smoothLinePath,
|
|
13
|
+
smoothSegments,
|
|
14
|
+
traceSmoothSegments,
|
|
15
|
+
type Pt,
|
|
16
|
+
} from './smoothPath';
|
|
17
|
+
export type {
|
|
18
|
+
SmoothLineProps,
|
|
19
|
+
SmoothLineColor,
|
|
20
|
+
SmoothSeriesOptions,
|
|
21
|
+
} from './types';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createLazyComponent } from '../../../../common/lazy-wrapper';
|
|
4
|
+
import type { SmoothLineProps } from './types';
|
|
5
|
+
|
|
6
|
+
export const LazySmoothLine = createLazyComponent<SmoothLineProps>(
|
|
7
|
+
() => import('./SmoothLine').then((mod) => ({ default: mod.SmoothLine })),
|
|
8
|
+
{
|
|
9
|
+
displayName: 'LazySmoothLine',
|
|
10
|
+
fallback: (
|
|
11
|
+
<span
|
|
12
|
+
data-slot="smooth-line-skeleton"
|
|
13
|
+
className="block h-10 w-full animate-pulse rounded bg-muted"
|
|
14
|
+
/>
|
|
15
|
+
),
|
|
16
|
+
},
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export type {
|
|
20
|
+
SmoothLineProps,
|
|
21
|
+
SmoothLineColor,
|
|
22
|
+
SmoothSeriesOptions,
|
|
23
|
+
} from './types';
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// smoothPath — turn a polyline of points into a flowing SMOOTH curve.
|
|
2
|
+
//
|
|
3
|
+
// The straight-segment builders (`M x,y L x,y L x,y …`) draw the line as a
|
|
4
|
+
// jagged polyline: every data point is a sharp angle. This converts the same
|
|
5
|
+
// points into a MONOTONE-CUBIC spline (Fritsch–Carlson, == d3's curveMonotoneX)
|
|
6
|
+
// emitted as cubic-bezier segments (`M x,y C c1 c2 p …`), so the line flows in
|
|
7
|
+
// gentle curves through every point instead of kinking at each one.
|
|
8
|
+
//
|
|
9
|
+
// Why monotone-cubic and NOT Catmull-Rom (the previous curve):
|
|
10
|
+
// Catmull-Rom makes pretty curves but "often produces inappropriate OVERSHOOTS
|
|
11
|
+
// even for monotone data points" — it can shoot above a local max or below a
|
|
12
|
+
// local min, i.e. it WOBBLES between points and invents values the data never
|
|
13
|
+
// had. On a streaming chart those overshoots MOVE as the trailing control
|
|
14
|
+
// points change, adding shimmer on top of the scroll. Monotone-cubic
|
|
15
|
+
// (Fritsch–Carlson) guarantees "no overshoots beyond the given data points" —
|
|
16
|
+
// the interpolant stays within the data's own bounds, so the curve can never
|
|
17
|
+
// wobble or overshoot. It is the standard choice for data charts precisely
|
|
18
|
+
// because it doesn't fabricate between-point values. See
|
|
19
|
+
// en.wikipedia.org/wiki/Monotone_cubic_interpolation and d3-shape
|
|
20
|
+
// `curveMonotoneX`.
|
|
21
|
+
//
|
|
22
|
+
// The algorithm (2-pass Fritsch–Carlson):
|
|
23
|
+
// pass 1 — secant slopes between consecutive points, then a 3-point tangent at
|
|
24
|
+
// each interior point (the Catmull-Rom-ish average);
|
|
25
|
+
// pass 2 — clamp each tangent so it cannot exceed 3× the adjacent secant,
|
|
26
|
+
// which is exactly what enforces monotonicity (kills overshoot).
|
|
27
|
+
// The bezier control handles are then placed one third of the x-gap along each
|
|
28
|
+
// point's tangent — the standard Hermite→bezier conversion.
|
|
29
|
+
//
|
|
30
|
+
// Pure geometry: this is NOT animation. The streaming engine scrolls the whole
|
|
31
|
+
// `<g>`; this builds the curve once per data change from the true points.
|
|
32
|
+
// `tension` still exists for API compat (and `0` is the SHARP disable path), but
|
|
33
|
+
// it no longer scales overshoot — monotone tangents need no tension knob.
|
|
34
|
+
|
|
35
|
+
/** A point in viewBox space. */
|
|
36
|
+
export interface Pt {
|
|
37
|
+
x: number;
|
|
38
|
+
y: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Default smoothing tension. Retained for API compatibility — any value > 0
|
|
43
|
+
* selects the monotone-cubic curve (which has no tension parameter of its own);
|
|
44
|
+
* `0` degenerates to straight `L` segments for charts that need sharp corners
|
|
45
|
+
* (ECG QRS spikes, which keep their own bespoke builder rather than this one).
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_TENSION = 0.5;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build a smooth `d` path through `points` using monotone-cubic → cubic bezier.
|
|
51
|
+
*
|
|
52
|
+
* @param points anchor points, in draw order. The curve passes through each.
|
|
53
|
+
* @param tension `0` → straight (`L`) segments; any value > 0 → monotone curve.
|
|
54
|
+
* @param _bounds viewBox extent (kept for signature compat; monotone-cubic does
|
|
55
|
+
* not overshoot, so no clamp is needed).
|
|
56
|
+
* @returns the full `M…`-prefixed path. For the BODY only (no leading `M`), use
|
|
57
|
+
* {@link smoothSegments}.
|
|
58
|
+
*/
|
|
59
|
+
export function smoothLinePath(
|
|
60
|
+
points: readonly Pt[],
|
|
61
|
+
tension: number,
|
|
62
|
+
_bounds?: { width: number; height: number },
|
|
63
|
+
): string {
|
|
64
|
+
if (points.length === 0) return '';
|
|
65
|
+
const head = `${points[0].x.toFixed(2)},${points[0].y.toFixed(2)}`;
|
|
66
|
+
if (points.length === 1) return `M${head}`;
|
|
67
|
+
return `M${head} ${smoothSegments(points, tension)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The curve BODY (no leading `M`) — the `C …`/`L …` segments that connect the
|
|
72
|
+
* points. Split out so the area builder can reuse the exact same curve between
|
|
73
|
+
* its own `M0,baseline` open and `L…baseline Z` close (the fill must hug the
|
|
74
|
+
* same flowing line as the stroke, not a straight re-trace).
|
|
75
|
+
*
|
|
76
|
+
* @param tension `0` → straight `L` segments (disable path); > 0 → monotone.
|
|
77
|
+
* @param _bounds ignored (monotone-cubic does not overshoot) — kept for compat.
|
|
78
|
+
*/
|
|
79
|
+
export function smoothSegments(
|
|
80
|
+
points: readonly Pt[],
|
|
81
|
+
tension: number,
|
|
82
|
+
_bounds?: { width: number; height: number },
|
|
83
|
+
): string {
|
|
84
|
+
if (points.length < 2) return '';
|
|
85
|
+
|
|
86
|
+
// tension <= 0 → straight polyline (the disable path; SHARP geometry).
|
|
87
|
+
if (tension <= 0) {
|
|
88
|
+
return points
|
|
89
|
+
.slice(1)
|
|
90
|
+
.map((p) => `L${p.x.toFixed(2)},${p.y.toFixed(2)}`)
|
|
91
|
+
.join(' ');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const m = monotoneTangents(points);
|
|
95
|
+
const out: string[] = [];
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
98
|
+
const p1 = points[i];
|
|
99
|
+
const p2 = points[i + 1];
|
|
100
|
+
const dx = (p2.x - p1.x) / 3;
|
|
101
|
+
// Hermite → cubic bezier: handles sit one third of the x-gap along each
|
|
102
|
+
// endpoint's tangent. Monotone tangents guarantee no overshoot, so no clamp.
|
|
103
|
+
const c1x = p1.x + dx;
|
|
104
|
+
const c1y = p1.y + dx * m[i];
|
|
105
|
+
const c2x = p2.x - dx;
|
|
106
|
+
const c2y = p2.y - dx * m[i + 1];
|
|
107
|
+
out.push(
|
|
108
|
+
`C${c1x.toFixed(2)},${c1y.toFixed(2)} ${c2x.toFixed(2)},${c2y.toFixed(2)} ${p2.x.toFixed(2)},${p2.y.toFixed(2)}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return out.join(' ');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Trace a monotone-cubic curve through `points` onto a Canvas2D context using the
|
|
117
|
+
* SAME Fritsch–Carlson tangents as {@link smoothSegments} — the canvas analogue
|
|
118
|
+
* of emitting the `C …`/`L …` body, via `bezierCurveTo`/`lineTo`. The caller owns
|
|
119
|
+
* `beginPath()` and the opening `moveTo` (so the area filler can open at the
|
|
120
|
+
* baseline and close after); this only appends the curve through the points.
|
|
121
|
+
*
|
|
122
|
+
* @param tension `0` → straight `lineTo` segments (the SHARP disable path); any
|
|
123
|
+
* value > 0 → the monotone curve.
|
|
124
|
+
*/
|
|
125
|
+
export function traceSmoothSegments(
|
|
126
|
+
ctx: CanvasRenderingContext2D,
|
|
127
|
+
points: readonly Pt[],
|
|
128
|
+
tension: number,
|
|
129
|
+
): void {
|
|
130
|
+
if (points.length < 2) return;
|
|
131
|
+
|
|
132
|
+
// tension <= 0 → straight polyline (the disable path; SHARP geometry).
|
|
133
|
+
if (tension <= 0) {
|
|
134
|
+
for (let i = 1; i < points.length; i += 1) {
|
|
135
|
+
ctx.lineTo(points[i].x, points[i].y);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const m = monotoneTangents(points);
|
|
141
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
142
|
+
const p1 = points[i];
|
|
143
|
+
const p2 = points[i + 1];
|
|
144
|
+
const dx = (p2.x - p1.x) / 3;
|
|
145
|
+
// Hermite → cubic bezier: handles sit one third of the x-gap along each
|
|
146
|
+
// endpoint's tangent. Monotone tangents guarantee no overshoot, so no clamp.
|
|
147
|
+
ctx.bezierCurveTo(
|
|
148
|
+
p1.x + dx,
|
|
149
|
+
p1.y + dx * m[i],
|
|
150
|
+
p2.x - dx,
|
|
151
|
+
p2.y - dx * m[i + 1],
|
|
152
|
+
p2.x,
|
|
153
|
+
p2.y,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Fritsch–Carlson monotone tangents (the slope dy/dx at each point). Two passes:
|
|
160
|
+
* a 3-point initial tangent, then a clamp that enforces monotonicity so the
|
|
161
|
+
* resulting cubic cannot overshoot the data between points. Mirrors d3-shape's
|
|
162
|
+
* `curveMonotoneX`.
|
|
163
|
+
*/
|
|
164
|
+
function monotoneTangents(points: readonly Pt[]): number[] {
|
|
165
|
+
const n = points.length;
|
|
166
|
+
const tangents = new Array<number>(n).fill(0);
|
|
167
|
+
if (n < 2) return tangents;
|
|
168
|
+
|
|
169
|
+
// Secant slopes between consecutive points.
|
|
170
|
+
const secants = new Array<number>(n - 1);
|
|
171
|
+
for (let i = 0; i < n - 1; i += 1) {
|
|
172
|
+
const dx = points[i + 1].x - points[i].x;
|
|
173
|
+
secants[i] = dx === 0 ? 0 : (points[i + 1].y - points[i].y) / dx;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Pass 1 — endpoint tangents = adjacent secant; interior = average of the two
|
|
177
|
+
// neighbouring secants (the 3-point tangent).
|
|
178
|
+
tangents[0] = secants[0];
|
|
179
|
+
tangents[n - 1] = secants[n - 2];
|
|
180
|
+
for (let i = 1; i < n - 1; i += 1) {
|
|
181
|
+
tangents[i] = (secants[i - 1] + secants[i]) / 2;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Pass 2 — clamp tangents to enforce monotonicity (Fritsch–Carlson). Where a
|
|
185
|
+
// secant is flat, pin both surrounding tangents to 0 (a local extreme stays
|
|
186
|
+
// flat). Otherwise scale the tangent so (α, β) lie in the monotone circle of
|
|
187
|
+
// radius 3, which is what kills overshoot.
|
|
188
|
+
for (let i = 0; i < n - 1; i += 1) {
|
|
189
|
+
const s = secants[i];
|
|
190
|
+
if (s === 0) {
|
|
191
|
+
tangents[i] = 0;
|
|
192
|
+
tangents[i + 1] = 0;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const a = tangents[i] / s;
|
|
196
|
+
const b = tangents[i + 1] / s;
|
|
197
|
+
// If a tangent points the "wrong" way relative to the secant, flatten it.
|
|
198
|
+
if (a < 0) tangents[i] = 0;
|
|
199
|
+
if (b < 0) tangents[i + 1] = 0;
|
|
200
|
+
const h = a * a + b * b;
|
|
201
|
+
if (h > 9) {
|
|
202
|
+
const t = 3 / Math.sqrt(h);
|
|
203
|
+
tangents[i] = t * a * s;
|
|
204
|
+
tangents[i + 1] = t * b * s;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return tangents;
|
|
209
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// SmoothLine — streaming sibling of Sparkline.
|
|
2
|
+
//
|
|
3
|
+
// Drawn on a CANVAS: a single wall-clock rAF loop redraws the whole line each
|
|
4
|
+
// frame at its scrolled X (one writer, one clock — the Smoothie/uPlot model), so
|
|
5
|
+
// the trace SCROLLS left at constant velocity with no SVG-era path/transform
|
|
6
|
+
// desync. Theme-color via the same token bridge as Sparkline.
|
|
7
|
+
|
|
8
|
+
import type * as React from 'react';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Color source for the stroke / area fill.
|
|
12
|
+
*
|
|
13
|
+
* - A semantic token (`'primary' | 'success' | …`) resolves at runtime via
|
|
14
|
+
* `useThemeColor()` from `@djangocfg/ui-core/styles/palette`.
|
|
15
|
+
* - The literal `'currentColor'` is the **token bridge**: the SVG inherits the
|
|
16
|
+
* CSS `color` of an ancestor, so a host can tint via a `text-*` className
|
|
17
|
+
* (e.g. cmdop's `text-chart-3`) without going through the semantic set.
|
|
18
|
+
*/
|
|
19
|
+
export type SmoothLineColor =
|
|
20
|
+
| 'primary'
|
|
21
|
+
| 'success'
|
|
22
|
+
| 'warning'
|
|
23
|
+
| 'destructive'
|
|
24
|
+
| 'info'
|
|
25
|
+
| 'currentColor';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Options for {@link useSmoothSeries} (canvas engine). The seed cadence, the
|
|
29
|
+
* scroll toggle, and the right-edge delay.
|
|
30
|
+
*/
|
|
31
|
+
export interface SmoothSeriesOptions {
|
|
32
|
+
/**
|
|
33
|
+
* Seed cadence (ms) — the first-frame guess + clamp band for the MEASURED
|
|
34
|
+
* scroll velocity, NOT a fixed slide speed. The engine EMAs the real
|
|
35
|
+
* inter-sample interval and drives the scroll off that, so a jittery feed still
|
|
36
|
+
* glides steadily. Stats charts tick ≈ 1000; the quality series (~30 s cadence)
|
|
37
|
+
* wants a longer window. @default 1000
|
|
38
|
+
*/
|
|
39
|
+
tweenMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Continuous left-scroll: the canvas redraws the whole trace each frame at its
|
|
42
|
+
* wall-clock-scrolled X (a hospital-monitor slide) instead of teleporting. Set
|
|
43
|
+
* for live windowed charts (ECG / traffic). @default false
|
|
44
|
+
*/
|
|
45
|
+
scroll?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Hold the right edge back by one interval (Smoothie / chartjs-streaming
|
|
48
|
+
* `delay`) so the newest point is known before it scrolls into view — a
|
|
49
|
+
* continuous right edge instead of "grow then snap". The off-edge point is
|
|
50
|
+
* clipped for free by the per-frame `clearRect`. Requires `scroll`.
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
delay?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SmoothLineProps
|
|
57
|
+
extends Omit<React.ComponentProps<'div'>, 'children'> {
|
|
58
|
+
/** Series values, oldest-first. Empty renders a flat baseline. */
|
|
59
|
+
values: readonly number[];
|
|
60
|
+
/**
|
|
61
|
+
* Color token. A semantic token resolves via `useThemeColor()`; `'currentColor'`
|
|
62
|
+
* inherits the ancestor `text-*` tint (the cmdop bridge). @default 'primary'
|
|
63
|
+
*/
|
|
64
|
+
color?: SmoothLineColor;
|
|
65
|
+
/** Logical chart width (CSS px). The canvas stretches to its container. @default 240 */
|
|
66
|
+
width?: number;
|
|
67
|
+
/** Logical chart height in CSS px. @default 40 */
|
|
68
|
+
height?: number;
|
|
69
|
+
/** Line stroke width. @default 1.5 */
|
|
70
|
+
strokeWidth?: number;
|
|
71
|
+
/** Render an alpha area fill under the line. @default false */
|
|
72
|
+
area?: boolean;
|
|
73
|
+
/** Wall-clock interval per slot (ms) — the scroll cadence. @default 1000 */
|
|
74
|
+
tweenMs?: number;
|
|
75
|
+
/** Continuous constant-velocity left-scroll for live windowed data. @default false */
|
|
76
|
+
scroll?: boolean;
|
|
77
|
+
/** One-interval right-edge lag (requires `scroll`). @default true */
|
|
78
|
+
delay?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Curve smoothness (the line SHAPE, not the motion). The line is drawn as a
|
|
81
|
+
* MONOTONE-CUBIC curve (Fritsch–Carlson / d3 `curveMonotoneX`) through the
|
|
82
|
+
* points so it flows in gentle curves WITHOUT overshooting or inventing values
|
|
83
|
+
* between data points.
|
|
84
|
+
*
|
|
85
|
+
* - `0` → straight `L` segments (the angular sparkline look) — the disable
|
|
86
|
+
* path for charts that need SHARP geometry (e.g. ECG QRS spikes, which
|
|
87
|
+
* keep their own bespoke builder rather than this generic one).
|
|
88
|
+
* - any value > 0 → the monotone flowing curve (the default). Monotone-cubic
|
|
89
|
+
* has no tension parameter of its own, so values above 0 all select the
|
|
90
|
+
* same non-overshooting curve.
|
|
91
|
+
*
|
|
92
|
+
* This is pure geometry: the curve is rebuilt from the true points once per
|
|
93
|
+
* data change, then the whole trace scrolls. @default 0.5
|
|
94
|
+
*/
|
|
95
|
+
tension?: number;
|
|
96
|
+
/** Optional accessible label. */
|
|
97
|
+
ariaLabel?: string;
|
|
98
|
+
}
|