v-uixy 1.9.0 → 1.9.2
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/package.json +1 -1
- package/templates/components/Button/Button.component.vue +2 -2
- package/templates/components/Chart/Chart.component.vue +895 -222
- package/templates/components/Chart/Chart.types.ts +7 -0
- package/templates/components/DatePicker/DatePicker.component.vue +25 -5
- package/templates/components/DatePicker/DatePicker.types.ts +1 -0
- package/templates/components/Input/Input.styles.ts +4 -2
- package/templates/components/Modal/Modal.component.vue +10 -1
- package/templates/components/Modal/Modal.styles.ts +1 -1
- package/templates/components/PieChart/PieChart.component.vue +515 -152
- package/templates/components/PieChart/PieChart.types.ts +7 -1
- package/templates/components/Sheet/Sheet.component.vue +4 -2
- package/templates/components/index.ts +8 -1
- package/templates/components.json +4 -4
- package/templates/composables/useGpuAcceleration.ts +79 -0
- package/templates/composables/useOverlayLayer.ts +9 -2
|
@@ -1,308 +1,981 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div
|
|
3
|
+
ref="wrapperRef"
|
|
4
|
+
class="relative h-full w-full"
|
|
5
|
+
:class="props.interactive ? 'cursor-crosshair touch-pan-y' : ''"
|
|
6
|
+
:tabindex="props.interactive ? 0 : undefined"
|
|
7
|
+
:role="props.interactive ? 'group' : 'img'"
|
|
8
|
+
:aria-roledescription="props.interactive ? 'chart' : undefined"
|
|
9
|
+
:aria-label="ariaLabel"
|
|
10
|
+
@pointermove="handlePointerMove"
|
|
11
|
+
@pointerdown="handlePointerDown"
|
|
12
|
+
@pointerleave="handlePointerLeave"
|
|
13
|
+
@pointercancel="handlePointerCancel"
|
|
14
|
+
@focus="handleFocus"
|
|
15
|
+
@blur="handleBlur"
|
|
16
|
+
@keydown="handleKeydown"
|
|
17
|
+
>
|
|
3
18
|
<svg
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class="block select-none pointer-events-none overflow-visible"
|
|
11
|
-
shape-rendering="crispEdges"
|
|
19
|
+
v-if="ready"
|
|
20
|
+
:width="chartWidth"
|
|
21
|
+
:height="chartHeight"
|
|
22
|
+
:viewBox="`0 0 ${chartWidth} ${chartHeight}`"
|
|
23
|
+
aria-hidden="true"
|
|
24
|
+
class="block select-none overflow-visible"
|
|
12
25
|
>
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
<defs>
|
|
27
|
+
<pattern
|
|
28
|
+
:id="patternId"
|
|
29
|
+
patternUnits="userSpaceOnUse"
|
|
30
|
+
:x="gridX"
|
|
31
|
+
:y="gridY"
|
|
32
|
+
:width="cellSize + GAP"
|
|
33
|
+
:height="cellSize + GAP"
|
|
34
|
+
>
|
|
35
|
+
<rect
|
|
36
|
+
:width="cellSize"
|
|
37
|
+
:height="cellSize"
|
|
38
|
+
class="fill-gray-200 dark:fill-gray-900"
|
|
39
|
+
shape-rendering="crispEdges"
|
|
40
|
+
/>
|
|
41
|
+
</pattern>
|
|
42
|
+
</defs>
|
|
22
43
|
|
|
23
44
|
<rect
|
|
24
|
-
|
|
25
|
-
:
|
|
26
|
-
:
|
|
27
|
-
:
|
|
28
|
-
:
|
|
29
|
-
:height="CELL"
|
|
30
|
-
:class="cell.cls"
|
|
31
|
-
:fill="cell.fill"
|
|
32
|
-
:style="{
|
|
33
|
-
opacity: cell.filled ? 1 : 0,
|
|
34
|
-
transition: props.animate
|
|
35
|
-
? `opacity ${REVEAL_DURATION}s ease-out`
|
|
36
|
-
: 'none',
|
|
37
|
-
}"
|
|
45
|
+
:x="gridX"
|
|
46
|
+
:y="gridY"
|
|
47
|
+
:width="gridWidth"
|
|
48
|
+
:height="gridHeight"
|
|
49
|
+
:fill="`url(#${patternId})`"
|
|
38
50
|
/>
|
|
51
|
+
|
|
52
|
+
<g v-if="props.axis" class="pointer-events-none">
|
|
53
|
+
<template v-for="tick in yTicks" :key="tick.value">
|
|
54
|
+
<text
|
|
55
|
+
:x="yLabelX"
|
|
56
|
+
:y="tick.y"
|
|
57
|
+
text-anchor="end"
|
|
58
|
+
dominant-baseline="middle"
|
|
59
|
+
class="fill-gray-500 text-[11px] font-500 tabular-nums dark:fill-gray-600"
|
|
60
|
+
>
|
|
61
|
+
{{ tick.label }}
|
|
62
|
+
</text>
|
|
63
|
+
<rect
|
|
64
|
+
:x="yDotX - 1"
|
|
65
|
+
:y="tick.y - 1"
|
|
66
|
+
width="2"
|
|
67
|
+
height="2"
|
|
68
|
+
class="fill-gray-400 dark:fill-gray-700"
|
|
69
|
+
shape-rendering="crispEdges"
|
|
70
|
+
/>
|
|
71
|
+
</template>
|
|
72
|
+
</g>
|
|
73
|
+
|
|
74
|
+
<g
|
|
75
|
+
v-if="props.interactive"
|
|
76
|
+
class="pointer-events-none"
|
|
77
|
+
:style="followStyle"
|
|
78
|
+
>
|
|
79
|
+
<rect
|
|
80
|
+
:x="-(cellSize + GAP) / 2"
|
|
81
|
+
:y="gridY"
|
|
82
|
+
:width="cellSize + GAP"
|
|
83
|
+
:height="gridHeight"
|
|
84
|
+
class="fill-black/[0.04] dark:fill-white/[0.06]"
|
|
85
|
+
/>
|
|
86
|
+
</g>
|
|
87
|
+
|
|
88
|
+
<g class="pointer-events-none" shape-rendering="crispEdges">
|
|
89
|
+
<rect
|
|
90
|
+
v-for="cell in cells"
|
|
91
|
+
:key="cell.key"
|
|
92
|
+
:x="cellX(cell.col)"
|
|
93
|
+
:y="cellY(cell.row)"
|
|
94
|
+
:width="cellSize"
|
|
95
|
+
:height="cellSize"
|
|
96
|
+
:class="cell.className"
|
|
97
|
+
:fill="cell.fill"
|
|
98
|
+
:style="{
|
|
99
|
+
opacity: cell.filled ? 1 : 0,
|
|
100
|
+
transition: revealTransition,
|
|
101
|
+
}"
|
|
102
|
+
/>
|
|
103
|
+
</g>
|
|
104
|
+
|
|
105
|
+
<g
|
|
106
|
+
v-if="props.interactive"
|
|
107
|
+
class="pointer-events-none"
|
|
108
|
+
:style="followStyle"
|
|
109
|
+
>
|
|
110
|
+
<line
|
|
111
|
+
x1="0"
|
|
112
|
+
x2="0"
|
|
113
|
+
:y1="gridY"
|
|
114
|
+
:y2="gridBottom"
|
|
115
|
+
stroke-width="1"
|
|
116
|
+
stroke-dasharray="2 4"
|
|
117
|
+
class="stroke-gray-500 dark:stroke-gray-600"
|
|
118
|
+
/>
|
|
119
|
+
<g :style="markerStyle">
|
|
120
|
+
<circle
|
|
121
|
+
r="4.5"
|
|
122
|
+
stroke-width="2"
|
|
123
|
+
class="fill-black stroke-white dark:fill-white dark:stroke-black"
|
|
124
|
+
/>
|
|
125
|
+
</g>
|
|
126
|
+
</g>
|
|
127
|
+
|
|
128
|
+
<g v-if="props.axis" class="pointer-events-none">
|
|
129
|
+
<text
|
|
130
|
+
v-for="tick in xTicks"
|
|
131
|
+
:key="tick.start"
|
|
132
|
+
:x="tick.x"
|
|
133
|
+
:y="xLabelY"
|
|
134
|
+
text-anchor="middle"
|
|
135
|
+
dominant-baseline="middle"
|
|
136
|
+
class="text-[11px] font-500 tracking-[0.08em] transition-[fill] duration-150"
|
|
137
|
+
:class="
|
|
138
|
+
tick.active
|
|
139
|
+
? 'fill-black dark:fill-white'
|
|
140
|
+
: 'fill-gray-500 dark:fill-gray-600'
|
|
141
|
+
"
|
|
142
|
+
>
|
|
143
|
+
{{ tick.label }}
|
|
144
|
+
</text>
|
|
145
|
+
<rect
|
|
146
|
+
v-for="x in xSeparators"
|
|
147
|
+
:key="x"
|
|
148
|
+
:x="x - 1"
|
|
149
|
+
:y="xLabelY - 1"
|
|
150
|
+
width="2"
|
|
151
|
+
height="2"
|
|
152
|
+
class="fill-gray-400 dark:fill-gray-700"
|
|
153
|
+
shape-rendering="crispEdges"
|
|
154
|
+
/>
|
|
155
|
+
</g>
|
|
39
156
|
</svg>
|
|
157
|
+
|
|
158
|
+
<div
|
|
159
|
+
v-if="props.interactive"
|
|
160
|
+
ref="tooltipRef"
|
|
161
|
+
class="pointer-events-none absolute top-0 left-0 z-10 origin-top-left"
|
|
162
|
+
:style="tooltipStyle"
|
|
163
|
+
aria-hidden="true"
|
|
164
|
+
>
|
|
165
|
+
<div
|
|
166
|
+
class="min-w-[168px] rounded-3 border border-solid border-gray-300 bg-white p-1 shadow-lg dark:border-gray-900 dark:bg-gray-1000"
|
|
167
|
+
>
|
|
168
|
+
<div
|
|
169
|
+
class="rounded-2 bg-gray-200 px-2 py-1 text-sm font-500 text-gray-600 dark:bg-gray-900 dark:text-gray-500"
|
|
170
|
+
>
|
|
171
|
+
{{ tooltipTitle }}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div class="flex flex-col gap-1 px-2 py-2">
|
|
175
|
+
<div
|
|
176
|
+
v-for="(row, index) in tooltipRows"
|
|
177
|
+
:key="index"
|
|
178
|
+
class="flex items-center gap-2"
|
|
179
|
+
>
|
|
180
|
+
<span
|
|
181
|
+
class="size-1.5 shrink-0 rounded-full"
|
|
182
|
+
:class="row.swatchClass"
|
|
183
|
+
:style="row.swatchStyle"
|
|
184
|
+
/>
|
|
185
|
+
<span class="text-sm text-gray-600 dark:text-gray-500">
|
|
186
|
+
{{ row.label }}
|
|
187
|
+
</span>
|
|
188
|
+
<span
|
|
189
|
+
class="ml-auto text-sm font-600 tabular-nums text-black dark:text-white"
|
|
190
|
+
>
|
|
191
|
+
{{ row.value }}
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<span v-if="props.interactive" class="sr-only" aria-live="polite">
|
|
199
|
+
{{ announcement }}
|
|
200
|
+
</span>
|
|
40
201
|
</div>
|
|
41
202
|
</template>
|
|
42
203
|
|
|
43
204
|
<script setup lang="ts">
|
|
44
|
-
import { computed, onUnmounted, ref, watch } from "vue";
|
|
205
|
+
import { computed, onMounted, onUnmounted, ref, useId, watch } from "vue";
|
|
45
206
|
import type { UixyChartProps, UixyChartSeries } from "./Chart.types";
|
|
46
207
|
|
|
47
|
-
|
|
208
|
+
type Point = { x: number; y: number };
|
|
209
|
+
|
|
210
|
+
type Paint = { fill?: string; className?: string };
|
|
211
|
+
|
|
212
|
+
type Cell = Paint & {
|
|
213
|
+
key: string;
|
|
214
|
+
col: number;
|
|
215
|
+
row: number;
|
|
216
|
+
filled: boolean;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
type Segment = { series: number; start: number; end: number };
|
|
220
|
+
|
|
221
|
+
type Stack = { segments: Segment[]; height: number };
|
|
222
|
+
|
|
223
|
+
type LabelRun = { label: string; start: number; end: number };
|
|
224
|
+
|
|
225
|
+
type Unit = { divisor: number; suffix: string };
|
|
226
|
+
|
|
227
|
+
type Batch = { timer: ReturnType<typeof setTimeout>; tasks: (() => void)[] };
|
|
228
|
+
|
|
48
229
|
const GAP = 2;
|
|
230
|
+
const FADE_ROWS = 2;
|
|
231
|
+
const TOP_HEADROOM = 0.08;
|
|
49
232
|
|
|
50
233
|
const COLUMN_STEP = 0.05;
|
|
51
234
|
const ROW_STEP = 0.04;
|
|
52
235
|
const REVEAL_DURATION = 0.18;
|
|
53
236
|
|
|
54
|
-
const
|
|
55
|
-
const
|
|
237
|
+
const MOVE_TRANSITION = "transform 0.18s cubic-bezier(0.22, 1, 0.36, 1)";
|
|
238
|
+
const FADE_TRANSITION = "opacity 0.12s ease-out";
|
|
239
|
+
const SCALE_TRANSITION = "scale 0.12s ease-out";
|
|
240
|
+
|
|
241
|
+
const CHAR_WIDTH = 6.6;
|
|
242
|
+
const X_AXIS_HEIGHT = 26;
|
|
243
|
+
const X_LABEL_OFFSET = 16;
|
|
244
|
+
const X_LABEL_PADDING = 24;
|
|
245
|
+
const Y_LABEL_GAP = 16;
|
|
246
|
+
const Y_DOT_GAP = 8;
|
|
247
|
+
const Y_TICK_MIN_SPACING = 26;
|
|
248
|
+
|
|
249
|
+
const TOOLTIP_OFFSET = 14;
|
|
250
|
+
const TOOLTIP_MARGIN = 4;
|
|
251
|
+
|
|
252
|
+
const HEX = /^#?([\da-f]{3}|[\da-f]{6})$/i;
|
|
253
|
+
|
|
254
|
+
const UNITS: Unit[] = [
|
|
255
|
+
{ divisor: 1e9, suffix: "B" },
|
|
256
|
+
{ divisor: 1e6, suffix: "M" },
|
|
257
|
+
{ divisor: 1e3, suffix: "k" },
|
|
258
|
+
];
|
|
56
259
|
|
|
57
|
-
const
|
|
260
|
+
const FADE = [
|
|
58
261
|
"fill-gray-500 dark:fill-gray-600",
|
|
59
262
|
"fill-gray-800 dark:fill-gray-300",
|
|
60
263
|
"fill-gray-900 dark:fill-gray-200",
|
|
61
|
-
|
|
264
|
+
];
|
|
265
|
+
|
|
266
|
+
const PALETTE = [
|
|
267
|
+
{ fill: "fill-black dark:fill-white", swatch: "bg-black dark:bg-white" },
|
|
268
|
+
{
|
|
269
|
+
fill: "fill-gray-400 dark:fill-gray-600",
|
|
270
|
+
swatch: "bg-gray-400 dark:bg-gray-600",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
fill: "fill-gray-600 dark:fill-gray-500",
|
|
274
|
+
swatch: "bg-gray-600 dark:bg-gray-500",
|
|
275
|
+
},
|
|
62
276
|
];
|
|
63
277
|
|
|
64
278
|
const props = withDefaults(defineProps<UixyChartProps>(), {
|
|
65
279
|
rows: 8,
|
|
66
280
|
animate: true,
|
|
281
|
+
axis: true,
|
|
282
|
+
interactive: true,
|
|
67
283
|
});
|
|
68
284
|
|
|
69
|
-
|
|
285
|
+
const patternId = `uixy-chart-grid-${useId()}`;
|
|
70
286
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
287
|
+
const wrapperRef = ref<HTMLElement | null>(null);
|
|
288
|
+
const tooltipRef = ref<HTMLElement | null>(null);
|
|
289
|
+
|
|
290
|
+
const chartWidth = ref(0);
|
|
291
|
+
const parentHeight = ref(0);
|
|
292
|
+
const heightFromParent = ref<boolean | null>(null);
|
|
293
|
+
const tooltipSize = ref({ width: 168, height: 92 });
|
|
294
|
+
|
|
295
|
+
const hoveredColumn = ref<number | null>(null);
|
|
296
|
+
const focusedColumn = ref<number | null>(null);
|
|
297
|
+
|
|
298
|
+
const cells = ref<Cell[]>([]);
|
|
299
|
+
|
|
300
|
+
const crosshairAt = ref<Point>({ x: 0, y: 0 });
|
|
301
|
+
const tooltipAt = ref<Point>({ x: 0, y: 0 });
|
|
302
|
+
const instant = ref(true);
|
|
303
|
+
|
|
304
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
305
|
+
Math.max(min, Math.min(max, value));
|
|
306
|
+
|
|
307
|
+
const valueAt = (entry: UixyChartSeries, col: number) =>
|
|
308
|
+
Math.max(0, entry.values[col] ?? 0);
|
|
309
|
+
|
|
310
|
+
const series = computed<UixyChartSeries[]>(() => {
|
|
311
|
+
if (props.series?.length) return props.series;
|
|
312
|
+
if (props.data?.length)
|
|
313
|
+
return [{ label: "Value", values: props.data.map((d) => d.value) }];
|
|
314
|
+
return [];
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const labels = computed<string[]>(() => {
|
|
318
|
+
if (props.labels?.length) return props.labels;
|
|
319
|
+
if (props.data?.length) return props.data.map((d) => d.label);
|
|
320
|
+
return [];
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const rows = computed(() => Math.max(1, Math.floor(props.rows)));
|
|
324
|
+
|
|
325
|
+
const columnCount = computed(() =>
|
|
326
|
+
Math.max(0, ...series.value.map((entry) => entry.values.length)),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const maxTotal = computed(() =>
|
|
330
|
+
Math.max(
|
|
331
|
+
0,
|
|
332
|
+
...Array.from({ length: columnCount.value }, (_, col) =>
|
|
333
|
+
series.value.reduce((sum, entry) => sum + valueAt(entry, col), 0),
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const niceStep = (raw: number) => {
|
|
339
|
+
if (raw <= 0) return 1;
|
|
340
|
+
|
|
341
|
+
const magnitude = 10 ** Math.floor(Math.log10(raw));
|
|
342
|
+
const fraction = raw / magnitude;
|
|
343
|
+
const nice =
|
|
344
|
+
fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10;
|
|
345
|
+
|
|
346
|
+
return nice * magnitude;
|
|
78
347
|
};
|
|
79
348
|
|
|
349
|
+
const yScale = computed(() => {
|
|
350
|
+
const target = maxTotal.value * (1 + TOP_HEADROOM) || 1;
|
|
351
|
+
let best = { steps: 0, step: 0, max: Infinity };
|
|
352
|
+
|
|
353
|
+
for (const steps of [3, 4, 5, 6]) {
|
|
354
|
+
const step = niceStep(target / steps);
|
|
355
|
+
const max = step * steps;
|
|
356
|
+
|
|
357
|
+
if (max < best.max || (max === best.max && steps > best.steps))
|
|
358
|
+
best = { steps, step, max };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return best;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const yTickValues = computed(() =>
|
|
365
|
+
Array.from(
|
|
366
|
+
{ length: yScale.value.steps + 1 },
|
|
367
|
+
(_, index) => index * yScale.value.step,
|
|
368
|
+
),
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const unitFor = (value: number): Unit =>
|
|
372
|
+
UNITS.find((unit) => value >= unit.divisor) ?? { divisor: 1, suffix: "" };
|
|
373
|
+
|
|
374
|
+
const compact = (value: number, { divisor, suffix }: Unit) => {
|
|
375
|
+
const scaled = value / divisor;
|
|
376
|
+
const text =
|
|
377
|
+
Number.isInteger(scaled) || Math.abs(scaled) >= 100
|
|
378
|
+
? String(Math.round(scaled))
|
|
379
|
+
: scaled.toFixed(1).replace(/\.0$/, "");
|
|
380
|
+
|
|
381
|
+
return `${text}${suffix}`;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const tickUnit = computed(() => unitFor(yScale.value.max));
|
|
385
|
+
|
|
386
|
+
const formatTick = (value: number) =>
|
|
387
|
+
props.format ? props.format(value) : compact(value, tickUnit.value);
|
|
388
|
+
|
|
389
|
+
const formatValue = (value: number) =>
|
|
390
|
+
props.format ? props.format(value) : compact(value, unitFor(value));
|
|
391
|
+
|
|
80
392
|
const parseHex = (hex: string) => {
|
|
81
|
-
let
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const n = parseInt(h, 16);
|
|
88
|
-
return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
|
|
393
|
+
let digits = hex.replace("#", "");
|
|
394
|
+
if (digits.length === 3)
|
|
395
|
+
digits = [...digits].map((digit) => digit + digit).join("");
|
|
396
|
+
|
|
397
|
+
const n = parseInt(digits, 16);
|
|
398
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
89
399
|
};
|
|
90
400
|
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
401
|
+
const mix = (from: string, to: string, amount: number) => {
|
|
402
|
+
const a = parseHex(from);
|
|
403
|
+
const b = parseHex(to);
|
|
404
|
+
|
|
405
|
+
const channels = a.map((channel, index) =>
|
|
406
|
+
Math.round(channel + (b[index]! - channel) * amount)
|
|
94
407
|
.toString(16)
|
|
95
|
-
.padStart(2, "0")
|
|
96
|
-
|
|
408
|
+
.padStart(2, "0"),
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
return `#${channels.join("")}`;
|
|
97
412
|
};
|
|
98
413
|
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
414
|
+
const colorsOf = (index: number) => {
|
|
415
|
+
const entry = series.value[index];
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
color: entry?.color ?? (index === 0 ? props.color : undefined),
|
|
419
|
+
baseColor:
|
|
420
|
+
entry?.baseColor ?? (index === 0 ? props.baseColor : undefined),
|
|
421
|
+
};
|
|
107
422
|
};
|
|
108
423
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
() =>
|
|
112
|
-
props.baseColor ??
|
|
113
|
-
(props.color ? mix(props.color, "#000000", 0.4) : "#000000"),
|
|
424
|
+
const colorKey = computed(() =>
|
|
425
|
+
series.value.map((_, index) => Object.values(colorsOf(index))).join(),
|
|
114
426
|
);
|
|
115
427
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
428
|
+
const isSingleSeries = computed(() => series.value.length === 1);
|
|
429
|
+
|
|
430
|
+
const fadedPaint = (depth: number): Paint => {
|
|
431
|
+
const { color, baseColor } = colorsOf(0);
|
|
432
|
+
|
|
433
|
+
if (!color)
|
|
434
|
+
return { className: depth > FADE_ROWS ? PALETTE[0]!.fill : FADE[depth] };
|
|
435
|
+
|
|
436
|
+
const blendable = HEX.test(color) && (!baseColor || HEX.test(baseColor));
|
|
437
|
+
const body = baseColor ?? (blendable ? mix(color, "#000000", 0.4) : color);
|
|
438
|
+
|
|
439
|
+
if (depth <= 0) return { fill: color };
|
|
440
|
+
if (depth > FADE_ROWS || !blendable) return { fill: body };
|
|
441
|
+
return { fill: mix(color, body, depth / (FADE_ROWS + 1)) };
|
|
124
442
|
};
|
|
125
443
|
|
|
126
|
-
const
|
|
127
|
-
|
|
444
|
+
const seriesPaint = (index: number): Paint => {
|
|
445
|
+
const { color } = colorsOf(index);
|
|
446
|
+
if (color) return { fill: color };
|
|
447
|
+
return { className: PALETTE[index % PALETTE.length]!.fill };
|
|
448
|
+
};
|
|
128
449
|
|
|
129
|
-
const
|
|
450
|
+
const swatchOf = (index: number) => {
|
|
451
|
+
const { color } = colorsOf(index);
|
|
452
|
+
if (color) return { swatchStyle: { backgroundColor: color } };
|
|
453
|
+
return { swatchClass: PALETTE[index % PALETTE.length]!.swatch };
|
|
454
|
+
};
|
|
130
455
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
456
|
+
const stacks = computed<Stack[]>(() =>
|
|
457
|
+
Array.from({ length: columnCount.value }, (_, col) => {
|
|
458
|
+
const segments: Segment[] = [];
|
|
459
|
+
let total = 0;
|
|
460
|
+
let height = 0;
|
|
461
|
+
|
|
462
|
+
series.value.forEach((entry, index) => {
|
|
463
|
+
const value = valueAt(entry, col);
|
|
464
|
+
total += value;
|
|
465
|
+
|
|
466
|
+
let end = Math.round((total / yScale.value.max) * rows.value);
|
|
467
|
+
if (value > 0 && end <= height) end = height + 1;
|
|
468
|
+
end = Math.min(rows.value, end);
|
|
469
|
+
|
|
470
|
+
if (end > height) segments.push({ series: index, start: height, end });
|
|
471
|
+
height = end;
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
return { segments, height };
|
|
475
|
+
}),
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
const yAxisWidth = computed(() => {
|
|
479
|
+
if (!props.axis) return 0;
|
|
480
|
+
|
|
481
|
+
const longest = Math.max(
|
|
482
|
+
...yTickValues.value.map((value) => formatTick(value).length),
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
return Math.ceil(longest * CHAR_WIDTH) + Y_LABEL_GAP;
|
|
136
486
|
});
|
|
137
487
|
|
|
138
|
-
const
|
|
139
|
-
|
|
488
|
+
const xAxisHeight = computed(() => (props.axis ? X_AXIS_HEIGHT : 0));
|
|
489
|
+
|
|
490
|
+
const fitCells = (space: number, count: number) =>
|
|
491
|
+
Math.floor((space - (count - 1) * GAP) / count);
|
|
492
|
+
|
|
493
|
+
const plotWidth = computed(() =>
|
|
494
|
+
Math.max(0, chartWidth.value - yAxisWidth.value),
|
|
140
495
|
);
|
|
141
496
|
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
497
|
+
const naturalHeight = computed(() => {
|
|
498
|
+
if (!columnCount.value) return 0;
|
|
499
|
+
|
|
500
|
+
const size = Math.max(1, fitCells(plotWidth.value, columnCount.value));
|
|
501
|
+
return rows.value * size + (rows.value - 1) * GAP + xAxisHeight.value;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const chartHeight = computed(() =>
|
|
505
|
+
heightFromParent.value ? parentHeight.value : naturalHeight.value,
|
|
146
506
|
);
|
|
147
507
|
|
|
148
|
-
const
|
|
149
|
-
Math.max(
|
|
508
|
+
const plotHeight = computed(() =>
|
|
509
|
+
Math.max(0, chartHeight.value - xAxisHeight.value),
|
|
150
510
|
);
|
|
151
511
|
|
|
152
|
-
const
|
|
153
|
-
() =>
|
|
512
|
+
const ready = computed(
|
|
513
|
+
() =>
|
|
514
|
+
chartWidth.value > 0 && chartHeight.value > 0 && columnCount.value > 0,
|
|
154
515
|
);
|
|
155
516
|
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
517
|
+
const cellSize = computed(() => {
|
|
518
|
+
if (!columnCount.value) return 0;
|
|
519
|
+
|
|
520
|
+
return Math.max(
|
|
521
|
+
1,
|
|
522
|
+
Math.min(
|
|
523
|
+
fitCells(plotWidth.value, columnCount.value),
|
|
524
|
+
fitCells(plotHeight.value, rows.value),
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const spanOf = (count: number) => count * cellSize.value + (count - 1) * GAP;
|
|
530
|
+
|
|
531
|
+
const gridWidth = computed(() => spanOf(columnCount.value));
|
|
532
|
+
const gridHeight = computed(() => spanOf(rows.value));
|
|
533
|
+
|
|
534
|
+
const gridX = computed(
|
|
535
|
+
() =>
|
|
536
|
+
yAxisWidth.value +
|
|
537
|
+
Math.max(0, Math.round((plotWidth.value - gridWidth.value) / 2)),
|
|
160
538
|
);
|
|
161
539
|
|
|
162
|
-
const
|
|
163
|
-
|
|
540
|
+
const gridY = computed(() =>
|
|
541
|
+
Math.max(0, plotHeight.value - gridHeight.value),
|
|
164
542
|
);
|
|
165
|
-
const vbHeight = computed(() => rows.value * CELL + (rows.value - 1) * GAP);
|
|
166
543
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
544
|
+
const gridBottom = computed(() => gridY.value + gridHeight.value);
|
|
545
|
+
|
|
546
|
+
const cellX = (col: number) => gridX.value + col * (cellSize.value + GAP);
|
|
547
|
+
|
|
548
|
+
const cellY = (row: number) =>
|
|
549
|
+
gridBottom.value - (row + 1) * cellSize.value - row * GAP;
|
|
550
|
+
|
|
551
|
+
const columnCenter = (col: number) => cellX(col) + cellSize.value / 2;
|
|
552
|
+
|
|
553
|
+
const stackTop = (col: number) => {
|
|
554
|
+
const height = stacks.value[col]?.height ?? 0;
|
|
555
|
+
return height ? cellY(height - 1) : gridBottom.value;
|
|
171
556
|
};
|
|
172
557
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
558
|
+
const yLabelX = computed(() => gridX.value - Y_LABEL_GAP);
|
|
559
|
+
const yDotX = computed(() => gridX.value - Y_DOT_GAP);
|
|
560
|
+
const xLabelY = computed(() => gridBottom.value + X_LABEL_OFFSET);
|
|
561
|
+
|
|
562
|
+
const yTicks = computed(() => {
|
|
563
|
+
const spacing = gridHeight.value / yScale.value.steps;
|
|
564
|
+
const every = Math.max(
|
|
565
|
+
1,
|
|
566
|
+
Math.ceil(Y_TICK_MIN_SPACING / Math.max(1, spacing)),
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
return yTickValues.value
|
|
570
|
+
.filter((_, index) => index % every === 0)
|
|
571
|
+
.map((value) => ({
|
|
572
|
+
value,
|
|
573
|
+
label: formatTick(value),
|
|
574
|
+
y: gridBottom.value - (value / yScale.value.max) * gridHeight.value,
|
|
575
|
+
}));
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const labelRuns = computed<LabelRun[]>(() => {
|
|
579
|
+
const source = props.axisLabels?.length ? props.axisLabels : labels.value;
|
|
580
|
+
const count = columnCount.value;
|
|
176
581
|
|
|
177
|
-
|
|
582
|
+
if (!source.length || !count) return [];
|
|
178
583
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
584
|
+
if (source.length !== count)
|
|
585
|
+
return source
|
|
586
|
+
.map((label, index) => ({
|
|
587
|
+
label,
|
|
588
|
+
start: Math.round((index * count) / source.length),
|
|
589
|
+
end: Math.round(((index + 1) * count) / source.length),
|
|
590
|
+
}))
|
|
591
|
+
.filter((run) => run.end > run.start);
|
|
592
|
+
|
|
593
|
+
const runs: LabelRun[] = [];
|
|
594
|
+
|
|
595
|
+
source.forEach((label, index) => {
|
|
596
|
+
const previous = runs[runs.length - 1];
|
|
597
|
+
if (previous?.label === label) previous.end = index + 1;
|
|
598
|
+
else runs.push({ label, start: index, end: index + 1 });
|
|
183
599
|
});
|
|
184
|
-
|
|
600
|
+
|
|
601
|
+
return runs;
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const xTicks = computed(() => {
|
|
605
|
+
const runs = labelRuns.value;
|
|
606
|
+
if (!runs.length) return [];
|
|
607
|
+
|
|
608
|
+
const widest = Math.max(
|
|
609
|
+
...runs.map((run) => run.label.length * CHAR_WIDTH + X_LABEL_PADDING),
|
|
610
|
+
);
|
|
611
|
+
const every = Math.max(
|
|
612
|
+
1,
|
|
613
|
+
Math.ceil(widest / Math.max(1, gridWidth.value / runs.length)),
|
|
614
|
+
);
|
|
615
|
+
const visible = runs.filter((_, index) => index % every === 0);
|
|
616
|
+
const active = activeColumn.value;
|
|
617
|
+
|
|
618
|
+
return visible.map((run, index) => {
|
|
619
|
+
const until = visible[index + 1]?.start ?? columnCount.value;
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
label: run.label,
|
|
623
|
+
start: run.start,
|
|
624
|
+
x: (columnCenter(run.start) + columnCenter(run.end - 1)) / 2,
|
|
625
|
+
active: active !== null && active >= run.start && active < until,
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const xSeparators = computed(() =>
|
|
631
|
+
xTicks.value
|
|
632
|
+
.slice(1)
|
|
633
|
+
.map((tick, index) => (xTicks.value[index]!.x + tick.x) / 2),
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const activeColumn = computed(() => {
|
|
637
|
+
if (!props.interactive) return null;
|
|
638
|
+
|
|
639
|
+
const col = hoveredColumn.value ?? focusedColumn.value;
|
|
640
|
+
return col !== null && col < columnCount.value ? col : null;
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const crosshair = computed<Point | null>(() => {
|
|
644
|
+
const col = activeColumn.value;
|
|
645
|
+
if (col === null || !ready.value) return null;
|
|
646
|
+
|
|
647
|
+
return { x: columnCenter(col), y: stackTop(col) };
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const tooltipTitle = computed(() => {
|
|
651
|
+
const col = activeColumn.value;
|
|
652
|
+
if (col === null) return "";
|
|
653
|
+
|
|
654
|
+
return labels.value[col] ?? `#${col + 1}`;
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const tooltipRows = computed(() => {
|
|
658
|
+
const col = activeColumn.value;
|
|
659
|
+
if (col === null) return [];
|
|
660
|
+
|
|
661
|
+
return series.value.map((entry, index) => ({
|
|
662
|
+
label: entry.label,
|
|
663
|
+
value: formatValue(valueAt(entry, col)),
|
|
664
|
+
...swatchOf(index),
|
|
665
|
+
}));
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const announcement = computed(() => {
|
|
669
|
+
if (focusedColumn.value === null || !tooltipRows.value.length) return "";
|
|
670
|
+
|
|
671
|
+
const values = tooltipRows.value
|
|
672
|
+
.map((row) => `${row.label} ${row.value}`)
|
|
673
|
+
.join(", ");
|
|
674
|
+
|
|
675
|
+
return `${tooltipTitle.value}: ${values}`;
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const ariaLabel = computed(() => {
|
|
679
|
+
if (!series.value.length) return "Chart";
|
|
680
|
+
|
|
681
|
+
const names = series.value.map((entry) => entry.label).join(", ");
|
|
682
|
+
|
|
683
|
+
return [
|
|
684
|
+
`Chart with ${columnCount.value} columns`,
|
|
685
|
+
`series: ${names}`,
|
|
686
|
+
`peak ${formatValue(maxTotal.value)}`,
|
|
687
|
+
].join(", ");
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const revealTransition = computed(() =>
|
|
691
|
+
props.animate ? `opacity ${REVEAL_DURATION}s ease-out` : "none",
|
|
692
|
+
);
|
|
693
|
+
|
|
694
|
+
const followStyle = computed(() => ({
|
|
695
|
+
transform: `translateX(${crosshairAt.value.x}px)`,
|
|
696
|
+
opacity: crosshair.value ? 1 : 0,
|
|
697
|
+
transition: instant.value
|
|
698
|
+
? FADE_TRANSITION
|
|
699
|
+
: `${MOVE_TRANSITION}, ${FADE_TRANSITION}`,
|
|
700
|
+
}));
|
|
701
|
+
|
|
702
|
+
const markerStyle = computed(() => ({
|
|
703
|
+
transform: `translateY(${crosshairAt.value.y}px)`,
|
|
704
|
+
transition: instant.value ? "none" : MOVE_TRANSITION,
|
|
705
|
+
}));
|
|
706
|
+
|
|
707
|
+
const tooltipStyle = computed(() => ({
|
|
708
|
+
transform: `translate3d(${tooltipAt.value.x}px, ${tooltipAt.value.y}px, 0)`,
|
|
709
|
+
scale: crosshair.value ? 1 : 0.96,
|
|
710
|
+
opacity: crosshair.value ? 1 : 0,
|
|
711
|
+
transition: instant.value
|
|
712
|
+
? `${FADE_TRANSITION}, ${SCALE_TRANSITION}`
|
|
713
|
+
: `${MOVE_TRANSITION}, ${FADE_TRANSITION}, ${SCALE_TRANSITION}`,
|
|
714
|
+
}));
|
|
715
|
+
|
|
716
|
+
const placeTooltip = ({ x, y }: Point) => {
|
|
717
|
+
const size = tooltipSize.value;
|
|
718
|
+
const right = x + TOOLTIP_OFFSET + size.width;
|
|
719
|
+
const flipped = right > chartWidth.value - TOOLTIP_MARGIN;
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
flipped,
|
|
723
|
+
x: clamp(
|
|
724
|
+
flipped ? x - TOOLTIP_OFFSET - size.width : x + TOOLTIP_OFFSET,
|
|
725
|
+
TOOLTIP_MARGIN,
|
|
726
|
+
chartWidth.value - size.width - TOOLTIP_MARGIN,
|
|
727
|
+
),
|
|
728
|
+
y: clamp(
|
|
729
|
+
y - TOOLTIP_OFFSET,
|
|
730
|
+
TOOLTIP_MARGIN,
|
|
731
|
+
chartHeight.value - size.height - TOOLTIP_MARGIN,
|
|
732
|
+
),
|
|
733
|
+
};
|
|
185
734
|
};
|
|
186
735
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
736
|
+
let instantFrame = 0;
|
|
737
|
+
let tooltipFlipped = false;
|
|
738
|
+
|
|
739
|
+
const skipTransition = () => {
|
|
740
|
+
instant.value = true;
|
|
741
|
+
|
|
742
|
+
cancelAnimationFrame(instantFrame);
|
|
743
|
+
instantFrame = requestAnimationFrame(() => {
|
|
744
|
+
instantFrame = requestAnimationFrame(() => {
|
|
745
|
+
instant.value = false;
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
watch(
|
|
751
|
+
crosshair,
|
|
752
|
+
(next, previous) => {
|
|
753
|
+
if (!next) return;
|
|
754
|
+
|
|
755
|
+
const { flipped, x, y } = placeTooltip(next);
|
|
756
|
+
if (!previous || flipped !== tooltipFlipped) skipTransition();
|
|
757
|
+
|
|
758
|
+
tooltipFlipped = flipped;
|
|
759
|
+
crosshairAt.value = next;
|
|
760
|
+
tooltipAt.value = { x, y };
|
|
761
|
+
},
|
|
762
|
+
{ flush: "sync" },
|
|
763
|
+
);
|
|
764
|
+
|
|
765
|
+
const columnAt = (clientX: number) => {
|
|
766
|
+
const left = wrapperRef.value!.getBoundingClientRect().left;
|
|
767
|
+
const offset = clientX - left - gridX.value - cellSize.value / 2;
|
|
768
|
+
|
|
769
|
+
return clamp(
|
|
770
|
+
Math.round(offset / (cellSize.value + GAP)),
|
|
771
|
+
0,
|
|
772
|
+
columnCount.value - 1,
|
|
773
|
+
);
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
const nextColumn = (key: string, from: number) => {
|
|
777
|
+
switch (key) {
|
|
778
|
+
case "ArrowRight":
|
|
779
|
+
return from + 1;
|
|
780
|
+
case "ArrowLeft":
|
|
781
|
+
return from - 1;
|
|
782
|
+
case "Home":
|
|
783
|
+
return 0;
|
|
784
|
+
case "End":
|
|
785
|
+
return columnCount.value - 1;
|
|
786
|
+
default:
|
|
787
|
+
return null;
|
|
204
788
|
}
|
|
789
|
+
};
|
|
205
790
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
prevPaint = {};
|
|
791
|
+
const handlePointerMove = (event: PointerEvent) => {
|
|
792
|
+
hoveredColumn.value = columnAt(event.clientX);
|
|
209
793
|
};
|
|
210
794
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
paint: Paint;
|
|
215
|
-
waveStep: number;
|
|
216
|
-
changed: boolean;
|
|
795
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
796
|
+
focusedColumn.value = null;
|
|
797
|
+
handlePointerMove(event);
|
|
217
798
|
};
|
|
218
799
|
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
800
|
+
const handlePointerLeave = (event: PointerEvent) => {
|
|
801
|
+
if (event.pointerType !== "touch") hoveredColumn.value = null;
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
const handlePointerCancel = () => {
|
|
805
|
+
hoveredColumn.value = null;
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
const handleOutsidePointerDown = (event: PointerEvent) => {
|
|
809
|
+
if (!wrapperRef.value?.contains(event.target as Node))
|
|
810
|
+
hoveredColumn.value = null;
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
const handleFocus = (event: FocusEvent) => {
|
|
814
|
+
if ((event.target as HTMLElement).matches(":focus-visible"))
|
|
815
|
+
focusedColumn.value ??= 0;
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const handleBlur = () => {
|
|
819
|
+
focusedColumn.value = null;
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
823
|
+
if (event.key === "Escape") {
|
|
824
|
+
hoveredColumn.value = null;
|
|
825
|
+
focusedColumn.value = null;
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const target = nextColumn(event.key, activeColumn.value ?? 0);
|
|
830
|
+
if (target === null) return;
|
|
222
831
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (changed) columnChanged = true;
|
|
246
|
-
|
|
247
|
-
targets.push({
|
|
248
|
-
cell,
|
|
249
|
-
filled,
|
|
250
|
-
paint,
|
|
251
|
-
waveStep: rising ? row : rows.value - 1 - row,
|
|
252
|
-
changed,
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return { columnChanged, targets };
|
|
832
|
+
event.preventDefault();
|
|
833
|
+
hoveredColumn.value = null;
|
|
834
|
+
focusedColumn.value = clamp(target, 0, columnCount.value - 1);
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const batches = new Map<number, Batch>();
|
|
838
|
+
let cellsShape = "";
|
|
839
|
+
|
|
840
|
+
const schedule = (delay: number, task: () => void) => {
|
|
841
|
+
const batch = batches.get(delay);
|
|
842
|
+
|
|
843
|
+
if (batch) {
|
|
844
|
+
batch.tasks.push(task);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
batches.set(delay, {
|
|
849
|
+
tasks: [task],
|
|
850
|
+
timer: setTimeout(() => {
|
|
851
|
+
batches.get(delay)?.tasks.forEach((run) => run());
|
|
852
|
+
batches.delete(delay);
|
|
853
|
+
}, delay),
|
|
257
854
|
});
|
|
855
|
+
};
|
|
258
856
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
857
|
+
const cancelBatches = () => {
|
|
858
|
+
batches.forEach(({ timer }) => clearTimeout(timer));
|
|
859
|
+
batches.clear();
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const flushBatches = () => {
|
|
863
|
+
const tasks = [...batches.values()].flatMap((batch) => batch.tasks);
|
|
864
|
+
|
|
865
|
+
cancelBatches();
|
|
866
|
+
tasks.forEach((run) => run());
|
|
867
|
+
};
|
|
868
|
+
|
|
869
|
+
const ensureCells = () => {
|
|
870
|
+
const shape = `${columnCount.value}x${rows.value}`;
|
|
871
|
+
if (shape === cellsShape) return;
|
|
872
|
+
cellsShape = shape;
|
|
873
|
+
|
|
874
|
+
cells.value = Array.from({ length: columnCount.value }, (_, col) =>
|
|
875
|
+
Array.from({ length: rows.value }, (_, row) => ({
|
|
876
|
+
key: `${col}-${row}`,
|
|
877
|
+
col,
|
|
878
|
+
row,
|
|
879
|
+
filled: false,
|
|
880
|
+
className: PALETTE[0]!.fill,
|
|
881
|
+
})),
|
|
882
|
+
).flat();
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
const targetPaint = (segment: Segment, row: number, height: number) =>
|
|
886
|
+
isSingleSeries.value
|
|
887
|
+
? fadedPaint(height - 1 - row)
|
|
888
|
+
: seriesPaint(segment.series);
|
|
889
|
+
|
|
890
|
+
const syncCells = () => {
|
|
891
|
+
flushBatches();
|
|
892
|
+
ensureCells();
|
|
893
|
+
|
|
894
|
+
if (!ready.value) return;
|
|
895
|
+
|
|
896
|
+
let wave = 0;
|
|
897
|
+
|
|
898
|
+
stacks.value.forEach(({ segments, height }, col) => {
|
|
899
|
+
const firstCell = col * rows.value;
|
|
900
|
+
const column = cells.value.slice(firstCell, firstCell + rows.value);
|
|
901
|
+
const rising = height >= column.filter((cell) => cell.filled).length;
|
|
902
|
+
const columnDelay = wave * COLUMN_STEP;
|
|
903
|
+
let changed = false;
|
|
904
|
+
|
|
905
|
+
column.forEach((cell, row) => {
|
|
906
|
+
const segment = segments.find(
|
|
907
|
+
({ start, end }) => row >= start && row < end,
|
|
908
|
+
);
|
|
909
|
+
const filled = !!segment;
|
|
910
|
+
const { fill, className } = segment
|
|
911
|
+
? targetPaint(segment, row, height)
|
|
912
|
+
: cell;
|
|
913
|
+
|
|
914
|
+
const unchanged =
|
|
915
|
+
filled === cell.filled &&
|
|
916
|
+
fill === cell.fill &&
|
|
917
|
+
className === cell.className;
|
|
918
|
+
|
|
919
|
+
if (unchanged) return;
|
|
920
|
+
|
|
921
|
+
changed = true;
|
|
922
|
+
|
|
923
|
+
const apply = () => Object.assign(cell, { filled, fill, className });
|
|
924
|
+
const step = rising ? row : rows.value - 1 - row;
|
|
925
|
+
const delay = props.animate
|
|
926
|
+
? Math.round((columnDelay + step * ROW_STEP) * 1000)
|
|
927
|
+
: 0;
|
|
928
|
+
|
|
929
|
+
if (delay > 0) schedule(delay, apply);
|
|
930
|
+
else apply();
|
|
285
931
|
});
|
|
932
|
+
|
|
933
|
+
if (changed) wave++;
|
|
286
934
|
});
|
|
935
|
+
};
|
|
287
936
|
|
|
288
|
-
|
|
289
|
-
|
|
937
|
+
watch([stacks, colorKey, () => props.animate, ready], syncCells, {
|
|
938
|
+
immediate: true,
|
|
939
|
+
flush: "post",
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
let observer: ResizeObserver | null = null;
|
|
943
|
+
|
|
944
|
+
const measure = () => {
|
|
945
|
+
const wrapper = wrapperRef.value;
|
|
946
|
+
const tooltip = tooltipRef.value;
|
|
947
|
+
|
|
948
|
+
if (wrapper) {
|
|
949
|
+
const rect = wrapper.getBoundingClientRect();
|
|
950
|
+
|
|
951
|
+
if (heightFromParent.value === null && rect.width > 0)
|
|
952
|
+
heightFromParent.value = rect.height > 0;
|
|
953
|
+
|
|
954
|
+
chartWidth.value = rect.width;
|
|
955
|
+
parentHeight.value = rect.height;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (tooltip) {
|
|
959
|
+
tooltipSize.value = {
|
|
960
|
+
width: tooltip.offsetWidth,
|
|
961
|
+
height: tooltip.offsetHeight,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
290
964
|
};
|
|
291
965
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
],
|
|
301
|
-
build,
|
|
302
|
-
{ immediate: true },
|
|
303
|
-
);
|
|
966
|
+
onMounted(() => {
|
|
967
|
+
observer = new ResizeObserver(measure);
|
|
968
|
+
|
|
969
|
+
if (wrapperRef.value) observer.observe(wrapperRef.value);
|
|
970
|
+
if (tooltipRef.value) observer.observe(tooltipRef.value);
|
|
971
|
+
|
|
972
|
+
document.addEventListener("pointerdown", handleOutsidePointerDown);
|
|
973
|
+
});
|
|
304
974
|
|
|
305
975
|
onUnmounted(() => {
|
|
306
|
-
|
|
976
|
+
observer?.disconnect();
|
|
977
|
+
document.removeEventListener("pointerdown", handleOutsidePointerDown);
|
|
978
|
+
cancelAnimationFrame(instantFrame);
|
|
979
|
+
cancelBatches();
|
|
307
980
|
});
|
|
308
981
|
</script>
|