@squirkle-ui/charts 0.0.0-stage → 0.1.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.
@@ -0,0 +1,975 @@
1
+ import { defineComponent, resolveComponent, openBlock, createElementBlock, normalizeClass, createBlock, createCommentVNode, toDisplayString, Fragment, renderList, createElementVNode, normalizeStyle, createTextVNode, renderSlot, ref, onMounted, onBeforeUnmount, computed, withCtx, withDirectives, vShow, mergeProps } from "vue";
2
+ import { SqShape, colorValue } from "@squirkle-ui/ui";
3
+ import { niceTicks } from "@squirkle-ui/core";
4
+ const _sfc_main$4 = defineComponent({
5
+ name: "SqChartFrame",
6
+ components: { SqShape },
7
+ props: {
8
+ title: { type: String, default: void 0 },
9
+ subtitle: { type: String, default: void 0 },
10
+ flat: Boolean,
11
+ legend: { type: Array, default: () => [] },
12
+ table: { type: Object, default: () => ({ head: [], rows: [] }) }
13
+ }
14
+ });
15
+ const _export_sfc = (sfc, props) => {
16
+ const target = sfc.__vccOpts || sfc;
17
+ for (const [key, val] of props) {
18
+ target[key] = val;
19
+ }
20
+ return target;
21
+ };
22
+ const _hoisted_1$4 = {
23
+ key: 1,
24
+ class: "sq-chart__caption"
25
+ };
26
+ const _hoisted_2$4 = {
27
+ key: 0,
28
+ class: "sq-chart__title"
29
+ };
30
+ const _hoisted_3$4 = {
31
+ key: 1,
32
+ class: "sq-chart__subtitle"
33
+ };
34
+ const _hoisted_4$4 = {
35
+ key: 2,
36
+ class: "sq-chart__legend",
37
+ role: "list"
38
+ };
39
+ const _hoisted_5$3 = { class: "sq-sr-only" };
40
+ function _sfc_render$4(_ctx, _cache, $props, $setup, $data, $options) {
41
+ const _component_SqShape = resolveComponent("SqShape");
42
+ return openBlock(), createElementBlock(
43
+ "figure",
44
+ {
45
+ class: normalizeClass(["sq-chart sq-surface", { "sq-chart--flat": _ctx.flat }])
46
+ },
47
+ [
48
+ !_ctx.flat ? (openBlock(), createBlock(_component_SqShape, { key: 0 })) : createCommentVNode("v-if", true),
49
+ _ctx.title || _ctx.subtitle ? (openBlock(), createElementBlock("figcaption", _hoisted_1$4, [
50
+ _ctx.title ? (openBlock(), createElementBlock(
51
+ "div",
52
+ _hoisted_2$4,
53
+ toDisplayString(_ctx.title),
54
+ 1
55
+ /* TEXT */
56
+ )) : createCommentVNode("v-if", true),
57
+ _ctx.subtitle ? (openBlock(), createElementBlock(
58
+ "div",
59
+ _hoisted_3$4,
60
+ toDisplayString(_ctx.subtitle),
61
+ 1
62
+ /* TEXT */
63
+ )) : createCommentVNode("v-if", true)
64
+ ])) : createCommentVNode("v-if", true),
65
+ _ctx.legend.length > 1 ? (openBlock(), createElementBlock("div", _hoisted_4$4, [
66
+ (openBlock(true), createElementBlock(
67
+ Fragment,
68
+ null,
69
+ renderList(_ctx.legend, (l, i) => {
70
+ return openBlock(), createElementBlock("span", {
71
+ key: l.name,
72
+ class: "sq-chart__legend-item",
73
+ role: "listitem"
74
+ }, [
75
+ createElementVNode(
76
+ "span",
77
+ {
78
+ class: "sq-chart__swatch",
79
+ style: normalizeStyle({ background: l.color })
80
+ },
81
+ null,
82
+ 4
83
+ /* STYLE */
84
+ ),
85
+ createTextVNode(
86
+ toDisplayString(l.name),
87
+ 1
88
+ /* TEXT */
89
+ )
90
+ ]);
91
+ }),
92
+ 128
93
+ /* KEYED_FRAGMENT */
94
+ ))
95
+ ])) : createCommentVNode("v-if", true),
96
+ renderSlot(_ctx.$slots, "default"),
97
+ createElementVNode("table", _hoisted_5$3, [
98
+ createElementVNode(
99
+ "caption",
100
+ null,
101
+ toDisplayString(_ctx.title || "Chart data"),
102
+ 1
103
+ /* TEXT */
104
+ ),
105
+ createElementVNode("thead", null, [
106
+ createElementVNode("tr", null, [
107
+ (openBlock(true), createElementBlock(
108
+ Fragment,
109
+ null,
110
+ renderList(_ctx.table.head, (h) => {
111
+ return openBlock(), createElementBlock(
112
+ "th",
113
+ { key: h },
114
+ toDisplayString(h),
115
+ 1
116
+ /* TEXT */
117
+ );
118
+ }),
119
+ 128
120
+ /* KEYED_FRAGMENT */
121
+ ))
122
+ ])
123
+ ]),
124
+ createElementVNode("tbody", null, [
125
+ (openBlock(true), createElementBlock(
126
+ Fragment,
127
+ null,
128
+ renderList(_ctx.table.rows, (r, i) => {
129
+ return openBlock(), createElementBlock("tr", { key: i }, [
130
+ (openBlock(true), createElementBlock(
131
+ Fragment,
132
+ null,
133
+ renderList(r, (c, j) => {
134
+ return openBlock(), createElementBlock(
135
+ "td",
136
+ { key: j },
137
+ toDisplayString(c),
138
+ 1
139
+ /* TEXT */
140
+ );
141
+ }),
142
+ 128
143
+ /* KEYED_FRAGMENT */
144
+ ))
145
+ ]);
146
+ }),
147
+ 128
148
+ /* KEYED_FRAGMENT */
149
+ ))
150
+ ])
151
+ ])
152
+ ],
153
+ 2
154
+ /* CLASS */
155
+ );
156
+ }
157
+ const ChartFrame = /* @__PURE__ */ _export_sfc(_sfc_main$4, [["render", _sfc_render$4]]);
158
+ const seriesColor = (i, override) => override ?? `var(--sq-series-${i % 8 + 1})`;
159
+ function useSize(el, fallbackHeight) {
160
+ const width = ref(600);
161
+ const height = ref(fallbackHeight);
162
+ let ro;
163
+ onMounted(() => {
164
+ const measure = () => {
165
+ if (!el.value) return;
166
+ width.value = el.value.clientWidth || 600;
167
+ height.value = el.value.clientHeight || fallbackHeight;
168
+ };
169
+ measure();
170
+ if (typeof ResizeObserver !== "undefined" && el.value) {
171
+ ro = new ResizeObserver(measure);
172
+ ro.observe(el.value);
173
+ }
174
+ });
175
+ onBeforeUnmount(() => ro?.disconnect());
176
+ return { width, height };
177
+ }
178
+ function linearScale(d0, d1, r0, r1) {
179
+ const k = d1 === d0 ? 0 : (r1 - r0) / (d1 - d0);
180
+ return (v) => r0 + (v - d0) * k;
181
+ }
182
+ function yDomain(values, zero = true) {
183
+ let min = Math.min(...values);
184
+ let max = Math.max(...values);
185
+ if (zero) {
186
+ min = Math.min(0, min);
187
+ max = Math.max(0, max);
188
+ }
189
+ if (!isFinite(min) || !isFinite(max)) return { ticks: [0, 1], min: 0, max: 1 };
190
+ if (min === max) max = min + 1;
191
+ const ticks = niceTicks(min, max, 5);
192
+ return { ticks, min: ticks[0], max: ticks[ticks.length - 1] };
193
+ }
194
+ function smoothPath(pts) {
195
+ if (pts.length < 2) return pts.length ? `M${pts[0][0]},${pts[0][1]}` : "";
196
+ const n = pts.length;
197
+ const dx = [];
198
+ const m = [];
199
+ for (let i = 0; i < n - 1; i++) {
200
+ dx.push(pts[i + 1][0] - pts[i][0]);
201
+ m.push((pts[i + 1][1] - pts[i][1]) / (dx[i] || 1));
202
+ }
203
+ const t = [m[0]];
204
+ for (let i = 1; i < n - 1; i++) t.push(m[i - 1] * m[i] <= 0 ? 0 : 3 * (dx[i - 1] + dx[i]) / ((2 * dx[i] + dx[i - 1]) / m[i - 1] + (dx[i] + 2 * dx[i - 1]) / m[i]));
205
+ t.push(m[n - 2]);
206
+ let d = `M${pts[0][0]},${pts[0][1]}`;
207
+ for (let i = 0; i < n - 1; i++) {
208
+ const h = dx[i] / 3;
209
+ d += ` C${pts[i][0] + h},${pts[i][1] + t[i] * h} ${pts[i + 1][0] - h},${pts[i + 1][1] - t[i + 1] * h} ${pts[i + 1][0]},${pts[i + 1][1]}`;
210
+ }
211
+ return d;
212
+ }
213
+ const compact = (v, locale = "en-US") => new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 1 }).format(v);
214
+ function barPath(x, y, w, h, r, horizontal = false, negative = false) {
215
+ if (w <= 0 || h <= 0) return "";
216
+ const rr = Math.min(r, (horizontal ? h : w) / 2, horizontal ? w : h);
217
+ const k = rr * 0.45;
218
+ if (horizontal) {
219
+ if (negative) return `M${x + w},${y} H${x + rr} C${x + rr - k},${y} ${x},${y + rr - k} ${x},${y + rr} V${y + h - rr} C${x},${y + h - rr + k} ${x + rr - k},${y + h} ${x + rr},${y + h} H${x + w} Z`;
220
+ return `M${x},${y} H${x + w - rr} C${x + w - rr + k},${y} ${x + w},${y + rr - k} ${x + w},${y + rr} V${y + h - rr} C${x + w},${y + h - rr + k} ${x + w - rr + k},${y + h} ${x + w - rr},${y + h} H${x} Z`;
221
+ }
222
+ if (negative) return `M${x},${y} H${x + w} V${y + h - rr} C${x + w},${y + h - rr + k} ${x + w - rr + k},${y + h} ${x + w - rr},${y + h} H${x + rr} C${x + rr - k},${y + h} ${x},${y + h - rr + k} ${x},${y + h - rr} Z`;
223
+ return `M${x},${y + h} V${y + rr} C${x},${y + rr - k} ${x + rr - k},${y} ${x + rr},${y} H${x + w - rr} C${x + w - rr + k},${y} ${x + w},${y + rr - k} ${x + w},${y + rr} V${y + h} Z`;
224
+ }
225
+ const _sfc_main$3 = defineComponent({
226
+ name: "SqLineChart",
227
+ components: { ChartFrame },
228
+ props: {
229
+ labels: { type: Array, default: () => [] },
230
+ series: { type: Array, default: () => [] },
231
+ title: { type: String, default: void 0 },
232
+ subtitle: { type: String, default: void 0 },
233
+ height: { type: Number, default: 260 },
234
+ area: Boolean,
235
+ smooth: { type: Boolean, default: true },
236
+ points: Boolean,
237
+ /** Start y at zero (recommended for areas). */
238
+ zero: { type: Boolean, default: true },
239
+ flat: Boolean,
240
+ format: { type: Function, default: void 0 }
241
+ },
242
+ setup(props) {
243
+ const box = ref(null);
244
+ const { width: w } = useSize(box, props.height);
245
+ const pad = { t: 12, r: 16, b: 26, l: 44 };
246
+ const hover = ref(-1);
247
+ const y = computed(() => yDomain(props.series.flatMap((s) => s.data.filter((v) => v != null)), props.zero || props.area));
248
+ const sy = computed(() => linearScale(y.value.min, y.value.max, props.height - pad.b, pad.t));
249
+ const sx = computed(() => {
250
+ const n = Math.max(1, props.labels.length - 1);
251
+ return (i) => pad.l + (w.value - pad.l - pad.r) * i / n;
252
+ });
253
+ const colorOf = (i) => seriesColor(i, props.series[i]?.color);
254
+ const fmt = (v) => props.format ? props.format(v) : compact(v);
255
+ const fmtCell = (v) => v == null ? "–" : fmt(v);
256
+ const paths = computed(
257
+ () => props.series.map((s, i) => {
258
+ const pts = s.data.map((v, k) => [sx.value(k), v == null ? null : sy.value(v)]);
259
+ const valid = pts.filter((p) => p[1] != null);
260
+ const line = props.smooth ? smoothPath(valid) : valid.map((p, k) => `${k ? "L" : "M"}${p[0]},${p[1]}`).join(" ");
261
+ const base = sy.value(Math.max(y.value.min, 0));
262
+ const area = valid.length ? `${line} L${valid[valid.length - 1][0]},${base} L${valid[0][0]},${base} Z` : "";
263
+ return { name: s.name, color: colorOf(i), pts, line, area };
264
+ })
265
+ );
266
+ const showLabel = (i) => {
267
+ const n = props.labels.length;
268
+ const every = Math.ceil(n / Math.max(2, Math.floor((w.value - pad.l) / 64)));
269
+ return i % every === 0 || i === n - 1;
270
+ };
271
+ const onMove = (e) => {
272
+ const r = box.value.getBoundingClientRect();
273
+ const n = props.labels.length;
274
+ if (!n) return;
275
+ const i = Math.round((e.clientX - r.left - pad.l) / (w.value - pad.l - pad.r) * (n - 1));
276
+ hover.value = Math.max(0, Math.min(n - 1, i));
277
+ };
278
+ const tipStyle = computed(() => {
279
+ const x = sx.value(hover.value);
280
+ return x > w.value / 2 ? { right: `${w.value - x + 12}px`, top: "8px" } : { left: `${x + 12}px`, top: "8px" };
281
+ });
282
+ const legend = computed(() => props.series.map((s, i) => ({ name: s.name, color: colorOf(i) })));
283
+ const table = computed(() => ({ head: ["", ...props.series.map((s) => s.name)], rows: props.labels.map((l, k) => [l, ...props.series.map((s) => s.data[k] ?? "")]) }));
284
+ return { fmtCell, box, w, pad, hover, y, sy, sx, paths, colorOf, fmt, showLabel, onMove, tipStyle, legend, table };
285
+ }
286
+ });
287
+ const _hoisted_1$3 = ["width", "height", "aria-label"];
288
+ const _hoisted_2$3 = { class: "sq-chart__grid" };
289
+ const _hoisted_3$3 = ["x1", "x2", "y1", "y2"];
290
+ const _hoisted_4$3 = { class: "sq-chart__axis" };
291
+ const _hoisted_5$2 = ["x", "y"];
292
+ const _hoisted_6 = ["x", "y"];
293
+ const _hoisted_7 = ["d", "fill"];
294
+ const _hoisted_8 = ["d", "stroke"];
295
+ const _hoisted_9 = ["x1", "x2", "y1", "y2"];
296
+ const _hoisted_10 = { key: 1 };
297
+ const _hoisted_11 = ["cx", "cy", "fill"];
298
+ const _hoisted_12 = { key: 2 };
299
+ const _hoisted_13 = ["cx", "cy", "fill"];
300
+ const _hoisted_14 = { class: "sq-chart__tooltip-title" };
301
+ function _sfc_render$3(_ctx, _cache, $props, $setup, $data, $options) {
302
+ const _component_ChartFrame = resolveComponent("ChartFrame");
303
+ return openBlock(), createBlock(_component_ChartFrame, {
304
+ title: _ctx.title,
305
+ subtitle: _ctx.subtitle,
306
+ flat: _ctx.flat,
307
+ legend: _ctx.legend,
308
+ table: _ctx.table
309
+ }, {
310
+ default: withCtx(() => [
311
+ createElementVNode(
312
+ "div",
313
+ {
314
+ ref: "box",
315
+ class: "sq-chart__plot",
316
+ style: normalizeStyle({ height: _ctx.height + "px" }),
317
+ onPointermove: _cache[0] || (_cache[0] = (...args) => _ctx.onMove && _ctx.onMove(...args)),
318
+ onPointerleave: _cache[1] || (_cache[1] = ($event) => _ctx.hover = -1)
319
+ },
320
+ [
321
+ (openBlock(), createElementBlock("svg", {
322
+ width: _ctx.w,
323
+ height: _ctx.height,
324
+ role: "img",
325
+ "aria-label": _ctx.title || "Line chart"
326
+ }, [
327
+ createElementVNode("g", _hoisted_2$3, [
328
+ (openBlock(true), createElementBlock(
329
+ Fragment,
330
+ null,
331
+ renderList(_ctx.y.ticks, (t) => {
332
+ return openBlock(), createElementBlock("line", {
333
+ key: "g" + t,
334
+ x1: _ctx.pad.l,
335
+ x2: _ctx.w - _ctx.pad.r,
336
+ y1: _ctx.sy(t),
337
+ y2: _ctx.sy(t)
338
+ }, null, 8, _hoisted_3$3);
339
+ }),
340
+ 128
341
+ /* KEYED_FRAGMENT */
342
+ ))
343
+ ]),
344
+ createElementVNode("g", _hoisted_4$3, [
345
+ (openBlock(true), createElementBlock(
346
+ Fragment,
347
+ null,
348
+ renderList(_ctx.y.ticks, (t) => {
349
+ return openBlock(), createElementBlock("text", {
350
+ key: "y" + t,
351
+ x: _ctx.pad.l - 8,
352
+ y: _ctx.sy(t),
353
+ "text-anchor": "end",
354
+ "dominant-baseline": "middle"
355
+ }, toDisplayString(_ctx.fmt(t)), 9, _hoisted_5$2);
356
+ }),
357
+ 128
358
+ /* KEYED_FRAGMENT */
359
+ )),
360
+ (openBlock(true), createElementBlock(
361
+ Fragment,
362
+ null,
363
+ renderList(_ctx.labels, (l, i) => {
364
+ return withDirectives((openBlock(), createElementBlock("text", {
365
+ key: "x" + i,
366
+ x: _ctx.sx(i),
367
+ y: _ctx.height - 6,
368
+ "text-anchor": "middle"
369
+ }, toDisplayString(l), 9, _hoisted_6)), [
370
+ [vShow, _ctx.showLabel(i)]
371
+ ]);
372
+ }),
373
+ 128
374
+ /* KEYED_FRAGMENT */
375
+ ))
376
+ ]),
377
+ (openBlock(true), createElementBlock(
378
+ Fragment,
379
+ null,
380
+ renderList(_ctx.paths, (s, si) => {
381
+ return openBlock(), createElementBlock("g", {
382
+ key: s.name
383
+ }, [
384
+ _ctx.area ? (openBlock(), createElementBlock("path", {
385
+ key: 0,
386
+ d: s.area,
387
+ fill: s.color,
388
+ class: "sq-chart__area"
389
+ }, null, 8, _hoisted_7)) : createCommentVNode("v-if", true),
390
+ createElementVNode("path", {
391
+ d: s.line,
392
+ stroke: s.color,
393
+ class: "sq-chart__line"
394
+ }, null, 8, _hoisted_8)
395
+ ]);
396
+ }),
397
+ 128
398
+ /* KEYED_FRAGMENT */
399
+ )),
400
+ _ctx.hover >= 0 ? (openBlock(), createElementBlock("line", {
401
+ key: 0,
402
+ class: "sq-chart__crosshair",
403
+ x1: _ctx.sx(_ctx.hover),
404
+ x2: _ctx.sx(_ctx.hover),
405
+ y1: _ctx.pad.t,
406
+ y2: _ctx.height - _ctx.pad.b
407
+ }, null, 8, _hoisted_9)) : createCommentVNode("v-if", true),
408
+ _ctx.hover >= 0 ? (openBlock(), createElementBlock("g", _hoisted_10, [
409
+ (openBlock(true), createElementBlock(
410
+ Fragment,
411
+ null,
412
+ renderList(_ctx.paths, (s) => {
413
+ return withDirectives((openBlock(), createElementBlock("circle", {
414
+ key: "p" + s.name,
415
+ cx: _ctx.sx(_ctx.hover),
416
+ cy: s.pts[_ctx.hover] ? s.pts[_ctx.hover][1] ?? 0 : 0,
417
+ r: "4.5",
418
+ fill: s.color,
419
+ class: "sq-chart__dot"
420
+ }, null, 8, _hoisted_11)), [
421
+ [vShow, s.pts[_ctx.hover] && s.pts[_ctx.hover][1] != null]
422
+ ]);
423
+ }),
424
+ 128
425
+ /* KEYED_FRAGMENT */
426
+ ))
427
+ ])) : createCommentVNode("v-if", true),
428
+ _ctx.points && _ctx.hover < 0 ? (openBlock(), createElementBlock("g", _hoisted_12, [
429
+ (openBlock(true), createElementBlock(
430
+ Fragment,
431
+ null,
432
+ renderList(_ctx.paths, (s) => {
433
+ return openBlock(), createElementBlock("g", {
434
+ key: "pp" + s.name
435
+ }, [
436
+ (openBlock(true), createElementBlock(
437
+ Fragment,
438
+ null,
439
+ renderList(s.pts, (p, i) => {
440
+ return withDirectives((openBlock(), createElementBlock("circle", {
441
+ key: i,
442
+ cx: p[0],
443
+ cy: p[1] ?? 0,
444
+ r: "4",
445
+ fill: s.color,
446
+ class: "sq-chart__dot"
447
+ }, null, 8, _hoisted_13)), [
448
+ [vShow, p[1] != null]
449
+ ]);
450
+ }),
451
+ 128
452
+ /* KEYED_FRAGMENT */
453
+ ))
454
+ ]);
455
+ }),
456
+ 128
457
+ /* KEYED_FRAGMENT */
458
+ ))
459
+ ])) : createCommentVNode("v-if", true)
460
+ ], 8, _hoisted_1$3)),
461
+ _ctx.hover >= 0 ? (openBlock(), createElementBlock(
462
+ "div",
463
+ {
464
+ key: 0,
465
+ class: "sq-chart__tooltip sq-squircle",
466
+ style: normalizeStyle(_ctx.tipStyle)
467
+ },
468
+ [
469
+ createElementVNode(
470
+ "div",
471
+ _hoisted_14,
472
+ toDisplayString(_ctx.labels[_ctx.hover]),
473
+ 1
474
+ /* TEXT */
475
+ ),
476
+ (openBlock(true), createElementBlock(
477
+ Fragment,
478
+ null,
479
+ renderList(_ctx.series, (s, i) => {
480
+ return openBlock(), createElementBlock("div", {
481
+ key: s.name,
482
+ class: "sq-chart__tooltip-row"
483
+ }, [
484
+ createElementVNode(
485
+ "span",
486
+ {
487
+ class: "sq-chart__swatch",
488
+ style: normalizeStyle({ background: _ctx.colorOf(i) })
489
+ },
490
+ null,
491
+ 4
492
+ /* STYLE */
493
+ ),
494
+ createTextVNode(
495
+ toDisplayString(s.name),
496
+ 1
497
+ /* TEXT */
498
+ ),
499
+ createElementVNode(
500
+ "b",
501
+ null,
502
+ toDisplayString(_ctx.fmtCell(s.data[_ctx.hover])),
503
+ 1
504
+ /* TEXT */
505
+ )
506
+ ]);
507
+ }),
508
+ 128
509
+ /* KEYED_FRAGMENT */
510
+ ))
511
+ ],
512
+ 4
513
+ /* STYLE */
514
+ )) : createCommentVNode("v-if", true)
515
+ ],
516
+ 36
517
+ /* STYLE, NEED_HYDRATION */
518
+ )
519
+ ]),
520
+ _: 1
521
+ /* STABLE */
522
+ }, 8, ["title", "subtitle", "flat", "legend", "table"]);
523
+ }
524
+ const SqLineChart = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["render", _sfc_render$3]]);
525
+ const _sfc_main$2 = defineComponent({
526
+ name: "SqBarChart",
527
+ components: { ChartFrame },
528
+ props: {
529
+ labels: { type: Array, default: () => [] },
530
+ series: { type: Array, default: () => [] },
531
+ title: { type: String, default: void 0 },
532
+ subtitle: { type: String, default: void 0 },
533
+ height: { type: Number, default: 260 },
534
+ stacked: Boolean,
535
+ horizontal: Boolean,
536
+ radius: { type: Number, default: 6 },
537
+ flat: Boolean,
538
+ format: { type: Function, default: void 0 }
539
+ },
540
+ setup(props) {
541
+ const box = ref(null);
542
+ const { width: w } = useSize(box, props.height);
543
+ const pad = computed(() => props.horizontal ? { t: 8, r: 16, b: 24, l: 84 } : { t: 12, r: 16, b: 26, l: 44 });
544
+ const hover = ref(null);
545
+ const colorOf = (i) => seriesColor(i, props.series[i]?.color);
546
+ const fmt = (v) => props.format ? props.format(v) : compact(v);
547
+ const fmtCell = (v) => v == null ? "–" : fmt(v);
548
+ const y = computed(() => {
549
+ const vals = props.stacked ? props.labels.flatMap((_, k) => {
550
+ let pos = 0;
551
+ let neg = 0;
552
+ for (const s of props.series) {
553
+ const v = s.data[k] ?? 0;
554
+ v >= 0 ? pos += v : neg += v;
555
+ }
556
+ return [pos, neg];
557
+ }) : props.series.flatMap((s) => s.data.filter((v) => v != null));
558
+ return yDomain(vals, true);
559
+ });
560
+ const sv = computed(() => props.horizontal ? linearScale(y.value.min, y.value.max, pad.value.l, w.value - pad.value.r) : linearScale(y.value.min, y.value.max, props.height - pad.value.b, pad.value.t));
561
+ const band = computed(() => {
562
+ const n = Math.max(1, props.labels.length);
563
+ const len = props.horizontal ? props.height - pad.value.t - pad.value.b : w.value - pad.value.l - pad.value.r;
564
+ const step = len / n;
565
+ return { step, inner: step * 0.72, start: props.horizontal ? pad.value.t : pad.value.l };
566
+ });
567
+ const bars = computed(() => {
568
+ const out = [];
569
+ const gap = 2;
570
+ const zero = sv.value(0);
571
+ props.labels.forEach((_, k) => {
572
+ const c0 = band.value.start + band.value.step * k + (band.value.step - band.value.inner) / 2;
573
+ let pos = 0;
574
+ let neg = 0;
575
+ const n = props.series.length;
576
+ props.series.forEach((s, i) => {
577
+ const v = s.data[k];
578
+ if (v == null) return;
579
+ let a;
580
+ let b;
581
+ let c;
582
+ let thick;
583
+ if (props.stacked) {
584
+ const from = v >= 0 ? pos : neg;
585
+ const to = from + v;
586
+ v >= 0 ? pos = to : neg = to;
587
+ a = sv.value(from);
588
+ b = sv.value(to);
589
+ c = c0;
590
+ thick = band.value.inner;
591
+ } else {
592
+ a = zero;
593
+ b = sv.value(v);
594
+ thick = Math.max(1, (band.value.inner - gap * (n - 1)) / n);
595
+ c = c0 + i * (thick + gap);
596
+ }
597
+ const lo = Math.min(a, b);
598
+ const len = Math.abs(b - a) - (props.stacked ? gap : 0);
599
+ const top = props.stacked ? props.series.slice(i + 1).every((x) => (x.data[k] ?? 0) === 0 || Math.sign(x.data[k] ?? 0) !== Math.sign(v)) : true;
600
+ const r = top ? props.radius : 0;
601
+ const d = props.horizontal ? barPath(v >= 0 ? lo : lo + (props.stacked ? gap : 0), c, Math.max(0, len), thick, r, true, v < 0) : barPath(c, v >= 0 ? lo + (props.stacked ? gap : 0) : lo, thick, Math.max(0, len), r, false, v < 0);
602
+ out.push({ key: `${k}-${i}`, d, color: colorOf(i), cat: k, cx: props.horizontal ? Math.max(a, b) : c + thick / 2, cy: props.horizontal ? c : Math.min(a, b) });
603
+ });
604
+ });
605
+ return out;
606
+ });
607
+ const gridLine = (t) => props.horizontal ? { x1: sv.value(t), x2: sv.value(t), y1: pad.value.t, y2: props.height - pad.value.b } : { x1: pad.value.l, x2: w.value - pad.value.r, y1: sv.value(t), y2: sv.value(t) };
608
+ const valueTick = (t) => props.horizontal ? { x: sv.value(t), y: props.height - 6, "text-anchor": "middle" } : { x: pad.value.l - 8, y: sv.value(t), "text-anchor": "end", "dominant-baseline": "middle" };
609
+ const catTick = (i) => {
610
+ const mid = band.value.start + band.value.step * (i + 0.5);
611
+ return props.horizontal ? { x: pad.value.l - 8, y: mid, "text-anchor": "end", "dominant-baseline": "middle" } : { x: mid, y: props.height - 6, "text-anchor": "middle" };
612
+ };
613
+ const tipStyle = computed(() => {
614
+ const h = hover.value;
615
+ if (!h) return {};
616
+ return h.x > w.value / 2 ? { right: `${w.value - h.x + 10}px`, top: `${Math.max(4, h.y - 10)}px` } : { left: `${h.x + 10}px`, top: `${Math.max(4, h.y - 10)}px` };
617
+ });
618
+ const legend = computed(() => props.series.map((s, i) => ({ name: s.name, color: colorOf(i) })));
619
+ const table = computed(() => ({ head: ["", ...props.series.map((s) => s.name)], rows: props.labels.map((l, k) => [l, ...props.series.map((s) => s.data[k] ?? "")]) }));
620
+ return { fmtCell, box, w, hover, y, bars, gridLine, valueTick, catTick, colorOf, fmt, tipStyle, legend, table };
621
+ }
622
+ });
623
+ const _hoisted_1$2 = ["width", "height", "aria-label"];
624
+ const _hoisted_2$2 = { class: "sq-chart__grid" };
625
+ const _hoisted_3$2 = { class: "sq-chart__axis" };
626
+ const _hoisted_4$2 = ["d", "fill", "onPointerenter"];
627
+ const _hoisted_5$1 = { class: "sq-chart__tooltip-title" };
628
+ function _sfc_render$2(_ctx, _cache, $props, $setup, $data, $options) {
629
+ const _component_ChartFrame = resolveComponent("ChartFrame");
630
+ return openBlock(), createBlock(_component_ChartFrame, {
631
+ title: _ctx.title,
632
+ subtitle: _ctx.subtitle,
633
+ flat: _ctx.flat,
634
+ legend: _ctx.legend,
635
+ table: _ctx.table
636
+ }, {
637
+ default: withCtx(() => [
638
+ createElementVNode(
639
+ "div",
640
+ {
641
+ ref: "box",
642
+ class: "sq-chart__plot",
643
+ style: normalizeStyle({ height: _ctx.height + "px" }),
644
+ onPointerleave: _cache[0] || (_cache[0] = ($event) => _ctx.hover = null)
645
+ },
646
+ [
647
+ (openBlock(), createElementBlock("svg", {
648
+ width: _ctx.w,
649
+ height: _ctx.height,
650
+ role: "img",
651
+ "aria-label": _ctx.title || "Bar chart"
652
+ }, [
653
+ createElementVNode("g", _hoisted_2$2, [
654
+ (openBlock(true), createElementBlock(
655
+ Fragment,
656
+ null,
657
+ renderList(_ctx.y.ticks, (t) => {
658
+ return openBlock(), createElementBlock(
659
+ "line",
660
+ mergeProps({
661
+ key: "g" + t
662
+ }, { ref_for: true }, _ctx.gridLine(t)),
663
+ null,
664
+ 16
665
+ /* FULL_PROPS */
666
+ );
667
+ }),
668
+ 128
669
+ /* KEYED_FRAGMENT */
670
+ ))
671
+ ]),
672
+ createElementVNode("g", _hoisted_3$2, [
673
+ (openBlock(true), createElementBlock(
674
+ Fragment,
675
+ null,
676
+ renderList(_ctx.y.ticks, (t) => {
677
+ return openBlock(), createElementBlock(
678
+ "text",
679
+ mergeProps({
680
+ key: "y" + t
681
+ }, { ref_for: true }, _ctx.valueTick(t)),
682
+ toDisplayString(_ctx.fmt(t)),
683
+ 17
684
+ /* TEXT, FULL_PROPS */
685
+ );
686
+ }),
687
+ 128
688
+ /* KEYED_FRAGMENT */
689
+ )),
690
+ (openBlock(true), createElementBlock(
691
+ Fragment,
692
+ null,
693
+ renderList(_ctx.labels, (l, i) => {
694
+ return openBlock(), createElementBlock(
695
+ "text",
696
+ mergeProps({
697
+ key: "x" + i
698
+ }, { ref_for: true }, _ctx.catTick(i)),
699
+ toDisplayString(l),
700
+ 17
701
+ /* TEXT, FULL_PROPS */
702
+ );
703
+ }),
704
+ 128
705
+ /* KEYED_FRAGMENT */
706
+ ))
707
+ ]),
708
+ (openBlock(true), createElementBlock(
709
+ Fragment,
710
+ null,
711
+ renderList(_ctx.bars, (b) => {
712
+ return openBlock(), createElementBlock("path", {
713
+ key: b.key,
714
+ d: b.d,
715
+ fill: b.color,
716
+ class: normalizeClass(["sq-chart__bar", { "sq-chart__bar--dim": _ctx.hover && _ctx.hover.cat !== b.cat }]),
717
+ onPointerenter: ($event) => _ctx.hover = { cat: b.cat, x: b.cx, y: b.cy }
718
+ }, null, 42, _hoisted_4$2);
719
+ }),
720
+ 128
721
+ /* KEYED_FRAGMENT */
722
+ ))
723
+ ], 8, _hoisted_1$2)),
724
+ _ctx.hover ? (openBlock(), createElementBlock(
725
+ "div",
726
+ {
727
+ key: 0,
728
+ class: "sq-chart__tooltip sq-squircle",
729
+ style: normalizeStyle(_ctx.tipStyle)
730
+ },
731
+ [
732
+ createElementVNode(
733
+ "div",
734
+ _hoisted_5$1,
735
+ toDisplayString(_ctx.labels[_ctx.hover.cat]),
736
+ 1
737
+ /* TEXT */
738
+ ),
739
+ (openBlock(true), createElementBlock(
740
+ Fragment,
741
+ null,
742
+ renderList(_ctx.series, (s, i) => {
743
+ return openBlock(), createElementBlock("div", {
744
+ key: s.name,
745
+ class: "sq-chart__tooltip-row"
746
+ }, [
747
+ createElementVNode(
748
+ "span",
749
+ {
750
+ class: "sq-chart__swatch",
751
+ style: normalizeStyle({ background: _ctx.colorOf(i) })
752
+ },
753
+ null,
754
+ 4
755
+ /* STYLE */
756
+ ),
757
+ createTextVNode(
758
+ toDisplayString(s.name),
759
+ 1
760
+ /* TEXT */
761
+ ),
762
+ createElementVNode(
763
+ "b",
764
+ null,
765
+ toDisplayString(_ctx.fmtCell(s.data[_ctx.hover.cat])),
766
+ 1
767
+ /* TEXT */
768
+ )
769
+ ]);
770
+ }),
771
+ 128
772
+ /* KEYED_FRAGMENT */
773
+ ))
774
+ ],
775
+ 4
776
+ /* STYLE */
777
+ )) : createCommentVNode("v-if", true)
778
+ ],
779
+ 36
780
+ /* STYLE, NEED_HYDRATION */
781
+ )
782
+ ]),
783
+ _: 1
784
+ /* STABLE */
785
+ }, 8, ["title", "subtitle", "flat", "legend", "table"]);
786
+ }
787
+ const SqBarChart = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["render", _sfc_render$2]]);
788
+ const _sfc_main$1 = defineComponent({
789
+ name: "SqDonutChart",
790
+ components: { ChartFrame },
791
+ props: {
792
+ data: { type: Array, default: () => [] },
793
+ title: { type: String, default: void 0 },
794
+ subtitle: { type: String, default: void 0 },
795
+ size: { type: Number, default: 220 },
796
+ /** Ring thickness as a fraction of the radius; 1 = pie. */
797
+ thickness: { type: Number, default: 0.34 },
798
+ totalLabel: { type: String, default: "Total" },
799
+ flat: Boolean,
800
+ format: { type: Function, default: void 0 }
801
+ },
802
+ setup(props) {
803
+ const hover = ref(-1);
804
+ const total = computed(() => props.data.reduce((s, d) => s + Math.max(0, d.value), 0));
805
+ const fmt = (v) => props.format ? props.format(v) : compact(v);
806
+ const arcs = computed(() => {
807
+ const c = props.size / 2;
808
+ const R = c - 4;
809
+ const r = R * (1 - props.thickness);
810
+ const gap = props.data.length > 1 ? 2 / R : 0;
811
+ let a0 = -Math.PI / 2;
812
+ return props.data.map((d, i) => {
813
+ const sweep = total.value ? Math.max(0, d.value) / total.value * Math.PI * 2 : 0;
814
+ const s = a0 + gap / 2;
815
+ const e = a0 + sweep - gap / 2;
816
+ a0 += sweep;
817
+ const large = e - s > Math.PI ? 1 : 0;
818
+ const p = (rad, ang) => `${c + rad * Math.cos(ang)},${c + rad * Math.sin(ang)}`;
819
+ const dPath = e <= s ? "" : r > 0 ? `M${p(R, s)} A${R},${R} 0 ${large} 1 ${p(R, e)} L${p(r, e)} A${r},${r} 0 ${large} 0 ${p(r, s)} Z` : `M${c},${c} L${p(R, s)} A${R},${R} 0 ${large} 1 ${p(R, e)} Z`;
820
+ return { label: d.label, color: seriesColor(i, d.color), d: dPath };
821
+ });
822
+ });
823
+ const legend = computed(() => props.data.map((d, i) => ({ name: `${d.label} · ${total.value ? Math.round(d.value / total.value * 100) : 0}%`, color: seriesColor(i, d.color) })));
824
+ const table = computed(() => ({ head: ["Label", "Value"], rows: props.data.map((d) => [d.label, d.value]) }));
825
+ return { hover, total, fmt, arcs, legend, table };
826
+ }
827
+ });
828
+ const _hoisted_1$1 = ["width", "height", "viewBox", "aria-label"];
829
+ const _hoisted_2$1 = ["d", "fill", "onPointerenter"];
830
+ const _hoisted_3$1 = { class: "sq-chart__donut-center" };
831
+ const _hoisted_4$1 = { class: "sq-chart__donut-value" };
832
+ const _hoisted_5 = { class: "sq-chart__donut-label" };
833
+ function _sfc_render$1(_ctx, _cache, $props, $setup, $data, $options) {
834
+ const _component_ChartFrame = resolveComponent("ChartFrame");
835
+ return openBlock(), createBlock(_component_ChartFrame, {
836
+ title: _ctx.title,
837
+ subtitle: _ctx.subtitle,
838
+ flat: _ctx.flat,
839
+ legend: _ctx.legend,
840
+ table: _ctx.table
841
+ }, {
842
+ default: withCtx(() => [
843
+ createElementVNode(
844
+ "div",
845
+ {
846
+ class: "sq-chart__donut",
847
+ style: normalizeStyle({ height: _ctx.size + "px" })
848
+ },
849
+ [
850
+ (openBlock(), createElementBlock("svg", {
851
+ width: _ctx.size,
852
+ height: _ctx.size,
853
+ viewBox: `0 0 ${_ctx.size} ${_ctx.size}`,
854
+ role: "img",
855
+ "aria-label": _ctx.title || "Donut chart",
856
+ onPointerleave: _cache[0] || (_cache[0] = ($event) => _ctx.hover = -1)
857
+ }, [
858
+ (openBlock(true), createElementBlock(
859
+ Fragment,
860
+ null,
861
+ renderList(_ctx.arcs, (a, i) => {
862
+ return openBlock(), createElementBlock("path", {
863
+ key: a.label,
864
+ d: a.d,
865
+ fill: a.color,
866
+ class: normalizeClass(["sq-chart__arc", { "sq-chart__arc--dim": _ctx.hover >= 0 && _ctx.hover !== i }]),
867
+ onPointerenter: ($event) => _ctx.hover = i
868
+ }, null, 42, _hoisted_2$1);
869
+ }),
870
+ 128
871
+ /* KEYED_FRAGMENT */
872
+ ))
873
+ ], 40, _hoisted_1$1)),
874
+ createElementVNode("div", _hoisted_3$1, [
875
+ renderSlot(_ctx.$slots, "center", {
876
+ hover: _ctx.hover >= 0 ? _ctx.data[_ctx.hover] : null,
877
+ total: _ctx.total
878
+ }, () => [
879
+ createElementVNode(
880
+ "div",
881
+ _hoisted_4$1,
882
+ toDisplayString(_ctx.hover >= 0 ? _ctx.fmt(_ctx.data[_ctx.hover].value) : _ctx.fmt(_ctx.total)),
883
+ 1
884
+ /* TEXT */
885
+ ),
886
+ createElementVNode(
887
+ "div",
888
+ _hoisted_5,
889
+ toDisplayString(_ctx.hover >= 0 ? _ctx.data[_ctx.hover].label : _ctx.totalLabel),
890
+ 1
891
+ /* TEXT */
892
+ )
893
+ ])
894
+ ])
895
+ ],
896
+ 4
897
+ /* STYLE */
898
+ )
899
+ ]),
900
+ _: 3
901
+ /* FORWARDED */
902
+ }, 8, ["title", "subtitle", "flat", "legend", "table"]);
903
+ }
904
+ const SqDonutChart = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["render", _sfc_render$1]]);
905
+ const _sfc_main = defineComponent({
906
+ name: "SqSparkline",
907
+ props: {
908
+ data: { type: Array, default: () => [] },
909
+ width: { type: Number, default: 120 },
910
+ height: { type: Number, default: 32 },
911
+ color: { type: String, default: "primary" },
912
+ fill: Boolean,
913
+ label: { type: String, default: "Trend" }
914
+ },
915
+ setup(props) {
916
+ const pts = computed(() => {
917
+ const d = props.data;
918
+ if (!d.length) return [];
919
+ const min = Math.min(...d);
920
+ const max = Math.max(...d);
921
+ const span = max - min || 1;
922
+ return d.map((v, i) => [3 + (props.width - 6) * i / Math.max(1, d.length - 1), 3 + (props.height - 6) * (1 - (v - min) / span)]);
923
+ });
924
+ const lineD = computed(() => smoothPath(pts.value));
925
+ const areaD = computed(() => pts.value.length ? `${lineD.value} L${pts.value[pts.value.length - 1][0]},${props.height} L${pts.value[0][0]},${props.height} Z` : "");
926
+ return { pts, lineD, areaD, stroke: computed(() => colorValue(props.color)) };
927
+ }
928
+ });
929
+ const _hoisted_1 = ["width", "height", "viewBox", "aria-label"];
930
+ const _hoisted_2 = ["d", "fill"];
931
+ const _hoisted_3 = ["d", "stroke"];
932
+ const _hoisted_4 = ["cx", "cy", "fill"];
933
+ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
934
+ return openBlock(), createElementBlock("svg", {
935
+ ref: "el",
936
+ class: "sq-sparkline",
937
+ width: _ctx.width,
938
+ height: _ctx.height,
939
+ viewBox: `0 0 ${_ctx.width} ${_ctx.height}`,
940
+ role: "img",
941
+ "aria-label": _ctx.label
942
+ }, [
943
+ _ctx.fill ? (openBlock(), createElementBlock("path", {
944
+ key: 0,
945
+ d: _ctx.areaD,
946
+ fill: _ctx.stroke,
947
+ class: "sq-sparkline__area"
948
+ }, null, 8, _hoisted_2)) : createCommentVNode("v-if", true),
949
+ createElementVNode("path", {
950
+ d: _ctx.lineD,
951
+ stroke: _ctx.stroke,
952
+ class: "sq-sparkline__line"
953
+ }, null, 8, _hoisted_3),
954
+ _ctx.pts.length ? (openBlock(), createElementBlock("circle", {
955
+ key: 1,
956
+ cx: _ctx.pts[_ctx.pts.length - 1][0],
957
+ cy: _ctx.pts[_ctx.pts.length - 1][1],
958
+ r: "2.5",
959
+ fill: _ctx.stroke
960
+ }, null, 8, _hoisted_4)) : createCommentVNode("v-if", true)
961
+ ], 8, _hoisted_1);
962
+ }
963
+ const SqSparkline = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
964
+ const componentNames = ["SqLineChart", "SqBarChart", "SqDonutChart", "SqSparkline"];
965
+ export {
966
+ SqBarChart,
967
+ SqDonutChart,
968
+ SqLineChart,
969
+ SqSparkline,
970
+ barPath,
971
+ componentNames,
972
+ seriesColor,
973
+ smoothPath
974
+ };
975
+ //# sourceMappingURL=index.mjs.map