@joewinke/jatui 0.37.0 → 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/package.json +11 -2
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* ChartBlock — SVG line / bar / area / scatter renderer (block type "chart").
|
|
4
|
+
*
|
|
5
|
+
* Hand-rolled SVG, no chart dependency: scales, ticks and stacking live in
|
|
6
|
+
* ./schema.ts (pure, unit-tested); this file is the render layer.
|
|
7
|
+
*
|
|
8
|
+
* Dataviz discipline (the ruby-kim-soak rules):
|
|
9
|
+
* - recessive grid (base-content/10, horizontal only),
|
|
10
|
+
* - honest axes (value axis always includes 0 for bar/area — an explicit
|
|
11
|
+
* yMin/yMax can extend the domain but never truncate it),
|
|
12
|
+
* - identity never by color alone (series name in the legend chips, in the
|
|
13
|
+
* direct end-labels, in the tooltip, and in the data-table fallback).
|
|
14
|
+
*
|
|
15
|
+
* Colors are tokens only: series strokes come from the `--chart-series-1..6`
|
|
16
|
+
* contract (src/lib/theme-bridge/layerchart.css, mapped to DaisyUI palette
|
|
17
|
+
* tokens) — this wrapper carries the `lc-root-container` class so those vars
|
|
18
|
+
* resolve, with `var(--color-primary)` as the fallback.
|
|
19
|
+
*/
|
|
20
|
+
import type { BlockProps } from "../../types.js"
|
|
21
|
+
import {
|
|
22
|
+
chartSchema,
|
|
23
|
+
normalizeChart,
|
|
24
|
+
linearTicks,
|
|
25
|
+
valueDomain,
|
|
26
|
+
timeTicks,
|
|
27
|
+
timeTickFormat,
|
|
28
|
+
formatYValue,
|
|
29
|
+
formatYTick,
|
|
30
|
+
nearestIndex,
|
|
31
|
+
stackByX,
|
|
32
|
+
layoutEndLabels,
|
|
33
|
+
dataTableRows,
|
|
34
|
+
formatTableValue,
|
|
35
|
+
valueMap,
|
|
36
|
+
parseTimeX,
|
|
37
|
+
END_LABEL_SERIES_LIMIT,
|
|
38
|
+
type ChartParams,
|
|
39
|
+
} from "./schema.js"
|
|
40
|
+
import SectionTools from "../../SectionTools.svelte"
|
|
41
|
+
|
|
42
|
+
let { params: rawParams, ctx }: BlockProps<ChartParams> = $props()
|
|
43
|
+
|
|
44
|
+
/** Params are parsed here, not assumed pre-parsed: the host validates at
|
|
45
|
+
* publish but stores what was sent, so defaults (xType, axis, goalLines…)
|
|
46
|
+
* must be applied at render. Corrupt params fall back to an empty render —
|
|
47
|
+
* a bad agent payload must never throw inside the artifact page. */
|
|
48
|
+
const EMPTY_PARAMS: ChartParams = {
|
|
49
|
+
chartType: "line",
|
|
50
|
+
xType: "category",
|
|
51
|
+
series: [],
|
|
52
|
+
stacked: false,
|
|
53
|
+
goalLines: [],
|
|
54
|
+
annotations: [],
|
|
55
|
+
yFormat: "raw",
|
|
56
|
+
currency: "USD",
|
|
57
|
+
showLegend: true,
|
|
58
|
+
endLabels: true,
|
|
59
|
+
height: 280,
|
|
60
|
+
}
|
|
61
|
+
const parsed = $derived(chartSchema.safeParse(rawParams))
|
|
62
|
+
const params = $derived(parsed.success ? parsed.data : EMPTY_PARAMS)
|
|
63
|
+
|
|
64
|
+
// ── Responsive width (SSR/tests: 640 fallback) ──────────────────────────
|
|
65
|
+
let containerEl = $state<HTMLDivElement | undefined>(undefined)
|
|
66
|
+
let measured = $state(0)
|
|
67
|
+
$effect(() => {
|
|
68
|
+
if (!containerEl) return
|
|
69
|
+
const update = () => {
|
|
70
|
+
measured = containerEl?.offsetWidth ?? 0
|
|
71
|
+
}
|
|
72
|
+
update()
|
|
73
|
+
if (typeof ResizeObserver === "undefined") return
|
|
74
|
+
const ro = new ResizeObserver(update)
|
|
75
|
+
ro.observe(containerEl)
|
|
76
|
+
return () => ro.disconnect()
|
|
77
|
+
})
|
|
78
|
+
const W = $derived(measured > 160 ? measured : 640)
|
|
79
|
+
const H = $derived(params.height)
|
|
80
|
+
|
|
81
|
+
// ── Normalized data ──────────────────────────────────────────────────────
|
|
82
|
+
const norm = $derived(normalizeChart(params))
|
|
83
|
+
|
|
84
|
+
const endLabelsOn = $derived(
|
|
85
|
+
params.endLabels &&
|
|
86
|
+
(params.chartType === "line" || params.chartType === "area") &&
|
|
87
|
+
norm.series.length <= END_LABEL_SERIES_LIMIT &&
|
|
88
|
+
norm.xNums.length > 0,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const seriesColor = (i: number) =>
|
|
92
|
+
`var(--chart-series-${(i % 6) + 1}, var(--color-primary))`
|
|
93
|
+
const seriesAreaFill = (i: number) =>
|
|
94
|
+
`color-mix(in oklch, ${seriesColor(i)} 22%, transparent)`
|
|
95
|
+
const seriesAreaFillStacked = (i: number) =>
|
|
96
|
+
`color-mix(in oklch, ${seriesColor(i)} 45%, transparent)`
|
|
97
|
+
|
|
98
|
+
// ── Geometry ─────────────────────────────────────────────────────────────
|
|
99
|
+
const padLeft = $derived(params.yFormat === "currency" ? 58 : 46)
|
|
100
|
+
const padRight = $derived((endLabelsOn ? 88 : 12) + (norm.dual ? 48 : 0))
|
|
101
|
+
const padTop = 14
|
|
102
|
+
const padBottom = 28
|
|
103
|
+
const plotW = $derived(Math.max(40, W - padLeft - padRight))
|
|
104
|
+
const plotH = $derived(Math.max(40, H - padTop - padBottom))
|
|
105
|
+
const plotBottom = $derived(padTop + plotH)
|
|
106
|
+
|
|
107
|
+
// X: category = one slot per value (bars and lines share slot geometry);
|
|
108
|
+
// time and number = padded linear scale (bars still slot per distinct x).
|
|
109
|
+
const usesSlots = $derived(
|
|
110
|
+
params.chartType === "bar" || norm.xType === "category",
|
|
111
|
+
)
|
|
112
|
+
const slotW = $derived(plotW / Math.max(1, norm.xNums.length))
|
|
113
|
+
const catPos = $derived(new Map(norm.xNums.map((x, i) => [x, i] as const)))
|
|
114
|
+
const xLinear = $derived.by(() => {
|
|
115
|
+
const xs = norm.xNums
|
|
116
|
+
if (usesSlots || xs.length === 0) return null
|
|
117
|
+
const min = xs[0]
|
|
118
|
+
const max = xs[xs.length - 1]
|
|
119
|
+
if (min === max) {
|
|
120
|
+
const pad = norm.xType === "time" ? 86.4e6 : 1
|
|
121
|
+
return { d0: min - pad, d1: max + pad }
|
|
122
|
+
}
|
|
123
|
+
const pad = (max - min) * 0.02
|
|
124
|
+
return { d0: min - pad, d1: max + pad }
|
|
125
|
+
})
|
|
126
|
+
const xPx = (xNum: number): number => {
|
|
127
|
+
if (usesSlots) {
|
|
128
|
+
const i = catPos.get(xNum) ?? 0
|
|
129
|
+
return padLeft + (i + 0.5) * slotW
|
|
130
|
+
}
|
|
131
|
+
const lin = xLinear
|
|
132
|
+
if (!lin) return padLeft + plotW / 2
|
|
133
|
+
return padLeft + ((xNum - lin.d0) / (lin.d1 - lin.d0)) * plotW
|
|
134
|
+
}
|
|
135
|
+
const slotLeft = (i: number) => padLeft + i * slotW
|
|
136
|
+
const slotCenterOf = (i: number) => padLeft + (i + 0.5) * slotW
|
|
137
|
+
|
|
138
|
+
// ── Stacking (per value-axis group) ─────────────────────────────────────
|
|
139
|
+
type Seg = { xNum: number; y0: number; y1: number }
|
|
140
|
+
const stackedSegs = $derived.by(() => {
|
|
141
|
+
if (!params.stacked) return null
|
|
142
|
+
const segs: (Seg[] | undefined)[] = new Array(norm.series.length)
|
|
143
|
+
for (const axis of ["left", "right"] as const) {
|
|
144
|
+
const idxs = norm.series
|
|
145
|
+
.map((s, i) => (s.axis === axis ? i : -1))
|
|
146
|
+
.filter((i) => i >= 0)
|
|
147
|
+
const stacked = stackByX(idxs.map((i) => norm.series[i].points))
|
|
148
|
+
idxs.forEach((si, k) => (segs[si] = stacked[k]))
|
|
149
|
+
}
|
|
150
|
+
return segs
|
|
151
|
+
})
|
|
152
|
+
/** Per-series x → segment map (bar marks hit this per slot). */
|
|
153
|
+
const stackedSegMaps = $derived.by(() => {
|
|
154
|
+
if (!params.stacked || !stackedSegs) return null
|
|
155
|
+
return stackedSegs.map(
|
|
156
|
+
(segs) => new Map((segs ?? []).map((s) => [s.xNum, s] as const)),
|
|
157
|
+
)
|
|
158
|
+
})
|
|
159
|
+
/** Per-series x → y map (the render loop hits this per slot — a `.find()`
|
|
160
|
+
* per cell would be quadratic on 5000-point charts). */
|
|
161
|
+
const pointMaps = $derived(norm.series.map((s) => valueMap(s.points)))
|
|
162
|
+
function valueAt(si: number, xNum: number): number | null {
|
|
163
|
+
return pointMaps[si]?.get(xNum) ?? null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Value scales ─────────────────────────────────────────────────────────
|
|
167
|
+
function axisVals(axis: "left" | "right"): number[] {
|
|
168
|
+
const vals: number[] = []
|
|
169
|
+
if (params.stacked && stackedSegs) {
|
|
170
|
+
norm.series.forEach((s, i) => {
|
|
171
|
+
if (s.axis !== axis) return
|
|
172
|
+
for (const seg of stackedSegs[i] ?? []) vals.push(seg.y0, seg.y1)
|
|
173
|
+
})
|
|
174
|
+
} else {
|
|
175
|
+
for (const s of norm.series) {
|
|
176
|
+
if (s.axis !== axis) continue
|
|
177
|
+
for (const p of s.points) vals.push(p.y)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const g of params.goalLines) if (g.axis === axis) vals.push(g.value)
|
|
181
|
+
if (axis === "left")
|
|
182
|
+
for (const a of params.annotations) if (a.y !== undefined) vals.push(a.y)
|
|
183
|
+
return vals
|
|
184
|
+
}
|
|
185
|
+
const leftScale = $derived(
|
|
186
|
+
linearTicks(
|
|
187
|
+
...valueDomain(axisVals("left"), params.chartType, {
|
|
188
|
+
yMin: params.yMin,
|
|
189
|
+
yMax: params.yMax,
|
|
190
|
+
}),
|
|
191
|
+
5,
|
|
192
|
+
),
|
|
193
|
+
)
|
|
194
|
+
const rightScale = $derived(
|
|
195
|
+
norm.dual
|
|
196
|
+
? linearTicks(...valueDomain(axisVals("right"), params.chartType, {}), 4)
|
|
197
|
+
: null,
|
|
198
|
+
)
|
|
199
|
+
const yPxLeft = (v: number) =>
|
|
200
|
+
padTop +
|
|
201
|
+
plotH -
|
|
202
|
+
((v - leftScale.domain[0]) /
|
|
203
|
+
(leftScale.domain[1] - leftScale.domain[0] || 1)) *
|
|
204
|
+
plotH
|
|
205
|
+
const yPxRight = (v: number) => {
|
|
206
|
+
const s = rightScale
|
|
207
|
+
if (!s) return yPxLeft(v)
|
|
208
|
+
return (
|
|
209
|
+
padTop +
|
|
210
|
+
plotH -
|
|
211
|
+
((v - s.domain[0]) / (s.domain[1] - s.domain[0] || 1)) * plotH
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
const yPxFor = (axis: "left" | "right") =>
|
|
215
|
+
axis === "right" ? yPxRight : yPxLeft
|
|
216
|
+
/** Bars rise from zero (or below zero when negatives exist). */
|
|
217
|
+
function zeroYFor(axis: "left" | "right"): number {
|
|
218
|
+
if (axis === "right") {
|
|
219
|
+
const s = rightScale
|
|
220
|
+
if (!s) return yPxLeft(0)
|
|
221
|
+
return yPxRight(Math.max(s.domain[0], Math.min(0, s.domain[1])))
|
|
222
|
+
}
|
|
223
|
+
return yPxLeft(
|
|
224
|
+
Math.max(leftScale.domain[0], Math.min(0, leftScale.domain[1])),
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
const baselineY = $derived(zeroYFor("left"))
|
|
228
|
+
|
|
229
|
+
// ── Paths ────────────────────────────────────────────────────────────────
|
|
230
|
+
const r1 = (n: number) => Math.round(n * 10) / 10
|
|
231
|
+
function linePathOf(si: number): string {
|
|
232
|
+
const s = norm.series[si]
|
|
233
|
+
const yPx = yPxFor(s.axis)
|
|
234
|
+
const segs = params.stacked ? stackedSegs?.[si] : undefined
|
|
235
|
+
if (segs)
|
|
236
|
+
return segs
|
|
237
|
+
.map(
|
|
238
|
+
(seg, k) =>
|
|
239
|
+
`${k ? "L" : "M"} ${r1(xPx(seg.xNum))} ${r1(yPx(seg.y1))}`,
|
|
240
|
+
)
|
|
241
|
+
.join(" ")
|
|
242
|
+
return s.points
|
|
243
|
+
.map((p, k) => `${k ? "L" : "M"} ${r1(xPx(p.xNum))} ${r1(yPx(p.y))}`)
|
|
244
|
+
.join(" ")
|
|
245
|
+
}
|
|
246
|
+
function areaPathOf(si: number): string {
|
|
247
|
+
const s = norm.series[si]
|
|
248
|
+
const yPx = yPxFor(s.axis)
|
|
249
|
+
const segs = params.stacked ? stackedSegs?.[si] : undefined
|
|
250
|
+
if (segs) {
|
|
251
|
+
if (!segs.length) return ""
|
|
252
|
+
const down = [...segs]
|
|
253
|
+
.reverse()
|
|
254
|
+
.map((seg) => `L ${r1(xPx(seg.xNum))} ${r1(yPx(seg.y0))}`)
|
|
255
|
+
.join(" ")
|
|
256
|
+
return (
|
|
257
|
+
segs
|
|
258
|
+
.map(
|
|
259
|
+
(seg, k) =>
|
|
260
|
+
`${k ? "L" : "M"} ${r1(xPx(seg.xNum))} ${r1(yPx(seg.y1))}`,
|
|
261
|
+
)
|
|
262
|
+
.join(" ") +
|
|
263
|
+
" " +
|
|
264
|
+
down +
|
|
265
|
+
" Z"
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
const pts = s.points
|
|
269
|
+
if (!pts.length) return ""
|
|
270
|
+
const path = pts
|
|
271
|
+
.map((p, k) => `${k ? "L" : "M"} ${r1(xPx(p.xNum))} ${r1(yPx(p.y))}`)
|
|
272
|
+
.join(" ")
|
|
273
|
+
const first = pts[0]
|
|
274
|
+
const last = pts[pts.length - 1]
|
|
275
|
+
return `${path} L ${r1(xPx(last.xNum))} ${r1(baselineY)} L ${r1(xPx(first.xNum))} ${r1(baselineY)} Z`
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Bars ─────────────────────────────────────────────────────────────────
|
|
279
|
+
const barW = $derived(
|
|
280
|
+
Math.max(1, Math.min((slotW * 0.78) / Math.max(1, norm.series.length), 36)),
|
|
281
|
+
)
|
|
282
|
+
const stackedBarW = $derived(Math.max(1, Math.min(slotW * 0.78, 36)))
|
|
283
|
+
|
|
284
|
+
// ── Annotations ──────────────────────────────────────────────────────────
|
|
285
|
+
function resolveAnnX(raw: string | number): number | null {
|
|
286
|
+
if (norm.xType === "category") {
|
|
287
|
+
const i = norm.categories.indexOf(String(raw))
|
|
288
|
+
return i === -1 ? null : (norm.xNums[i] ?? null)
|
|
289
|
+
}
|
|
290
|
+
const n =
|
|
291
|
+
norm.xType === "time"
|
|
292
|
+
? parseTimeX(raw)
|
|
293
|
+
: typeof raw === "number"
|
|
294
|
+
? raw
|
|
295
|
+
: Number.parseFloat(raw)
|
|
296
|
+
return Number.isFinite(n) ? n : null
|
|
297
|
+
}
|
|
298
|
+
const renderedAnnotations = $derived.by(() => {
|
|
299
|
+
const out: {
|
|
300
|
+
kind: "point" | "range"
|
|
301
|
+
label: string
|
|
302
|
+
y?: number
|
|
303
|
+
px1: number
|
|
304
|
+
px2?: number
|
|
305
|
+
}[] = []
|
|
306
|
+
for (const a of params.annotations) {
|
|
307
|
+
const dx1 = resolveAnnX(a.x)
|
|
308
|
+
if (dx1 === null) continue
|
|
309
|
+
const px1 = xPx(dx1)
|
|
310
|
+
if (a.kind === "range") {
|
|
311
|
+
if (a.x2 === undefined) continue
|
|
312
|
+
const dx2 = resolveAnnX(a.x2)
|
|
313
|
+
if (dx2 === null) continue
|
|
314
|
+
const px2 = xPx(dx2)
|
|
315
|
+
if (
|
|
316
|
+
Math.max(px1, px2) < padLeft ||
|
|
317
|
+
Math.min(px1, px2) > padLeft + plotW
|
|
318
|
+
)
|
|
319
|
+
continue
|
|
320
|
+
out.push({ kind: "range", label: a.label, px1, px2 })
|
|
321
|
+
} else {
|
|
322
|
+
if (px1 < padLeft - 1 || px1 > padLeft + plotW + 1) continue
|
|
323
|
+
out.push({ kind: "point", label: a.label, y: a.y, px1 })
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return out
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
// ── Ticks / labels ───────────────────────────────────────────────────────
|
|
330
|
+
const yTickFmt = $derived((v: number) =>
|
|
331
|
+
formatYTick(v, params.yFormat, params.currency),
|
|
332
|
+
)
|
|
333
|
+
const xTicks = $derived.by(() => {
|
|
334
|
+
if (usesSlots) return []
|
|
335
|
+
const lin = xLinear
|
|
336
|
+
if (!lin) return []
|
|
337
|
+
if (norm.xType === "time")
|
|
338
|
+
return timeTicks(lin.d0, lin.d1, Math.max(3, Math.floor(plotW / 90)))
|
|
339
|
+
return linearTicks(lin.d0, lin.d1, Math.max(3, Math.floor(plotW / 80)))
|
|
340
|
+
.ticks
|
|
341
|
+
})
|
|
342
|
+
const xTickFmt = $derived.by(() => {
|
|
343
|
+
if (norm.xType === "time") {
|
|
344
|
+
const lin = xLinear
|
|
345
|
+
const f = timeTickFormat(lin?.d0 ?? 0, lin?.d1 ?? 0)
|
|
346
|
+
return (v: number) => f(v)
|
|
347
|
+
}
|
|
348
|
+
if (norm.xType === "number")
|
|
349
|
+
return (v: number) =>
|
|
350
|
+
new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(v)
|
|
351
|
+
return (v: number) => norm.categories[v] ?? String(v)
|
|
352
|
+
})
|
|
353
|
+
const xLabelBudget = $derived(Math.max(2, Math.floor(plotW / 58)))
|
|
354
|
+
const categoryLabelStep = $derived(
|
|
355
|
+
Math.max(1, Math.ceil(norm.xNums.length / xLabelBudget)),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
function truncate(s: string, n: number): string {
|
|
359
|
+
return s.length > n ? `${s.slice(0, n - 1)}…` : s
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// End labels: series name at the last visible point, pushed apart so
|
|
363
|
+
// neighbors stay readable.
|
|
364
|
+
const endLabels = $derived.by(() => {
|
|
365
|
+
if (!endLabelsOn) return []
|
|
366
|
+
const items = norm.series.map((s, i) => {
|
|
367
|
+
const yPx = yPxFor(s.axis)
|
|
368
|
+
const last = params.stacked
|
|
369
|
+
? (stackedSegs?.[i] ?? []).at(-1)
|
|
370
|
+
: s.points.at(-1)
|
|
371
|
+
const lastX = params.stacked
|
|
372
|
+
? (stackedSegs?.[i] ?? []).at(-1)?.xNum
|
|
373
|
+
: s.points.at(-1)?.xNum
|
|
374
|
+
return {
|
|
375
|
+
label: s.label,
|
|
376
|
+
y: last ? yPx(params.stacked ? last.y1 : last.y) : plotBottom,
|
|
377
|
+
x: lastX !== undefined ? xPx(lastX) : padLeft + plotW,
|
|
378
|
+
i,
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
const ys = layoutEndLabels(items, padTop + 4, plotBottom - 2)
|
|
382
|
+
return items.map((it, k) => ({ ...it, y: ys[k] }))
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// ── Crosshair + tooltip ──────────────────────────────────────────────────
|
|
386
|
+
let svgEl = $state<SVGSVGElement | undefined>(undefined)
|
|
387
|
+
let hoverIdx = $state<number | null>(null)
|
|
388
|
+
let hoverPx = $state(0)
|
|
389
|
+
|
|
390
|
+
function invertX(px: number): number {
|
|
391
|
+
if (usesSlots) {
|
|
392
|
+
const i = Math.floor((px - padLeft) / slotW)
|
|
393
|
+
const idx = Math.min(Math.max(i, 0), norm.xNums.length - 1)
|
|
394
|
+
return norm.xNums[idx] ?? 0
|
|
395
|
+
}
|
|
396
|
+
const lin = xLinear
|
|
397
|
+
if (!lin) return norm.xNums[0] ?? 0
|
|
398
|
+
return lin.d0 + ((px - padLeft) / plotW) * (lin.d1 - lin.d0)
|
|
399
|
+
}
|
|
400
|
+
function onPointerMove(e: PointerEvent) {
|
|
401
|
+
const rect = svgEl?.getBoundingClientRect()
|
|
402
|
+
if (!rect || rect.width === 0 || !norm.xNums.length) {
|
|
403
|
+
hoverIdx = null
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
const px = e.clientX - rect.left
|
|
407
|
+
if (px < padLeft - 6 || px > padLeft + plotW + 6) {
|
|
408
|
+
hoverIdx = null
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
hoverPx = px
|
|
412
|
+
hoverIdx = nearestIndex(norm.xNums, invertX(px))
|
|
413
|
+
}
|
|
414
|
+
function onPointerLeave() {
|
|
415
|
+
hoverIdx = null
|
|
416
|
+
}
|
|
417
|
+
const hoverSnapX = $derived(
|
|
418
|
+
hoverIdx === null
|
|
419
|
+
? 0
|
|
420
|
+
: usesSlots
|
|
421
|
+
? slotCenterOf(hoverIdx)
|
|
422
|
+
: xLinear
|
|
423
|
+
? padLeft +
|
|
424
|
+
((norm.xNums[hoverIdx] - xLinear.d0) / (xLinear.d1 - xLinear.d0)) *
|
|
425
|
+
plotW
|
|
426
|
+
: padLeft,
|
|
427
|
+
)
|
|
428
|
+
const hoverXLabel = $derived.by(() => {
|
|
429
|
+
if (hoverIdx === null) return ""
|
|
430
|
+
const xNum = norm.xNums[hoverIdx]
|
|
431
|
+
if (norm.xType === "category") return norm.categories[xNum] ?? ""
|
|
432
|
+
if (norm.xType === "time") {
|
|
433
|
+
const step =
|
|
434
|
+
norm.xNums.length > 1 ? norm.xNums[1] - norm.xNums[0] : 86.4e6
|
|
435
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
436
|
+
dateStyle: "medium",
|
|
437
|
+
...(step < 86.4e6 ? { timeStyle: "short" as const } : {}),
|
|
438
|
+
}).format(xNum)
|
|
439
|
+
}
|
|
440
|
+
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(
|
|
441
|
+
xNum,
|
|
442
|
+
)
|
|
443
|
+
})
|
|
444
|
+
const hoverData = $derived.by(() => {
|
|
445
|
+
if (hoverIdx === null) return null
|
|
446
|
+
const xNum = norm.xNums[hoverIdx]
|
|
447
|
+
const rows = norm.series.map((s, si) => ({
|
|
448
|
+
si,
|
|
449
|
+
v: valueAt(si, xNum),
|
|
450
|
+
yPx: yPxFor(s.axis)(valueAt(si, xNum) ?? 0),
|
|
451
|
+
}))
|
|
452
|
+
return { xNum, rows: rows.filter((r) => r.v !== null) }
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
// ── A11y data fallback ───────────────────────────────────────────────────
|
|
456
|
+
let dataOpen = $state(false)
|
|
457
|
+
const tableId = $derived(`block-data-${ctx.blockId || "chart"}`)
|
|
458
|
+
const tableRows = $derived(dataTableRows(norm))
|
|
459
|
+
</script>
|
|
460
|
+
|
|
461
|
+
<figure class="lc-root-container w-full" data-block-type="chart">
|
|
462
|
+
{#if params.title || params.subtitle}
|
|
463
|
+
<div class="mb-1">
|
|
464
|
+
{#if params.title}
|
|
465
|
+
<div class="flex items-center gap-2">
|
|
466
|
+
<h4 class="min-w-0 text-sm font-semibold text-base-content">
|
|
467
|
+
{params.title}
|
|
468
|
+
</h4>
|
|
469
|
+
<SectionTools {ctx} inline />
|
|
470
|
+
</div>
|
|
471
|
+
{/if}
|
|
472
|
+
{#if params.subtitle}
|
|
473
|
+
<p class="text-xs text-base-content/60">{params.subtitle}</p>
|
|
474
|
+
{/if}
|
|
475
|
+
</div>
|
|
476
|
+
{/if}
|
|
477
|
+
|
|
478
|
+
{#if norm.series.length === 0 || norm.xNums.length === 0}
|
|
479
|
+
<p class="text-sm text-base-content/45">No data to chart.</p>
|
|
480
|
+
{:else}
|
|
481
|
+
<div class="relative w-full" bind:this={containerEl}>
|
|
482
|
+
<svg
|
|
483
|
+
bind:this={svgEl}
|
|
484
|
+
width={W}
|
|
485
|
+
height={H}
|
|
486
|
+
viewBox="0 0 {W} {H}"
|
|
487
|
+
class="block max-w-full"
|
|
488
|
+
role="img"
|
|
489
|
+
aria-label="{params.chartType} chart{params.title
|
|
490
|
+
? `: ${params.title}`
|
|
491
|
+
: ''}. Series: {norm.series.map((s) => s.label).join(', ')}."
|
|
492
|
+
onpointermove={onPointerMove}
|
|
493
|
+
onpointerleave={onPointerLeave}
|
|
494
|
+
>
|
|
495
|
+
<!-- recessive horizontal grid (left scale only) -->
|
|
496
|
+
{#each leftScale.ticks as t (t)}
|
|
497
|
+
<line
|
|
498
|
+
x1={padLeft}
|
|
499
|
+
x2={padLeft + plotW}
|
|
500
|
+
y1={yPxLeft(t)}
|
|
501
|
+
y2={yPxLeft(t)}
|
|
502
|
+
class="stroke-base-content/10"
|
|
503
|
+
stroke-width="1"
|
|
504
|
+
shape-rendering="crispEdges"
|
|
505
|
+
/>
|
|
506
|
+
<text
|
|
507
|
+
x={padLeft - 6}
|
|
508
|
+
y={yPxLeft(t) + 3}
|
|
509
|
+
text-anchor="end"
|
|
510
|
+
class="fill-base-content/45 text-2xs tabular-nums"
|
|
511
|
+
>
|
|
512
|
+
{yTickFmt(t)}
|
|
513
|
+
</text>
|
|
514
|
+
{/each}
|
|
515
|
+
{#if rightScale}
|
|
516
|
+
{#each rightScale.ticks as t (t)}
|
|
517
|
+
<text
|
|
518
|
+
x={padLeft + plotW + 6}
|
|
519
|
+
y={yPxRight(t) + 3}
|
|
520
|
+
text-anchor="start"
|
|
521
|
+
class="fill-base-content/45 text-2xs tabular-nums"
|
|
522
|
+
>
|
|
523
|
+
{yTickFmt(t)}
|
|
524
|
+
</text>
|
|
525
|
+
{/each}
|
|
526
|
+
{/if}
|
|
527
|
+
|
|
528
|
+
<!-- honest baseline (0 or domain floor) -->
|
|
529
|
+
<line
|
|
530
|
+
x1={padLeft}
|
|
531
|
+
x2={padLeft + plotW}
|
|
532
|
+
y1={baselineY}
|
|
533
|
+
y2={baselineY}
|
|
534
|
+
class="stroke-base-content/25"
|
|
535
|
+
stroke-width="1"
|
|
536
|
+
shape-rendering="crispEdges"
|
|
537
|
+
/>
|
|
538
|
+
|
|
539
|
+
<!-- y axis titles -->
|
|
540
|
+
{#if params.yLabel}
|
|
541
|
+
<text
|
|
542
|
+
x={12}
|
|
543
|
+
y={padTop + plotH / 2}
|
|
544
|
+
text-anchor="middle"
|
|
545
|
+
transform="rotate(-90, 12, {padTop + plotH / 2})"
|
|
546
|
+
class="fill-base-content/60 text-2xs font-medium"
|
|
547
|
+
>
|
|
548
|
+
{params.yLabel}
|
|
549
|
+
</text>
|
|
550
|
+
{/if}
|
|
551
|
+
{#if norm.dual && params.y2Label}
|
|
552
|
+
<text
|
|
553
|
+
x={W - 12}
|
|
554
|
+
y={padTop + plotH / 2}
|
|
555
|
+
text-anchor="middle"
|
|
556
|
+
transform="rotate(90, {W - 12}, {padTop + plotH / 2})"
|
|
557
|
+
class="fill-base-content/60 text-2xs font-medium"
|
|
558
|
+
>
|
|
559
|
+
{params.y2Label}
|
|
560
|
+
</text>
|
|
561
|
+
{/if}
|
|
562
|
+
|
|
563
|
+
<!-- annotations (below marks) -->
|
|
564
|
+
{#each renderedAnnotations as a, ai (ai)}
|
|
565
|
+
{#if a.kind === "range"}
|
|
566
|
+
<rect
|
|
567
|
+
x={Math.min(a.px1, a.px2 ?? a.px1)}
|
|
568
|
+
y={padTop}
|
|
569
|
+
width={Math.max(1, Math.abs((a.px2 ?? a.px1) - a.px1))}
|
|
570
|
+
height={plotH}
|
|
571
|
+
class="fill-base-content/5"
|
|
572
|
+
/>
|
|
573
|
+
<text
|
|
574
|
+
x={Math.min(a.px1, a.px2 ?? a.px1) + 4}
|
|
575
|
+
y={padTop + 10}
|
|
576
|
+
class="fill-base-content/60 text-2xs"
|
|
577
|
+
>
|
|
578
|
+
{truncate(a.label, 36)}
|
|
579
|
+
</text>
|
|
580
|
+
{:else}
|
|
581
|
+
<line
|
|
582
|
+
x1={a.px1}
|
|
583
|
+
x2={a.px1}
|
|
584
|
+
y1={padTop}
|
|
585
|
+
y2={padTop + plotH}
|
|
586
|
+
class="stroke-base-content/30"
|
|
587
|
+
stroke-dasharray="4 3"
|
|
588
|
+
/>
|
|
589
|
+
{#if a.y !== undefined}
|
|
590
|
+
<circle
|
|
591
|
+
cx={a.px1}
|
|
592
|
+
cy={yPxLeft(a.y)}
|
|
593
|
+
r="3.5"
|
|
594
|
+
class="fill-base-content/40"
|
|
595
|
+
/>
|
|
596
|
+
{/if}
|
|
597
|
+
<text
|
|
598
|
+
x={a.px1 + 4}
|
|
599
|
+
y={padTop + 10}
|
|
600
|
+
class="fill-base-content/60 text-2xs"
|
|
601
|
+
>
|
|
602
|
+
{truncate(a.label, 36)}
|
|
603
|
+
</text>
|
|
604
|
+
{/if}
|
|
605
|
+
{/each}
|
|
606
|
+
|
|
607
|
+
<!-- goal / threshold lines -->
|
|
608
|
+
{#each params.goalLines as g, gi (gi)}
|
|
609
|
+
{@const gy = yPxFor(g.axis)(g.value)}
|
|
610
|
+
{#if gy >= padTop && gy <= plotBottom}
|
|
611
|
+
<line
|
|
612
|
+
x1={padLeft}
|
|
613
|
+
x2={padLeft + plotW}
|
|
614
|
+
y1={gy}
|
|
615
|
+
y2={gy}
|
|
616
|
+
class="stroke-base-content/30"
|
|
617
|
+
stroke-dasharray="5 4"
|
|
618
|
+
/>
|
|
619
|
+
{#if g.label}
|
|
620
|
+
<text
|
|
621
|
+
x={padLeft + 4}
|
|
622
|
+
y={gy - 4}
|
|
623
|
+
class="fill-base-content/60 text-2xs"
|
|
624
|
+
>
|
|
625
|
+
{g.label}
|
|
626
|
+
</text>
|
|
627
|
+
{/if}
|
|
628
|
+
{/if}
|
|
629
|
+
{/each}
|
|
630
|
+
|
|
631
|
+
<!-- marks -->
|
|
632
|
+
{#if params.chartType === "bar"}
|
|
633
|
+
{#each norm.xNums as xNum, xi (xNum)}
|
|
634
|
+
{#each norm.series as s, si (si)}
|
|
635
|
+
{@const seg = params.stacked
|
|
636
|
+
? stackedSegMaps?.[si]?.get(xNum)
|
|
637
|
+
: undefined}
|
|
638
|
+
{#if params.stacked ? seg !== undefined : valueAt(si, xNum) !== null}
|
|
639
|
+
{@const yPx = yPxFor(s.axis)}
|
|
640
|
+
{@const topV = params.stacked
|
|
641
|
+
? (seg?.y1 ?? 0)
|
|
642
|
+
: (valueAt(si, xNum) ?? 0)}
|
|
643
|
+
{@const botV = params.stacked ? (seg?.y0 ?? 0) : 0}
|
|
644
|
+
{@const x = params.stacked
|
|
645
|
+
? slotLeft(xi) + (slotW - stackedBarW) / 2
|
|
646
|
+
: slotLeft(xi) +
|
|
647
|
+
(slotW - barW * norm.series.length) / 2 +
|
|
648
|
+
si * barW}
|
|
649
|
+
<rect
|
|
650
|
+
{x}
|
|
651
|
+
y={Math.min(yPx(topV), yPx(botV))}
|
|
652
|
+
width={params.stacked ? stackedBarW : barW}
|
|
653
|
+
height={Math.max(1, Math.abs(yPx(topV) - yPx(botV)))}
|
|
654
|
+
fill={seriesColor(si)}
|
|
655
|
+
/>
|
|
656
|
+
{/if}
|
|
657
|
+
{/each}
|
|
658
|
+
{/each}
|
|
659
|
+
{:else if params.chartType === "area"}
|
|
660
|
+
{#each norm.series as _, si (si)}
|
|
661
|
+
<path
|
|
662
|
+
d={areaPathOf(si)}
|
|
663
|
+
fill={params.stacked
|
|
664
|
+
? seriesAreaFillStacked(si)
|
|
665
|
+
: seriesAreaFill(si)}
|
|
666
|
+
stroke={seriesColor(si)}
|
|
667
|
+
stroke-width="1.5"
|
|
668
|
+
/>
|
|
669
|
+
{/each}
|
|
670
|
+
{:else if params.chartType === "line"}
|
|
671
|
+
{#each norm.series as _, si (si)}
|
|
672
|
+
<path
|
|
673
|
+
d={linePathOf(si)}
|
|
674
|
+
fill="none"
|
|
675
|
+
stroke={seriesColor(si)}
|
|
676
|
+
stroke-width="2"
|
|
677
|
+
stroke-linejoin="round"
|
|
678
|
+
stroke-linecap="round"
|
|
679
|
+
/>
|
|
680
|
+
{/each}
|
|
681
|
+
{:else}
|
|
682
|
+
{#each norm.series as s, si (si)}
|
|
683
|
+
{#each s.points as p, pi (pi)}
|
|
684
|
+
<circle
|
|
685
|
+
cx={r1(xPx(p.xNum))}
|
|
686
|
+
cy={r1(yPxFor(s.axis)(p.y))}
|
|
687
|
+
r="3.5"
|
|
688
|
+
fill={seriesColor(si)}
|
|
689
|
+
/>
|
|
690
|
+
{/each}
|
|
691
|
+
{/each}
|
|
692
|
+
{/if}
|
|
693
|
+
|
|
694
|
+
<!-- direct end-labels -->
|
|
695
|
+
{#each endLabels as el (el.i)}
|
|
696
|
+
<text
|
|
697
|
+
x={el.x + 7}
|
|
698
|
+
y={el.y + 3}
|
|
699
|
+
class="text-2xs font-medium"
|
|
700
|
+
fill={seriesColor(el.i)}
|
|
701
|
+
>
|
|
702
|
+
{truncate(el.label, 14)}
|
|
703
|
+
</text>
|
|
704
|
+
{/each}
|
|
705
|
+
|
|
706
|
+
<!-- x tick labels -->
|
|
707
|
+
{#if usesSlots}
|
|
708
|
+
{#each norm.xNums as xNum, xi (xNum)}
|
|
709
|
+
{#if xi % categoryLabelStep === 0}
|
|
710
|
+
<text
|
|
711
|
+
x={slotCenterOf(xi)}
|
|
712
|
+
y={plotBottom + 16}
|
|
713
|
+
text-anchor="middle"
|
|
714
|
+
class="fill-base-content/45 text-2xs"
|
|
715
|
+
>
|
|
716
|
+
{truncate(norm.categories[xNum] ?? String(xNum), 12)}
|
|
717
|
+
</text>
|
|
718
|
+
{/if}
|
|
719
|
+
{/each}
|
|
720
|
+
{:else}
|
|
721
|
+
{#each xTicks as t (t)}
|
|
722
|
+
<text
|
|
723
|
+
x={xPx(t)}
|
|
724
|
+
y={plotBottom + 16}
|
|
725
|
+
text-anchor="middle"
|
|
726
|
+
class="fill-base-content/45 text-2xs"
|
|
727
|
+
>
|
|
728
|
+
{truncate(xTickFmt(t), 20)}
|
|
729
|
+
</text>
|
|
730
|
+
{/each}
|
|
731
|
+
{/if}
|
|
732
|
+
|
|
733
|
+
<!-- crosshair -->
|
|
734
|
+
{#if hoverData}
|
|
735
|
+
{#if params.chartType === "bar"}
|
|
736
|
+
<rect
|
|
737
|
+
x={slotLeft(hoverIdx ?? 0)}
|
|
738
|
+
y={padTop}
|
|
739
|
+
width={slotW}
|
|
740
|
+
height={plotH}
|
|
741
|
+
class="fill-base-content/5"
|
|
742
|
+
/>
|
|
743
|
+
{/if}
|
|
744
|
+
<line
|
|
745
|
+
x1={hoverSnapX}
|
|
746
|
+
x2={hoverSnapX}
|
|
747
|
+
y1={padTop}
|
|
748
|
+
y2={padTop + plotH}
|
|
749
|
+
class="stroke-base-content/25"
|
|
750
|
+
stroke-width="1"
|
|
751
|
+
/>
|
|
752
|
+
{#each hoverData.rows as d (d.si)}
|
|
753
|
+
<circle
|
|
754
|
+
cx={r1(hoverSnapX)}
|
|
755
|
+
cy={r1(d.yPx)}
|
|
756
|
+
r="3.5"
|
|
757
|
+
fill={seriesColor(d.si)}
|
|
758
|
+
stroke="var(--color-base-100)"
|
|
759
|
+
stroke-width="1.5"
|
|
760
|
+
/>
|
|
761
|
+
{/each}
|
|
762
|
+
{/if}
|
|
763
|
+
</svg>
|
|
764
|
+
|
|
765
|
+
<!-- tooltip -->
|
|
766
|
+
{#if hoverData}
|
|
767
|
+
<div
|
|
768
|
+
class="pointer-events-none absolute top-1 z-10 min-w-32 rounded-box border border-base-content/10 bg-base-100/95 px-2.5 py-1.5 text-xs"
|
|
769
|
+
style:left={hoverPx > padLeft + plotW * 0.62
|
|
770
|
+
? "auto"
|
|
771
|
+
: `${hoverPx + 12}px`}
|
|
772
|
+
style:right={hoverPx > padLeft + plotW * 0.62
|
|
773
|
+
? `${W - hoverPx + 10}px`
|
|
774
|
+
: "auto"}
|
|
775
|
+
>
|
|
776
|
+
<div class="mb-0.5 font-medium text-base-content/60">
|
|
777
|
+
{hoverXLabel}
|
|
778
|
+
</div>
|
|
779
|
+
{#each hoverData.rows as d (d.si)}
|
|
780
|
+
<div class="flex items-center gap-1.5 whitespace-nowrap">
|
|
781
|
+
<span
|
|
782
|
+
class="h-2 w-2 shrink-0 rounded-[2px]"
|
|
783
|
+
style:background={seriesColor(d.si)}
|
|
784
|
+
aria-hidden="true"
|
|
785
|
+
></span>
|
|
786
|
+
<span class="text-base-content/70">{norm.series[d.si].label}</span
|
|
787
|
+
>
|
|
788
|
+
<span
|
|
789
|
+
class="ml-auto pl-2 font-medium tabular-nums text-base-content"
|
|
790
|
+
>
|
|
791
|
+
{formatYValue(d.v ?? 0, params.yFormat, params.currency)}
|
|
792
|
+
</span>
|
|
793
|
+
</div>
|
|
794
|
+
{/each}
|
|
795
|
+
</div>
|
|
796
|
+
{/if}
|
|
797
|
+
</div>
|
|
798
|
+
{/if}
|
|
799
|
+
|
|
800
|
+
<!-- legend: swatch + TEXT label (identity never by color alone) -->
|
|
801
|
+
{#if params.showLegend && norm.series.length > 1}
|
|
802
|
+
<div class="mt-1.5 flex flex-wrap items-center gap-x-4 gap-y-1">
|
|
803
|
+
{#each norm.series as s, si (si)}
|
|
804
|
+
<span
|
|
805
|
+
class="inline-flex items-center gap-1.5 text-xs text-base-content/70"
|
|
806
|
+
>
|
|
807
|
+
<span
|
|
808
|
+
class="h-2.5 w-2.5 shrink-0 rounded-[3px]"
|
|
809
|
+
style:background={seriesColor(si)}
|
|
810
|
+
aria-hidden="true"
|
|
811
|
+
></span>
|
|
812
|
+
{s.label}
|
|
813
|
+
{#if norm.dual && s.axis === "right"}
|
|
814
|
+
<span class="badge badge-ghost badge-xs font-normal"
|
|
815
|
+
>right axis</span
|
|
816
|
+
>
|
|
817
|
+
{/if}
|
|
818
|
+
</span>
|
|
819
|
+
{/each}
|
|
820
|
+
</div>
|
|
821
|
+
{/if}
|
|
822
|
+
|
|
823
|
+
<!-- accessible data fallback: sr-only table always; expandable for everyone -->
|
|
824
|
+
<div class="mt-1.5 flex items-center justify-between gap-2">
|
|
825
|
+
{#if params.xLabel}
|
|
826
|
+
<span class="text-2xs text-base-content/45">{params.xLabel}</span>
|
|
827
|
+
{:else}
|
|
828
|
+
<span></span>
|
|
829
|
+
{/if}
|
|
830
|
+
<button
|
|
831
|
+
type="button"
|
|
832
|
+
class="btn btn-ghost btn-xs text-base-content/70"
|
|
833
|
+
aria-expanded={dataOpen}
|
|
834
|
+
aria-controls={tableId}
|
|
835
|
+
onclick={() => (dataOpen = !dataOpen)}
|
|
836
|
+
>
|
|
837
|
+
{dataOpen ? "Hide data" : "View data"}
|
|
838
|
+
</button>
|
|
839
|
+
</div>
|
|
840
|
+
<div
|
|
841
|
+
class={dataOpen
|
|
842
|
+
? "mt-1 max-h-64 overflow-auto rounded-box border border-base-content/10"
|
|
843
|
+
: "sr-only"}
|
|
844
|
+
>
|
|
845
|
+
<!-- sr-only sits on the wrapper: a <table> ignores width:1px and grows to
|
|
846
|
+
its content, so on the table itself it widened the page at 390px. -->
|
|
847
|
+
<table id={tableId} class={dataOpen ? "table table-sm" : undefined}>
|
|
848
|
+
<caption class="sr-only">
|
|
849
|
+
Chart data{params.title ? `: ${params.title}` : ""}
|
|
850
|
+
</caption>
|
|
851
|
+
<thead>
|
|
852
|
+
<tr>
|
|
853
|
+
<th scope="col" class="text-left">x</th>
|
|
854
|
+
{#each norm.series as s, si (si)}
|
|
855
|
+
<th scope="col" class="text-right">{s.label}</th>
|
|
856
|
+
{/each}
|
|
857
|
+
</tr>
|
|
858
|
+
</thead>
|
|
859
|
+
<tbody>
|
|
860
|
+
{#each tableRows as row (row.x)}
|
|
861
|
+
<tr>
|
|
862
|
+
<th scope="row" class="text-left font-normal">{row.xLabel}</th>
|
|
863
|
+
{#each row.values as v, vi (vi)}
|
|
864
|
+
<td class="text-right tabular-nums">
|
|
865
|
+
{v === null
|
|
866
|
+
? "n/a"
|
|
867
|
+
: formatTableValue(v, params.yFormat, params.currency)}
|
|
868
|
+
</td>
|
|
869
|
+
{/each}
|
|
870
|
+
</tr>
|
|
871
|
+
{/each}
|
|
872
|
+
</tbody>
|
|
873
|
+
</table>
|
|
874
|
+
</div>
|
|
875
|
+
</figure>
|