@joewinke/jatui 0.36.3 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/artifact-blocks/AttachmentChip.svelte +66 -0
- package/dist/artifact-blocks/AttachmentChip.svelte.d.ts +7 -0
- package/dist/artifact-blocks/BlockComments.svelte +309 -0
- package/dist/artifact-blocks/BlockComments.svelte.d.ts +33 -0
- package/dist/artifact-blocks/BlockRenderer.svelte +674 -0
- package/dist/artifact-blocks/BlockRenderer.svelte.d.ts +55 -0
- package/dist/artifact-blocks/BlockVerdict.svelte +151 -0
- package/dist/artifact-blocks/BlockVerdict.svelte.d.ts +13 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte +1616 -0
- package/dist/artifact-blocks/BlocksArtifactView.svelte.d.ts +113 -0
- package/dist/artifact-blocks/CONTRACT.md +225 -0
- package/dist/artifact-blocks/CommentPill.svelte +49 -0
- package/dist/artifact-blocks/CommentPill.svelte.d.ts +16 -0
- package/dist/artifact-blocks/FoldedRounds.svelte +95 -0
- package/dist/artifact-blocks/FoldedRounds.svelte.d.ts +9 -0
- package/dist/artifact-blocks/Glyph.svelte +54 -0
- package/dist/artifact-blocks/Glyph.svelte.d.ts +10 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte +500 -0
- package/dist/artifact-blocks/HistoryScrubber.svelte.d.ts +15 -0
- package/dist/artifact-blocks/Markdown.svelte +186 -0
- package/dist/artifact-blocks/Markdown.svelte.d.ts +30 -0
- package/dist/artifact-blocks/NoteComposer.svelte +31 -0
- package/dist/artifact-blocks/NoteComposer.svelte.d.ts +8 -0
- package/dist/artifact-blocks/PlainComposer.svelte +200 -0
- package/dist/artifact-blocks/PlainComposer.svelte.d.ts +15 -0
- package/dist/artifact-blocks/SectionTools.svelte +102 -0
- package/dist/artifact-blocks/SectionTools.svelte.d.ts +19 -0
- package/dist/artifact-blocks/adapter.d.ts +13 -0
- package/dist/artifact-blocks/adapter.js +31 -0
- package/dist/artifact-blocks/autoSend.d.ts +49 -0
- package/dist/artifact-blocks/autoSend.js +103 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte +557 -0
- package/dist/artifact-blocks/blocks/actions/ActionsBlock.svelte.d.ts +48 -0
- package/dist/artifact-blocks/blocks/actions/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/actions/block.js +74 -0
- package/dist/artifact-blocks/blocks/actions/requests.d.ts +62 -0
- package/dist/artifact-blocks/blocks/actions/requests.js +26 -0
- package/dist/artifact-blocks/blocks/actions/schema.d.ts +168 -0
- package/dist/artifact-blocks/blocks/actions/schema.js +253 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte +779 -0
- package/dist/artifact-blocks/blocks/callout/CalloutBlock.svelte.d.ts +40 -0
- package/dist/artifact-blocks/blocks/callout/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/callout/block.js +39 -0
- package/dist/artifact-blocks/blocks/callout/schema.d.ts +72 -0
- package/dist/artifact-blocks/blocks/callout/schema.js +115 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte +875 -0
- package/dist/artifact-blocks/blocks/chart/ChartBlock.svelte.d.ts +59 -0
- package/dist/artifact-blocks/blocks/chart/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/chart/block.js +70 -0
- package/dist/artifact-blocks/blocks/chart/schema.d.ts +171 -0
- package/dist/artifact-blocks/blocks/chart/schema.js +498 -0
- package/dist/artifact-blocks/blocks/chart/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/chart/thumb.js +201 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte +1048 -0
- package/dist/artifact-blocks/blocks/checklist/ChecklistBlock.svelte.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte +105 -0
- package/dist/artifact-blocks/blocks/checklist/Icon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/blocks/checklist/block.d.ts +10 -0
- package/dist/artifact-blocks/blocks/checklist/block.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.d.ts +16 -0
- package/dist/artifact-blocks/blocks/checklist/itemReview.js +63 -0
- package/dist/artifact-blocks/blocks/checklist/labels.d.ts +62 -0
- package/dist/artifact-blocks/blocks/checklist/labels.js +96 -0
- package/dist/artifact-blocks/blocks/checklist/retest.d.ts +43 -0
- package/dist/artifact-blocks/blocks/checklist/retest.js +68 -0
- package/dist/artifact-blocks/blocks/checklist/schema.d.ts +84 -0
- package/dist/artifact-blocks/blocks/checklist/schema.js +91 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte +128 -0
- package/dist/artifact-blocks/blocks/code/CodeBlock.svelte.d.ts +12 -0
- package/dist/artifact-blocks/blocks/code/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/code/block.js +31 -0
- package/dist/artifact-blocks/blocks/code/highlight.d.ts +34 -0
- package/dist/artifact-blocks/blocks/code/highlight.js +280 -0
- package/dist/artifact-blocks/blocks/code/schema.d.ts +32 -0
- package/dist/artifact-blocks/blocks/code/schema.js +39 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte +290 -0
- package/dist/artifact-blocks/blocks/collage/CollageBlock.svelte.d.ts +29 -0
- package/dist/artifact-blocks/blocks/collage/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/collage/block.js +29 -0
- package/dist/artifact-blocks/blocks/collage/schema.d.ts +52 -0
- package/dist/artifact-blocks/blocks/collage/schema.js +86 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte +484 -0
- package/dist/artifact-blocks/blocks/compare/CompareBlock.svelte.d.ts +25 -0
- package/dist/artifact-blocks/blocks/compare/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/compare/block.js +32 -0
- package/dist/artifact-blocks/blocks/compare/schema.d.ts +74 -0
- package/dist/artifact-blocks/blocks/compare/schema.js +148 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte +392 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +30 -0
- package/dist/artifact-blocks/blocks/diagram/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/diagram/block.js +50 -0
- package/dist/artifact-blocks/blocks/diagram/constants.d.ts +48 -0
- package/dist/artifact-blocks/blocks/diagram/constants.js +53 -0
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +161 -0
- package/dist/artifact-blocks/blocks/diagram/schema.js +621 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/diagram/thumb.js +157 -0
- package/dist/artifact-blocks/blocks/diagram/types.d.ts +74 -0
- package/dist/artifact-blocks/blocks/diagram/types.js +1 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte +81 -0
- package/dist/artifact-blocks/blocks/diff/DiffBlock.svelte.d.ts +10 -0
- package/dist/artifact-blocks/blocks/diff/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/diff/block.js +36 -0
- package/dist/artifact-blocks/blocks/diff/schema.d.ts +18 -0
- package/dist/artifact-blocks/blocks/diff/schema.js +35 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte +1055 -0
- package/dist/artifact-blocks/blocks/diff-review/DiffReviewBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/diff-review/block.d.ts +15 -0
- package/dist/artifact-blocks/blocks/diff-review/block.js +64 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.d.ts +57 -0
- package/dist/artifact-blocks/blocks/diff-review/decisions.js +80 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.d.ts +13 -0
- package/dist/artifact-blocks/blocks/diff-review/schema.js +84 -0
- package/dist/artifact-blocks/blocks/diff-review/types.d.ts +35 -0
- package/dist/artifact-blocks/blocks/diff-review/types.js +1 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte +145 -0
- package/dist/artifact-blocks/blocks/explainer/ExplainerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/explainer/block.d.ts +8 -0
- package/dist/artifact-blocks/blocks/explainer/block.js +21 -0
- package/dist/artifact-blocks/blocks/explainer/schema.d.ts +26 -0
- package/dist/artifact-blocks/blocks/explainer/schema.js +69 -0
- package/dist/artifact-blocks/blocks/group-content.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-content.js +16 -0
- package/dist/artifact-blocks/blocks/group-data.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-data.js +7 -0
- package/dist/artifact-blocks/blocks/group-review.d.ts +10 -0
- package/dist/artifact-blocks/blocks/group-review.js +12 -0
- package/dist/artifact-blocks/blocks/group-visual.d.ts +17 -0
- package/dist/artifact-blocks/blocks/group-visual.js +14 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte +413 -0
- package/dist/artifact-blocks/blocks/map/MapBlock.svelte.d.ts +53 -0
- package/dist/artifact-blocks/blocks/map/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/map/block.js +62 -0
- package/dist/artifact-blocks/blocks/map/schema.d.ts +123 -0
- package/dist/artifact-blocks/blocks/map/schema.js +113 -0
- package/dist/artifact-blocks/blocks/map/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/map/thumb.js +69 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte +33 -0
- package/dist/artifact-blocks/blocks/markdown/MarkdownBlock.svelte.d.ts +15 -0
- package/dist/artifact-blocks/blocks/markdown/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/markdown/block.js +28 -0
- package/dist/artifact-blocks/blocks/markdown/render.d.ts +17 -0
- package/dist/artifact-blocks/blocks/markdown/render.js +115 -0
- package/dist/artifact-blocks/blocks/markdown/schema.d.ts +10 -0
- package/dist/artifact-blocks/blocks/markdown/schema.js +23 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte +2112 -0
- package/dist/artifact-blocks/blocks/media-picker/MediaPickerBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/media-picker/block.d.ts +24 -0
- package/dist/artifact-blocks/blocks/media-picker/block.js +137 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.d.ts +28 -0
- package/dist/artifact-blocks/blocks/media-picker/choices.js +35 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/decisions.js +75 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/media-picker/schema.js +199 -0
- package/dist/artifact-blocks/blocks/media-picker/types.d.ts +92 -0
- package/dist/artifact-blocks/blocks/media-picker/types.js +1 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.d.ts +51 -0
- package/dist/artifact-blocks/blocks/media-picker/versions.js +118 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte +213 -0
- package/dist/artifact-blocks/blocks/mockup/MockupBlock.svelte.d.ts +5 -0
- package/dist/artifact-blocks/blocks/mockup/block.d.ts +26 -0
- package/dist/artifact-blocks/blocks/mockup/block.js +52 -0
- package/dist/artifact-blocks/blocks/mockup/doc.d.ts +24 -0
- package/dist/artifact-blocks/blocks/mockup/doc.js +44 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.d.ts +21 -0
- package/dist/artifact-blocks/blocks/mockup/sandbox.js +27 -0
- package/dist/artifact-blocks/blocks/mockup/schema.d.ts +7 -0
- package/dist/artifact-blocks/blocks/mockup/schema.js +90 -0
- package/dist/artifact-blocks/blocks/mockup/types.d.ts +36 -0
- package/dist/artifact-blocks/blocks/mockup/types.js +17 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte +359 -0
- package/dist/artifact-blocks/blocks/program/ProgramBlock.svelte.d.ts +32 -0
- package/dist/artifact-blocks/blocks/program/block.d.ts +35 -0
- package/dist/artifact-blocks/blocks/program/block.js +68 -0
- package/dist/artifact-blocks/blocks/program/schema.d.ts +132 -0
- package/dist/artifact-blocks/blocks/program/schema.js +268 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte +315 -0
- package/dist/artifact-blocks/blocks/stats/StatsBlock.svelte.d.ts +24 -0
- package/dist/artifact-blocks/blocks/stats/block.d.ts +7 -0
- package/dist/artifact-blocks/blocks/stats/block.js +49 -0
- package/dist/artifact-blocks/blocks/stats/schema.d.ts +82 -0
- package/dist/artifact-blocks/blocks/stats/schema.js +43 -0
- package/dist/artifact-blocks/blocks/stats/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/stats/thumb.js +102 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte +490 -0
- package/dist/artifact-blocks/blocks/table/TableBlock.svelte.d.ts +28 -0
- package/dist/artifact-blocks/blocks/table/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/table/block.js +100 -0
- package/dist/artifact-blocks/blocks/table/schema.d.ts +143 -0
- package/dist/artifact-blocks/blocks/table/schema.js +278 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte +347 -0
- package/dist/artifact-blocks/blocks/timeline/TimelineBlock.svelte.d.ts +42 -0
- package/dist/artifact-blocks/blocks/timeline/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/timeline/block.js +96 -0
- package/dist/artifact-blocks/blocks/timeline/constants.d.ts +26 -0
- package/dist/artifact-blocks/blocks/timeline/constants.js +27 -0
- package/dist/artifact-blocks/blocks/timeline/schema.d.ts +102 -0
- package/dist/artifact-blocks/blocks/timeline/schema.js +405 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.d.ts +2 -0
- package/dist/artifact-blocks/blocks/timeline/thumb.js +117 -0
- package/dist/artifact-blocks/blocks/timeline/types.d.ts +66 -0
- package/dist/artifact-blocks/blocks/timeline/types.js +1 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte +169 -0
- package/dist/artifact-blocks/blocks/transcript/TranscriptBlock.svelte.d.ts +26 -0
- package/dist/artifact-blocks/blocks/transcript/block.d.ts +9 -0
- package/dist/artifact-blocks/blocks/transcript/block.js +86 -0
- package/dist/artifact-blocks/blocks/transcript/schema.d.ts +114 -0
- package/dist/artifact-blocks/blocks/transcript/schema.js +126 -0
- package/dist/artifact-blocks/builtins.d.ts +40 -0
- package/dist/artifact-blocks/builtins.js +22 -0
- package/dist/artifact-blocks/contractBlocks.d.ts +92 -0
- package/dist/artifact-blocks/contractBlocks.js +279 -0
- package/dist/artifact-blocks/history.d.ts +140 -0
- package/dist/artifact-blocks/history.js +363 -0
- package/dist/artifact-blocks/index.d.ts +40 -0
- package/dist/artifact-blocks/index.js +46 -0
- package/dist/artifact-blocks/listen/context.d.ts +30 -0
- package/dist/artifact-blocks/listen/context.js +30 -0
- package/dist/artifact-blocks/liveData.d.ts +78 -0
- package/dist/artifact-blocks/liveData.js +137 -0
- package/dist/artifact-blocks/liveDataPoller.d.ts +41 -0
- package/dist/artifact-blocks/liveDataPoller.js +140 -0
- package/dist/artifact-blocks/openNav.d.ts +46 -0
- package/dist/artifact-blocks/openNav.js +106 -0
- package/dist/artifact-blocks/pageReviewKeys.d.ts +17 -0
- package/dist/artifact-blocks/pageReviewKeys.js +115 -0
- package/dist/artifact-blocks/params.d.ts +98 -0
- package/dist/artifact-blocks/params.js +159 -0
- package/dist/artifact-blocks/registry.d.ts +27 -0
- package/dist/artifact-blocks/registry.js +83 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte +99 -0
- package/dist/artifact-blocks/review/BlockIcon.svelte.d.ts +17 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte +94 -0
- package/dist/artifact-blocks/review/BulkRejectForm.svelte.d.ts +11 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte +40 -0
- package/dist/artifact-blocks/review/BulkUndoBar.svelte.d.ts +9 -0
- package/dist/artifact-blocks/review/ItemNote.svelte +193 -0
- package/dist/artifact-blocks/review/ItemNote.svelte.d.ts +33 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte +52 -0
- package/dist/artifact-blocks/review/NoteToggle.svelte.d.ts +12 -0
- package/dist/artifact-blocks/review/TeamChip.svelte +71 -0
- package/dist/artifact-blocks/review/TeamChip.svelte.d.ts +8 -0
- package/dist/artifact-blocks/review/attachments.d.ts +10 -0
- package/dist/artifact-blocks/review/attachments.js +37 -0
- package/dist/artifact-blocks/review/bulk.d.ts +52 -0
- package/dist/artifact-blocks/review/bulk.js +61 -0
- package/dist/artifact-blocks/review/itemReview.d.ts +33 -0
- package/dist/artifact-blocks/review/itemReview.js +112 -0
- package/dist/artifact-blocks/review/roundLines.d.ts +14 -0
- package/dist/artifact-blocks/review/roundLines.js +20 -0
- package/dist/artifact-blocks/review/rounds.d.ts +86 -0
- package/dist/artifact-blocks/review/rounds.js +302 -0
- package/dist/artifact-blocks/review/saver.d.ts +23 -0
- package/dist/artifact-blocks/review/saver.js +48 -0
- package/dist/artifact-blocks/review/team.d.ts +31 -0
- package/dist/artifact-blocks/review/team.js +42 -0
- package/dist/artifact-blocks/review/visualOrder.d.ts +29 -0
- package/dist/artifact-blocks/review/visualOrder.js +66 -0
- package/dist/artifact-blocks/review.d.ts +180 -0
- package/dist/artifact-blocks/review.js +716 -0
- package/dist/artifact-blocks/thumb-kit.d.ts +80 -0
- package/dist/artifact-blocks/thumb-kit.js +149 -0
- package/dist/artifact-blocks/types.d.ts +513 -0
- package/dist/artifact-blocks/types.js +1 -0
- package/dist/components/Button.svelte +8 -0
- package/dist/components/TextInput.svelte +39 -6
- package/dist/components/TextInput.svelte.d.ts +15 -7
- package/dist/components/basic/Checkbox.svelte +52 -14
- package/dist/components/basic/Checkbox.svelte.d.ts +11 -1
- package/package.json +11 -2
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart block — params schema, normalization and geometry helpers.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only (no DOM) so `block.test.ts` can exercise the math and
|
|
5
|
+
* `ChartBlock.svelte` stays a thin renderer. Colors NEVER appear here — the
|
|
6
|
+
* renderer addresses the `--chart-series-N` token contract from
|
|
7
|
+
* `src/lib/theme-bridge/layerchart.css` (which maps them to DaisyUI palette
|
|
8
|
+
* tokens), so every tenant theme reskins the chart for free.
|
|
9
|
+
*
|
|
10
|
+
* Untrusted input rules (CONTRACT.md): text params are length-capped strings,
|
|
11
|
+
* numbers are finite, arrays are capped. Series/point caps exist so one
|
|
12
|
+
* artifact can't ship a 50MB SVG: 20 series, 5000 points total.
|
|
13
|
+
*/
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
const CHART_TYPES = ["line", "bar", "area", "scatter"];
|
|
16
|
+
const X_TYPES = ["time", "category", "number"];
|
|
17
|
+
const Y_FORMATS = ["raw", "compact", "percent", "currency"];
|
|
18
|
+
const MAX_SERIES = 20;
|
|
19
|
+
const MAX_POINTS_TOTAL = 5000;
|
|
20
|
+
const MAX_POINTS_PER_SERIES = 2500;
|
|
21
|
+
/** Series count past which direct end-labels stop being readable. */
|
|
22
|
+
export const END_LABEL_SERIES_LIMIT = 8;
|
|
23
|
+
const xValueSchema = z.union([z.string().max(80), z.number().finite()]);
|
|
24
|
+
export const chartSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
/** Mark kind. Stacking applies to bar/area only. */
|
|
27
|
+
chartType: z.enum(CHART_TYPES).default("line"),
|
|
28
|
+
title: z.string().max(200).optional(),
|
|
29
|
+
subtitle: z.string().max(300).optional(),
|
|
30
|
+
xLabel: z.string().max(80).optional(),
|
|
31
|
+
yLabel: z.string().max(80).optional(),
|
|
32
|
+
y2Label: z.string().max(80).optional(),
|
|
33
|
+
/** How to read each point's x. Category = as-given order. */
|
|
34
|
+
xType: z.enum(X_TYPES).default("category"),
|
|
35
|
+
series: z
|
|
36
|
+
.array(z.object({
|
|
37
|
+
label: z.string().min(1).max(80),
|
|
38
|
+
/**
|
|
39
|
+
* Dual axis: only when the two units differ (e.g. visitors vs
|
|
40
|
+
* revenue). Identity stays in the legend/end-label text either way.
|
|
41
|
+
*/
|
|
42
|
+
axis: z.enum(["left", "right"]).default("left"),
|
|
43
|
+
points: z
|
|
44
|
+
.array(z.object({
|
|
45
|
+
/** time: ISO date(-time) or epoch ms · category: label · number: value */
|
|
46
|
+
x: xValueSchema,
|
|
47
|
+
y: z.number().finite(),
|
|
48
|
+
}))
|
|
49
|
+
.min(1)
|
|
50
|
+
.max(MAX_POINTS_PER_SERIES),
|
|
51
|
+
}))
|
|
52
|
+
.min(1)
|
|
53
|
+
.max(MAX_SERIES),
|
|
54
|
+
/** Stack bar/area series (cumulative per axis group). */
|
|
55
|
+
stacked: z.boolean().default(false),
|
|
56
|
+
/** Horizontal dashed target/threshold lines (from the value axis). */
|
|
57
|
+
goalLines: z
|
|
58
|
+
.array(z.object({
|
|
59
|
+
value: z.number().finite(),
|
|
60
|
+
label: z.string().max(80).optional(),
|
|
61
|
+
axis: z.enum(["left", "right"]).default("left"),
|
|
62
|
+
}))
|
|
63
|
+
.max(10)
|
|
64
|
+
.default([]),
|
|
65
|
+
/** Vertical markers: a full-height line (point) or shaded band (range). */
|
|
66
|
+
annotations: z
|
|
67
|
+
.array(z.object({
|
|
68
|
+
kind: z.enum(["point", "range"]),
|
|
69
|
+
x: xValueSchema,
|
|
70
|
+
/** range only: the end of the band. */
|
|
71
|
+
x2: xValueSchema.optional(),
|
|
72
|
+
/** point only: dot position on the value axis (line still drawn). */
|
|
73
|
+
y: z.number().finite().optional(),
|
|
74
|
+
label: z.string().min(1).max(120),
|
|
75
|
+
}))
|
|
76
|
+
.max(20)
|
|
77
|
+
.default([]),
|
|
78
|
+
/** Value-axis tick/tooltip formatting (raw/compact/percent/currency). */
|
|
79
|
+
yFormat: z.enum(Y_FORMATS).default("raw"),
|
|
80
|
+
/** ISO 4217 code used when yFormat is "currency". */
|
|
81
|
+
currency: z
|
|
82
|
+
.string()
|
|
83
|
+
.regex(/^[A-Za-z]{3}$/)
|
|
84
|
+
.default("USD"),
|
|
85
|
+
showLegend: z.boolean().default(true),
|
|
86
|
+
/** Direct series labels at the last point (auto-off past 8 series). */
|
|
87
|
+
endLabels: z.boolean().default(true),
|
|
88
|
+
height: z.number().int().min(160).max(800).default(280),
|
|
89
|
+
/**
|
|
90
|
+
* Value-axis domain overrides. bar/area NEVER truncate at 0: an explicit
|
|
91
|
+
* yMin/yMax can only extend the domain, never lift it off zero.
|
|
92
|
+
*/
|
|
93
|
+
yMin: z.number().finite().optional(),
|
|
94
|
+
yMax: z.number().finite().optional(),
|
|
95
|
+
/** Phase-3 live data: key into ctx.data whose value replaces params. */
|
|
96
|
+
dataKey: z.string().max(80).optional(),
|
|
97
|
+
})
|
|
98
|
+
.superRefine((params, ctx) => {
|
|
99
|
+
const total = params.series.reduce((n, s) => n + s.points.length, 0);
|
|
100
|
+
if (total > MAX_POINTS_TOTAL)
|
|
101
|
+
ctx.addIssue({
|
|
102
|
+
code: "custom",
|
|
103
|
+
path: ["series"],
|
|
104
|
+
message: `at most ${MAX_POINTS_TOTAL} points across all series (got ${total})`,
|
|
105
|
+
});
|
|
106
|
+
if (params.stacked &&
|
|
107
|
+
(params.chartType === "line" || params.chartType === "scatter"))
|
|
108
|
+
ctx.addIssue({
|
|
109
|
+
code: "custom",
|
|
110
|
+
path: ["stacked"],
|
|
111
|
+
message: "stacked applies to bar and area charts only",
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
// ── X parsing/normalization ────────────────────────────────────────────────
|
|
115
|
+
/** ISO date-only strings are parsed as LOCAL midnight so a viewer's
|
|
116
|
+
* timezone can't shift which day a tick names (a UTC parse + local format
|
|
117
|
+
* renders "Jan 4" for the 5th west of Greenwich). Full ISO timestamps and
|
|
118
|
+
* epoch milliseconds parse normally. */
|
|
119
|
+
export function parseTimeX(x) {
|
|
120
|
+
if (typeof x === "number")
|
|
121
|
+
return x;
|
|
122
|
+
const m = /^(\d{4})-(\d{2})(?:-(\d{2}))?$/.exec(x.trim());
|
|
123
|
+
if (m) {
|
|
124
|
+
const d = new Date(Number(m[1]), Number(m[2]) - 1, m[3] ? Number(m[3]) : 1);
|
|
125
|
+
const ms = d.getTime();
|
|
126
|
+
return Number.isFinite(ms) ? ms : NaN;
|
|
127
|
+
}
|
|
128
|
+
const ms = new Date(x).getTime();
|
|
129
|
+
return Number.isFinite(ms) ? ms : NaN;
|
|
130
|
+
}
|
|
131
|
+
function parseXNum(raw, xType) {
|
|
132
|
+
if (xType === "time")
|
|
133
|
+
return parseTimeX(raw);
|
|
134
|
+
if (xType === "number") {
|
|
135
|
+
const n = typeof raw === "number" ? raw : Number.parseFloat(raw);
|
|
136
|
+
return Number.isFinite(n) ? n : NaN;
|
|
137
|
+
}
|
|
138
|
+
// category — handled by the caller (needs the label→index map)
|
|
139
|
+
return NaN;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Normalize params for rendering: parse x per axis type, drop unparseable
|
|
143
|
+
* points, sort each series by x, and dedupe repeated x values keeping the
|
|
144
|
+
* LAST write (an agent re-sending a corrected value reads as an update).
|
|
145
|
+
*/
|
|
146
|
+
export function normalizeChart(params) {
|
|
147
|
+
const categories = [];
|
|
148
|
+
const catIndex = new Map();
|
|
149
|
+
const series = params.series.map((s) => {
|
|
150
|
+
const parsed = [];
|
|
151
|
+
for (const p of s.points) {
|
|
152
|
+
let xNum;
|
|
153
|
+
if (params.xType === "category") {
|
|
154
|
+
const label = String(p.x);
|
|
155
|
+
let idx = catIndex.get(label);
|
|
156
|
+
if (idx === undefined) {
|
|
157
|
+
idx = categories.length;
|
|
158
|
+
catIndex.set(label, idx);
|
|
159
|
+
categories.push(label);
|
|
160
|
+
}
|
|
161
|
+
xNum = idx;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
xNum = parseXNum(p.x, params.xType);
|
|
165
|
+
}
|
|
166
|
+
if (Number.isFinite(xNum))
|
|
167
|
+
parsed.push({ xNum, y: p.y });
|
|
168
|
+
}
|
|
169
|
+
parsed.sort((a, b) => a.xNum - b.xNum);
|
|
170
|
+
const points = [];
|
|
171
|
+
for (const p of parsed) {
|
|
172
|
+
const prev = points.at(-1);
|
|
173
|
+
if (prev && prev.xNum === p.xNum)
|
|
174
|
+
prev.y = p.y;
|
|
175
|
+
else
|
|
176
|
+
points.push(p);
|
|
177
|
+
}
|
|
178
|
+
return { label: s.label, axis: s.axis, points };
|
|
179
|
+
});
|
|
180
|
+
const xNumSet = new Set();
|
|
181
|
+
for (const s of series)
|
|
182
|
+
for (const p of s.points)
|
|
183
|
+
xNumSet.add(p.xNum);
|
|
184
|
+
const xNums = [...xNumSet].sort((a, b) => a - b);
|
|
185
|
+
return {
|
|
186
|
+
xType: params.xType,
|
|
187
|
+
categories,
|
|
188
|
+
xNums,
|
|
189
|
+
series,
|
|
190
|
+
dual: params.series.some((s) => s.axis === "right"),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// ── Value domain + ticks ──────────────────────────────────────────────────
|
|
194
|
+
/** d3-style "nice" linear ticks: a round step whose multiples enclose
|
|
195
|
+
* [min, max]. Returns the ticks and the enclosing domain. */
|
|
196
|
+
export function linearTicks(min, max, count = 5) {
|
|
197
|
+
if (!Number.isFinite(min) || !Number.isFinite(max))
|
|
198
|
+
return { ticks: [], domain: [0, 1] };
|
|
199
|
+
if (min === max) {
|
|
200
|
+
if (min === 0)
|
|
201
|
+
return { ticks: [0, 1], domain: [0, 1] };
|
|
202
|
+
const pad = Math.abs(min) * 0.1;
|
|
203
|
+
min -= pad;
|
|
204
|
+
max += pad;
|
|
205
|
+
}
|
|
206
|
+
const rough = (max - min) / Math.max(1, count);
|
|
207
|
+
const mag = 10 ** Math.floor(Math.log10(rough));
|
|
208
|
+
const norm = rough / mag;
|
|
209
|
+
const step = (norm > 5 ? 10 : norm > 2 ? 5 : norm > 1 ? 2 : 1) * mag;
|
|
210
|
+
const lo = Math.floor(min / step) * step;
|
|
211
|
+
const hi = Math.ceil(max / step) * step;
|
|
212
|
+
const ticks = [];
|
|
213
|
+
for (let v = lo; v <= hi && ticks.length < 400; v += step)
|
|
214
|
+
ticks.push(Math.round(v * 1e10) / 1e10);
|
|
215
|
+
return { ticks, domain: [lo, hi] };
|
|
216
|
+
}
|
|
217
|
+
/** Value-axis domain for a chart. `valueAxis` marks charts that encode
|
|
218
|
+
* magnitude (bar/area): their domain always includes 0 — an explicit
|
|
219
|
+
* yMin/yMax may extend it downward/upward but never truncate. */
|
|
220
|
+
export function valueDomain(values, chartType, opts = {}) {
|
|
221
|
+
const finite = values.filter((v) => Number.isFinite(v));
|
|
222
|
+
const dataMin = finite.length ? Math.min(...finite) : 0;
|
|
223
|
+
const dataMax = finite.length ? Math.max(...finite) : 1;
|
|
224
|
+
let min;
|
|
225
|
+
let max;
|
|
226
|
+
if (chartType === "bar" || chartType === "area") {
|
|
227
|
+
min = Math.min(0, dataMin, opts.yMin ?? Number.POSITIVE_INFINITY);
|
|
228
|
+
max = Math.max(0, dataMax, opts.yMax ?? Number.NEGATIVE_INFINITY);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
min = Math.min(opts.yMin ?? dataMin, dataMin);
|
|
232
|
+
max = Math.max(opts.yMax ?? dataMax, dataMax);
|
|
233
|
+
}
|
|
234
|
+
if (min === max) {
|
|
235
|
+
if (min === 0)
|
|
236
|
+
return [0, 1];
|
|
237
|
+
const pad = Math.abs(min) * 0.1;
|
|
238
|
+
min -= pad;
|
|
239
|
+
max += pad;
|
|
240
|
+
}
|
|
241
|
+
return [min, max];
|
|
242
|
+
}
|
|
243
|
+
const TIME_STEPS = [
|
|
244
|
+
{ unit: "hour", step: 1, approxMs: 3.6e6 },
|
|
245
|
+
{ unit: "hour", step: 3, approxMs: 3 * 3.6e6 },
|
|
246
|
+
{ unit: "hour", step: 6, approxMs: 6 * 3.6e6 },
|
|
247
|
+
{ unit: "hour", step: 12, approxMs: 12 * 3.6e6 },
|
|
248
|
+
{ unit: "day", step: 1, approxMs: 86.4e6 },
|
|
249
|
+
{ unit: "day", step: 2, approxMs: 2 * 86.4e6 },
|
|
250
|
+
{ unit: "week", step: 1, approxMs: 7 * 86.4e6 },
|
|
251
|
+
{ unit: "month", step: 1, approxMs: 30.4 * 86.4e6 },
|
|
252
|
+
{ unit: "month", step: 3, approxMs: 3 * 30.4 * 86.4e6 },
|
|
253
|
+
{ unit: "year", step: 1, approxMs: 365.25 * 86.4e6 },
|
|
254
|
+
{ unit: "year", step: 2, approxMs: 2 * 365.25 * 86.4e6 },
|
|
255
|
+
{ unit: "year", step: 5, approxMs: 5 * 365.25 * 86.4e6 },
|
|
256
|
+
];
|
|
257
|
+
// Local-time calendar math on native Date (what dayjs did in MOS — jatui
|
|
258
|
+
// carries no date library). Weeks start on Sunday, as dayjs' default locale.
|
|
259
|
+
function floorToStep(ms, unit, step) {
|
|
260
|
+
const d = new Date(ms);
|
|
261
|
+
switch (unit) {
|
|
262
|
+
case "hour":
|
|
263
|
+
d.setMinutes(0, 0, 0);
|
|
264
|
+
d.setHours(d.getHours() - (d.getHours() % step));
|
|
265
|
+
return d.getTime();
|
|
266
|
+
case "day":
|
|
267
|
+
d.setHours(0, 0, 0, 0);
|
|
268
|
+
return d.getTime();
|
|
269
|
+
case "week":
|
|
270
|
+
d.setHours(0, 0, 0, 0);
|
|
271
|
+
d.setDate(d.getDate() - d.getDay());
|
|
272
|
+
return d.getTime();
|
|
273
|
+
case "month":
|
|
274
|
+
d.setHours(0, 0, 0, 0);
|
|
275
|
+
d.setDate(1);
|
|
276
|
+
d.setMonth(d.getMonth() - (d.getMonth() % step));
|
|
277
|
+
return d.getTime();
|
|
278
|
+
case "year":
|
|
279
|
+
d.setHours(0, 0, 0, 0);
|
|
280
|
+
d.setMonth(0, 1);
|
|
281
|
+
d.setFullYear(d.getFullYear() - (d.getFullYear() % step));
|
|
282
|
+
return d.getTime();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Advance a tick by `step` units. Hours are absolute (DST-proof spacing);
|
|
286
|
+
* days, weeks, months and years are calendar steps from an already-floored
|
|
287
|
+
* tick, so a month step never lands on a clamped day-of-month. */
|
|
288
|
+
function addStep(ms, unit, step) {
|
|
289
|
+
if (unit === "hour")
|
|
290
|
+
return ms + step * 3.6e6;
|
|
291
|
+
const d = new Date(ms);
|
|
292
|
+
if (unit === "day")
|
|
293
|
+
d.setDate(d.getDate() + step);
|
|
294
|
+
else if (unit === "week")
|
|
295
|
+
d.setDate(d.getDate() + 7 * step);
|
|
296
|
+
else if (unit === "month")
|
|
297
|
+
d.setMonth(d.getMonth() + step);
|
|
298
|
+
else
|
|
299
|
+
d.setFullYear(d.getFullYear() + step);
|
|
300
|
+
return d.getTime();
|
|
301
|
+
}
|
|
302
|
+
/** Calendar-aware tick positions for a time axis: ~`target` ticks, snapped
|
|
303
|
+
* to real hour/day/week/month/year boundaries (never "Jan 31 15:37").
|
|
304
|
+
* Picks the step whose tick count is CLOSEST to the target — "first step
|
|
305
|
+
* above the ideal" jumps from a week to a month the moment a span crosses
|
|
306
|
+
* 7·target days, collapsing 7 good ticks into 2. */
|
|
307
|
+
export function timeTicks(minMs, maxMs, target = 6) {
|
|
308
|
+
if (!Number.isFinite(minMs) || !Number.isFinite(maxMs))
|
|
309
|
+
return [];
|
|
310
|
+
const span = Math.max(1, maxMs - minMs);
|
|
311
|
+
let pick = TIME_STEPS[TIME_STEPS.length - 1];
|
|
312
|
+
let bestErr = Infinity;
|
|
313
|
+
for (const s of TIME_STEPS) {
|
|
314
|
+
const err = Math.abs(span / s.approxMs - target);
|
|
315
|
+
if (err < bestErr) {
|
|
316
|
+
bestErr = err;
|
|
317
|
+
pick = s;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
let t = floorToStep(minMs, pick.unit, pick.step);
|
|
321
|
+
const out = [];
|
|
322
|
+
while (t <= maxMs && out.length < 60) {
|
|
323
|
+
if (t >= minMs)
|
|
324
|
+
out.push(t);
|
|
325
|
+
t = addStep(t, pick.unit, pick.step);
|
|
326
|
+
if (!Number.isFinite(t))
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
return out.length ? out : [minMs, maxMs];
|
|
330
|
+
}
|
|
331
|
+
/** A tick formatter that says what the span needs (no redundant year in a
|
|
332
|
+
* same-year span; hours get their date so a 3-day hourly chart reads). */
|
|
333
|
+
export function timeTickFormat(minMs, maxMs) {
|
|
334
|
+
const spanDays = (maxMs - minMs) / 86.4e6;
|
|
335
|
+
const fmt = (opts) => new Intl.DateTimeFormat("en-US", opts).format;
|
|
336
|
+
if (spanDays < 1)
|
|
337
|
+
return fmt({ month: "short", day: "numeric", hour: "numeric" });
|
|
338
|
+
const crossesYear = new Date(minMs).getFullYear() !== new Date(maxMs).getFullYear();
|
|
339
|
+
if (spanDays <= 92)
|
|
340
|
+
return fmt({ month: "short", day: "numeric" });
|
|
341
|
+
if (spanDays <= 730)
|
|
342
|
+
return fmt(crossesYear
|
|
343
|
+
? { month: "short", year: "numeric" }
|
|
344
|
+
: { month: "short", day: "numeric" });
|
|
345
|
+
return fmt({ year: "numeric" });
|
|
346
|
+
}
|
|
347
|
+
// ── Formatting ────────────────────────────────────────────────────────────
|
|
348
|
+
/** Fixed en-US so a chart reads identically for every viewer and every
|
|
349
|
+
* snapshot/test; the locale is a formatting choice, not user data. */
|
|
350
|
+
export function formatYValue(v, fmt, currency) {
|
|
351
|
+
if (!Number.isFinite(v))
|
|
352
|
+
return "n/a";
|
|
353
|
+
switch (fmt) {
|
|
354
|
+
case "compact":
|
|
355
|
+
return new Intl.NumberFormat("en-US", {
|
|
356
|
+
notation: "compact",
|
|
357
|
+
maximumFractionDigits: 1,
|
|
358
|
+
}).format(v);
|
|
359
|
+
case "percent":
|
|
360
|
+
return `${new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 }).format(v)}%`;
|
|
361
|
+
case "currency":
|
|
362
|
+
return new Intl.NumberFormat("en-US", {
|
|
363
|
+
style: "currency",
|
|
364
|
+
currency,
|
|
365
|
+
notation: Math.abs(v) >= 10000 ? "compact" : "standard",
|
|
366
|
+
maximumFractionDigits: Math.abs(v) >= 10000 ? 1 : 2,
|
|
367
|
+
}).format(v);
|
|
368
|
+
default:
|
|
369
|
+
return new Intl.NumberFormat("en-US", {
|
|
370
|
+
maximumFractionDigits: 2,
|
|
371
|
+
}).format(v);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/** Compact axis ticks stay round ("2k", "2M") — maximumFractionDigits 0. */
|
|
375
|
+
export function formatYTick(v, fmt, currency) {
|
|
376
|
+
if (fmt === "raw")
|
|
377
|
+
return Math.abs(v) >= 10000
|
|
378
|
+
? new Intl.NumberFormat("en-US", {
|
|
379
|
+
notation: "compact",
|
|
380
|
+
maximumFractionDigits: 1,
|
|
381
|
+
}).format(v)
|
|
382
|
+
: new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(v);
|
|
383
|
+
if (fmt === "currency" && Math.abs(v) >= 10000)
|
|
384
|
+
return new Intl.NumberFormat("en-US", {
|
|
385
|
+
style: "currency",
|
|
386
|
+
currency,
|
|
387
|
+
notation: "compact",
|
|
388
|
+
maximumFractionDigits: 1,
|
|
389
|
+
}).format(v);
|
|
390
|
+
return formatYValue(v, fmt, currency);
|
|
391
|
+
}
|
|
392
|
+
/** The data-table fallback always shows FULL precision — it exists to give
|
|
393
|
+
* exact values (screen readers, copy/paste); compact notation belongs to the
|
|
394
|
+
* chart marks and tooltip. */
|
|
395
|
+
export function formatTableValue(v, fmt, currency) {
|
|
396
|
+
if (!Number.isFinite(v))
|
|
397
|
+
return "n/a";
|
|
398
|
+
if (fmt === "currency")
|
|
399
|
+
return new Intl.NumberFormat("en-US", {
|
|
400
|
+
style: "currency",
|
|
401
|
+
currency,
|
|
402
|
+
maximumFractionDigits: 2,
|
|
403
|
+
}).format(v);
|
|
404
|
+
if (fmt === "percent")
|
|
405
|
+
return `${new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(v)}%`;
|
|
406
|
+
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(v);
|
|
407
|
+
}
|
|
408
|
+
// ── Interaction geometry ──────────────────────────────────────────────────
|
|
409
|
+
/** Index of the value closest to `target` (the crosshair's x pick). */
|
|
410
|
+
export function nearestIndex(values, target) {
|
|
411
|
+
if (!values.length)
|
|
412
|
+
return -1;
|
|
413
|
+
let best = 0;
|
|
414
|
+
for (let i = 1; i < values.length; i++)
|
|
415
|
+
if (Math.abs(values[i] - target) < Math.abs(values[best] - target))
|
|
416
|
+
best = i;
|
|
417
|
+
return best;
|
|
418
|
+
}
|
|
419
|
+
/** Stack series into cumulative [y0, y1] segments per shared x. Missing
|
|
420
|
+
* points contribute 0 (later series still stack in their lane). Points at
|
|
421
|
+
* the same x within one series are summed (dedupe already keeps the last,
|
|
422
|
+
* so this only fires when an agent stacks pre-aggregated data). */
|
|
423
|
+
export function stackByX(series) {
|
|
424
|
+
const n = series.length;
|
|
425
|
+
const byX = new Map();
|
|
426
|
+
series.forEach((pts, si) => {
|
|
427
|
+
for (const p of pts) {
|
|
428
|
+
let row = byX.get(p.xNum);
|
|
429
|
+
if (!row) {
|
|
430
|
+
row = new Array(n).fill(undefined);
|
|
431
|
+
byX.set(p.xNum, row);
|
|
432
|
+
}
|
|
433
|
+
row[si] = (row[si] ?? 0) + p.y;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
const xNums = [...byX.keys()].sort((a, b) => a - b);
|
|
437
|
+
return series.map((_, si) => {
|
|
438
|
+
const segs = [];
|
|
439
|
+
for (const xNum of xNums) {
|
|
440
|
+
const row = byX.get(xNum);
|
|
441
|
+
const present = row?.[si] !== undefined;
|
|
442
|
+
if (!present)
|
|
443
|
+
continue;
|
|
444
|
+
let acc = 0;
|
|
445
|
+
for (let j = 0; j < si; j++)
|
|
446
|
+
acc += row?.[j] ?? 0;
|
|
447
|
+
segs.push({ xNum, y0: acc, y1: acc + (row?.[si] ?? 0) });
|
|
448
|
+
}
|
|
449
|
+
return segs;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
// ── End-label collision layout ────────────────────────────────────────────
|
|
453
|
+
/** Push apart y positions so series labels at line ends never overlap:
|
|
454
|
+
* forward pass pushes down, backward pass pulls the overflow back up.
|
|
455
|
+
* Positions are clamped inside [top, bottom]. */
|
|
456
|
+
export function layoutEndLabels(items, top, bottom, gap = 13) {
|
|
457
|
+
const order = items.map((it, i) => ({ i, y: it.y })).sort((a, b) => a.y - b.y);
|
|
458
|
+
const half = gap / 2;
|
|
459
|
+
const placed = new Array(items.length).fill(0);
|
|
460
|
+
let prev = -Infinity;
|
|
461
|
+
for (const o of order) {
|
|
462
|
+
const y = Math.min(Math.max(o.y, top + half), bottom - half);
|
|
463
|
+
placed[o.i] = Math.max(y, prev + gap);
|
|
464
|
+
prev = placed[o.i];
|
|
465
|
+
}
|
|
466
|
+
let next = Infinity;
|
|
467
|
+
for (let k = order.length - 1; k >= 0; k--) {
|
|
468
|
+
const o = order[k];
|
|
469
|
+
placed[o.i] = Math.min(placed[o.i], next - gap, bottom - half);
|
|
470
|
+
next = placed[o.i];
|
|
471
|
+
}
|
|
472
|
+
return placed.map((y) => Math.min(Math.max(y, top + half), bottom - half));
|
|
473
|
+
}
|
|
474
|
+
// ── A11y table rows ───────────────────────────────────────────────────────
|
|
475
|
+
/** Per-series x → y lookup (O(points) build; the render loop hits it per
|
|
476
|
+
* slot, so a `.find()` per cell would be quadratic on 5000-point charts). */
|
|
477
|
+
export function valueMap(points) {
|
|
478
|
+
const m = new Map();
|
|
479
|
+
for (const p of points)
|
|
480
|
+
m.set(p.xNum, p.y);
|
|
481
|
+
return m;
|
|
482
|
+
}
|
|
483
|
+
/** One row per shared x; series values aligned by x (null = not present). */
|
|
484
|
+
export function dataTableRows(chart) {
|
|
485
|
+
const maps = chart.series.map((s) => valueMap(s.points));
|
|
486
|
+
return chart.xNums.map((xNum) => ({
|
|
487
|
+
x: xNum,
|
|
488
|
+
xLabel: chart.xType === "category"
|
|
489
|
+
? (chart.categories[xNum] ?? String(xNum))
|
|
490
|
+
: chart.xType === "time"
|
|
491
|
+
? new Intl.DateTimeFormat("en-US", {
|
|
492
|
+
dateStyle: "medium",
|
|
493
|
+
timeStyle: "short",
|
|
494
|
+
}).format(xNum)
|
|
495
|
+
: String(xNum),
|
|
496
|
+
values: maps.map((m) => m.get(xNum) ?? null),
|
|
497
|
+
}));
|
|
498
|
+
}
|