@juspay/svelte-ui-components 4.27.6 → 4.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -36
- package/dist/Animations/ModalAnimation.svelte +15 -1
- package/dist/Animations/OverlayAnimation.svelte +7 -1
- package/dist/AreaChart/AreaChart.svelte +366 -85
- package/dist/AspectRatio/AspectRatio.svelte +51 -0
- package/dist/AspectRatio/AspectRatio.svelte.d.ts +3 -0
- package/dist/AspectRatio/properties.d.ts +27 -0
- package/dist/AspectRatio/properties.js +1 -0
- package/dist/Banner/Banner.svelte +10 -4
- package/dist/Banner/properties.d.ts +8 -0
- package/dist/BarChart/BarChart.svelte +156 -49
- package/dist/Book/Book.svelte +14 -1
- package/dist/BrandLoader/BrandLoader.svelte +76 -12
- package/dist/Button/Button.svelte +56 -11
- package/dist/Calendar/Calendar.svelte +88 -61
- package/dist/Card/Card.svelte +1 -1
- package/dist/Carousel/Carousel.svelte +6 -3
- package/dist/ChatComposer/ChatComposer.svelte +54 -4
- package/dist/ChatComposer/dictationState.d.ts +15 -0
- package/dist/ChatComposer/dictationState.js +9 -0
- package/dist/ChatComposer/properties.d.ts +28 -1
- package/dist/ChatMessage/ChatMessage.svelte +27 -1
- package/dist/CheckListItem/CheckListItem.svelte +3 -3
- package/dist/Checkbox/Checkbox.svelte +84 -3
- package/dist/Checkbox/properties.d.ts +45 -13
- package/dist/ChipInput/ChipInput.svelte +70 -3
- package/dist/ChipInput/properties.d.ts +16 -0
- package/dist/Choicebox/Choicebox.svelte +156 -1
- package/dist/Choicebox/group.d.ts +63 -0
- package/dist/Choicebox/group.js +127 -0
- package/dist/Choicebox/properties.d.ts +46 -0
- package/dist/ColorPicker/ColorPicker.svelte +169 -20
- package/dist/ColorPicker/properties.d.ts +16 -0
- package/dist/Combobox/Combobox.svelte +87 -25
- package/dist/Combobox/properties.d.ts +23 -0
- package/dist/CommandMenu/CommandMenu.svelte +27 -11
- package/dist/ContextMenu/ContextMenu.svelte +31 -37
- package/dist/DateRangePicker/DateRangePicker.svelte +171 -73
- package/dist/DeltaIndicator/DeltaIndicator.svelte +3 -3
- package/dist/Draggable/Draggable.svelte +12 -7
- package/dist/DualAxisBarChart/DualAxisBarChart.svelte +11 -4
- package/dist/EmptyState/EmptyState.svelte +1 -1
- package/dist/FileDropzoneTrigger/FileDropzoneTrigger.svelte +1 -1
- package/dist/FileInput/FileInput.svelte +50 -1
- package/dist/FileInput/properties.d.ts +7 -0
- package/dist/FunnelChart/FunnelChart.svelte +40 -12
- package/dist/FunnelChart/properties.d.ts +7 -1
- package/dist/Gallery/Gallery.svelte +20 -13
- package/dist/Gallery/properties.d.ts +8 -0
- package/dist/GridItem/GridItem.svelte +31 -1
- package/dist/HITL/HITL.svelte +35 -9
- package/dist/Icon/Icon.svelte +19 -3
- package/dist/Input/Input.svelte +24 -10
- package/dist/Input/properties.d.ts +37 -1
- package/dist/InputButton/InputButton.svelte +86 -11
- package/dist/InputButton/properties.d.ts +10 -1
- package/dist/Label/Label.svelte +54 -0
- package/dist/Label/Label.svelte.d.ts +3 -0
- package/dist/Label/properties.d.ts +26 -0
- package/dist/Label/properties.js +1 -0
- package/dist/LineChart/LineChart.svelte +385 -94
- package/dist/ListItem/ListItem.svelte +121 -15
- package/dist/Loader/Loader.svelte +22 -2
- package/dist/LoadingDots/LoadingDots.svelte +21 -0
- package/dist/MediaUpload/MediaUpload.svelte +13 -3
- package/dist/Menu/Menu.svelte +56 -28
- package/dist/Modal/Modal.svelte +77 -42
- package/dist/Pagination/Pagination.svelte +9 -2
- package/dist/Phone/Phone.svelte +4 -0
- package/dist/PieChart/PieChart.svelte +282 -38
- package/dist/PieChart/properties.d.ts +9 -0
- package/dist/Pill/Pill.svelte +14 -5
- package/dist/Progress/Progress.svelte +20 -0
- package/dist/Radio/Radio.svelte +58 -1
- package/dist/Radio/properties.d.ts +23 -0
- package/dist/RatingGroup/RatingGroup.svelte +283 -0
- package/dist/RatingGroup/RatingGroup.svelte.d.ts +4 -0
- package/dist/RatingGroup/properties.d.ts +73 -0
- package/dist/RatingGroup/properties.js +1 -0
- package/dist/RelativeTime/RelativeTime.svelte +1 -1
- package/dist/SankeyChart/SankeyChart.svelte +272 -50
- package/dist/SankeyChart/properties.d.ts +9 -0
- package/dist/Scroller/Scroller.svelte +37 -2
- package/dist/Select/Select.svelte +166 -46
- package/dist/Select/properties.d.ts +7 -0
- package/dist/Separator/Separator.svelte +48 -0
- package/dist/Separator/Separator.svelte.d.ts +3 -0
- package/dist/Separator/properties.d.ts +22 -0
- package/dist/Separator/properties.js +1 -0
- package/dist/Sheet/Sheet.svelte +60 -21
- package/dist/Shimmer/Shimmer.svelte +15 -0
- package/dist/Slider/Slider.svelte +87 -6
- package/dist/Slider/properties.d.ts +26 -0
- package/dist/Snippet/Snippet.svelte +4 -0
- package/dist/Snippet/copyState.svelte.d.ts +13 -1
- package/dist/Snippet/copyState.svelte.js +27 -1
- package/dist/Snippet/properties.d.ts +7 -0
- package/dist/SoundKit/SoundKit.js +9 -8
- package/dist/SplitInput/SplitInput.svelte +68 -2
- package/dist/SplitInput/properties.d.ts +23 -0
- package/dist/StatCard/StatCard.svelte +6 -6
- package/dist/Status/Status.svelte +2 -2
- package/dist/Stepper/Step.svelte +19 -6
- package/dist/Stepper/Stepper.svelte +108 -35
- package/dist/Table/BuiltinCell.svelte +2 -2
- package/dist/Table/Table.svelte +373 -55
- package/dist/Table/properties.d.ts +134 -3
- package/dist/Tabs/Tabs.svelte +263 -65
- package/dist/Tabs/properties.d.ts +23 -0
- package/dist/TaskList/TaskList.svelte +14 -5
- package/dist/ThemeSwitcher/ThemeSwitcher.svelte +23 -0
- package/dist/ThinkingIndicator/ThinkingIndicator.svelte +13 -5
- package/dist/Toast/Toast.svelte +27 -2
- package/dist/Toggle/Toggle.svelte +79 -7
- package/dist/Toggle/Toggle.svelte.d.ts +1 -1
- package/dist/Toggle/properties.d.ts +30 -7
- package/dist/ToolCallLog/ToolCallLog.svelte +59 -21
- package/dist/ToolCallLog/properties.d.ts +10 -0
- package/dist/Toolbar/Toolbar.svelte +1 -1
- package/dist/Tooltip/Tooltip.svelte +20 -2
- package/dist/Tooltip/tooltip-action.js +39 -4
- package/dist/TypewriterText/TypewriterText.svelte +34 -2
- package/dist/TypewriterText/properties.d.ts +18 -5
- package/dist/_chart/ChartContainer.svelte +12 -0
- package/dist/_chart/ChartTooltip.svelte +18 -4
- package/dist/_chart/Legend.svelte +21 -0
- package/dist/_chart/Legend.svelte.d.ts +15 -0
- package/dist/_chart/geometry.d.ts +54 -1
- package/dist/_chart/geometry.js +122 -14
- package/dist/_chart/interactions.d.ts +5 -1
- package/dist/_chart/interactions.js +9 -5
- package/dist/_chart/scales.js +16 -0
- package/dist/_chart/temporal.d.ts +45 -0
- package/dist/_chart/temporal.js +116 -0
- package/dist/_field/description.d.ts +50 -0
- package/dist/_field/description.js +51 -0
- package/dist/_interaction/dismissal.d.ts +67 -0
- package/dist/_interaction/dismissal.js +137 -0
- package/dist/_interaction/focus.d.ts +73 -0
- package/dist/_interaction/focus.js +143 -0
- package/dist/assets/star.svg +5 -0
- package/dist/index.d.ts +13 -1
- package/dist/index.js +21 -1
- package/dist/reduced-motion.svelte.d.ts +37 -0
- package/dist/reduced-motion.svelte.js +44 -0
- package/dist/styles/theme-dark.css +778 -0
- package/dist/types.d.ts +4 -2
- package/dist/utils.d.ts +62 -1
- package/dist/utils.js +80 -21
- package/dist-codemod/{bin.js → codemod/bin.js} +13 -2
- package/dist-codemod/{cli.js → codemod/cli.js} +48 -3
- package/dist-codemod/codemod/migrate-audit.js +395 -0
- package/dist-codemod/codemod/migration-assets/legacy-display.css +120 -0
- package/dist-codemod/codemod/migration-assets/legacy-palette.css +106 -0
- package/dist-codemod/codemod/migration-assets/wc-display.json +492 -0
- package/dist-codemod/migrate/analyze.js +804 -0
- package/dist-codemod/migrate/chart-tooltip-selector.js +301 -0
- package/dist-wc/index.js +13992 -4323
- package/package.json +13 -6
- package/dist/Modal/focus-trap.d.ts +0 -42
- package/dist/Modal/focus-trap.js +0 -57
- /package/dist-codemod/{legacy-pairs.js → codemod/legacy-pairs.js} +0 -0
- /package/dist-codemod/{transform.js → codemod/transform.js} +0 -0
- /package/dist-codemod/{wc-children.js → codemod/wc-children.js} +0 -0
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Attaches hover and focus listeners to the host element without injecting any wrapper
|
|
5
5
|
* div, which prevents flex-child sizing breakage inside toolbars and icon rows.
|
|
6
|
-
* The tooltip bubble is mounted directly on `document.body
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
6
|
+
* The tooltip bubble is mounted directly on the host node's own root — `document.body`,
|
|
7
|
+
* or the shadow root when the host is inside one (see `portalTarget`) — with
|
|
8
|
+
* `position:fixed` coordinates derived from `getBoundingClientRect`, so it is never
|
|
9
|
+
* clipped by `overflow:hidden` ancestors.
|
|
9
10
|
*
|
|
10
11
|
* Usage:
|
|
11
12
|
* ```svelte
|
|
@@ -17,6 +18,40 @@
|
|
|
17
18
|
*/
|
|
18
19
|
/** Monotonically-incrementing counter used to generate unique tooltip bubble IDs. */
|
|
19
20
|
let tooltipIdCounter = 0;
|
|
21
|
+
/**
|
|
22
|
+
* Where the bubble is allowed to land, asked of the HOST node rather than the
|
|
23
|
+
* bubble: a freshly created, still-detached element is its own root, so it can
|
|
24
|
+
* answer nothing useful about where the component lives.
|
|
25
|
+
*
|
|
26
|
+
* This action has no in-place fallback to fall back to -- not injecting a wrapper
|
|
27
|
+
* div is its entire reason to exist -- so the bubble is portalled either way, and
|
|
28
|
+
* the only question is where. `document.body` is the wrong answer from inside a
|
|
29
|
+
* `<sui-*>` element three times over: the `--tooltip-*` custom properties the
|
|
30
|
+
* inline styles below read inherit from the shadow host, not from `<body>`, so a
|
|
31
|
+
* themed host reverts to the literal fallbacks; `options.classes` names a class
|
|
32
|
+
* whose stylesheet is in the shadow root the bubble just left; and
|
|
33
|
+
* `aria-describedby` is an IDREF, which does not cross a shadow boundary at all,
|
|
34
|
+
* so the association the ARIA tooltip pattern requires simply does not exist.
|
|
35
|
+
* The node's own root keeps all three.
|
|
36
|
+
*
|
|
37
|
+
* Both the `ShadowRoot` constructor and `getRootNode` are checked for rather
|
|
38
|
+
* than assumed, which the five component copies of this helper do not need to
|
|
39
|
+
* do. Unlike them, this module is exercised outside a DOM: tooltip-action.test.ts
|
|
40
|
+
* declares `@vitest-environment node` and drives the action with a hand-rolled
|
|
41
|
+
* element stub carrying only the handful of methods it uses, so `ShadowRoot` is
|
|
42
|
+
* not a global there and the stub has no `getRootNode`. Either way the answer is
|
|
43
|
+
* the same one the pre-existing code gave -- a node that cannot say where it
|
|
44
|
+
* lives is not in a shadow root as far as this can tell. Duck-typing over an
|
|
45
|
+
* `instanceof` check is the idiom `_interaction/focus.ts` already sets for
|
|
46
|
+
* exactly this reason.
|
|
47
|
+
*/
|
|
48
|
+
const portalTarget = (node) => {
|
|
49
|
+
if (typeof ShadowRoot === 'undefined' || typeof node.getRootNode !== 'function') {
|
|
50
|
+
return document.body;
|
|
51
|
+
}
|
|
52
|
+
const root = node.getRootNode();
|
|
53
|
+
return root instanceof ShadowRoot ? root : document.body;
|
|
54
|
+
};
|
|
20
55
|
export const tooltip = (node, options) => {
|
|
21
56
|
let currentOptions = { ...options };
|
|
22
57
|
let bubbleEl = null;
|
|
@@ -84,7 +119,7 @@ export const tooltip = (node, options) => {
|
|
|
84
119
|
textNode.textContent = currentOptions.text;
|
|
85
120
|
bubbleEl.appendChild(arrowEl);
|
|
86
121
|
bubbleEl.appendChild(textNode);
|
|
87
|
-
|
|
122
|
+
portalTarget(node).appendChild(bubbleEl);
|
|
88
123
|
};
|
|
89
124
|
/**
|
|
90
125
|
* Compute and apply `top`/`left` fixed coordinates plus arrow styles based on the
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
text,
|
|
8
8
|
speed = 15,
|
|
9
9
|
isStreaming = false,
|
|
10
|
+
markdown = false,
|
|
11
|
+
markdownOptions,
|
|
10
12
|
renderText,
|
|
11
13
|
variableDelay,
|
|
12
14
|
resolveDelay,
|
|
@@ -16,6 +18,23 @@
|
|
|
16
18
|
classes
|
|
17
19
|
}: TypewriterTextProperties = $props();
|
|
18
20
|
|
|
21
|
+
// Keep the promise stable across streamed prop updates so the await block does
|
|
22
|
+
// not flash escaped source between already-rendered Markdown frames.
|
|
23
|
+
let rendererPromise: Promise<typeof import('../MarkdownText/markdown')> | null = null;
|
|
24
|
+
function loadMarkdownRenderer(): Promise<typeof import('../MarkdownText/markdown')> {
|
|
25
|
+
rendererPromise ??= import('../MarkdownText/markdown');
|
|
26
|
+
return rendererPromise;
|
|
27
|
+
}
|
|
28
|
+
const markdownRenderer = $derived(markdown === true ? loadMarkdownRenderer() : null);
|
|
29
|
+
// Object-valued custom-element props may receive JSON null or other non-objects.
|
|
30
|
+
const safeMarkdownOptions = $derived(
|
|
31
|
+
typeof markdownOptions === 'object' &&
|
|
32
|
+
markdownOptions !== null &&
|
|
33
|
+
!Array.isArray(markdownOptions)
|
|
34
|
+
? markdownOptions
|
|
35
|
+
: {}
|
|
36
|
+
);
|
|
37
|
+
|
|
19
38
|
let displayedText = $state('');
|
|
20
39
|
let currentIndex = $state(0);
|
|
21
40
|
let timeoutId = $state<ReturnType<typeof setTimeout> | null>(null);
|
|
@@ -182,11 +201,24 @@
|
|
|
182
201
|
|
|
183
202
|
<div
|
|
184
203
|
class="typewriter-text {classes ?? ''}"
|
|
185
|
-
class:plain={typeof renderText !== 'function'}
|
|
204
|
+
class:plain={markdown !== true && typeof renderText !== 'function'}
|
|
186
205
|
data-pw={typeof testId === 'string' ? testId : null}
|
|
187
206
|
testID={typeof testId === 'string' ? testId : null}
|
|
188
207
|
>
|
|
189
|
-
{#if
|
|
208
|
+
{#if markdown === true}
|
|
209
|
+
{#await markdownRenderer}
|
|
210
|
+
{displayedText}
|
|
211
|
+
{:then renderer}
|
|
212
|
+
{#if renderer !== null}
|
|
213
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
214
|
+
{@html renderer.renderMarkdown(displayedText, safeMarkdownOptions)}
|
|
215
|
+
{:else}
|
|
216
|
+
{displayedText}
|
|
217
|
+
{/if}
|
|
218
|
+
{:catch}
|
|
219
|
+
{displayedText}
|
|
220
|
+
{/await}
|
|
221
|
+
{:else if typeof renderText === 'function'}
|
|
190
222
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
191
223
|
{@html renderText(displayedText)}
|
|
192
224
|
{:else if renderCharacter}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { RenderMarkdownOptions } from '../MarkdownText/properties';
|
|
2
3
|
export type TypewriterTextProperties = OptionalTypewriterTextProperties & MandatoryTypewriterTextProperties;
|
|
3
4
|
export type MandatoryTypewriterTextProperties = {
|
|
4
5
|
text: string;
|
|
@@ -74,9 +75,21 @@ export type OptionalTypewriterTextProperties = {
|
|
|
74
75
|
*/
|
|
75
76
|
isStreaming?: boolean;
|
|
76
77
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
78
|
+
* Render every revealed prefix through the library's safe Markdown pipeline.
|
|
79
|
+
* Raw HTML is escaped and unsafe link/image protocols are stripped. Defaults to
|
|
80
|
+
* false; when true, takes precedence over both renderText and renderCharacter.
|
|
81
|
+
*/
|
|
82
|
+
markdown?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Options for safe Markdown rendering, including narrower sanitization policies.
|
|
85
|
+
* Only used when markdown is true; never applied to a trusted renderText callback.
|
|
86
|
+
*/
|
|
87
|
+
markdownOptions?: RenderMarkdownOptions;
|
|
88
|
+
/**
|
|
89
|
+
* Trusted custom-HTML escape hatch. The caller owns the safety of every returned
|
|
90
|
+
* string, including incomplete streaming prefixes; sanitize untrusted input inside
|
|
91
|
+
* the callback. markdownOptions do not apply here. Ignored when markdown is true;
|
|
92
|
+
* otherwise takes precedence over renderCharacter and escaped plain text.
|
|
80
93
|
*/
|
|
81
94
|
renderText?: (text: string) => string;
|
|
82
95
|
/**
|
|
@@ -108,8 +121,8 @@ export type OptionalTypewriterTextProperties = {
|
|
|
108
121
|
onprogress?: (progress: TypewriterProgress) => void;
|
|
109
122
|
/**
|
|
110
123
|
* Render each revealed character yourself — highlight a token, wrap a number — instead
|
|
111
|
-
* of the plain text node. Ignored when
|
|
112
|
-
*
|
|
124
|
+
* of the plain text node. Ignored when markdown is true or renderText is set, since
|
|
125
|
+
* those renderers own the full markup. Falls back to plain text when omitted.
|
|
113
126
|
*/
|
|
114
127
|
renderCharacter?: Snippet<[TypewriterCharacterContext]>;
|
|
115
128
|
testId?: string;
|
|
@@ -87,6 +87,18 @@
|
|
|
87
87
|
<style>
|
|
88
88
|
.chart-container {
|
|
89
89
|
width: 100%;
|
|
90
|
+
/* Floor against the silent zero-collapse bug: a flex/grid ancestor that
|
|
91
|
+
sizes itself by shrink-to-fit/fit-content resolves this div's
|
|
92
|
+
`width: 100%` as an indefinite percentage and contributes 0 to that
|
|
93
|
+
calculation, so the ancestor -- and this div -- settle at 0px with no
|
|
94
|
+
signal, and the `{#if width > 0 && height > 0}` guard below then
|
|
95
|
+
renders nothing at all. A definite min-width isn't subject to that;
|
|
96
|
+
it gives the ancestor a real, non-zero size to lay out against, so
|
|
97
|
+
the collapse can't happen in the first place. Overridable per
|
|
98
|
+
instance via --chart-min-width; PieChart's legend-right layout
|
|
99
|
+
already sets this to 0 for its own case, where flex-basis is
|
|
100
|
+
definite (0, not auto) so shrink-to-fit never applies. */
|
|
101
|
+
min-width: var(--chart-min-width, 160px);
|
|
90
102
|
background: var(--chart-background, transparent);
|
|
91
103
|
font-family: var(--chart-font-family, inherit);
|
|
92
104
|
}
|
|
@@ -39,12 +39,26 @@
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
42
|
+
* Where a portalled node is allowed to land. Svelte scopes a custom element's
|
|
43
|
+
* CSS to its shadow root, so `document.body` is the one place the tooltip must
|
|
44
|
+
* never go from inside a `<sui-*>` element: it keeps its `svelte-*` scoping
|
|
45
|
+
* class and loses every rule behind it, plus the host's custom properties.
|
|
46
|
+
* Measured in Chromium -- position fell back to `static`, background to
|
|
47
|
+
* transparent, border and shadow to none. Its own root holds that stylesheet
|
|
48
|
+
* and still sits above every overflow/scroll ancestor between the two.
|
|
49
|
+
*/
|
|
50
|
+
const portalTarget = (node: Node): Node => {
|
|
51
|
+
const root = node.getRootNode();
|
|
52
|
+
return root instanceof ShadowRoot ? root : document.body;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Svelte action: relocates the tooltip out to its root so a position:fixed
|
|
43
57
|
* tooltip is never clipped by an overflow/scroll ancestor. `use:` actions
|
|
44
58
|
* never run during SSR.
|
|
45
59
|
*/
|
|
46
|
-
const
|
|
47
|
-
|
|
60
|
+
const portalToRoot = (node: HTMLElement) => {
|
|
61
|
+
portalTarget(node).appendChild(node);
|
|
48
62
|
return { destroy: () => node.remove() };
|
|
49
63
|
};
|
|
50
64
|
|
|
@@ -105,7 +119,7 @@
|
|
|
105
119
|
bind:clientHeight={tooltipHeight}
|
|
106
120
|
class="chart-tooltip portal {unstyled ? 'unstyled' : ''} {classes ?? ''}"
|
|
107
121
|
style="left: {pos.left}px; top: {pos.top}px;"
|
|
108
|
-
use:
|
|
122
|
+
use:portalToRoot
|
|
109
123
|
data-pw="chart-tooltip"
|
|
110
124
|
testID="chart-tooltip"
|
|
111
125
|
>
|
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Resolves a caller-stable id (a category label, series name, or any other id
|
|
4
|
+
* the CALLER chose to remember) to its CURRENT position in a chart's live
|
|
5
|
+
* category list -- e.g. the array `ChartHighlightAPI.getCategories()` returns.
|
|
6
|
+
*
|
|
7
|
+
* Both `highlightedIndex` and `ChartHighlightAPI.highlight()` take a numeric
|
|
8
|
+
* index, but that index shifts whenever the chart reorders, filters, or
|
|
9
|
+
* folds tail categories into a `topN` "Other" bucket. A synchronized legend
|
|
10
|
+
* (or any external driver) should not cache the index it first saw; it
|
|
11
|
+
* should re-resolve the id against the current `getCategories()` output
|
|
12
|
+
* right before calling `highlight()`. Returns `null` when the id is not
|
|
13
|
+
* currently present (filtered out, or absorbed into an aggregate bucket),
|
|
14
|
+
* which callers should treat as "clear the highlight", not "highlight -1".
|
|
15
|
+
*/
|
|
16
|
+
export function resolveLegendIndex(categories: readonly string[], id: string): number | null {
|
|
17
|
+
const index = categories.indexOf(id);
|
|
18
|
+
return index === -1 ? null : index;
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
1
22
|
<script lang="ts">
|
|
2
23
|
import type { LegendProperties } from './types';
|
|
3
24
|
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves a caller-stable id (a category label, series name, or any other id
|
|
3
|
+
* the CALLER chose to remember) to its CURRENT position in a chart's live
|
|
4
|
+
* category list -- e.g. the array `ChartHighlightAPI.getCategories()` returns.
|
|
5
|
+
*
|
|
6
|
+
* Both `highlightedIndex` and `ChartHighlightAPI.highlight()` take a numeric
|
|
7
|
+
* index, but that index shifts whenever the chart reorders, filters, or
|
|
8
|
+
* folds tail categories into a `topN` "Other" bucket. A synchronized legend
|
|
9
|
+
* (or any external driver) should not cache the index it first saw; it
|
|
10
|
+
* should re-resolve the id against the current `getCategories()` output
|
|
11
|
+
* right before calling `highlight()`. Returns `null` when the id is not
|
|
12
|
+
* currently present (filtered out, or absorbed into an aggregate bucket),
|
|
13
|
+
* which callers should treat as "clear the highlight", not "highlight -1".
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveLegendIndex(categories: readonly string[], id: string): number | null;
|
|
1
16
|
import type { LegendProperties } from './types';
|
|
2
17
|
declare const Legend: import("svelte").Component<LegendProperties, {}, "">;
|
|
3
18
|
type Legend = ReturnType<typeof Legend>;
|
|
@@ -30,6 +30,7 @@ export type AutoLayout = ChartDimensions & {
|
|
|
30
30
|
* then bottom derives from the rotation outcome.
|
|
31
31
|
*/
|
|
32
32
|
export declare function computeAutoLayout(input: AutoLayoutInput): AutoLayout;
|
|
33
|
+
export declare const pieSliceValue: (value: number) => number;
|
|
33
34
|
export declare function computePieLayout(data: Array<{
|
|
34
35
|
label: string;
|
|
35
36
|
value: number;
|
|
@@ -39,7 +40,7 @@ export declare function computeSankeyLayout(nodes: Array<{
|
|
|
39
40
|
id: string;
|
|
40
41
|
label?: string;
|
|
41
42
|
color?: string;
|
|
42
|
-
}>,
|
|
43
|
+
}>, rawLinks: Array<{
|
|
43
44
|
source: string;
|
|
44
45
|
target: string;
|
|
45
46
|
value: number;
|
|
@@ -48,6 +49,58 @@ export declare function computeSankeyLayout(nodes: Array<{
|
|
|
48
49
|
nodes: ComputedSankeyNode[];
|
|
49
50
|
links: ComputedSankeyLink[];
|
|
50
51
|
};
|
|
52
|
+
/**
|
|
53
|
+
* Minimal shape `joinByX` needs from a data point. `label` stays optional so
|
|
54
|
+
* both `LineChartDataPoint` and `AreaChartDataPoint` satisfy it structurally
|
|
55
|
+
* without either chart importing the other's types.
|
|
56
|
+
*/
|
|
57
|
+
export type JoinableXPoint = {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
label?: string;
|
|
61
|
+
};
|
|
62
|
+
export type JoinableXSeries<P extends JoinableXPoint = JoinableXPoint> = {
|
|
63
|
+
data: ReadonlyArray<P>;
|
|
64
|
+
};
|
|
65
|
+
export type JoinedXValue<P extends JoinableXPoint = JoinableXPoint> = {
|
|
66
|
+
/** Index of `point` within that series' own `data` array (last-wins, see below). */
|
|
67
|
+
index: number;
|
|
68
|
+
point: P;
|
|
69
|
+
};
|
|
70
|
+
export type JoinedXRow<P extends JoinableXPoint = JoinableXPoint> = {
|
|
71
|
+
x: number;
|
|
72
|
+
/**
|
|
73
|
+
* One slot per input series, in the same order as `seriesList`. `null`
|
|
74
|
+
* marks a series with no sample at this x -- an absence, never a fabricated
|
|
75
|
+
* zero. A gap point (finite x, non-finite y) still gets a slot here; callers
|
|
76
|
+
* that must skip gaps check `Number.isFinite(value.point.y)` themselves.
|
|
77
|
+
*/
|
|
78
|
+
values: ReadonlyArray<JoinedXValue<P> | null>;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Aligns multiple series by their x VALUE instead of by array position, so a
|
|
82
|
+
* shorter, offset, reordered or duplicate-x series never has another
|
|
83
|
+
* series' sample attributed to its column (array-position joins silently
|
|
84
|
+
* draw series B's 3rd point at series A's 3rd x instead of B's own x).
|
|
85
|
+
*
|
|
86
|
+
* One row is produced per distinct finite x across all series, sorted
|
|
87
|
+
* ascending -- callers do not need to pre-sort their data. Non-finite x
|
|
88
|
+
* (an unusual, distinct case from a gap's non-finite Y) cannot be placed in
|
|
89
|
+
* a column at all and is excluded from the join.
|
|
90
|
+
*
|
|
91
|
+
* Duplicate x within a single series: the LAST matching point wins. This
|
|
92
|
+
* matches `computeStackedValues` below (which is built on this same join)
|
|
93
|
+
* and avoids privileging an arbitrary "first sample" when a caller's array
|
|
94
|
+
* isn't time-ordered.
|
|
95
|
+
*
|
|
96
|
+
* This is the ONE alignment contract shared by tooltip context, hit-testing
|
|
97
|
+
* and stacking/normalization -- there is no second, position-based path for
|
|
98
|
+
* the common case where every series already shares one x sequence: with
|
|
99
|
+
* aligned input this produces exactly the positional pairing callers relied
|
|
100
|
+
* on before, because "nearest by x" and "same index" agree when the x's
|
|
101
|
+
* already match one-for-one.
|
|
102
|
+
*/
|
|
103
|
+
export declare function joinByX<P extends JoinableXPoint>(seriesList: ReadonlyArray<JoinableXSeries<P>>): JoinedXRow<P>[];
|
|
51
104
|
export declare function computeStackedValues(seriesData: Array<Array<{
|
|
52
105
|
x: number;
|
|
53
106
|
y: number;
|
package/dist/_chart/geometry.js
CHANGED
|
@@ -64,8 +64,19 @@ export function computeAutoLayout(input) {
|
|
|
64
64
|
return { ...dims, xRotate: rotate, xEvery: every, xLabelOffset: xLabelDepth + TITLE_BAND };
|
|
65
65
|
}
|
|
66
66
|
// ── Pie layout ──────────────────────────────────────────────────
|
|
67
|
+
// Pie/funnel/sankey are value-accumulating charts: per docs/CHART_INPUT_POLICY.md
|
|
68
|
+
// a negative contribution has no visual meaning and is a 0 contribution, and
|
|
69
|
+
// (per computeStackedValues' precedent for this file) the same treatment is
|
|
70
|
+
// extended to non-finite values here rather than excluding them as a gap --
|
|
71
|
+
// unlike LineChart/AreaChart, a pie slice has no x-position to drop and
|
|
72
|
+
// omitting a data row would shift every later slice's array index instead of
|
|
73
|
+
// just rendering that one slice as zero-size. Exported so PieChart.svelte's
|
|
74
|
+
// independent total (feeds isEmpty and the percentage denominator) can't drift
|
|
75
|
+
// from this one -- two copies of an unguarded reduce is exactly how the
|
|
76
|
+
// original defect stayed invisible in half the places it needed the guard.
|
|
77
|
+
export const pieSliceValue = (value) => Number.isFinite(value) && value > 0 ? value : 0;
|
|
67
78
|
export function computePieLayout(data, startAngle = -Math.PI / 2, padAngle = 0) {
|
|
68
|
-
const total = data.reduce((sum, d) => sum +
|
|
79
|
+
const total = data.reduce((sum, d) => sum + pieSliceValue(d.value), 0);
|
|
69
80
|
if (total === 0) {
|
|
70
81
|
return [];
|
|
71
82
|
}
|
|
@@ -73,7 +84,7 @@ export function computePieLayout(data, startAngle = -Math.PI / 2, padAngle = 0)
|
|
|
73
84
|
let angle = startAngle;
|
|
74
85
|
for (let i = 0; i < data.length; i++) {
|
|
75
86
|
const d = data[i];
|
|
76
|
-
const val =
|
|
87
|
+
const val = pieSliceValue(d.value);
|
|
77
88
|
const sliceAngle = (val / total) * Math.PI * 2;
|
|
78
89
|
const start = angle + padAngle / 2;
|
|
79
90
|
const end = angle + sliceAngle - padAngle / 2;
|
|
@@ -92,10 +103,22 @@ export function computePieLayout(data, startAngle = -Math.PI / 2, padAngle = 0)
|
|
|
92
103
|
return slices;
|
|
93
104
|
}
|
|
94
105
|
// ── Sankey layout ───────────────────────────────────────────────
|
|
95
|
-
export function computeSankeyLayout(nodes,
|
|
106
|
+
export function computeSankeyLayout(nodes, rawLinks, width, height, nodeWidth = 16, nodePadding = 8, iterations = 6, minLinkWidth = 1) {
|
|
96
107
|
if (nodes.length === 0) {
|
|
97
108
|
return { nodes: [], links: [] };
|
|
98
109
|
}
|
|
110
|
+
// Single choke point: every downstream read of a link's value -- adjacency,
|
|
111
|
+
// node heights, the relaxation loop's weighted centring, link widths, and
|
|
112
|
+
// the returned links themselves -- goes through this sanitized array, not
|
|
113
|
+
// the raw prop. A non-finite value that reached any one of those sites was
|
|
114
|
+
// enough to broadcast NaN to the whole diagram (the per-column recentre
|
|
115
|
+
// step below spreads one node's NaN to every node sharing its column, and
|
|
116
|
+
// the relaxation loop then carries that into every downstream column too),
|
|
117
|
+
// so guarding only the site that happened to be probed was not sufficient.
|
|
118
|
+
// Zero-contribution, not gap-exclusion: matches computeStackedValues'
|
|
119
|
+
// precedent for this file (see pieSliceValue above) since a link has no
|
|
120
|
+
// x-position to drop the way a LineChart/AreaChart gap point does.
|
|
121
|
+
const links = rawLinks.map((l) => Number.isFinite(l.value) && l.value >= 0 ? l : { ...l, value: 0 });
|
|
99
122
|
// Build adjacency
|
|
100
123
|
const outgoing = new Map();
|
|
101
124
|
const incoming = new Map();
|
|
@@ -161,7 +184,12 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
|
|
|
161
184
|
}
|
|
162
185
|
}
|
|
163
186
|
if (!Number.isFinite(pxPerValue)) {
|
|
164
|
-
//
|
|
187
|
+
// Nothing carries volume in any column, so every bar collapses to the
|
|
188
|
+
// minimum. Two different inputs land here: genuinely all-zero data, and
|
|
189
|
+
// a graph where every link was non-finite/negative and sanitized to 0
|
|
190
|
+
// above -- both reduce to the same "no volume anywhere" state, and the
|
|
191
|
+
// same minimum-width fallback is the correct degenerate render for
|
|
192
|
+
// either one (never NaN), so they do not need to stay distinguishable.
|
|
165
193
|
pxPerValue = 0;
|
|
166
194
|
}
|
|
167
195
|
const linkRenderWidth = (value) => Math.max(minLinkWidth, value * pxPerValue);
|
|
@@ -263,7 +291,15 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
|
|
|
263
291
|
column: columns.get(n.id) ?? 0
|
|
264
292
|
}));
|
|
265
293
|
const nodeById = new Map(computedNodes.map((n) => [n.id, n]));
|
|
266
|
-
|
|
294
|
+
// The separator is written as the ESCAPE `\u0000`, not as a raw NUL byte. A
|
|
295
|
+
// literal NUL in the source makes the whole file binary to every text tool:
|
|
296
|
+
// `file` reports it as data, `grep` stops reporting matches in it, and a
|
|
297
|
+
// review diff renders it as unviewable. This file carried one from the commit
|
|
298
|
+
// that added the Sankey layout, and it silently hid `computeStackedValues`
|
|
299
|
+
// from a repo-wide grep. The runtime string is identical either way -- a NUL
|
|
300
|
+
// is still the delimiter, because it is the one character a node id cannot
|
|
301
|
+
// contain, which is the whole reason it was chosen.
|
|
302
|
+
const linkKey = (l) => `${l.source}\u0000${l.target}`;
|
|
267
303
|
// Link widths use the same global scale as node heights, so each node's link
|
|
268
304
|
// stack fills its bar exactly and never runs past its bottom edge.
|
|
269
305
|
const linkWidths = new Map();
|
|
@@ -327,21 +363,93 @@ export function computeSankeyLayout(nodes, links, width, height, nodeWidth = 16,
|
|
|
327
363
|
});
|
|
328
364
|
return { nodes: computedNodes, links: computedLinks };
|
|
329
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Aligns multiple series by their x VALUE instead of by array position, so a
|
|
368
|
+
* shorter, offset, reordered or duplicate-x series never has another
|
|
369
|
+
* series' sample attributed to its column (array-position joins silently
|
|
370
|
+
* draw series B's 3rd point at series A's 3rd x instead of B's own x).
|
|
371
|
+
*
|
|
372
|
+
* One row is produced per distinct finite x across all series, sorted
|
|
373
|
+
* ascending -- callers do not need to pre-sort their data. Non-finite x
|
|
374
|
+
* (an unusual, distinct case from a gap's non-finite Y) cannot be placed in
|
|
375
|
+
* a column at all and is excluded from the join.
|
|
376
|
+
*
|
|
377
|
+
* Duplicate x within a single series: the LAST matching point wins. This
|
|
378
|
+
* matches `computeStackedValues` below (which is built on this same join)
|
|
379
|
+
* and avoids privileging an arbitrary "first sample" when a caller's array
|
|
380
|
+
* isn't time-ordered.
|
|
381
|
+
*
|
|
382
|
+
* This is the ONE alignment contract shared by tooltip context, hit-testing
|
|
383
|
+
* and stacking/normalization -- there is no second, position-based path for
|
|
384
|
+
* the common case where every series already shares one x sequence: with
|
|
385
|
+
* aligned input this produces exactly the positional pairing callers relied
|
|
386
|
+
* on before, because "nearest by x" and "same index" agree when the x's
|
|
387
|
+
* already match one-for-one.
|
|
388
|
+
*/
|
|
389
|
+
export function joinByX(seriesList) {
|
|
390
|
+
const perSeriesByX = seriesList.map((series) => {
|
|
391
|
+
const byX = new Map();
|
|
392
|
+
series.data.forEach((point, index) => {
|
|
393
|
+
if (Number.isFinite(point.x)) {
|
|
394
|
+
byX.set(point.x, { index, point });
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
return byX;
|
|
398
|
+
});
|
|
399
|
+
const allX = new Set();
|
|
400
|
+
for (const byX of perSeriesByX) {
|
|
401
|
+
for (const x of byX.keys()) {
|
|
402
|
+
allX.add(x);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return [...allX]
|
|
406
|
+
.sort((a, b) => a - b)
|
|
407
|
+
.map((x) => ({
|
|
408
|
+
x,
|
|
409
|
+
values: perSeriesByX.map((byX) => byX.get(x) ?? null)
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
330
412
|
// ── Stacked values ──────────────────────────────────────────────
|
|
331
413
|
export function computeStackedValues(seriesData) {
|
|
332
414
|
if (seriesData.length === 0) {
|
|
333
415
|
return [];
|
|
334
416
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
417
|
+
// Built on joinByX -- the same by-x alignment used for tooltip/hit-testing
|
|
418
|
+
// -- rather than a second, independent accumulation. A series absent at a
|
|
419
|
+
// given x (including a NaN-gap point there) contributes no segment and no
|
|
420
|
+
// baseline shift at that column, instead of a fabricated zero-height slice
|
|
421
|
+
// or a NaN that would poison every series stacked above it.
|
|
422
|
+
const rows = joinByX(seriesData.map((data) => ({ data })));
|
|
423
|
+
const stacked = seriesData.map(() => []);
|
|
424
|
+
for (const row of rows) {
|
|
425
|
+
let base = 0;
|
|
426
|
+
for (let si = 0; si < seriesData.length; si++) {
|
|
427
|
+
const entry = row.values[si];
|
|
428
|
+
if (entry === null || !Number.isFinite(entry.point.y)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
// A negative contributes ZERO height, it does not subtract.
|
|
432
|
+
//
|
|
433
|
+
// docs/CHART_INPUT_POLICY.md has always said so -- "a negative
|
|
434
|
+
// contribution to a stack or a percent-of-total does not have a
|
|
435
|
+
// well-defined visual meaning (a slice cannot have negative height), so it
|
|
436
|
+
// is treated as a 0 contribution rather than inverting the stack" -- and
|
|
437
|
+
// the clamp existed only in AreaChart's normalized path, behind
|
|
438
|
+
// `stackNormalize`. Plain `stacked: true` renders through here, where
|
|
439
|
+
// `base + y` let a negative REDUCE the baseline for every series above it.
|
|
440
|
+
//
|
|
441
|
+
// It was invisible rather than merely wrong: AreaChart's stacked y-domain
|
|
442
|
+
// starts at 0, so the inverted segment had nowhere to be drawn and landed
|
|
443
|
+
// at pixel 510 of a 340px plot -- off the bottom of the chart entirely.
|
|
444
|
+
//
|
|
445
|
+
// The segment is still pushed, with zero height, rather than skipped:
|
|
446
|
+
// skipping means "this series has no point at this x", which is what a gap
|
|
447
|
+
// already means, and would silently drop the series from the column.
|
|
448
|
+
const contribution = Math.max(0, entry.point.y);
|
|
449
|
+
const y1 = base + contribution;
|
|
450
|
+
stacked[si].push({ x: row.x, y0: base, y1 });
|
|
451
|
+
base = y1;
|
|
343
452
|
}
|
|
344
|
-
stacked.push(stackedSeries);
|
|
345
453
|
}
|
|
346
454
|
return stacked;
|
|
347
455
|
}
|
|
@@ -8,8 +8,12 @@ export type RelativePointerPosition = {
|
|
|
8
8
|
/**
|
|
9
9
|
* Pointer position relative to the top-left corner of `el`, or null when the
|
|
10
10
|
* element is not mounted yet.
|
|
11
|
+
*
|
|
12
|
+
* Takes `MouseEvent` (which `PointerEvent` extends) since it only reads
|
|
13
|
+
* clientX/clientY — that also lets the mouse-only chart components (PieChart,
|
|
14
|
+
* SankeyChart) share this instead of hand-rolling the same formula.
|
|
11
15
|
*/
|
|
12
|
-
export declare function pointerPositionIn(el: HTMLElement | null, event:
|
|
16
|
+
export declare function pointerPositionIn(el: HTMLElement | null, event: MouseEvent): RelativePointerPosition | null;
|
|
13
17
|
/**
|
|
14
18
|
* Touch taps never fire pointerleave, so a tap-opened tooltip would otherwise
|
|
15
19
|
* stay stuck: dismiss when a pointerdown lands outside `containerEl`.
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
* Shared pointer-interaction helpers used by the chart components.
|
|
3
|
-
*/
|
|
1
|
+
import { eventHitsInside } from '../_interaction/dismissal';
|
|
4
2
|
/**
|
|
5
3
|
* Pointer position relative to the top-left corner of `el`, or null when the
|
|
6
4
|
* element is not mounted yet.
|
|
5
|
+
*
|
|
6
|
+
* Takes `MouseEvent` (which `PointerEvent` extends) since it only reads
|
|
7
|
+
* clientX/clientY — that also lets the mouse-only chart components (PieChart,
|
|
8
|
+
* SankeyChart) share this instead of hand-rolling the same formula.
|
|
7
9
|
*/
|
|
8
10
|
export function pointerPositionIn(el, event) {
|
|
9
11
|
if (el === null) {
|
|
@@ -24,8 +26,10 @@ export function dismissOnOutsidePointerDown(containerEl, onDismiss) {
|
|
|
24
26
|
return () => { };
|
|
25
27
|
}
|
|
26
28
|
const dismiss = (event) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
// Through the custom-element build this listener sits outside the shadow
|
|
30
|
+
// root the chart renders in, so `event.target` is the host rather than
|
|
31
|
+
// anything the pointer actually hit. See eventHitsInside.
|
|
32
|
+
if (containerEl !== null && !eventHitsInside(containerEl, event)) {
|
|
29
33
|
onDismiss();
|
|
30
34
|
}
|
|
31
35
|
};
|
package/dist/_chart/scales.js
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
export function niceLinearDomain(min, max) {
|
|
2
|
+
// Defense-in-depth: every current caller already filters to finite values
|
|
3
|
+
// before reaching here (LineChart/AreaChart/BarChart/DualAxisBarChart), so
|
|
4
|
+
// this never fires for them. It exists so a future caller that forgets to
|
|
5
|
+
// filter degrades to a neutral domain instead of propagating NaN through
|
|
6
|
+
// every downstream tick and pixel computation (min === max never catches
|
|
7
|
+
// NaN, since NaN === NaN is false).
|
|
8
|
+
//
|
|
9
|
+
// It also MASKS those callers' own filters, which matters when testing them:
|
|
10
|
+
// remove a chart's finite-filter and the axis no longer loses its ticks, it
|
|
11
|
+
// silently scales to [0, 1] instead. So a `ticks.length > 0` assertion is
|
|
12
|
+
// vacuous as a test of a caller-side guard — it passes either way. Assert on
|
|
13
|
+
// tick *content* against a reference render (see BarChart/non-finite.test.ts)
|
|
14
|
+
// instead.
|
|
15
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
16
|
+
return [0, 1];
|
|
17
|
+
}
|
|
2
18
|
if (min === max) {
|
|
3
19
|
return min === 0 ? [0, 1] : [min > 0 ? 0 : min * 2, max > 0 ? max * 2 : 0];
|
|
4
20
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documented, tested calendar-tick helpers for the temporal limitation in
|
|
3
|
+
* Line/AreaChart's x axis -- see docs/CHART_INPUT_POLICY.md#calendar--temporal-axes
|
|
4
|
+
* for the full policy this satisfies.
|
|
5
|
+
*
|
|
6
|
+
* Line/AreaChart's x axis is a plain numeric linear scale (`createLinearScale`
|
|
7
|
+
* in `./scales`). Epoch-millisecond x values preserve relative distance --
|
|
8
|
+
* `t2 - t1` still means the same thing in pixels as it does in time -- but
|
|
9
|
+
* `computeLinearTicks`'s "nice round number" ticks are not calendar-aware:
|
|
10
|
+
* they land on values like 1_700_000_000_000 or a rounded step in
|
|
11
|
+
* milliseconds, not on UTC midnight/week/month boundaries. `xTickFormat`
|
|
12
|
+
* only changes how a tick's existing numeric position is labelled; it cannot
|
|
13
|
+
* move that position onto a calendar boundary, and neither chart accepts an
|
|
14
|
+
* explicit tick-position override today (`Axis.svelte`, shared by every chart
|
|
15
|
+
* type in this package, always derives its own tick positions from the
|
|
16
|
+
* scale) -- wiring one in is deliberately out of scope here, since it would
|
|
17
|
+
* change a contract every other chart consumes, not just these two.
|
|
18
|
+
*
|
|
19
|
+
* `computeUtcTicks`/`formatUtcTick` are the disclosed alternative: pure,
|
|
20
|
+
* fully-tested functions a caller can use to compute correct UTC calendar
|
|
21
|
+
* tick positions and labels for their own presentation (a custom axis
|
|
22
|
+
* overlay, a legend of period boundaries, or pre-bucketing data into
|
|
23
|
+
* calendar-aligned x values before it ever reaches the chart). They do not
|
|
24
|
+
* replace the chart's linear scale with array indices, and they impose
|
|
25
|
+
* nothing on a caller who does not import this module -- both charts behave
|
|
26
|
+
* exactly as before.
|
|
27
|
+
*/
|
|
28
|
+
export type UtcTickUnit = 'day' | 'week' | 'month';
|
|
29
|
+
/**
|
|
30
|
+
* Computes UTC calendar-aligned tick positions (epoch ms) covering
|
|
31
|
+
* `[domain[0], domain[1]]`. `unit` picks the calendar grain; ticks are
|
|
32
|
+
* thinned so the count stays at or under `maxTicks` (mirrors Line/AreaChart's
|
|
33
|
+
* own tick-count capping).
|
|
34
|
+
*
|
|
35
|
+
* Returns `[]` for a non-finite or inverted domain -- callers pass this
|
|
36
|
+
* straight into `xTicks`, and an empty array there just falls back to no
|
|
37
|
+
* override (see LineChart/AreaChart docs).
|
|
38
|
+
*/
|
|
39
|
+
export declare function computeUtcTicks(domain: readonly [number, number], unit: UtcTickUnit, maxTicks?: number): number[];
|
|
40
|
+
/**
|
|
41
|
+
* Formats an epoch-ms tick value produced by `computeUtcTicks` as a UTC
|
|
42
|
+
* calendar label ("Mar 4" for day/week, "Mar 2026" for month). Pass as
|
|
43
|
+
* `xTickFormat` alongside `xTicks={computeUtcTicks(...)}`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function formatUtcTick(value: number, unit?: UtcTickUnit): string;
|