@squirkle-ui/charts 0.0.0-stage → 0.1.1

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