@dataengineeringformachinelearning/viking-ui 3.0.0-alpha.2 → 4.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/LICENSE +201 -0
- package/README.md +55 -12
- package/dist/LICENSE +201 -0
- package/dist/README.md +251 -0
- package/dist/fesm2022/dataengineeringformachinelearning-viking-ui.mjs +13751 -0
- package/dist/fesm2022/dataengineeringformachinelearning-viking-ui.mjs.map +1 -0
- package/dist/index.js +1 -4051
- package/dist/package.json +87 -0
- package/dist/scripts/build-web-components.mjs +37 -0
- package/dist/src/tokens/series-presets.ts +15 -0
- package/dist/src/tokens/tailwind.preset.js +209 -0
- package/dist/src/tokens/viking-tokens.json +318 -0
- package/dist/src/web/badge/viking-badge-wc.ts +116 -0
- package/dist/src/web/button/viking-button-wc.ts +165 -0
- package/dist/src/web/callout/viking-callout-wc.ts +105 -0
- package/dist/src/web/card/viking-card-wc.ts +66 -0
- package/dist/src/web/core/base.ts +56 -0
- package/dist/src/web/core/dom.ts +53 -0
- package/dist/src/web/core/icons-inline.ts +44 -0
- package/dist/src/web/core/styles.ts +1138 -0
- package/dist/src/web/core/types.ts +19 -0
- package/dist/src/web/field/viking-field-wc.ts +154 -0
- package/dist/src/web/index.ts +73 -0
- package/dist/src/web/input/viking-input-wc.ts +211 -0
- package/dist/src/web/modal/viking-modal-wc.ts +187 -0
- package/dist/src/web/search-palette/viking-search-palette-wc.ts +494 -0
- package/dist/src/web/select/viking-select-wc.ts +218 -0
- package/dist/src/web/suite-header/viking-suite-header-wc.ts +714 -0
- package/dist/src/web/suite-search-palette/viking-suite-search-palette-wc.ts +234 -0
- package/dist/src/web/theme-toggle/viking-theme-toggle-wc.ts +139 -0
- package/dist/src/web/types.ts +37 -0
- package/dist/src/web/web-components.spec.ts +297 -0
- package/dist/types/dataengineeringformachinelearning-viking-ui.d.ts +2658 -0
- package/dist/viking.manifest.json +271 -0
- package/dist/web-components.js +2427 -126
- package/package.json +25 -11
- package/src/assets/drakkar-favicon.svg +17 -0
- package/src/core/brand/deml-lockup.svg +5 -0
- package/src/core/brand/deml-mark-compact.svg +4 -0
- package/src/core/brand/deml-mark-filled.svg +3 -0
- package/src/core/brand/deml-mark-outline.svg +4 -0
- package/src/core/brand-icons.ts +62 -0
- package/src/core/icons.spec.ts +52 -0
- package/src/core/icons.ts +206 -0
- package/src/core/integration-brand-icons.ts +77 -0
- package/src/core/lucide-paths.generated.ts +130 -0
- package/src/core/types.ts +85 -0
- package/src/core/uid.ts +7 -0
- package/src/lib/accordion/accordion.ts +126 -0
- package/src/lib/app-header/app-header.scss +274 -0
- package/src/lib/app-header/app-header.ts +43 -0
- package/src/lib/app-sidebar/app-sidebar.scss +415 -0
- package/src/lib/app-sidebar/app-sidebar.ts +29 -0
- package/src/lib/auth-panel/auth-panel.spec.ts +132 -0
- package/src/lib/auth-panel/auth-panel.ts +228 -0
- package/src/lib/autocomplete/autocomplete.ts +223 -0
- package/src/lib/avatar/avatar.ts +126 -0
- package/src/lib/badge/badge.ts +182 -0
- package/src/lib/bar/bar.ts +31 -0
- package/src/lib/brand/brand.ts +60 -0
- package/src/lib/breadcrumbs/breadcrumbs.ts +81 -0
- package/src/lib/button/button-group.ts +40 -0
- package/src/lib/button/button.scss +96 -0
- package/src/lib/button/button.ts +98 -0
- package/src/lib/calendar/calendar.ts +253 -0
- package/src/lib/callout/callout.ts +211 -0
- package/src/lib/card/card-title.spec.ts +27 -0
- package/src/lib/card/card-title.ts +50 -0
- package/src/lib/card/card.ts +170 -0
- package/src/lib/carousel/carousel.ts +206 -0
- package/src/lib/chart/chart-zoom.spec.ts +65 -0
- package/src/lib/chart/chart-zoom.ts +71 -0
- package/src/lib/chart/chart.ts +1257 -0
- package/src/lib/chart-card-header/chart-card-header.ts +80 -0
- package/src/lib/chart-empty-state/chart-empty-state.ts +146 -0
- package/src/lib/chart-panel/chart-panel.ts +133 -0
- package/src/lib/checkbox/checkbox.ts +139 -0
- package/src/lib/color-picker/color-picker.ts +148 -0
- package/src/lib/command/command.ts +260 -0
- package/src/lib/composer/composer.ts +180 -0
- package/src/lib/confirm-dialog/confirm-dialog.ts +163 -0
- package/src/lib/confirm-dialog/dialog.service.ts +79 -0
- package/src/lib/context/context.ts +75 -0
- package/src/lib/core/cva.ts +33 -0
- package/src/lib/date-picker/date-picker.ts +139 -0
- package/src/lib/dropdown/dropdown.ts +188 -0
- package/src/lib/editor/editor.ts +170 -0
- package/src/lib/empty-state/empty-state.ts +100 -0
- package/src/lib/fab/fab.ts +76 -0
- package/src/lib/field/field.ts +111 -0
- package/src/lib/file-upload/file-upload.ts +208 -0
- package/src/lib/footer/footer.scss +155 -0
- package/src/lib/footer/footer.ts +28 -0
- package/src/lib/form-section/form-section.ts +200 -0
- package/src/lib/forms.spec.ts +92 -0
- package/src/lib/gauge-arc/gauge-arc.ts +85 -0
- package/src/lib/hud-panel/hud-panel.ts +27 -0
- package/src/lib/icon/icon.ts +284 -0
- package/src/lib/icon-badge/icon-badge.ts +87 -0
- package/src/lib/icon-heading/icon-heading.spec.ts +32 -0
- package/src/lib/icon-heading/icon-heading.ts +38 -0
- package/src/lib/icon-text/icon-text.ts +120 -0
- package/src/lib/input/input.ts +119 -0
- package/src/lib/kanban/kanban.ts +211 -0
- package/src/lib/kbd/kbd.ts +32 -0
- package/src/lib/label/label.ts +39 -0
- package/src/lib/loading-overlay/loading-overlay.ts +80 -0
- package/src/lib/menubar/menubar.ts +77 -0
- package/src/lib/metric-card/metric-card.ts +33 -0
- package/src/lib/modal/modal.ts +207 -0
- package/src/lib/native-select/native-select.ts +124 -0
- package/src/lib/navbar/navbar.ts +124 -0
- package/src/lib/navigation-menu/navigation-menu.ts +116 -0
- package/src/lib/otp-input/otp-input.ts +164 -0
- package/src/lib/page-back-link/page-back-link.ts +74 -0
- package/src/lib/page-header/page-header.ts +70 -0
- package/src/lib/pagination/pagination.ts +157 -0
- package/src/lib/pillbox/pillbox.ts +157 -0
- package/src/lib/popover/popover.ts +92 -0
- package/src/lib/profile/profile.ts +73 -0
- package/src/lib/progress/progress.ts +123 -0
- package/src/lib/radio/radio.ts +132 -0
- package/src/lib/ring-gauge/ring-gauge.ts +75 -0
- package/src/lib/scroll-area/scroll-area.ts +57 -0
- package/src/lib/search-palette/search-palette.scss +288 -0
- package/src/lib/search-palette/search-palette.ts +114 -0
- package/src/lib/select/select.ts +292 -0
- package/src/lib/separator/separator.ts +59 -0
- package/src/lib/sheet/sheet.ts +192 -0
- package/src/lib/sidebar-nav/sidebar-nav.scss +218 -0
- package/src/lib/sidebar-nav/sidebar-nav.ts +158 -0
- package/src/lib/site-drakkar/site-footer.ts +129 -0
- package/src/lib/site-drakkar/site-navbar.scss +2 -0
- package/src/lib/site-drakkar/site-navbar.ts +267 -0
- package/src/lib/site-drakkar/suite-search-items.spec.ts +61 -0
- package/src/lib/skeleton/skeleton.ts +51 -0
- package/src/lib/slider/slider.ts +133 -0
- package/src/lib/spinner/spinner.ts +75 -0
- package/src/lib/status-metric-row/status-metric-row.ts +74 -0
- package/src/lib/suite-search-palette/suite-search-palette.ts +95 -0
- package/src/lib/switch/switch.ts +131 -0
- package/src/lib/table/table.ts +137 -0
- package/src/lib/tabs/tab-panel.ts +61 -0
- package/src/lib/tabs/tab.ts +107 -0
- package/src/lib/tabs/tabs.ts +109 -0
- package/src/lib/textarea/textarea.ts +103 -0
- package/src/lib/theme-toggle/theme-toggle.spec.ts +40 -0
- package/src/lib/theme-toggle/theme-toggle.ts +39 -0
- package/src/lib/time-picker/time-picker.ts +197 -0
- package/src/lib/timeline/timeline.ts +152 -0
- package/src/lib/toast/toast.ts +196 -0
- package/src/lib/toggle/toggle-group.ts +60 -0
- package/src/lib/toggle/toggle.ts +101 -0
- package/src/lib/tooltip/tooltip.ts +119 -0
- package/src/lib/typography/heading.ts +52 -0
- package/src/lib/typography/text.ts +51 -0
- package/src/lib/uptime-bar/uptime-bar.ts +55 -0
- package/src/lib/verification-code-field/verification-code-field.ts +149 -0
- package/src/lib/whitepaper-cta/whitepaper-cta.scss +130 -0
- package/src/lib/whitepaper-cta/whitepaper-cta.ts +78 -0
- package/src/lib/wizard/wizard.ts +80 -0
- package/src/public-api.ts +172 -0
- package/src/tokens/series-presets.ts +15 -0
- package/src/tokens/tailwind.preset.js +209 -0
- package/src/viking-ui.spec.ts +416 -0
- package/src/web/web-components.spec.ts +297 -0
- package/dist/elements/button/viking-button.d.ts +0 -14
- package/dist/elements/card/viking-card.d.ts +0 -8
- package/dist/elements/core/dom.d.ts +0 -4
- package/dist/elements/core/styles.d.ts +0 -2
- package/dist/elements/modal/viking-modal.d.ts +0 -15
- package/dist/index.d.ts +0 -1
- package/dist/lib/core/brand-icons.d.ts +0 -29
- package/dist/lib/core/icons.d.ts +0 -132
- package/dist/lib/core/integration-brand-icons.d.ts +0 -18
- package/dist/lib/core/lucide-paths.generated.d.ts +0 -90
- package/dist/lib/site-drakkar/site-drakkar.config.d.ts +0 -50
- package/dist/lib/site-drakkar/suite-search-items.d.ts +0 -17
- package/dist/web/badge/viking-badge-wc.d.ts +0 -28
- package/dist/web/button/viking-button-wc.d.ts +0 -28
- package/dist/web/callout/viking-callout-wc.d.ts +0 -27
- package/dist/web/card/viking-card-wc.d.ts +0 -29
- package/dist/web/core/base.d.ts +0 -12
- package/dist/web/core/dom.d.ts +0 -11
- package/dist/web/core/icons-inline.d.ts +0 -7
- package/dist/web/core/styles.d.ts +0 -12
- package/dist/web/core/types.d.ts +0 -10
- package/dist/web/field/viking-field-wc.d.ts +0 -32
- package/dist/web/index.d.ts +0 -15
- package/dist/web/input/viking-input-wc.d.ts +0 -32
- package/dist/web/modal/viking-modal-wc.d.ts +0 -50
- package/dist/web/search-palette/viking-search-palette-wc.d.ts +0 -64
- package/dist/web/select/viking-select-wc.d.ts +0 -48
- package/dist/web/suite-header/viking-suite-header-wc.d.ts +0 -36
- package/dist/web/suite-search-palette/viking-suite-search-palette-wc.d.ts +0 -39
- package/dist/web/theme-toggle/viking-theme-toggle-wc.d.ts +0 -19
- package/dist/web/types.d.ts +0 -40
- package/dist/web-components/index.d.ts +0 -1
|
@@ -0,0 +1,1257 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeDetectionStrategy,
|
|
3
|
+
Component,
|
|
4
|
+
computed,
|
|
5
|
+
effect,
|
|
6
|
+
input,
|
|
7
|
+
signal,
|
|
8
|
+
} from "@angular/core";
|
|
9
|
+
import {
|
|
10
|
+
VikingChartCurve,
|
|
11
|
+
VikingChartKind,
|
|
12
|
+
VikingChartSeries,
|
|
13
|
+
VikingDonutSegment,
|
|
14
|
+
VikingTone,
|
|
15
|
+
} from "../../core/types";
|
|
16
|
+
import {
|
|
17
|
+
ChartZoomWindow,
|
|
18
|
+
clampChartWindow,
|
|
19
|
+
fullChartWindow,
|
|
20
|
+
panChartWindow,
|
|
21
|
+
sliceChartData,
|
|
22
|
+
zoomChartWindow,
|
|
23
|
+
} from "./chart-zoom";
|
|
24
|
+
|
|
25
|
+
interface ChartPath {
|
|
26
|
+
name: string;
|
|
27
|
+
tone: VikingTone;
|
|
28
|
+
line: string;
|
|
29
|
+
area: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ChartPoint {
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
tone: VikingTone;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface BarRect {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
tone: VikingTone;
|
|
44
|
+
radiusTop: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface DonutSlice {
|
|
48
|
+
label: string;
|
|
49
|
+
tone: VikingTone;
|
|
50
|
+
path: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface AxisLabel {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
text: string;
|
|
57
|
+
anchor: "start" | "middle" | "end";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface TickMark {
|
|
61
|
+
x: number;
|
|
62
|
+
y1: number;
|
|
63
|
+
y2: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Gutter {
|
|
67
|
+
top: number;
|
|
68
|
+
right: number;
|
|
69
|
+
bottom: number;
|
|
70
|
+
left: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const TONE_VARS: Record<VikingTone, string> = {
|
|
74
|
+
accent: "var(--viking-accent)",
|
|
75
|
+
secondary: "var(--viking-accent-secondary)",
|
|
76
|
+
success: "var(--viking-success)",
|
|
77
|
+
warning: "var(--viking-warning)",
|
|
78
|
+
danger: "var(--viking-danger)",
|
|
79
|
+
info: "var(--viking-info)",
|
|
80
|
+
muted: "var(--viking-text-muted)",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const WIDTH = 720;
|
|
84
|
+
const HEIGHT_DEFAULT = 240;
|
|
85
|
+
const HEIGHT_COMPACT = 200;
|
|
86
|
+
const HEIGHT_FILL = 400;
|
|
87
|
+
const HEIGHT_FILL_LINE = 240;
|
|
88
|
+
const HEIGHT_SPARKLINE = 48;
|
|
89
|
+
const BAR_WIDTH_MIN = 12;
|
|
90
|
+
const BAR_WIDTH_MAX = 96;
|
|
91
|
+
const SINGLE_BAR_WIDTH = 72;
|
|
92
|
+
const BAR_MIN_VISIBLE_HEIGHT_DEFAULT = 4;
|
|
93
|
+
const LABEL_MAX_DEFAULT = 10;
|
|
94
|
+
const LABEL_MAX_FILL = 18;
|
|
95
|
+
|
|
96
|
+
const resolveBarWidth = (
|
|
97
|
+
slotWidth: number,
|
|
98
|
+
count: number,
|
|
99
|
+
widthPercent: number,
|
|
100
|
+
): number => {
|
|
101
|
+
if (count <= 1) {
|
|
102
|
+
return SINGLE_BAR_WIDTH;
|
|
103
|
+
}
|
|
104
|
+
return Math.min(
|
|
105
|
+
BAR_WIDTH_MAX,
|
|
106
|
+
Math.max(BAR_WIDTH_MIN, slotWidth * (widthPercent / 100)),
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const resolveBarHeight = (
|
|
111
|
+
value: number,
|
|
112
|
+
normalized: number,
|
|
113
|
+
plotHeight: number,
|
|
114
|
+
minVisible: number,
|
|
115
|
+
): number => {
|
|
116
|
+
if (value === 0) {
|
|
117
|
+
return minVisible;
|
|
118
|
+
}
|
|
119
|
+
return Math.max(normalized * plotHeight, 0);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const formatTick = (value: number): string => {
|
|
123
|
+
if (value >= 1_000_000) {
|
|
124
|
+
return `${(value / 1_000_000).toFixed(1)}M`;
|
|
125
|
+
}
|
|
126
|
+
if (value >= 10_000) {
|
|
127
|
+
return `${Math.round(value / 1000)}K`;
|
|
128
|
+
}
|
|
129
|
+
if (value >= 1000) {
|
|
130
|
+
return `${(value / 1000).toFixed(1)}K`;
|
|
131
|
+
}
|
|
132
|
+
if (Number.isInteger(value)) {
|
|
133
|
+
return `${value}`;
|
|
134
|
+
}
|
|
135
|
+
return value.toFixed(value < 10 ? 1 : 0);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const truncateLabel = (label: string, max = 10): string =>
|
|
139
|
+
label.length > max ? `${label.slice(0, max - 1)}…` : label;
|
|
140
|
+
|
|
141
|
+
const parseGutter = (
|
|
142
|
+
value: number | string | undefined,
|
|
143
|
+
sparkline: boolean,
|
|
144
|
+
): Gutter => {
|
|
145
|
+
if (sparkline) {
|
|
146
|
+
return { top: 0, right: 0, bottom: 0, left: 0 };
|
|
147
|
+
}
|
|
148
|
+
if (value === undefined || value === null) {
|
|
149
|
+
return { top: 12, right: 20, bottom: 36, left: 48 };
|
|
150
|
+
}
|
|
151
|
+
if (typeof value === "number") {
|
|
152
|
+
return { top: value, right: value, bottom: value, left: value };
|
|
153
|
+
}
|
|
154
|
+
const parts = value
|
|
155
|
+
.trim()
|
|
156
|
+
.split(/\s+/)
|
|
157
|
+
.map((part) => Number(part));
|
|
158
|
+
if (parts.length === 1) {
|
|
159
|
+
return { top: parts[0], right: parts[0], bottom: parts[0], left: parts[0] };
|
|
160
|
+
}
|
|
161
|
+
if (parts.length === 2) {
|
|
162
|
+
return { top: parts[0], right: parts[1], bottom: parts[0], left: parts[1] };
|
|
163
|
+
}
|
|
164
|
+
if (parts.length === 3) {
|
|
165
|
+
return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[1] };
|
|
166
|
+
}
|
|
167
|
+
return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[3] };
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const buildLinearPath = (points: { x: number; y: number }[]): string => {
|
|
171
|
+
if (points.length === 0) {
|
|
172
|
+
return "";
|
|
173
|
+
}
|
|
174
|
+
return `M${points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(" L")}`;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const buildSmoothPath = (points: { x: number; y: number }[]): string => {
|
|
178
|
+
if (points.length < 3) {
|
|
179
|
+
return buildLinearPath(points);
|
|
180
|
+
}
|
|
181
|
+
let path = `M${points[0].x.toFixed(1)},${points[0].y.toFixed(1)}`;
|
|
182
|
+
for (let index = 0; index < points.length - 1; index += 1) {
|
|
183
|
+
const current = points[index];
|
|
184
|
+
const next = points[index + 1];
|
|
185
|
+
const previous = points[index - 1] ?? current;
|
|
186
|
+
const after = points[index + 2] ?? next;
|
|
187
|
+
const cp1x = current.x + (next.x - previous.x) / 6;
|
|
188
|
+
const cp1y = current.y + (next.y - previous.y) / 6;
|
|
189
|
+
const cp2x = next.x - (after.x - current.x) / 6;
|
|
190
|
+
const cp2y = next.y - (after.y - current.y) / 6;
|
|
191
|
+
path += ` C${cp1x.toFixed(1)},${cp1y.toFixed(1)} ${cp2x.toFixed(1)},${cp2y.toFixed(1)} ${next.x.toFixed(1)},${next.y.toFixed(1)}`;
|
|
192
|
+
}
|
|
193
|
+
return path;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* viking-chart — zero-dependency native SVG charts styled after Viking UI.
|
|
198
|
+
*/
|
|
199
|
+
@Component({
|
|
200
|
+
selector: "viking-chart",
|
|
201
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
202
|
+
host: {
|
|
203
|
+
"[class.viking-chart-fill-host]": "fill()",
|
|
204
|
+
"[class.viking-chart-sparkline-host]": "kind() === 'sparkline'",
|
|
205
|
+
},
|
|
206
|
+
template: `
|
|
207
|
+
<figure
|
|
208
|
+
class="viking-chart"
|
|
209
|
+
[class.viking-chart-fill]="fill()"
|
|
210
|
+
[class.viking-chart-compact]="compact() && !fill()"
|
|
211
|
+
[class.viking-chart-sparkline]="isSparkline()"
|
|
212
|
+
[class.viking-chart-has-legend]="legendVisible()"
|
|
213
|
+
[attr.aria-labelledby]="label() ? 'viking-chart-label-' + chartId : null"
|
|
214
|
+
[attr.aria-describedby]="
|
|
215
|
+
summary() ? 'viking-chart-summary-' + chartId : null
|
|
216
|
+
"
|
|
217
|
+
>
|
|
218
|
+
@if (label()) {
|
|
219
|
+
<figcaption class="sr-only" [id]="'viking-chart-label-' + chartId">
|
|
220
|
+
{{ label() }}
|
|
221
|
+
</figcaption>
|
|
222
|
+
}
|
|
223
|
+
@if (summary()) {
|
|
224
|
+
<p class="sr-only" [id]="'viking-chart-summary-' + chartId">
|
|
225
|
+
{{ summary() }}
|
|
226
|
+
</p>
|
|
227
|
+
}
|
|
228
|
+
<div
|
|
229
|
+
class="viking-chart-viewport"
|
|
230
|
+
[class.viking-chart-zoomable]="
|
|
231
|
+
zoomable() && !isSparkline() && kind() !== 'donut'
|
|
232
|
+
"
|
|
233
|
+
(wheel)="onWheel($event)"
|
|
234
|
+
(dblclick)="resetZoom()"
|
|
235
|
+
(pointerdown)="onPanStart($event)"
|
|
236
|
+
(pointermove)="onPanMove($event)"
|
|
237
|
+
(pointerup)="onPanEnd()"
|
|
238
|
+
(pointerleave)="onPanEnd()"
|
|
239
|
+
(pointercancel)="onPanEnd()"
|
|
240
|
+
>
|
|
241
|
+
<svg
|
|
242
|
+
[attr.viewBox]="'0 0 ' + width + ' ' + height()"
|
|
243
|
+
preserveAspectRatio="xMidYMid meet"
|
|
244
|
+
shape-rendering="geometricPrecision"
|
|
245
|
+
role="img"
|
|
246
|
+
[attr.aria-label]="label() || 'Chart'"
|
|
247
|
+
aria-hidden="true"
|
|
248
|
+
>
|
|
249
|
+
@if (kind() === "donut") {
|
|
250
|
+
@for (slice of donutSlices(); track slice.label) {
|
|
251
|
+
<path
|
|
252
|
+
class="viking-chart-donut-slice"
|
|
253
|
+
[attr.d]="slice.path"
|
|
254
|
+
[style.fill]="toneVar(slice.tone)"
|
|
255
|
+
/>
|
|
256
|
+
}
|
|
257
|
+
@if (donutSlices().length > 0) {
|
|
258
|
+
<circle
|
|
259
|
+
class="viking-chart-donut-hole"
|
|
260
|
+
[attr.cx]="width / 2"
|
|
261
|
+
[attr.cy]="plotCy()"
|
|
262
|
+
[attr.r]="donutInnerRadius()"
|
|
263
|
+
/>
|
|
264
|
+
<text
|
|
265
|
+
class="viking-chart-donut-total"
|
|
266
|
+
[attr.x]="width / 2"
|
|
267
|
+
[attr.y]="plotCy()"
|
|
268
|
+
text-anchor="middle"
|
|
269
|
+
dominant-baseline="central"
|
|
270
|
+
>
|
|
271
|
+
{{ donutTotalLabel() }}
|
|
272
|
+
</text>
|
|
273
|
+
}
|
|
274
|
+
} @else {
|
|
275
|
+
<defs>
|
|
276
|
+
<clipPath id="viking-plot-clip">
|
|
277
|
+
<rect
|
|
278
|
+
[attr.x]="resolvedGutter().left"
|
|
279
|
+
[attr.y]="plotTop()"
|
|
280
|
+
[attr.width]="
|
|
281
|
+
width - resolvedGutter().left - resolvedGutter().right
|
|
282
|
+
"
|
|
283
|
+
[attr.height]="plotBottom() - plotTop()"
|
|
284
|
+
/>
|
|
285
|
+
</clipPath>
|
|
286
|
+
</defs>
|
|
287
|
+
@if (!isSparkline()) {
|
|
288
|
+
@for (line of gridLines(); track line) {
|
|
289
|
+
<line
|
|
290
|
+
class="viking-chart-grid"
|
|
291
|
+
[attr.x1]="resolvedGutter().left"
|
|
292
|
+
[attr.x2]="width - resolvedGutter().right"
|
|
293
|
+
[attr.y1]="line"
|
|
294
|
+
[attr.y2]="line"
|
|
295
|
+
></line>
|
|
296
|
+
}
|
|
297
|
+
<line
|
|
298
|
+
class="viking-chart-axis-line"
|
|
299
|
+
[attr.x1]="resolvedGutter().left"
|
|
300
|
+
[attr.x2]="width - resolvedGutter().right"
|
|
301
|
+
[attr.y1]="plotBottom()"
|
|
302
|
+
[attr.y2]="plotBottom()"
|
|
303
|
+
></line>
|
|
304
|
+
@for (tick of yAxisLabels(); track tick.text + tick.y) {
|
|
305
|
+
<text
|
|
306
|
+
class="viking-chart-axis-y"
|
|
307
|
+
[attr.x]="resolvedGutter().left - 10"
|
|
308
|
+
[attr.y]="tick.y"
|
|
309
|
+
[attr.text-anchor]="tick.anchor"
|
|
310
|
+
dominant-baseline="middle"
|
|
311
|
+
>
|
|
312
|
+
{{ tick.text }}
|
|
313
|
+
</text>
|
|
314
|
+
}
|
|
315
|
+
@for (tick of xAxisLabels(); track tick.text + tick.x) {
|
|
316
|
+
<text
|
|
317
|
+
class="viking-chart-axis-x"
|
|
318
|
+
[attr.x]="tick.x"
|
|
319
|
+
[attr.y]="height() - 10"
|
|
320
|
+
[attr.text-anchor]="tick.anchor"
|
|
321
|
+
>
|
|
322
|
+
{{ tick.text }}
|
|
323
|
+
</text>
|
|
324
|
+
}
|
|
325
|
+
@for (mark of xTickMarks(); track mark.x) {
|
|
326
|
+
<line
|
|
327
|
+
class="viking-chart-tick"
|
|
328
|
+
[attr.x1]="mark.x"
|
|
329
|
+
[attr.x2]="mark.x"
|
|
330
|
+
[attr.y1]="mark.y1"
|
|
331
|
+
[attr.y2]="mark.y2"
|
|
332
|
+
></line>
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
@if (isBarKind()) {
|
|
336
|
+
@for (
|
|
337
|
+
bar of barRects();
|
|
338
|
+
track bar.x + "-" + bar.y + "-" + bar.tone
|
|
339
|
+
) {
|
|
340
|
+
<rect
|
|
341
|
+
class="viking-chart-bar"
|
|
342
|
+
[attr.x]="bar.x"
|
|
343
|
+
[attr.y]="bar.y"
|
|
344
|
+
[attr.width]="bar.width"
|
|
345
|
+
[attr.height]="bar.height"
|
|
346
|
+
[style.fill]="toneVar(bar.tone)"
|
|
347
|
+
[attr.rx]="bar.radiusTop ? barRadius() : 0"
|
|
348
|
+
[attr.ry]="bar.radiusTop ? barRadius() : 0"
|
|
349
|
+
/>
|
|
350
|
+
}
|
|
351
|
+
} @else {
|
|
352
|
+
<g clip-path="url(#viking-plot-clip)">
|
|
353
|
+
@for (path of paths(); track path.name) {
|
|
354
|
+
@if (renderArea()) {
|
|
355
|
+
<path
|
|
356
|
+
class="viking-chart-area"
|
|
357
|
+
[attr.d]="path.area"
|
|
358
|
+
[style.fill]="toneVar(path.tone)"
|
|
359
|
+
></path>
|
|
360
|
+
}
|
|
361
|
+
<path
|
|
362
|
+
class="viking-chart-line"
|
|
363
|
+
[attr.d]="path.line"
|
|
364
|
+
[style.stroke]="toneVar(path.tone)"
|
|
365
|
+
></path>
|
|
366
|
+
}
|
|
367
|
+
@if (renderPoints()) {
|
|
368
|
+
@for (
|
|
369
|
+
point of linePoints();
|
|
370
|
+
track point.x + "-" + point.y + "-" + point.tone
|
|
371
|
+
) {
|
|
372
|
+
<circle
|
|
373
|
+
class="viking-chart-point"
|
|
374
|
+
[attr.cx]="point.x"
|
|
375
|
+
[attr.cy]="point.y"
|
|
376
|
+
[attr.r]="pointRadius()"
|
|
377
|
+
[style.fill]="toneVar(point.tone)"
|
|
378
|
+
/>
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
</g>
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
</svg>
|
|
385
|
+
@if (zoomActive()) {
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
class="viking-chart-zoom-reset"
|
|
389
|
+
(click)="resetZoom()"
|
|
390
|
+
>
|
|
391
|
+
Reset zoom
|
|
392
|
+
</button>
|
|
393
|
+
}
|
|
394
|
+
</div>
|
|
395
|
+
@if (legendVisible()) {
|
|
396
|
+
<figcaption class="viking-chart-legend">
|
|
397
|
+
@if (kind() === "donut") {
|
|
398
|
+
@for (item of segments(); track item.label) {
|
|
399
|
+
@if (item.value > 0) {
|
|
400
|
+
<span class="viking-chart-legend-item">
|
|
401
|
+
<span
|
|
402
|
+
class="viking-chart-swatch"
|
|
403
|
+
[style.background]="toneVar(item.tone ?? 'accent')"
|
|
404
|
+
></span>
|
|
405
|
+
{{ item.label }}
|
|
406
|
+
</span>
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} @else {
|
|
410
|
+
@for (item of series(); track item.name) {
|
|
411
|
+
<span class="viking-chart-legend-item">
|
|
412
|
+
<span
|
|
413
|
+
class="viking-chart-swatch"
|
|
414
|
+
[style.background]="toneVar(item.tone ?? 'accent')"
|
|
415
|
+
></span>
|
|
416
|
+
{{ item.name }}
|
|
417
|
+
</span>
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
</figcaption>
|
|
421
|
+
}
|
|
422
|
+
</figure>
|
|
423
|
+
`,
|
|
424
|
+
styles: [
|
|
425
|
+
`
|
|
426
|
+
:host {
|
|
427
|
+
display: block;
|
|
428
|
+
width: 100%;
|
|
429
|
+
}
|
|
430
|
+
:host(.viking-chart-fill-host) {
|
|
431
|
+
display: flex;
|
|
432
|
+
flex-direction: column;
|
|
433
|
+
flex: 1 1 auto;
|
|
434
|
+
min-height: var(
|
|
435
|
+
--viking-chart-fill-min-height,
|
|
436
|
+
clamp(18rem, 36vw, 20rem)
|
|
437
|
+
);
|
|
438
|
+
height: 100%;
|
|
439
|
+
}
|
|
440
|
+
:host(.viking-chart-sparkline-host) {
|
|
441
|
+
display: inline-block;
|
|
442
|
+
width: auto;
|
|
443
|
+
max-width: 100%;
|
|
444
|
+
}
|
|
445
|
+
:host(.viking-chart-wide) {
|
|
446
|
+
display: block;
|
|
447
|
+
width: 100%;
|
|
448
|
+
}
|
|
449
|
+
.viking-chart {
|
|
450
|
+
margin: 0;
|
|
451
|
+
font-family: var(--viking-font-family);
|
|
452
|
+
overflow: hidden;
|
|
453
|
+
width: 100%;
|
|
454
|
+
max-width: 100%;
|
|
455
|
+
container-type: inline-size;
|
|
456
|
+
}
|
|
457
|
+
.viking-chart:not(.viking-chart-fill):not(.viking-chart-sparkline) {
|
|
458
|
+
aspect-ratio: var(--viking-chart-ratio, 3 / 1);
|
|
459
|
+
min-height: var(--viking-chart-min-height, clamp(9.5rem, 22vw, 12rem));
|
|
460
|
+
max-height: var(--viking-chart-max-height, clamp(13rem, 36vw, 17.5rem));
|
|
461
|
+
height: auto;
|
|
462
|
+
}
|
|
463
|
+
.viking-chart-fill {
|
|
464
|
+
flex: 1 1 auto;
|
|
465
|
+
display: flex;
|
|
466
|
+
flex-direction: column;
|
|
467
|
+
align-items: stretch;
|
|
468
|
+
justify-content: center;
|
|
469
|
+
min-height: var(
|
|
470
|
+
--viking-chart-fill-min-height,
|
|
471
|
+
clamp(12rem, 28vw, 16rem)
|
|
472
|
+
);
|
|
473
|
+
max-height: var(
|
|
474
|
+
--viking-chart-fill-max-height,
|
|
475
|
+
clamp(14rem, 32vw, 18rem)
|
|
476
|
+
);
|
|
477
|
+
height: 100%;
|
|
478
|
+
--viking-chart-axis-size: 13px;
|
|
479
|
+
}
|
|
480
|
+
.viking-chart-sparkline {
|
|
481
|
+
width: 5rem;
|
|
482
|
+
aspect-ratio: 3 / 1;
|
|
483
|
+
}
|
|
484
|
+
:host(.viking-chart-wide) .viking-chart-sparkline {
|
|
485
|
+
width: 100%;
|
|
486
|
+
aspect-ratio: auto;
|
|
487
|
+
height: var(--viking-chart-sparkline-height, 2.5rem);
|
|
488
|
+
}
|
|
489
|
+
.viking-chart-viewport {
|
|
490
|
+
position: relative;
|
|
491
|
+
width: 100%;
|
|
492
|
+
flex: 1 1 auto;
|
|
493
|
+
display: flex;
|
|
494
|
+
align-items: center;
|
|
495
|
+
justify-content: center;
|
|
496
|
+
min-height: 0;
|
|
497
|
+
}
|
|
498
|
+
.viking-chart-viewport.viking-chart-zoomable {
|
|
499
|
+
cursor: grab;
|
|
500
|
+
touch-action: none;
|
|
501
|
+
}
|
|
502
|
+
.viking-chart-viewport.viking-chart-zoomable:active {
|
|
503
|
+
cursor: grabbing;
|
|
504
|
+
}
|
|
505
|
+
.viking-chart-zoom-reset {
|
|
506
|
+
position: absolute;
|
|
507
|
+
top: var(--viking-space-half);
|
|
508
|
+
right: var(--viking-space-half);
|
|
509
|
+
z-index: 2;
|
|
510
|
+
padding: var(--viking-space-half) var(--viking-space-1);
|
|
511
|
+
font-size: var(--viking-font-size-sm);
|
|
512
|
+
font-family: var(--viking-font-family);
|
|
513
|
+
color: var(--viking-text-muted);
|
|
514
|
+
background: color-mix(in srgb, var(--viking-surface) 88%, transparent);
|
|
515
|
+
border: 1px solid var(--viking-border);
|
|
516
|
+
border-radius: var(--viking-radius-md);
|
|
517
|
+
cursor: pointer;
|
|
518
|
+
}
|
|
519
|
+
.viking-chart-zoom-reset:focus-visible {
|
|
520
|
+
outline: 2px solid var(--viking-ring);
|
|
521
|
+
outline-offset: 2px;
|
|
522
|
+
}
|
|
523
|
+
svg {
|
|
524
|
+
display: block;
|
|
525
|
+
width: 100%;
|
|
526
|
+
height: auto;
|
|
527
|
+
max-width: 100%;
|
|
528
|
+
background: transparent;
|
|
529
|
+
overflow: visible;
|
|
530
|
+
-webkit-backface-visibility: hidden;
|
|
531
|
+
backface-visibility: hidden;
|
|
532
|
+
}
|
|
533
|
+
.viking-chart:not(.viking-chart-fill):not(.viking-chart-sparkline) svg {
|
|
534
|
+
min-height: var(--viking-chart-min-height, clamp(9.5rem, 22vw, 12rem));
|
|
535
|
+
max-height: var(--viking-chart-max-height, clamp(13rem, 36vw, 17.5rem));
|
|
536
|
+
aspect-ratio: 720 / 240;
|
|
537
|
+
}
|
|
538
|
+
.viking-chart-fill svg {
|
|
539
|
+
width: 100%;
|
|
540
|
+
height: auto;
|
|
541
|
+
min-height: 0;
|
|
542
|
+
max-height: min(
|
|
543
|
+
100%,
|
|
544
|
+
var(--viking-chart-fill-max-height, clamp(14rem, 32vw, 18rem))
|
|
545
|
+
);
|
|
546
|
+
aspect-ratio: 720 / 400;
|
|
547
|
+
}
|
|
548
|
+
.viking-chart-fill:not(.viking-chart-has-legend) svg {
|
|
549
|
+
aspect-ratio: 720 / 240;
|
|
550
|
+
}
|
|
551
|
+
.viking-chart-sparkline svg {
|
|
552
|
+
min-height: 0;
|
|
553
|
+
border: none;
|
|
554
|
+
}
|
|
555
|
+
.viking-chart-compact svg {
|
|
556
|
+
max-height: min(
|
|
557
|
+
var(--viking-chart-max-height, clamp(13rem, 36vw, 17.5rem)),
|
|
558
|
+
13.75rem
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
.viking-chart-grid {
|
|
562
|
+
stroke: color-mix(in srgb, var(--viking-border) 55%, transparent);
|
|
563
|
+
stroke-width: 1;
|
|
564
|
+
}
|
|
565
|
+
.viking-chart-axis-line,
|
|
566
|
+
.viking-chart-tick {
|
|
567
|
+
stroke: color-mix(in srgb, var(--viking-border) 85%, transparent);
|
|
568
|
+
stroke-width: 1;
|
|
569
|
+
}
|
|
570
|
+
.viking-chart-axis-y,
|
|
571
|
+
.viking-chart-axis-x {
|
|
572
|
+
fill: var(--viking-text-muted);
|
|
573
|
+
font-size: max(11px, var(--viking-chart-axis-size, 12px));
|
|
574
|
+
font-family: var(--viking-font-family);
|
|
575
|
+
}
|
|
576
|
+
.viking-chart-line {
|
|
577
|
+
fill: none;
|
|
578
|
+
stroke-width: 2;
|
|
579
|
+
stroke-linecap: round;
|
|
580
|
+
stroke-linejoin: round;
|
|
581
|
+
vector-effect: non-scaling-stroke;
|
|
582
|
+
}
|
|
583
|
+
.viking-chart-fill .viking-chart-line {
|
|
584
|
+
stroke-width: 2.5;
|
|
585
|
+
}
|
|
586
|
+
.viking-chart-area {
|
|
587
|
+
opacity: 0.32;
|
|
588
|
+
stroke: none;
|
|
589
|
+
}
|
|
590
|
+
.viking-chart-fill .viking-chart-area {
|
|
591
|
+
opacity: 0.38;
|
|
592
|
+
}
|
|
593
|
+
.viking-chart-point {
|
|
594
|
+
stroke: var(--viking-surface);
|
|
595
|
+
stroke-width: 2;
|
|
596
|
+
vector-effect: non-scaling-stroke;
|
|
597
|
+
}
|
|
598
|
+
.viking-chart-bar {
|
|
599
|
+
opacity: 1;
|
|
600
|
+
}
|
|
601
|
+
.viking-chart-donut-slice {
|
|
602
|
+
stroke: var(--viking-surface);
|
|
603
|
+
stroke-width: 2;
|
|
604
|
+
}
|
|
605
|
+
.viking-chart-donut-hole {
|
|
606
|
+
fill: var(--viking-surface);
|
|
607
|
+
}
|
|
608
|
+
.viking-chart-donut-total {
|
|
609
|
+
fill: var(--viking-text);
|
|
610
|
+
font-size: max(12px, min(16px, 3.5cqw));
|
|
611
|
+
font-weight: 700;
|
|
612
|
+
font-family: var(--viking-font-family);
|
|
613
|
+
}
|
|
614
|
+
.viking-chart-fill .viking-chart-donut-total {
|
|
615
|
+
font-size: max(16px, min(24px, 5cqw));
|
|
616
|
+
}
|
|
617
|
+
.viking-chart-legend {
|
|
618
|
+
display: flex;
|
|
619
|
+
flex-wrap: wrap;
|
|
620
|
+
justify-content: center;
|
|
621
|
+
gap: var(--viking-space-2) var(--viking-space-3);
|
|
622
|
+
margin-top: var(--viking-space-2);
|
|
623
|
+
font-size: calc(var(--viking-font-size) * 0.9);
|
|
624
|
+
color: var(--viking-text-muted);
|
|
625
|
+
}
|
|
626
|
+
.viking-chart-fill .viking-chart-legend {
|
|
627
|
+
margin-top: var(--viking-space-3);
|
|
628
|
+
}
|
|
629
|
+
.viking-chart-legend-item {
|
|
630
|
+
display: inline-flex;
|
|
631
|
+
align-items: center;
|
|
632
|
+
gap: calc(var(--viking-space-1) / 1.5);
|
|
633
|
+
line-height: 1.2;
|
|
634
|
+
}
|
|
635
|
+
.viking-chart-swatch {
|
|
636
|
+
width: 10px;
|
|
637
|
+
height: 10px;
|
|
638
|
+
border-radius: 999px;
|
|
639
|
+
display: inline-block;
|
|
640
|
+
flex-shrink: 0;
|
|
641
|
+
}
|
|
642
|
+
.sr-only {
|
|
643
|
+
position: absolute !important;
|
|
644
|
+
width: 1px !important;
|
|
645
|
+
height: 1px !important;
|
|
646
|
+
padding: 0 !important;
|
|
647
|
+
margin: -1px !important;
|
|
648
|
+
overflow: hidden !important;
|
|
649
|
+
clip: rect(0, 0, 0, 0) !important;
|
|
650
|
+
white-space: nowrap !important;
|
|
651
|
+
border: 0 !important;
|
|
652
|
+
}
|
|
653
|
+
`,
|
|
654
|
+
],
|
|
655
|
+
})
|
|
656
|
+
export class VikingChart {
|
|
657
|
+
protected readonly chartId = `vc-${Math.random().toString(36).slice(2, 9)}`;
|
|
658
|
+
|
|
659
|
+
readonly kind = input<VikingChartKind>("line");
|
|
660
|
+
readonly series = input<VikingChartSeries[]>([{ name: "Series", data: [] }]);
|
|
661
|
+
readonly categories = input<string[]>([]);
|
|
662
|
+
readonly segments = input<VikingDonutSegment[]>([]);
|
|
663
|
+
readonly label = input<string>("");
|
|
664
|
+
readonly showArea = input<boolean>(false);
|
|
665
|
+
readonly compact = input<boolean>(false);
|
|
666
|
+
readonly fill = input<boolean>(false);
|
|
667
|
+
readonly curve = input<VikingChartCurve>("smooth");
|
|
668
|
+
readonly showPoints = input<boolean>(false);
|
|
669
|
+
readonly pointRadius = input<number>(4);
|
|
670
|
+
readonly barWidth = input<number>(85);
|
|
671
|
+
readonly barRadius = input<number>(6);
|
|
672
|
+
readonly barMinHeight = input<number>(BAR_MIN_VISIBLE_HEIGHT_DEFAULT);
|
|
673
|
+
readonly gutter = input<number | string | undefined>(undefined);
|
|
674
|
+
readonly tickCount = input<number>(4);
|
|
675
|
+
readonly showLegend = input<boolean | undefined>(undefined);
|
|
676
|
+
readonly summary = input<string>("");
|
|
677
|
+
/** Enable wheel zoom + drag pan on line/bar charts (Flux-style interaction). */
|
|
678
|
+
readonly zoomable = input<boolean>(false);
|
|
679
|
+
|
|
680
|
+
private readonly zoomWindow = signal<ChartZoomWindow | null>(null);
|
|
681
|
+
private panStartX: number | null = null;
|
|
682
|
+
private panStartWindow: ChartZoomWindow | null = null;
|
|
683
|
+
|
|
684
|
+
constructor() {
|
|
685
|
+
effect(() => {
|
|
686
|
+
const count = this.series()[0]?.data.length ?? 0;
|
|
687
|
+
if (count === 0) {
|
|
688
|
+
this.zoomWindow.set(null);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** Slice series/categories to the active zoom window. */
|
|
694
|
+
protected readonly dataWindow = computed(() => {
|
|
695
|
+
const count = this.series()[0]?.data.length ?? 0;
|
|
696
|
+
const zoom = this.zoomWindow();
|
|
697
|
+
if (
|
|
698
|
+
!this.zoomable() ||
|
|
699
|
+
!zoom ||
|
|
700
|
+
this.isSparkline() ||
|
|
701
|
+
this.kind() === "donut"
|
|
702
|
+
) {
|
|
703
|
+
return fullChartWindow(count);
|
|
704
|
+
}
|
|
705
|
+
return clampChartWindow(zoom, count);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
protected readonly effectiveSeries = computed(() => {
|
|
709
|
+
const window = this.dataWindow();
|
|
710
|
+
return this.series().map((item) => ({
|
|
711
|
+
...item,
|
|
712
|
+
data: sliceChartData(item.data, window),
|
|
713
|
+
}));
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
protected readonly effectiveCategories = computed(() =>
|
|
717
|
+
sliceChartData(this.categories(), this.dataWindow()),
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
protected readonly zoomActive = computed(() => {
|
|
721
|
+
const count = this.series()[0]?.data.length ?? 0;
|
|
722
|
+
const window = this.dataWindow();
|
|
723
|
+
return (
|
|
724
|
+
this.zoomable() && count > 0 && (window.start > 0 || window.end < count)
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
protected resetZoom = (): void => {
|
|
729
|
+
this.zoomWindow.set(null);
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
protected onWheel = (event: WheelEvent): void => {
|
|
733
|
+
if (!this.zoomable() || this.isSparkline() || this.kind() === "donut") {
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
event.preventDefault();
|
|
737
|
+
const count = this.series()[0]?.data.length ?? 0;
|
|
738
|
+
if (count <= 3) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
const current = this.zoomWindow() ?? fullChartWindow(count);
|
|
742
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
743
|
+
const fraction = Math.min(
|
|
744
|
+
1,
|
|
745
|
+
Math.max(0, (event.clientX - rect.left) / rect.width),
|
|
746
|
+
);
|
|
747
|
+
this.zoomWindow.set(
|
|
748
|
+
zoomChartWindow(current, count, fraction, event.deltaY),
|
|
749
|
+
);
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
protected onPanStart = (event: PointerEvent): void => {
|
|
753
|
+
if (
|
|
754
|
+
!this.zoomable() ||
|
|
755
|
+
this.isSparkline() ||
|
|
756
|
+
this.kind() === "donut" ||
|
|
757
|
+
event.button !== 0
|
|
758
|
+
) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const count = this.series()[0]?.data.length ?? 0;
|
|
762
|
+
if (count <= 3) {
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
this.panStartX = event.clientX;
|
|
766
|
+
this.panStartWindow = this.zoomWindow() ?? fullChartWindow(count);
|
|
767
|
+
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId);
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
protected onPanMove = (event: PointerEvent): void => {
|
|
771
|
+
if (this.panStartX === null || !this.panStartWindow) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const count = this.series()[0]?.data.length ?? 0;
|
|
775
|
+
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
776
|
+
const span = this.panStartWindow.end - this.panStartWindow.start;
|
|
777
|
+
const deltaPx = event.clientX - this.panStartX;
|
|
778
|
+
const deltaPoints = Math.round((deltaPx / rect.width) * span);
|
|
779
|
+
this.zoomWindow.set(
|
|
780
|
+
panChartWindow(this.panStartWindow, count, deltaPoints),
|
|
781
|
+
);
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
protected onPanEnd = (): void => {
|
|
785
|
+
this.panStartX = null;
|
|
786
|
+
this.panStartWindow = null;
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
protected readonly width = WIDTH;
|
|
790
|
+
|
|
791
|
+
protected readonly isSparkline = computed(() => this.kind() === "sparkline");
|
|
792
|
+
|
|
793
|
+
protected readonly resolvedGutter = computed(() => {
|
|
794
|
+
if (this.gutter() !== undefined && this.gutter() !== null) {
|
|
795
|
+
return parseGutter(this.gutter(), this.isSparkline());
|
|
796
|
+
}
|
|
797
|
+
if (this.fill() && !this.isSparkline()) {
|
|
798
|
+
return { top: 8, right: 8, bottom: 28, left: 36 };
|
|
799
|
+
}
|
|
800
|
+
return parseGutter(undefined, this.isSparkline());
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
protected readonly legendVisible = computed(() => {
|
|
804
|
+
const explicit = this.showLegend();
|
|
805
|
+
if (explicit !== undefined) {
|
|
806
|
+
return explicit;
|
|
807
|
+
}
|
|
808
|
+
const kind = this.kind();
|
|
809
|
+
if (kind === "donut") {
|
|
810
|
+
return this.segments().some((item) => item.value > 0);
|
|
811
|
+
}
|
|
812
|
+
if (kind === "sparkline") {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
if (kind === "grouped-bar" || kind === "stacked-bar") {
|
|
816
|
+
return this.series().length > 0;
|
|
817
|
+
}
|
|
818
|
+
return this.series().length > 1;
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
protected readonly height = computed(() => {
|
|
822
|
+
if (this.isSparkline()) {
|
|
823
|
+
return HEIGHT_SPARKLINE;
|
|
824
|
+
}
|
|
825
|
+
if (this.fill()) {
|
|
826
|
+
if (this.kind() === "donut") {
|
|
827
|
+
return WIDTH;
|
|
828
|
+
}
|
|
829
|
+
if (this.isLineKind()) {
|
|
830
|
+
return HEIGHT_FILL_LINE;
|
|
831
|
+
}
|
|
832
|
+
return HEIGHT_FILL;
|
|
833
|
+
}
|
|
834
|
+
return this.compact() ? HEIGHT_COMPACT : HEIGHT_DEFAULT;
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
protected readonly preserveAspectRatio = computed(() => "xMidYMid meet");
|
|
838
|
+
|
|
839
|
+
protected readonly plotBottom = computed(
|
|
840
|
+
() => this.height() - this.resolvedGutter().bottom,
|
|
841
|
+
);
|
|
842
|
+
protected readonly plotTop = computed(() => this.resolvedGutter().top);
|
|
843
|
+
protected readonly plotCy = computed(() => this.height() / 2);
|
|
844
|
+
|
|
845
|
+
protected readonly donutInnerRadius = computed(() => {
|
|
846
|
+
const plotH = this.plotBottom() - this.plotTop();
|
|
847
|
+
const plotW =
|
|
848
|
+
WIDTH - this.resolvedGutter().left - this.resolvedGutter().right;
|
|
849
|
+
const outer = Math.min(plotW, plotH) / 2 - 6;
|
|
850
|
+
return outer * 0.58;
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
protected readonly isBarKind = computed(() => {
|
|
854
|
+
const kind = this.kind();
|
|
855
|
+
return kind === "bar" || kind === "grouped-bar" || kind === "stacked-bar";
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
protected readonly isLineKind = computed(() => {
|
|
859
|
+
const kind = this.kind();
|
|
860
|
+
return kind === "line" || kind === "area";
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
protected readonly renderArea = computed(
|
|
864
|
+
() => this.kind() === "area" || this.showArea() || this.isSparkline(),
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
protected readonly renderPoints = computed(
|
|
868
|
+
() => this.showPoints() && !this.isSparkline() && !this.isBarKind(),
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
protected toneVar = (tone: VikingTone): string => TONE_VARS[tone];
|
|
872
|
+
|
|
873
|
+
protected readonly valueRange = computed(() => {
|
|
874
|
+
const kind = this.kind();
|
|
875
|
+
const series = this.effectiveSeries();
|
|
876
|
+
|
|
877
|
+
if (kind === "stacked-bar") {
|
|
878
|
+
const count = series[0]?.data.length ?? 0;
|
|
879
|
+
const sums = Array.from({ length: count }, (_, index) =>
|
|
880
|
+
series.reduce((sum, item) => sum + (item.data[index] ?? 0), 0),
|
|
881
|
+
);
|
|
882
|
+
if (sums.length === 0) {
|
|
883
|
+
return { min: 0, max: 1 };
|
|
884
|
+
}
|
|
885
|
+
const max = Math.max(...sums);
|
|
886
|
+
return { min: 0, max: max || 1 };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const all = series.flatMap((item) => item.data);
|
|
890
|
+
if (all.length === 0) {
|
|
891
|
+
return { min: 0, max: 1 };
|
|
892
|
+
}
|
|
893
|
+
const dataMin = Math.min(...all);
|
|
894
|
+
const dataMax = Math.max(...all);
|
|
895
|
+
|
|
896
|
+
if (this.isSparkline()) {
|
|
897
|
+
if (dataMax === dataMin) {
|
|
898
|
+
const pad = Math.max(Math.abs(dataMax) * 0.12, 0.5);
|
|
899
|
+
return { min: dataMax - pad, max: dataMax + pad };
|
|
900
|
+
}
|
|
901
|
+
const padding = Math.max((dataMax - dataMin) * 0.12, 0.5);
|
|
902
|
+
return { min: dataMin - padding, max: dataMax + padding };
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const min = Math.min(0, ...all);
|
|
906
|
+
const max = Math.max(...all);
|
|
907
|
+
return { min, max: max === min ? max + 1 : max };
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
protected readonly gridLines = computed(() => {
|
|
911
|
+
const bottom = this.plotBottom();
|
|
912
|
+
const top = this.plotTop();
|
|
913
|
+
const count = this.tickCount();
|
|
914
|
+
const lines: number[] = [];
|
|
915
|
+
for (let index = 0; index <= count; index += 1) {
|
|
916
|
+
lines.push(top + ((bottom - top) * index) / count);
|
|
917
|
+
}
|
|
918
|
+
return lines;
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
protected readonly yAxisLabels = computed<AxisLabel[]>(() => {
|
|
922
|
+
const { min, max } = this.valueRange();
|
|
923
|
+
const bottom = this.plotBottom();
|
|
924
|
+
const top = this.plotTop();
|
|
925
|
+
const count = this.tickCount();
|
|
926
|
+
const labels: AxisLabel[] = [];
|
|
927
|
+
|
|
928
|
+
for (let index = 0; index <= count; index += 1) {
|
|
929
|
+
const value = max - ((max - min) * index) / count;
|
|
930
|
+
const y = top + ((bottom - top) * index) / count;
|
|
931
|
+
labels.push({ x: 0, y, text: formatTick(value), anchor: "end" });
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return labels;
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
protected readonly xAxisLabels = computed<AxisLabel[]>(() => {
|
|
938
|
+
if (this.kind() === "donut" || this.isSparkline()) {
|
|
939
|
+
return [];
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const primary = this.effectiveSeries()[0];
|
|
943
|
+
const count = primary?.data.length ?? 0;
|
|
944
|
+
if (count === 0) {
|
|
945
|
+
return [];
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const cats = this.effectiveCategories();
|
|
949
|
+
const plotWidth =
|
|
950
|
+
WIDTH - this.resolvedGutter().left - this.resolvedGutter().right;
|
|
951
|
+
const maxLabels = this.fill() ? 8 : 6;
|
|
952
|
+
const labelMax = this.fill() ? LABEL_MAX_FILL : LABEL_MAX_DEFAULT;
|
|
953
|
+
const step = Math.max(1, Math.ceil(count / maxLabels));
|
|
954
|
+
const labels: AxisLabel[] = [];
|
|
955
|
+
|
|
956
|
+
for (let index = 0; index < count; index += step) {
|
|
957
|
+
const x =
|
|
958
|
+
this.resolvedGutter().left +
|
|
959
|
+
(count === 1
|
|
960
|
+
? plotWidth / 2
|
|
961
|
+
: (index / Math.max(1, count - 1)) * plotWidth);
|
|
962
|
+
const text = cats[index]
|
|
963
|
+
? truncateLabel(cats[index], labelMax)
|
|
964
|
+
: `${index + 1}`;
|
|
965
|
+
labels.push({ x, y: this.height(), text, anchor: "middle" });
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if ((count - 1) % step !== 0 && count > 1) {
|
|
969
|
+
const last = count - 1;
|
|
970
|
+
const x =
|
|
971
|
+
this.resolvedGutter().left +
|
|
972
|
+
(last / Math.max(1, count - 1)) * plotWidth;
|
|
973
|
+
const text = cats[last]
|
|
974
|
+
? truncateLabel(cats[last], labelMax)
|
|
975
|
+
: `${last + 1}`;
|
|
976
|
+
if (!labels.some((item) => item.text === text)) {
|
|
977
|
+
labels.push({ x, y: this.height(), text, anchor: "middle" });
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return labels;
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
protected readonly xTickMarks = computed<TickMark[]>(() => {
|
|
985
|
+
const bottom = this.plotBottom();
|
|
986
|
+
return this.xAxisLabels().map((label) => ({
|
|
987
|
+
x: label.x,
|
|
988
|
+
y1: bottom,
|
|
989
|
+
y2: bottom + 5,
|
|
990
|
+
}));
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
protected readonly paths = computed<ChartPath[]>(() => {
|
|
994
|
+
const all = this.effectiveSeries().flatMap((item) => item.data);
|
|
995
|
+
if (all.length === 0) {
|
|
996
|
+
return [];
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const { min, max } = this.valueRange();
|
|
1000
|
+
const range = max - min || 1;
|
|
1001
|
+
const bottom = this.plotBottom();
|
|
1002
|
+
const top = this.plotTop();
|
|
1003
|
+
const plotHeight = bottom - top;
|
|
1004
|
+
const plotWidth =
|
|
1005
|
+
WIDTH - this.resolvedGutter().left - this.resolvedGutter().right;
|
|
1006
|
+
const buildPath =
|
|
1007
|
+
this.curve() === "smooth" ? buildSmoothPath : buildLinearPath;
|
|
1008
|
+
|
|
1009
|
+
return this.effectiveSeries().map((item) => {
|
|
1010
|
+
const points = item.data.map((value, index) => {
|
|
1011
|
+
const x =
|
|
1012
|
+
this.resolvedGutter().left +
|
|
1013
|
+
(index / Math.max(1, item.data.length - 1)) * plotWidth;
|
|
1014
|
+
const y = bottom - ((value - min) / range) * plotHeight;
|
|
1015
|
+
return { x, y };
|
|
1016
|
+
});
|
|
1017
|
+
const line = buildPath(points);
|
|
1018
|
+
const area = `${line} L${WIDTH - this.resolvedGutter().right},${bottom} L${this.resolvedGutter().left},${bottom} Z`;
|
|
1019
|
+
return { name: item.name, tone: item.tone ?? "accent", line, area };
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
protected readonly linePoints = computed<ChartPoint[]>(() => {
|
|
1024
|
+
const all = this.effectiveSeries().flatMap((item) => item.data);
|
|
1025
|
+
if (all.length === 0) {
|
|
1026
|
+
return [];
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const { min, max } = this.valueRange();
|
|
1030
|
+
const range = max - min || 1;
|
|
1031
|
+
const bottom = this.plotBottom();
|
|
1032
|
+
const top = this.plotTop();
|
|
1033
|
+
const plotHeight = bottom - top;
|
|
1034
|
+
const plotWidth =
|
|
1035
|
+
WIDTH - this.resolvedGutter().left - this.resolvedGutter().right;
|
|
1036
|
+
|
|
1037
|
+
return this.effectiveSeries().flatMap((item) =>
|
|
1038
|
+
item.data.map((value, index) => {
|
|
1039
|
+
const x =
|
|
1040
|
+
this.resolvedGutter().left +
|
|
1041
|
+
(index / Math.max(1, item.data.length - 1)) * plotWidth;
|
|
1042
|
+
const y = bottom - ((value - min) / range) * plotHeight;
|
|
1043
|
+
return { x, y, tone: item.tone ?? "accent" };
|
|
1044
|
+
}),
|
|
1045
|
+
);
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
protected readonly barRects = computed<BarRect[]>(() => {
|
|
1049
|
+
const kind = this.kind();
|
|
1050
|
+
if (kind === "stacked-bar") {
|
|
1051
|
+
return this.stackedBarRects();
|
|
1052
|
+
}
|
|
1053
|
+
if (kind === "grouped-bar") {
|
|
1054
|
+
return this.groupedBarRects();
|
|
1055
|
+
}
|
|
1056
|
+
return this.singleBarRects();
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
private readonly singleBarRects = computed<BarRect[]>(() => {
|
|
1060
|
+
const primary = this.effectiveSeries()[0];
|
|
1061
|
+
if (!primary?.data.length) {
|
|
1062
|
+
return [];
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const { min, max } = this.valueRange();
|
|
1066
|
+
const range = max - min || 1;
|
|
1067
|
+
const bottom = this.plotBottom();
|
|
1068
|
+
const top = this.plotTop();
|
|
1069
|
+
const plotHeight = bottom - top;
|
|
1070
|
+
const plotWidth =
|
|
1071
|
+
WIDTH - this.resolvedGutter().left - this.resolvedGutter().right;
|
|
1072
|
+
const widthPercent = this.barWidth();
|
|
1073
|
+
const slotWidth = plotWidth / primary.data.length;
|
|
1074
|
+
const barWidth = resolveBarWidth(
|
|
1075
|
+
slotWidth,
|
|
1076
|
+
primary.data.length,
|
|
1077
|
+
widthPercent,
|
|
1078
|
+
);
|
|
1079
|
+
|
|
1080
|
+
const minVisible = this.barMinHeight();
|
|
1081
|
+
|
|
1082
|
+
return primary.data.map((value, index) => {
|
|
1083
|
+
const normalized = (value - min) / range;
|
|
1084
|
+
const height = resolveBarHeight(
|
|
1085
|
+
value,
|
|
1086
|
+
normalized,
|
|
1087
|
+
plotHeight,
|
|
1088
|
+
minVisible,
|
|
1089
|
+
);
|
|
1090
|
+
const x =
|
|
1091
|
+
this.resolvedGutter().left +
|
|
1092
|
+
index * slotWidth +
|
|
1093
|
+
(slotWidth - barWidth) / 2;
|
|
1094
|
+
const y = bottom - height;
|
|
1095
|
+
return {
|
|
1096
|
+
x,
|
|
1097
|
+
y,
|
|
1098
|
+
width: barWidth,
|
|
1099
|
+
height,
|
|
1100
|
+
tone: primary.tone ?? "accent",
|
|
1101
|
+
radiusTop: true,
|
|
1102
|
+
};
|
|
1103
|
+
});
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
private readonly groupedBarRects = computed<BarRect[]>(() => {
|
|
1107
|
+
const series = this.effectiveSeries();
|
|
1108
|
+
const count = series[0]?.data.length ?? 0;
|
|
1109
|
+
if (!count) {
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const { min, max } = this.valueRange();
|
|
1114
|
+
const range = max - min || 1;
|
|
1115
|
+
const bottom = this.plotBottom();
|
|
1116
|
+
const top = this.plotTop();
|
|
1117
|
+
const plotHeight = bottom - top;
|
|
1118
|
+
const plotWidth =
|
|
1119
|
+
WIDTH - this.resolvedGutter().left - this.resolvedGutter().right;
|
|
1120
|
+
const groupWidth = plotWidth / count;
|
|
1121
|
+
const widthPercent = this.barWidth();
|
|
1122
|
+
const groupBarWidth = resolveBarWidth(groupWidth, count, widthPercent);
|
|
1123
|
+
const barWidth = Math.max(3, groupBarWidth / series.length);
|
|
1124
|
+
const rects: BarRect[] = [];
|
|
1125
|
+
|
|
1126
|
+
for (let index = 0; index < count; index += 1) {
|
|
1127
|
+
const groupStart = this.resolvedGutter().left + index * groupWidth;
|
|
1128
|
+
const groupInner = groupBarWidth;
|
|
1129
|
+
const groupOffset = (groupWidth - groupInner) / 2;
|
|
1130
|
+
|
|
1131
|
+
const minVisible = this.barMinHeight();
|
|
1132
|
+
|
|
1133
|
+
series.forEach((item, seriesIndex) => {
|
|
1134
|
+
const value = item.data[index] ?? 0;
|
|
1135
|
+
const normalized = (value - min) / range;
|
|
1136
|
+
const height = resolveBarHeight(
|
|
1137
|
+
value,
|
|
1138
|
+
normalized,
|
|
1139
|
+
plotHeight,
|
|
1140
|
+
minVisible,
|
|
1141
|
+
);
|
|
1142
|
+
const x = groupStart + groupOffset + seriesIndex * barWidth;
|
|
1143
|
+
const y = bottom - height;
|
|
1144
|
+
rects.push({
|
|
1145
|
+
x,
|
|
1146
|
+
y,
|
|
1147
|
+
width: Math.max(3, barWidth - 2),
|
|
1148
|
+
height,
|
|
1149
|
+
tone: item.tone ?? "accent",
|
|
1150
|
+
radiusTop: true,
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return rects;
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
private readonly stackedBarRects = computed<BarRect[]>(() => {
|
|
1159
|
+
const series = this.effectiveSeries();
|
|
1160
|
+
const count = series[0]?.data.length ?? 0;
|
|
1161
|
+
if (!count) {
|
|
1162
|
+
return [];
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const { max } = this.valueRange();
|
|
1166
|
+
const range = max || 1;
|
|
1167
|
+
const bottom = this.plotBottom();
|
|
1168
|
+
const top = this.plotTop();
|
|
1169
|
+
const plotHeight = bottom - top;
|
|
1170
|
+
const plotWidth =
|
|
1171
|
+
WIDTH - this.resolvedGutter().left - this.resolvedGutter().right;
|
|
1172
|
+
const widthPercent = this.barWidth();
|
|
1173
|
+
const slotWidth = plotWidth / count;
|
|
1174
|
+
const barWidth = resolveBarWidth(slotWidth, count, widthPercent);
|
|
1175
|
+
const rects: BarRect[] = [];
|
|
1176
|
+
|
|
1177
|
+
for (let index = 0; index < count; index += 1) {
|
|
1178
|
+
let cursor = bottom;
|
|
1179
|
+
const x =
|
|
1180
|
+
this.resolvedGutter().left +
|
|
1181
|
+
index * slotWidth +
|
|
1182
|
+
(slotWidth - barWidth) / 2;
|
|
1183
|
+
|
|
1184
|
+
series.forEach((item, seriesIndex) => {
|
|
1185
|
+
const value = item.data[index] ?? 0;
|
|
1186
|
+
const height = Math.max(0, (value / range) * plotHeight);
|
|
1187
|
+
cursor -= height;
|
|
1188
|
+
rects.push({
|
|
1189
|
+
x,
|
|
1190
|
+
y: cursor,
|
|
1191
|
+
width: barWidth,
|
|
1192
|
+
height,
|
|
1193
|
+
tone: item.tone ?? "accent",
|
|
1194
|
+
radiusTop: seriesIndex === series.length - 1,
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
return rects;
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
protected readonly donutTotalLabel = computed(() => {
|
|
1203
|
+
const total = this.segments()
|
|
1204
|
+
.filter((item) => item.value > 0)
|
|
1205
|
+
.reduce((sum, item) => sum + item.value, 0);
|
|
1206
|
+
return formatTick(total);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
protected readonly donutSlices = computed<DonutSlice[]>(() => {
|
|
1210
|
+
const items = this.segments().filter((item) => item.value > 0);
|
|
1211
|
+
const total = items.reduce((sum, item) => sum + item.value, 0);
|
|
1212
|
+
if (total <= 0) {
|
|
1213
|
+
return [];
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const cx = WIDTH / 2;
|
|
1217
|
+
const cy = this.plotCy();
|
|
1218
|
+
const plotH = this.plotBottom() - this.plotTop();
|
|
1219
|
+
const plotW =
|
|
1220
|
+
WIDTH - this.resolvedGutter().left - this.resolvedGutter().right;
|
|
1221
|
+
const outer = Math.min(plotW, plotH) / 2 - 6;
|
|
1222
|
+
const inner = outer * 0.58;
|
|
1223
|
+
let cursor = -Math.PI / 2;
|
|
1224
|
+
|
|
1225
|
+
return items.map((item) => {
|
|
1226
|
+
const angle = (item.value / total) * Math.PI * 2;
|
|
1227
|
+
const start = cursor;
|
|
1228
|
+
const end = cursor + angle;
|
|
1229
|
+
cursor = end;
|
|
1230
|
+
|
|
1231
|
+
if (items.length === 1) {
|
|
1232
|
+
const path = [
|
|
1233
|
+
`M ${cx - outer} ${cy}`,
|
|
1234
|
+
`A ${outer} ${outer} 0 1 1 ${cx + outer} ${cy}`,
|
|
1235
|
+
`A ${outer} ${outer} 0 1 1 ${cx - outer} ${cy}`,
|
|
1236
|
+
`M ${cx - inner} ${cy}`,
|
|
1237
|
+
`A ${inner} ${inner} 0 1 0 ${cx + inner} ${cy}`,
|
|
1238
|
+
`A ${inner} ${inner} 0 1 0 ${cx - inner} ${cy}`,
|
|
1239
|
+
"Z",
|
|
1240
|
+
].join(" ");
|
|
1241
|
+
return { label: item.label, tone: item.tone ?? "accent", path };
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
const x1 = cx + outer * Math.cos(start);
|
|
1245
|
+
const y1 = cy + outer * Math.sin(start);
|
|
1246
|
+
const x2 = cx + outer * Math.cos(end);
|
|
1247
|
+
const y2 = cy + outer * Math.sin(end);
|
|
1248
|
+
const x3 = cx + inner * Math.cos(end);
|
|
1249
|
+
const y3 = cy + inner * Math.sin(end);
|
|
1250
|
+
const x4 = cx + inner * Math.cos(start);
|
|
1251
|
+
const y4 = cy + inner * Math.sin(start);
|
|
1252
|
+
const large = angle > Math.PI ? 1 : 0;
|
|
1253
|
+
const path = `M ${x1} ${y1} A ${outer} ${outer} 0 ${large} 1 ${x2} ${y2} L ${x3} ${y3} A ${inner} ${inner} 0 ${large} 0 ${x4} ${y4} Z`;
|
|
1254
|
+
return { label: item.label, tone: item.tone ?? "accent", path };
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
}
|