zabi-components 8.1.0 → 9.0.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/CHANGELOG.md +1011 -0
- package/README.md +1 -1
- package/THEME.md +18 -5
- package/THEMING.md +308 -74
- package/dist/atoms/ActionPanel.svelte +1 -1
- package/dist/atoms/Avatar.svelte +19 -3
- package/dist/atoms/Avatar.svelte.d.ts +12 -0
- package/dist/atoms/Badge.svelte +37 -8
- package/dist/atoms/Badge.svelte.d.ts +6 -0
- package/dist/atoms/Block.svelte +74 -0
- package/dist/atoms/Block.svelte.d.ts +29 -0
- package/dist/atoms/Button.svelte +52 -15
- package/dist/atoms/Button.svelte.d.ts +3 -2
- package/dist/atoms/Card.svelte +181 -25
- package/dist/atoms/Card.svelte.d.ts +55 -3
- package/dist/atoms/Checkbox.svelte +33 -0
- package/dist/atoms/Checkbox.svelte.d.ts +27 -0
- package/dist/atoms/Chip.svelte +351 -0
- package/dist/atoms/Chip.svelte.d.ts +57 -0
- package/dist/atoms/CodeField.svelte +385 -0
- package/dist/atoms/CodeField.svelte.d.ts +61 -0
- package/dist/atoms/ColorPicker.svelte +1 -0
- package/dist/atoms/DateField.svelte +41 -3
- package/dist/atoms/DateField.svelte.d.ts +31 -3
- package/dist/atoms/FloatingActionButton.svelte +5 -3
- package/dist/atoms/FloatingActionButton.svelte.d.ts +1 -1
- package/dist/atoms/Heading.svelte +27 -1
- package/dist/atoms/Heading.svelte.d.ts +7 -0
- package/dist/atoms/IconButton.svelte +81 -9
- package/dist/atoms/IconButton.svelte.d.ts +13 -0
- package/dist/atoms/Input.svelte +10 -5
- package/dist/atoms/Input.svelte.d.ts +3 -2
- package/dist/atoms/List.svelte +3 -2
- package/dist/atoms/ListItem.svelte +31 -7
- package/dist/atoms/ListItem.svelte.d.ts +6 -0
- package/dist/atoms/PickerField.svelte +239 -0
- package/dist/atoms/PickerField.svelte.d.ts +67 -0
- package/dist/atoms/Progress.svelte +78 -17
- package/dist/atoms/Progress.svelte.d.ts +11 -1
- package/dist/atoms/Radio.svelte +32 -0
- package/dist/atoms/Radio.svelte.d.ts +26 -0
- package/dist/atoms/Rating.svelte +132 -6
- package/dist/atoms/Rating.svelte.d.ts +24 -3
- package/dist/atoms/Select.svelte +24 -26
- package/dist/atoms/Select.svelte.d.ts +12 -0
- package/dist/atoms/SelectionControl.svelte +99 -6
- package/dist/atoms/SelectionControl.svelte.d.ts +25 -0
- package/dist/atoms/Skeleton.svelte +114 -18
- package/dist/atoms/Skeleton.svelte.d.ts +30 -3
- package/dist/atoms/SkeletonGroup.svelte +43 -0
- package/dist/atoms/SkeletonGroup.svelte.d.ts +20 -0
- package/dist/atoms/Slider.svelte +35 -0
- package/dist/atoms/Sparkline.svelte +109 -0
- package/dist/atoms/Sparkline.svelte.d.ts +27 -0
- package/dist/atoms/SpeechBubble.svelte +187 -0
- package/dist/atoms/SpeechBubble.svelte.d.ts +33 -0
- package/dist/atoms/Stat.svelte +72 -0
- package/dist/atoms/Stat.svelte.d.ts +19 -0
- package/dist/atoms/TemporalField.svelte +342 -10
- package/dist/atoms/TemporalField.svelte.d.ts +20 -4
- package/dist/atoms/Textarea.svelte +100 -5
- package/dist/atoms/Textarea.svelte.d.ts +21 -0
- package/dist/atoms/TimeColumn.svelte +150 -0
- package/dist/atoms/TimeColumn.svelte.d.ts +25 -0
- package/dist/atoms/TimeField.svelte +46 -3
- package/dist/atoms/TimeField.svelte.d.ts +34 -3
- package/dist/atoms/Toast.svelte +6 -5
- package/dist/atoms/Toggle.svelte +61 -10
- package/dist/atoms/Toggle.svelte.d.ts +15 -0
- package/dist/atoms/Tooltip.svelte +20 -1
- package/dist/atoms/Tooltip.svelte.d.ts +7 -0
- package/dist/atoms/index.d.ts +21 -0
- package/dist/atoms/index.js +17 -0
- package/dist/atoms/selection-control.styles.d.ts +15 -4
- package/dist/atoms/selection-control.styles.js +16 -5
- package/dist/components/atoms/index.d.ts +21 -0
- package/dist/components/atoms/index.d.ts.map +1 -1
- package/dist/components/atoms/selection-control.styles.d.ts +15 -4
- package/dist/components/atoms/selection-control.styles.d.ts.map +1 -1
- package/dist/components/index.d.ts +27 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/map.d.ts +11 -0
- package/dist/components/map.d.ts.map +1 -0
- package/dist/components/molecules/index.d.ts +7 -2
- package/dist/components/molecules/index.d.ts.map +1 -1
- package/dist/components/molecules/toast-store.d.ts +30 -0
- package/dist/components/molecules/toast-store.d.ts.map +1 -1
- package/dist/components/organisms/index.d.ts +6 -1
- package/dist/components/organisms/index.d.ts.map +1 -1
- package/dist/components/types/variants.d.ts +12 -3
- package/dist/components/types/variants.d.ts.map +1 -1
- package/dist/components/util/app-shell.d.ts +55 -4
- package/dist/components/util/app-shell.d.ts.map +1 -1
- package/dist/components/util/avatar.d.ts +3 -0
- package/dist/components/util/avatar.d.ts.map +1 -1
- package/dist/components/util/bottom-tab-bar.d.ts +6 -0
- package/dist/components/util/bottom-tab-bar.d.ts.map +1 -1
- package/dist/components/util/chart.d.ts +140 -0
- package/dist/components/util/chart.d.ts.map +1 -0
- package/dist/components/util/chip.d.ts +66 -0
- package/dist/components/util/chip.d.ts.map +1 -0
- package/dist/components/util/cn.d.ts.map +1 -1
- package/dist/components/util/code-field.d.ts +63 -0
- package/dist/components/util/code-field.d.ts.map +1 -0
- package/dist/components/util/collapsible.d.ts +6 -0
- package/dist/components/util/collapsible.d.ts.map +1 -1
- package/dist/components/util/display-voice.d.ts +10 -0
- package/dist/components/util/display-voice.d.ts.map +1 -0
- package/dist/components/util/field-control.d.ts +30 -0
- package/dist/components/util/field-control.d.ts.map +1 -0
- package/dist/components/util/field.d.ts +6 -0
- package/dist/components/util/field.d.ts.map +1 -1
- package/dist/components/util/focus-utils.d.ts +2 -0
- package/dist/components/util/focus-utils.d.ts.map +1 -1
- package/dist/components/util/map-view.d.ts +144 -0
- package/dist/components/util/map-view.d.ts.map +1 -0
- package/dist/components/util/photo.d.ts +19 -0
- package/dist/components/util/photo.d.ts.map +1 -1
- package/dist/components/util/progress.d.ts +19 -0
- package/dist/components/util/progress.d.ts.map +1 -0
- package/dist/components/util/scroll-edge.d.ts +33 -0
- package/dist/components/util/scroll-edge.d.ts.map +1 -0
- package/dist/components/util/sheet-drag.d.ts +25 -1
- package/dist/components/util/sheet-drag.d.ts.map +1 -1
- package/dist/components/util/skeleton.d.ts +15 -0
- package/dist/components/util/skeleton.d.ts.map +1 -0
- package/dist/components/util/sliding-indicator.d.ts +25 -0
- package/dist/components/util/sliding-indicator.d.ts.map +1 -0
- package/dist/components/util/temporal-field.d.ts +89 -0
- package/dist/components/util/temporal-field.d.ts.map +1 -0
- package/dist/components/util/toaster.d.ts +40 -0
- package/dist/components/util/toaster.d.ts.map +1 -1
- package/dist/components/util/tone-surface.d.ts +34 -0
- package/dist/components/util/tone-surface.d.ts.map +1 -0
- package/dist/components/util/top-navbar.d.ts +26 -2
- package/dist/components/util/top-navbar.d.ts.map +1 -1
- package/dist/components/util/zabi-strings.d.ts +9 -0
- package/dist/components/util/zabi-strings.d.ts.map +1 -1
- package/dist/create-theme/cli.js +6 -2
- package/dist/create-theme/index.d.ts +17 -3
- package/dist/create-theme/index.js +252 -35
- package/dist/create-theme/lib/resolve.js +97 -0
- package/dist/create-theme/theme-data.js +1618 -90
- package/dist/index.d.ts +27 -3
- package/dist/index.js +18 -0
- package/dist/map.d.ts +10 -0
- package/dist/map.js +9 -0
- package/dist/molecules/Alert.svelte +31 -12
- package/dist/molecules/Alert.svelte.d.ts +12 -1
- package/dist/molecules/AppBar.svelte +287 -28
- package/dist/molecules/AppBar.svelte.d.ts +62 -0
- package/dist/molecules/AvatarGroup.svelte +1 -1
- package/dist/molecules/BarChart.svelte +381 -0
- package/dist/molecules/BarChart.svelte.d.ts +45 -0
- package/dist/molecules/BottomSheet.svelte +88 -20
- package/dist/molecules/BottomSheet.svelte.d.ts +17 -1
- package/dist/molecules/BottomTabBar.svelte +134 -28
- package/dist/molecules/BottomTabBar.svelte.d.ts +36 -2
- package/dist/molecules/Calendar.svelte.d.ts +1 -1
- package/dist/molecules/ChipGroup.svelte +325 -0
- package/dist/molecules/ChipGroup.svelte.d.ts +41 -0
- package/dist/molecules/Collapsible.svelte +158 -54
- package/dist/molecules/Collapsible.svelte.d.ts +23 -6
- package/dist/molecules/CollapsibleGroup.svelte +16 -6
- package/dist/molecules/ConfirmDialog.svelte +5 -1
- package/dist/molecules/ConfirmDialog.svelte.d.ts +3 -0
- package/dist/molecules/Drawer.svelte +33 -12
- package/dist/molecules/Dropdown.svelte +9 -1
- package/dist/molecules/Dropdown.svelte.d.ts +2 -0
- package/dist/molecules/DropdownItem.svelte +1 -1
- package/dist/molecules/ImageUpload.svelte +230 -120
- package/dist/molecules/ImageUpload.svelte.d.ts +38 -3
- package/dist/molecules/LineChart.svelte +459 -0
- package/dist/molecules/LineChart.svelte.d.ts +53 -0
- package/dist/molecules/MapView.svelte +565 -0
- package/dist/molecules/MapView.svelte.d.ts +88 -0
- package/dist/molecules/Modal.svelte +11 -6
- package/dist/molecules/PhotoGrid.svelte +100 -10
- package/dist/molecules/PhotoGrid.svelte.d.ts +11 -0
- package/dist/molecules/PhotoViewer.svelte +121 -24
- package/dist/molecules/PhotoViewer.svelte.d.ts +18 -0
- package/dist/molecules/PieChart.svelte +421 -0
- package/dist/molecules/PieChart.svelte.d.ts +51 -0
- package/dist/molecules/RadioGroup.svelte +20 -4
- package/dist/molecules/RadioGroup.svelte.d.ts +5 -0
- package/dist/molecules/SegmentedControl.svelte +136 -3
- package/dist/molecules/SegmentedControl.svelte.d.ts +6 -0
- package/dist/molecules/SidebarFooter.svelte +1 -1
- package/dist/molecules/SidebarNavSection.svelte +1 -1
- package/dist/molecules/SortableList.svelte +1 -0
- package/dist/molecules/Stepper.svelte +21 -7
- package/dist/molecules/StickyActionBar.svelte +67 -8
- package/dist/molecules/StickyActionBar.svelte.d.ts +14 -1
- package/dist/molecules/Tabs.svelte +79 -2
- package/dist/molecules/Toaster.svelte +171 -7
- package/dist/molecules/Toaster.svelte.d.ts +60 -2
- package/dist/molecules/ToasterToast.svelte +292 -60
- package/dist/molecules/ToasterToast.svelte.d.ts +6 -0
- package/dist/molecules/UnsavedChangesBar.svelte +13 -2
- package/dist/molecules/index.d.ts +7 -2
- package/dist/molecules/index.js +4 -0
- package/dist/molecules/toast-store.d.ts +30 -0
- package/dist/molecules/toast-store.js +4 -1
- package/dist/organisms/AppNavigation.svelte +440 -0
- package/dist/organisms/AppNavigation.svelte.d.ts +72 -0
- package/dist/organisms/AppShell.svelte +539 -39
- package/dist/organisms/AppShell.svelte.d.ts +102 -6
- package/dist/organisms/AppShellHeaderPart.svelte +163 -0
- package/dist/organisms/AppShellHeaderPart.svelte.d.ts +57 -0
- package/dist/organisms/SidebarNavigation.svelte +23 -36
- package/dist/organisms/SidebarPanel.svelte +10 -14
- package/dist/organisms/SidebarPanel.svelte.d.ts +1 -0
- package/dist/organisms/SidebarShell.svelte +2 -3
- package/dist/organisms/TopNavbar.svelte +117 -28
- package/dist/organisms/TopNavbar.svelte.d.ts +22 -1
- package/dist/organisms/TopNavbarSheetContent.svelte +145 -0
- package/dist/organisms/TopNavbarSheetContent.svelte.d.ts +24 -0
- package/dist/organisms/index.d.ts +6 -1
- package/dist/organisms/index.js +4 -0
- package/dist/types/components.d.ts +17 -1
- package/dist/types/components.d.ts.map +1 -1
- package/dist/types/index.d.ts +497 -10
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/variants.ts +14 -3
- package/dist/util/app-shell.d.ts +55 -4
- package/dist/util/app-shell.js +32 -7
- package/dist/util/avatar.d.ts +3 -0
- package/dist/util/bottom-tab-bar.d.ts +6 -0
- package/dist/util/chart.d.ts +139 -0
- package/dist/util/chart.js +305 -0
- package/dist/util/chip.d.ts +65 -0
- package/dist/util/chip.js +58 -0
- package/dist/util/cn.js +28 -2
- package/dist/util/code-field.d.ts +62 -0
- package/dist/util/code-field.js +112 -0
- package/dist/util/collapsible.d.ts +6 -0
- package/dist/util/display-voice.d.ts +9 -0
- package/dist/util/display-voice.js +9 -0
- package/dist/util/field-control.d.ts +29 -0
- package/dist/util/field-control.js +32 -0
- package/dist/util/field.d.ts +6 -0
- package/dist/util/focus-utils.d.ts +2 -0
- package/dist/util/focus-utils.js +4 -0
- package/dist/util/map-view.d.ts +143 -0
- package/dist/util/map-view.js +121 -0
- package/dist/util/photo.d.ts +19 -0
- package/dist/util/photo.js +3 -0
- package/dist/util/progress.d.ts +18 -0
- package/dist/util/progress.js +17 -0
- package/dist/util/scroll-edge.d.ts +32 -0
- package/dist/util/scroll-edge.js +87 -0
- package/dist/util/sheet-drag.d.ts +25 -1
- package/dist/util/sheet-drag.js +71 -1
- package/dist/util/skeleton.d.ts +14 -0
- package/dist/util/skeleton.js +22 -0
- package/dist/util/sliding-indicator.d.ts +24 -0
- package/dist/util/sliding-indicator.js +44 -0
- package/dist/util/temporal-field.d.ts +88 -0
- package/dist/util/temporal-field.js +136 -0
- package/dist/util/toaster.d.ts +40 -0
- package/dist/util/toaster.js +55 -0
- package/dist/util/tone-surface.d.ts +33 -0
- package/dist/util/tone-surface.js +35 -0
- package/dist/util/top-navbar.d.ts +26 -2
- package/dist/util/top-navbar.js +23 -0
- package/dist/util/zabi-strings.d.ts +9 -0
- package/dist/zabi-components-colors.css +9 -3
- package/dist/zabi-components-theme-dark-only.css +474 -164
- package/dist/zabi-components-theme-dark.css +474 -164
- package/dist/zabi-components-theme-only.css +933 -99
- package/dist/zabi-components-theme.css +933 -99
- package/dist/zabi-components.css +2275 -321
- package/package.json +11 -1
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import Table from "../atoms/Table.svelte";
|
|
4
|
+
import { cn } from "../util/cn.js";
|
|
5
|
+
import {
|
|
6
|
+
CHART_GLASS,
|
|
7
|
+
chartColors,
|
|
8
|
+
chartFormatter,
|
|
9
|
+
chartMarker,
|
|
10
|
+
chartScale,
|
|
11
|
+
chartTextScale,
|
|
12
|
+
labelEvery,
|
|
13
|
+
measureWidth,
|
|
14
|
+
widestText,
|
|
15
|
+
linePath,
|
|
16
|
+
markerPath,
|
|
17
|
+
type ChartFormat,
|
|
18
|
+
type ChartSeries,
|
|
19
|
+
} from "../util/chart.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A line chart for one or a few series over shared x labels, drawn as
|
|
23
|
+
* plain SVG.
|
|
24
|
+
*
|
|
25
|
+
* It is a `figure`: the `label` is its caption, the drawing is hidden
|
|
26
|
+
* from assistive technology, and the same numbers are in a real table
|
|
27
|
+
* (off screen unless `showTable`). Each series has its own marker shape
|
|
28
|
+
* as well as its own colour. Pointing at or tapping the plot shows the
|
|
29
|
+
* values at that x label in a small pane of the library's thick material
|
|
30
|
+
* that floats over the plot beside the guide, as a Tooltip does.
|
|
31
|
+
*
|
|
32
|
+
* The six series colours are `--zabi-chart-1` to `--zabi-chart-6`, set on
|
|
33
|
+
* the chart or on anything around it; they default to colours made from
|
|
34
|
+
* theme tokens that follow light and dark. Each marker has a ring cut out
|
|
35
|
+
* of the lines around it, so it shows whatever the chart stands on;
|
|
36
|
+
* `--zabi-chart-surface` paints that ring a colour of your own.
|
|
37
|
+
*/
|
|
38
|
+
type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
|
|
39
|
+
/** The lines: a `name` and one value per x label (`null` leaves a gap). Up to six keep a colour of their own. */
|
|
40
|
+
series: ChartSeries[];
|
|
41
|
+
/** The labels along the x axis, one per point. */
|
|
42
|
+
xLabels?: string[];
|
|
43
|
+
/** The bottom of the y axis. Without it, the lowest value rounded down to a tick. */
|
|
44
|
+
yMin?: number;
|
|
45
|
+
/** The top of the y axis. Without it, the highest value rounded up to a tick. */
|
|
46
|
+
yMax?: number;
|
|
47
|
+
/** How a value is written: `number` (default), `percent` (72 is "72 %"), or a function. */
|
|
48
|
+
yFormat?: ChartFormat;
|
|
49
|
+
/** Height of the drawing in px. The width is the container's. */
|
|
50
|
+
height?: number;
|
|
51
|
+
/** The caption, and what the chart is called. */
|
|
52
|
+
label: string;
|
|
53
|
+
/** Keeps the caption for assistive technology and takes it off the screen. */
|
|
54
|
+
labelHidden?: boolean;
|
|
55
|
+
/** The key under the plot. Without it, shown when there is more than one series. */
|
|
56
|
+
legend?: boolean;
|
|
57
|
+
/** A marker on every point, a different shape per series. */
|
|
58
|
+
markers?: boolean;
|
|
59
|
+
/** Shows the data table under the chart; otherwise it is there for assistive technology only. */
|
|
60
|
+
showTable?: boolean;
|
|
61
|
+
/** The heading of the table's first column (what the x labels are: "Week"). */
|
|
62
|
+
xTitle?: string;
|
|
63
|
+
/** The language of the built-in number formats. Without it, the runtime's. */
|
|
64
|
+
locale?: string;
|
|
65
|
+
/** Stands the chart on a panel of the thick material (frosted glass), for a chart that floats over a wash, a tint or a picture. On a plain card it shows nothing: leave it off there. */
|
|
66
|
+
glass?: boolean;
|
|
67
|
+
class?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
let {
|
|
71
|
+
series,
|
|
72
|
+
xLabels = [],
|
|
73
|
+
yMin,
|
|
74
|
+
yMax,
|
|
75
|
+
yFormat = "number",
|
|
76
|
+
height = 240,
|
|
77
|
+
label,
|
|
78
|
+
labelHidden = false,
|
|
79
|
+
legend,
|
|
80
|
+
markers = true,
|
|
81
|
+
showTable = false,
|
|
82
|
+
xTitle = "",
|
|
83
|
+
locale,
|
|
84
|
+
glass = false,
|
|
85
|
+
class: className = "",
|
|
86
|
+
...restProps
|
|
87
|
+
}: Props = $props();
|
|
88
|
+
|
|
89
|
+
const uid = $props.id();
|
|
90
|
+
const maskId = `${uid}-rings`;
|
|
91
|
+
|
|
92
|
+
/** Before it is measured (on the server) the drawing is laid out this wide and scaled to fit. */
|
|
93
|
+
const FALLBACK_WIDTH = 600;
|
|
94
|
+
const TOP = 8;
|
|
95
|
+
/** Room at each end of the plot, so the first and last markers stand inside it. */
|
|
96
|
+
const INSET = 8;
|
|
97
|
+
const MARKER = 4;
|
|
98
|
+
|
|
99
|
+
let measured = $state(0);
|
|
100
|
+
let tickWidth = $state(0);
|
|
101
|
+
let active = $state<number | null>(null);
|
|
102
|
+
|
|
103
|
+
const width = $derived(measured > 0 ? measured : FALLBACK_WIDTH);
|
|
104
|
+
/** Chart text is set in rem; the room for it grows with the reader's text size. */
|
|
105
|
+
const rem = $derived(measured > 0 ? chartTextScale() : 1);
|
|
106
|
+
const plotHeight = $derived(Math.max(height, 80));
|
|
107
|
+
const bottom = $derived(Math.round(28 * rem));
|
|
108
|
+
const format = $derived(chartFormatter(yFormat, locale));
|
|
109
|
+
const count = $derived(Math.max(xLabels.length, ...series.map((line) => line.points.length), 0));
|
|
110
|
+
const scale = $derived(chartScale(series.flatMap((line) => line.points), { min: yMin, max: yMax }));
|
|
111
|
+
const tickLabels = $derived(scale.ticks.map((tick) => format(tick)));
|
|
112
|
+
/** The value axis takes the width of its widest label as drawn; until that is known, an estimate. */
|
|
113
|
+
const left = $derived((tickWidth || Math.max(...tickLabels.map((label) => label.length), 1) * 7 * rem) + 12);
|
|
114
|
+
const innerWidth = $derived(Math.max(width - left, 10));
|
|
115
|
+
const innerHeight = $derived(Math.max(plotHeight - TOP - bottom, 10));
|
|
116
|
+
const span = $derived(Math.max(innerWidth - INSET * 2, 1));
|
|
117
|
+
const every = $derived(labelEvery(xLabels, span, 7 * rem));
|
|
118
|
+
const showLegend = $derived(legend ?? series.length > 1);
|
|
119
|
+
/** A marker on every point only while the points stand apart; closer than that they would run into a band. */
|
|
120
|
+
const roomForMarkers = $derived(count <= 1 || span / (count - 1) >= MARKER * 3);
|
|
121
|
+
|
|
122
|
+
const x = (index: number) => (count <= 1 ? left + innerWidth / 2 : left + INSET + (index * span) / (count - 1));
|
|
123
|
+
const y = (value: number) => TOP + innerHeight - ((value - scale.min) / (scale.max - scale.min)) * innerHeight;
|
|
124
|
+
const isValue = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
125
|
+
|
|
126
|
+
const colors = $derived(chartColors(series.map((line) => line.color)));
|
|
127
|
+
const lines = $derived(
|
|
128
|
+
series.map((line, index) => {
|
|
129
|
+
const points = Array.from({ length: count }, (_, at) => {
|
|
130
|
+
const value = line.points[at];
|
|
131
|
+
return isValue(value) ? { x: x(at), y: y(value) } : null;
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
name: line.name,
|
|
135
|
+
color: colors[index],
|
|
136
|
+
marker: chartMarker(index),
|
|
137
|
+
points,
|
|
138
|
+
path: linePath(points),
|
|
139
|
+
};
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
function nearest(event: PointerEvent) {
|
|
144
|
+
if (count === 0) return;
|
|
145
|
+
const box = (event.currentTarget as SVGSVGElement).getBoundingClientRect();
|
|
146
|
+
if (!(box.width > 0)) return;
|
|
147
|
+
const at = ((event.clientX - box.left) / box.width) * width;
|
|
148
|
+
const index = count <= 1 ? 0 : Math.round(((at - left - INSET) / span) * (count - 1));
|
|
149
|
+
active = Math.min(count - 1, Math.max(0, index));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A finger lifting leaves the values up; a mouse leaving takes them away. */
|
|
153
|
+
function leave(event: PointerEvent) {
|
|
154
|
+
if (event.pointerType === "mouse") active = null;
|
|
155
|
+
}
|
|
156
|
+
</script>
|
|
157
|
+
|
|
158
|
+
<figure
|
|
159
|
+
class={cn("zabi-chart relative m-0 flex flex-col gap-3", glass && CHART_GLASS, className)}
|
|
160
|
+
{...restProps}
|
|
161
|
+
>
|
|
162
|
+
<figcaption class={cn("text-label text-sm font-medium", labelHidden && "sr-only")}>{label}</figcaption>
|
|
163
|
+
|
|
164
|
+
<div class="relative w-full" use:measureWidth={(value) => (measured = value)}>
|
|
165
|
+
<svg
|
|
166
|
+
class="plot block w-full"
|
|
167
|
+
viewBox="0 0 {width} {plotHeight}"
|
|
168
|
+
height={plotHeight}
|
|
169
|
+
aria-hidden="true"
|
|
170
|
+
focusable="false"
|
|
171
|
+
onpointermove={nearest}
|
|
172
|
+
onpointerdown={nearest}
|
|
173
|
+
onpointerleave={leave}
|
|
174
|
+
>
|
|
175
|
+
<g use:widestText={{ onwidth: (value) => (tickWidth = value), key: [tickLabels, measured] }}>
|
|
176
|
+
{#each scale.ticks as tick, index (tick)}
|
|
177
|
+
<line class="grid" class:axis={index === 0} x1={left} x2={left + innerWidth} y1={y(tick)} y2={y(tick)} />
|
|
178
|
+
<text class="tick" x={left - 12} y={y(tick)} text-anchor="end" dominant-baseline="central">
|
|
179
|
+
{tickLabels[index]}
|
|
180
|
+
</text>
|
|
181
|
+
{/each}
|
|
182
|
+
</g>
|
|
183
|
+
|
|
184
|
+
{#each xLabels as text, index (index)}
|
|
185
|
+
{#if index % every === 0 && index < count}
|
|
186
|
+
<text
|
|
187
|
+
class="tick"
|
|
188
|
+
class:on={index === active}
|
|
189
|
+
x={count > 1 && index === 0 ? left : count > 1 && index === count - 1 ? left + innerWidth : x(index)}
|
|
190
|
+
y={plotHeight - bottom + 20 * rem}
|
|
191
|
+
text-anchor={count > 1 && index === 0 ? "start" : count > 1 && index === count - 1 ? "end" : "middle"}
|
|
192
|
+
>
|
|
193
|
+
{text}
|
|
194
|
+
</text>
|
|
195
|
+
{/if}
|
|
196
|
+
{/each}
|
|
197
|
+
|
|
198
|
+
{#if active !== null}
|
|
199
|
+
<line class="guide" x1={x(active)} x2={x(active)} y1={TOP} y2={TOP + innerHeight} />
|
|
200
|
+
{/if}
|
|
201
|
+
|
|
202
|
+
<!-- A ring is cut out of the lines around every marker, so what shows in it is
|
|
203
|
+
whatever the chart stands on: a card, a tint, a pane of glass. -->
|
|
204
|
+
<mask id={maskId} maskUnits="userSpaceOnUse" x="0" y="0" {width} height={plotHeight}>
|
|
205
|
+
<rect {width} height={plotHeight} fill="white" />
|
|
206
|
+
{#each lines as line, lineIndex (lineIndex)}
|
|
207
|
+
{#each line.points as point, index (index)}
|
|
208
|
+
{#if point && ((markers && roomForMarkers) || index === active)}
|
|
209
|
+
<path
|
|
210
|
+
class="ring"
|
|
211
|
+
d={markerPath(line.marker, point.x, point.y, index === active ? MARKER + 1.5 : MARKER)}
|
|
212
|
+
/>
|
|
213
|
+
{/if}
|
|
214
|
+
{/each}
|
|
215
|
+
{/each}
|
|
216
|
+
</mask>
|
|
217
|
+
<g mask="url(#{maskId})">
|
|
218
|
+
{#each lines as line, lineIndex (lineIndex)}
|
|
219
|
+
<g class="series" style:--_c={line.color}>
|
|
220
|
+
<path class="line" d={line.path} />
|
|
221
|
+
</g>
|
|
222
|
+
{/each}
|
|
223
|
+
</g>
|
|
224
|
+
{#each lines as line, lineIndex (lineIndex)}
|
|
225
|
+
<g class="marks" style:--_c={line.color}>
|
|
226
|
+
{#each line.points as point, index (index)}
|
|
227
|
+
{#if point && ((markers && roomForMarkers) || index === active)}
|
|
228
|
+
<path
|
|
229
|
+
class="marker"
|
|
230
|
+
d={markerPath(line.marker, point.x, point.y, index === active ? MARKER + 1.5 : MARKER)}
|
|
231
|
+
/>
|
|
232
|
+
{/if}
|
|
233
|
+
{/each}
|
|
234
|
+
</g>
|
|
235
|
+
{/each}
|
|
236
|
+
</svg>
|
|
237
|
+
|
|
238
|
+
<!-- The values at the x label pointed at: a pane of the same material as a Tooltip, over the
|
|
239
|
+
plot and beside the guide, on the side with more room. The table says the same to
|
|
240
|
+
assistive technology. -->
|
|
241
|
+
{#if active !== null}
|
|
242
|
+
{@const before = x(active) > left + innerWidth / 2}
|
|
243
|
+
<div
|
|
244
|
+
class="readout material-thick text-body rounded-control px-3 py-2 text-sm leading-5"
|
|
245
|
+
style:top="{TOP}px"
|
|
246
|
+
style:left={before ? undefined : `${((x(active) + 12) / width) * 100}%`}
|
|
247
|
+
style:right={before ? `${((width - x(active) + 12) / width) * 100}%` : undefined}
|
|
248
|
+
aria-hidden="true"
|
|
249
|
+
>
|
|
250
|
+
{#if xLabels[active] !== undefined}
|
|
251
|
+
<div class="text-label font-medium">{xLabels[active]}</div>
|
|
252
|
+
{/if}
|
|
253
|
+
{#each lines as line, index (index)}
|
|
254
|
+
{@const value = series[index].points[active]}
|
|
255
|
+
{#if isValue(value)}
|
|
256
|
+
<div class="row">
|
|
257
|
+
<svg class="swatch" viewBox="0 0 12 12" width="12" height="12" style:--_c={line.color}>
|
|
258
|
+
<path class="marker" d={markerPath(line.marker, 6, 6, 4)} />
|
|
259
|
+
</svg>
|
|
260
|
+
<span class="name text-description">{line.name}</span>
|
|
261
|
+
<span class="text-headline font-medium tabular-nums">{format(value)}</span>
|
|
262
|
+
</div>
|
|
263
|
+
{/if}
|
|
264
|
+
{/each}
|
|
265
|
+
</div>
|
|
266
|
+
{/if}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
<!-- The key. The table below says the same to assistive technology. -->
|
|
270
|
+
<div class="key text-description text-sm" aria-hidden="true">
|
|
271
|
+
{#if showLegend}
|
|
272
|
+
{#each lines as line, index (index)}
|
|
273
|
+
<span class="entry">
|
|
274
|
+
<svg class="swatch" viewBox="0 0 20 12" width="20" height="12" style:--_c={line.color}>
|
|
275
|
+
<path class="line" d="M1 6H19" />
|
|
276
|
+
<path class="marker" d={markerPath(line.marker, 10, 6, 3.5)} />
|
|
277
|
+
</svg>
|
|
278
|
+
<span>{line.name}</span>
|
|
279
|
+
</span>
|
|
280
|
+
{/each}
|
|
281
|
+
{/if}
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<!-- The library's own Table, so the numbers look like every other table in the app. -->
|
|
285
|
+
<Table class={showTable ? "" : "sr-only"}>
|
|
286
|
+
<thead class="border-border bg-surface-elevated border-b">
|
|
287
|
+
<tr>
|
|
288
|
+
{#if xTitle}<th class="text-headline px-4 py-3 text-start font-medium" scope="col">{xTitle}</th>{:else}<td></td>{/if}
|
|
289
|
+
{#each series as line, column (column)}
|
|
290
|
+
<th class="text-headline px-4 py-3 text-end font-medium" scope="col">{line.name}</th>
|
|
291
|
+
{/each}
|
|
292
|
+
</tr>
|
|
293
|
+
</thead>
|
|
294
|
+
<tbody class="divide-border divide-y">
|
|
295
|
+
{#each { length: count } as _, index (index)}
|
|
296
|
+
<tr>
|
|
297
|
+
<th class="text-body px-4 py-3 text-start font-normal" scope="row">{xLabels[index] ?? index + 1}</th>
|
|
298
|
+
{#each series as line, column (column)}
|
|
299
|
+
{@const value = line.points[index]}
|
|
300
|
+
<td class="text-body px-4 py-3 text-end tabular-nums">{isValue(value) ? format(value) : ""}</td>
|
|
301
|
+
{/each}
|
|
302
|
+
</tr>
|
|
303
|
+
{/each}
|
|
304
|
+
</tbody>
|
|
305
|
+
</Table>
|
|
306
|
+
</figure>
|
|
307
|
+
|
|
308
|
+
<style>
|
|
309
|
+
/*
|
|
310
|
+
* What an app may set, on the chart or on anything around it; read with
|
|
311
|
+
* a fallback and never declared here:
|
|
312
|
+
* --zabi-chart-1 … --zabi-chart-6 the series colours (util/chart.ts)
|
|
313
|
+
* --zabi-chart-surface what the chart stands on; rings the markers
|
|
314
|
+
*/
|
|
315
|
+
.plot {
|
|
316
|
+
overflow: visible;
|
|
317
|
+
/* A sideways drag reads along the line; an up or down one still scrolls the page. */
|
|
318
|
+
touch-action: pan-y;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* The library's hairline, as between the rows of a list; the line the plot
|
|
322
|
+
stands on is the edge a Progress track has. */
|
|
323
|
+
.grid {
|
|
324
|
+
stroke: var(--color-border);
|
|
325
|
+
stroke-width: 1;
|
|
326
|
+
shape-rendering: crispEdges;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.grid.axis {
|
|
330
|
+
stroke: var(--color-border-strong);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.guide {
|
|
334
|
+
stroke: var(--color-border-medium);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.guide {
|
|
338
|
+
stroke-width: 1;
|
|
339
|
+
shape-rendering: crispEdges;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.tick {
|
|
343
|
+
fill: var(--color-caption);
|
|
344
|
+
font-size: 0.75rem;
|
|
345
|
+
font-variant-numeric: tabular-nums;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.tick.on {
|
|
349
|
+
fill: var(--color-headline);
|
|
350
|
+
font-weight: 500;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.line {
|
|
354
|
+
fill: none;
|
|
355
|
+
stroke: var(--_c);
|
|
356
|
+
stroke-width: 2;
|
|
357
|
+
stroke-linecap: round;
|
|
358
|
+
stroke-linejoin: round;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* A marker stands in a ring cut out of the lines (the mask above), so the ring is whatever
|
|
362
|
+
is behind the chart. An app that sets --zabi-chart-surface gets the ring painted instead. */
|
|
363
|
+
.marker {
|
|
364
|
+
fill: var(--_c);
|
|
365
|
+
stroke: var(--zabi-chart-surface, transparent);
|
|
366
|
+
stroke-width: 2;
|
|
367
|
+
stroke-linejoin: round;
|
|
368
|
+
paint-order: stroke;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.ring {
|
|
372
|
+
fill: black;
|
|
373
|
+
stroke: black;
|
|
374
|
+
stroke-width: 4;
|
|
375
|
+
stroke-linejoin: round;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.zabi-chart {
|
|
379
|
+
container-type: inline-size;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.key {
|
|
383
|
+
display: flex;
|
|
384
|
+
flex-wrap: wrap;
|
|
385
|
+
align-items: center;
|
|
386
|
+
gap: 0.25rem 1.25rem;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.key:empty {
|
|
390
|
+
display: none;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.entry {
|
|
394
|
+
display: inline-flex;
|
|
395
|
+
align-items: center;
|
|
396
|
+
gap: 0.5rem;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* The pane of values: it floats, so it is glass (the fill, blur, edge and every fallback
|
|
400
|
+
come with .material-thick), and it never takes the pointer from the plot under it. */
|
|
401
|
+
.readout {
|
|
402
|
+
position: absolute;
|
|
403
|
+
z-index: 1;
|
|
404
|
+
display: grid;
|
|
405
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
406
|
+
gap: 0.125rem 0.5rem;
|
|
407
|
+
max-inline-size: min(16rem, 70%);
|
|
408
|
+
pointer-events: none;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.readout > :first-child:not(.row) {
|
|
412
|
+
grid-column: 1 / -1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.row {
|
|
416
|
+
display: grid;
|
|
417
|
+
grid-column: 1 / -1;
|
|
418
|
+
grid-template-columns: subgrid;
|
|
419
|
+
align-items: center;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.row .name {
|
|
423
|
+
padding-inline-end: 0.5rem;
|
|
424
|
+
overflow-wrap: anywhere;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.swatch {
|
|
428
|
+
flex: none;
|
|
429
|
+
overflow: visible;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* In the key a marker sits on its own line: there the ring is the line knocked back. */
|
|
433
|
+
.swatch .marker {
|
|
434
|
+
stroke: none;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* Forced colours: the drawing is in the system's text colour, and the
|
|
438
|
+
marker shapes and the key tell the series apart. */
|
|
439
|
+
@media (forced-colors: active) {
|
|
440
|
+
.line {
|
|
441
|
+
stroke: CanvasText;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.marker {
|
|
445
|
+
fill: CanvasText;
|
|
446
|
+
stroke: Canvas;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.tick {
|
|
450
|
+
fill: CanvasText;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.grid,
|
|
454
|
+
.grid.axis,
|
|
455
|
+
.guide {
|
|
456
|
+
stroke: GrayText;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
</style>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
2
|
+
import { type ChartFormat, type ChartSeries } from "../util/chart.js";
|
|
3
|
+
/**
|
|
4
|
+
* A line chart for one or a few series over shared x labels, drawn as
|
|
5
|
+
* plain SVG.
|
|
6
|
+
*
|
|
7
|
+
* It is a `figure`: the `label` is its caption, the drawing is hidden
|
|
8
|
+
* from assistive technology, and the same numbers are in a real table
|
|
9
|
+
* (off screen unless `showTable`). Each series has its own marker shape
|
|
10
|
+
* as well as its own colour. Pointing at or tapping the plot shows the
|
|
11
|
+
* values at that x label in a small pane of the library's thick material
|
|
12
|
+
* that floats over the plot beside the guide, as a Tooltip does.
|
|
13
|
+
*
|
|
14
|
+
* The six series colours are `--zabi-chart-1` to `--zabi-chart-6`, set on
|
|
15
|
+
* the chart or on anything around it; they default to colours made from
|
|
16
|
+
* theme tokens that follow light and dark. Each marker has a ring cut out
|
|
17
|
+
* of the lines around it, so it shows whatever the chart stands on;
|
|
18
|
+
* `--zabi-chart-surface` paints that ring a colour of your own.
|
|
19
|
+
*/
|
|
20
|
+
type Props = Omit<HTMLAttributes<HTMLElement>, "class"> & {
|
|
21
|
+
/** The lines: a `name` and one value per x label (`null` leaves a gap). Up to six keep a colour of their own. */
|
|
22
|
+
series: ChartSeries[];
|
|
23
|
+
/** The labels along the x axis, one per point. */
|
|
24
|
+
xLabels?: string[];
|
|
25
|
+
/** The bottom of the y axis. Without it, the lowest value rounded down to a tick. */
|
|
26
|
+
yMin?: number;
|
|
27
|
+
/** The top of the y axis. Without it, the highest value rounded up to a tick. */
|
|
28
|
+
yMax?: number;
|
|
29
|
+
/** How a value is written: `number` (default), `percent` (72 is "72 %"), or a function. */
|
|
30
|
+
yFormat?: ChartFormat;
|
|
31
|
+
/** Height of the drawing in px. The width is the container's. */
|
|
32
|
+
height?: number;
|
|
33
|
+
/** The caption, and what the chart is called. */
|
|
34
|
+
label: string;
|
|
35
|
+
/** Keeps the caption for assistive technology and takes it off the screen. */
|
|
36
|
+
labelHidden?: boolean;
|
|
37
|
+
/** The key under the plot. Without it, shown when there is more than one series. */
|
|
38
|
+
legend?: boolean;
|
|
39
|
+
/** A marker on every point, a different shape per series. */
|
|
40
|
+
markers?: boolean;
|
|
41
|
+
/** Shows the data table under the chart; otherwise it is there for assistive technology only. */
|
|
42
|
+
showTable?: boolean;
|
|
43
|
+
/** The heading of the table's first column (what the x labels are: "Week"). */
|
|
44
|
+
xTitle?: string;
|
|
45
|
+
/** The language of the built-in number formats. Without it, the runtime's. */
|
|
46
|
+
locale?: string;
|
|
47
|
+
/** Stands the chart on a panel of the thick material (frosted glass), for a chart that floats over a wash, a tint or a picture. On a plain card it shows nothing: leave it off there. */
|
|
48
|
+
glass?: boolean;
|
|
49
|
+
class?: string;
|
|
50
|
+
};
|
|
51
|
+
declare const LineChart: import("svelte").Component<Props, {}, "">;
|
|
52
|
+
type LineChart = ReturnType<typeof LineChart>;
|
|
53
|
+
export default LineChart;
|