@classic-homes/charts-core 0.2.0 → 0.3.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.
package/dist/index.js CHANGED
@@ -1,17 +1,58 @@
1
- export { THEME_DARK, THEME_LIGHT, colorWithAlpha, darkChartColors, darkTheme, generateDarkTheme, generateLightTheme, getChartColors, lightChartColors, lightTheme } from './chunk-JM72YHZD.js';
1
+ export { THEME_DARK, THEME_LIGHT, colorWithAlpha, darkChartColors, darkTheme, generateDarkTheme, generateLightTheme, getChartColors, lightChartColors, lightTheme } from './chunk-WAHMKXRF.js';
2
2
 
3
3
  // src/utils/format.ts
4
- function formatNumber(value, options) {
5
- return new Intl.NumberFormat("en-US", options).format(value);
4
+ var numberFormats = /* @__PURE__ */ new Map();
5
+ function numberFormat(locale, options) {
6
+ const key = `${locale}|${options ? JSON.stringify(options) : ""}`;
7
+ let format = numberFormats.get(key);
8
+ if (!format) {
9
+ format = new Intl.NumberFormat(locale, options);
10
+ numberFormats.set(key, format);
11
+ }
12
+ return format;
13
+ }
14
+ function formatNumber(value, options, locale = "en-US") {
15
+ return numberFormat(locale, options).format(value);
16
+ }
17
+ var PRESETS = {
18
+ number: { maximumFractionDigits: 2 },
19
+ integer: { maximumFractionDigits: 0 },
20
+ compact: { notation: "compact", maximumFractionDigits: 1 },
21
+ currency: { style: "currency", currency: "USD", maximumFractionDigits: 0 },
22
+ currencyCents: { style: "currency", currency: "USD" },
23
+ percent: { style: "percent", maximumFractionDigits: 1 },
24
+ percentValue: { style: "percent", maximumFractionDigits: 1 }
25
+ };
26
+ function resolveValueFormatter(format, locale = "en-US") {
27
+ if (format === void 0) return void 0;
28
+ if (typeof format === "function") return format;
29
+ if (format === "days") {
30
+ return (value) => {
31
+ const days = Math.round(value);
32
+ return `${numberFormat(locale).format(days)} ${Math.abs(days) === 1 ? "day" : "days"}`;
33
+ };
34
+ }
35
+ if (format === "percentValue") {
36
+ const intl2 = numberFormat(locale, PRESETS.percentValue);
37
+ return (value) => intl2.format(value / 100);
38
+ }
39
+ const intl = numberFormat(locale, typeof format === "string" ? PRESETS[format] : format);
40
+ return (value) => intl.format(value);
41
+ }
42
+ function formatValue(value, formatter, empty = "") {
43
+ if (value == null) return empty;
44
+ if (typeof value === "string") return value;
45
+ if (!Number.isFinite(value)) return empty;
46
+ return formatter ? formatter(value) : formatNumber(value, { maximumFractionDigits: 2 });
6
47
  }
7
48
  function formatCurrency(value, currency = "USD", locale = "en-US") {
8
- return new Intl.NumberFormat(locale, {
49
+ return numberFormat(locale, {
9
50
  style: "currency",
10
51
  currency
11
52
  }).format(value);
12
53
  }
13
54
  function formatPercent(value, decimals = 1, locale = "en-US") {
14
- return new Intl.NumberFormat(locale, {
55
+ return numberFormat(locale, {
15
56
  style: "percent",
16
57
  minimumFractionDigits: decimals,
17
58
  maximumFractionDigits: decimals
@@ -63,52 +104,80 @@ function createTooltipFormatter(options = {}) {
63
104
  }
64
105
 
65
106
  // src/utils/accessibility.ts
66
- function generateLineChartDescription(title, data) {
107
+ var isNumber = (value) => typeof value === "number" && Number.isFinite(value);
108
+ function seriesList(names) {
109
+ return names.join(", ");
110
+ }
111
+ function describeTrends(title, kind, data, format) {
67
112
  const seriesCount = data.series.length;
68
- const categoryCount = data.categories.length;
69
- const seriesNames = data.series.map((s) => s.name).join(", ");
70
- let description = `${title}. Line chart with ${categoryCount} data points`;
113
+ let description = `${title}. ${kind} chart with ${data.categories.length} data points`;
71
114
  if (seriesCount > 1) {
72
- description += ` across ${seriesCount} series: ${seriesNames}`;
115
+ description += ` across ${seriesCount} series: ${seriesList(data.series.map((s) => s.name))}`;
73
116
  }
74
117
  data.series.forEach((series) => {
75
- if (series.data.length >= 2) {
76
- const first = series.data[0];
77
- const last = series.data[series.data.length - 1];
118
+ const present = series.data.filter(isNumber);
119
+ if (present.length >= 2) {
120
+ const first = present[0];
121
+ const last = present[present.length - 1];
78
122
  const trend = last > first ? "increasing" : last < first ? "decreasing" : "stable";
79
- description += `. ${series.name} shows ${trend} trend from ${first} to ${last}`;
123
+ description += `. ${series.name} shows ${trend} trend from ${formatValue(first, format)} to ${formatValue(last, format)}`;
80
124
  }
81
125
  });
126
+ const missing = data.series.reduce(
127
+ (sum, series) => sum + series.data.filter((value) => !isNumber(value)).length,
128
+ 0
129
+ );
130
+ if (missing > 0) description += `. ${missing} values are missing`;
82
131
  return description;
83
132
  }
84
- function generateBarChartDescription(title, data) {
133
+ function generateLineChartDescription(title, data, format) {
134
+ return describeTrends(title, "Line", data, format);
135
+ }
136
+ function generateAreaChartDescription(title, data, format) {
137
+ return describeTrends(title, "Area", data, format);
138
+ }
139
+ function generateComboChartDescription(title, data, format) {
140
+ return describeTrends(title, "Combined bar and line", data, format);
141
+ }
142
+ function extremes(data) {
143
+ let max;
144
+ let min;
145
+ data.series.forEach((series) => {
146
+ series.data.forEach((value, index) => {
147
+ if (!isNumber(value)) return;
148
+ const category = data.categories[index] ?? "";
149
+ if (!max || value > max.value) max = { value, category };
150
+ if (!min || value < min.value) min = { value, category };
151
+ });
152
+ });
153
+ return { max, min };
154
+ }
155
+ function generateBarChartDescription(title, data, format) {
85
156
  const categoryCount = data.categories.length;
86
157
  const seriesCount = data.series.length;
87
158
  let description = `${title}. Bar chart with ${categoryCount} categories`;
88
159
  if (seriesCount > 1) {
89
160
  description += ` and ${seriesCount} series`;
90
161
  }
91
- let maxValue = -Infinity;
92
- let maxCategory = "";
93
- let minValue = Infinity;
94
- let minCategory = "";
95
- data.series.forEach((series) => {
96
- series.data.forEach((value, index) => {
97
- if (value > maxValue) {
98
- maxValue = value;
99
- maxCategory = data.categories[index];
100
- }
101
- if (value < minValue) {
102
- minValue = value;
103
- minCategory = data.categories[index];
104
- }
105
- });
106
- });
107
- description += `. Highest value is ${maxValue} for ${maxCategory}. Lowest value is ${minValue} for ${minCategory}`;
162
+ const { max, min } = extremes(data);
163
+ if (max && min) {
164
+ description += `. Highest value is ${formatValue(max.value, format)} for ${max.category}. Lowest value is ${formatValue(min.value, format)} for ${min.category}`;
165
+ }
166
+ return description;
167
+ }
168
+ function generateDotPlotDescription(title, data, format) {
169
+ let description = `${title}. Dot plot with ${data.categories.length} rows`;
170
+ if (data.series.length > 1) {
171
+ description += ` comparing ${seriesList(data.series.map((s) => s.name))}`;
172
+ }
173
+ const { max, min } = extremes(data);
174
+ if (max && min) {
175
+ description += `. Highest value is ${formatValue(max.value, format)} for ${max.category}. Lowest value is ${formatValue(min.value, format)} for ${min.category}`;
176
+ }
108
177
  return description;
109
178
  }
110
179
  function generatePieChartDescription(title, data, isDonut = false) {
111
- const total = data.reduce((sum, slice) => sum + slice.value, 0);
180
+ const total = data.reduce((sum, slice) => sum + (isNumber(slice.value) ? slice.value : 0), 0);
112
181
  const chartType = isDonut ? "Donut" : "Pie";
113
182
  let description = `${title}. ${chartType} chart with ${data.length} segments`;
114
183
  if (total === 0) {
@@ -133,6 +202,44 @@ function generateScatterChartDescription(title, data) {
133
202
  }
134
203
  return description;
135
204
  }
205
+ function generateHistogramDescription(title, buckets, total) {
206
+ let description = `${title}. Histogram of ${total} values in ${buckets.length} buckets`;
207
+ const peak = buckets.reduce(
208
+ (best, bucket) => !best || bucket.count > best.count ? bucket : best,
209
+ void 0
210
+ );
211
+ if (peak && peak.count > 0) {
212
+ description += `. The most common range is ${peak.label} with ${peak.count} values`;
213
+ }
214
+ return description;
215
+ }
216
+ function generateBoxPlotDescription(title, groups, format) {
217
+ let description = `${title}. Box plot of ${groups.length} groups`;
218
+ const parts = groups.filter((group) => group.stats).slice(0, 5).map(
219
+ ({ name, stats }) => `${name}: median ${formatValue(stats.median, format)}, middle half ${formatValue(stats.q1, format)} to ${formatValue(stats.q3, format)}`
220
+ );
221
+ if (parts.length) description += `. ${parts.join("; ")}`;
222
+ return description;
223
+ }
224
+ function generateWaterfallDescription(title, steps, final, format) {
225
+ const changes = steps.filter((step) => !step.isTotal);
226
+ const increases = changes.filter((step) => step.value > 0).length;
227
+ const decreases = changes.filter((step) => step.value < 0).length;
228
+ return `${title}. Waterfall chart with ${steps.length} steps: ${increases} increases and ${decreases} decreases, ending at ${formatValue(final, format)}`;
229
+ }
230
+ function generateBulletDescription(title, items, format) {
231
+ const withTarget = items.filter((item) => isNumber(item.target));
232
+ let description = `${title}. Bullet chart with ${items.length} rows`;
233
+ if (withTarget.length) {
234
+ const met = withTarget.filter((item) => item.value >= item.target).length;
235
+ description += `. ${met} of ${withTarget.length} meet their target`;
236
+ }
237
+ const parts = items.slice(0, 5).map(
238
+ (item) => `${item.name} ${formatValue(item.value, format)}${isNumber(item.target) ? ` of ${formatValue(item.target, format)}` : ""}`
239
+ );
240
+ if (parts.length) description += `. ${parts.join(", ")}`;
241
+ return description;
242
+ }
136
243
  function generateDataTable(headers, rows) {
137
244
  const headerRow = headers.map((h) => `<th scope="col">${escapeHtml(h)}</th>`).join("");
138
245
  const bodyRows = rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`).join("");
@@ -292,7 +399,14 @@ function getLegendItems(names, palette, overrides, visible) {
292
399
  function buildAxis(base, config) {
293
400
  if (!config) return base;
294
401
  const axis = { ...base };
295
- const { show, name, nameLocation, nameGap, formatter, rotate, min, max, interval } = config;
402
+ const { type, show, name, nameLocation, nameGap, formatter, rotate, min, max, interval } = config;
403
+ if (type !== void 0) {
404
+ axis.type = type;
405
+ if (type !== "category") {
406
+ delete axis.data;
407
+ delete axis.boundaryGap;
408
+ }
409
+ }
296
410
  if (show !== void 0) axis.show = show;
297
411
  if (name !== void 0) {
298
412
  axis.name = name;
@@ -365,5 +479,709 @@ function formatTooltipValue(value, valueFormatter) {
365
479
  if (valueFormatter && typeof value === "number") return valueFormatter(value);
366
480
  return String(value ?? "");
367
481
  }
482
+ function valueLabelFormatter(format) {
483
+ return (params) => {
484
+ if (params.label != null && params.label !== "") return params.label;
485
+ const value = Array.isArray(params.value) ? params.value[params.value.length - 1] : params.value;
486
+ return formatValue(value, format);
487
+ };
488
+ }
489
+ function valueAxisConfig(config, format) {
490
+ if (!format || config?.formatter !== void 0) return config;
491
+ return { ...config, formatter: (value) => format(Number(value)) };
492
+ }
493
+ function chartFormatters({
494
+ valueFormat,
495
+ locale,
496
+ tooltipValueFormatter,
497
+ labelFormatter
498
+ }) {
499
+ const format = resolveValueFormatter(valueFormat, locale);
500
+ return {
501
+ format,
502
+ tooltip: tooltipValueFormatter ?? format,
503
+ label: labelFormatter ?? (format ? valueLabelFormatter(format) : void 0),
504
+ axis: (config) => valueAxisConfig(config, format)
505
+ };
506
+ }
507
+ var STATISTICS = ["average", "median", "min", "max"];
508
+ function buildMarkLine(lines, { valueAxis, format, color, inverse = false }) {
509
+ if (!lines?.length) return void 0;
510
+ return {
511
+ silent: true,
512
+ symbol: "none",
513
+ animation: false,
514
+ data: lines.map((line) => {
515
+ const statistic = STATISTICS.includes(line.value);
516
+ const lineColor = line.color ?? color;
517
+ const position = valueAxis === "y" ? "insideEndTop" : inverse ? "start" : "end";
518
+ return {
519
+ ...statistic ? { type: line.value, valueDim: valueAxis } : { [valueAxis === "y" ? "yAxis" : "xAxis"]: line.value },
520
+ name: line.label,
521
+ lineStyle: { type: line.style ?? "dashed", color: lineColor, width: 1.5 },
522
+ label: {
523
+ show: true,
524
+ position,
525
+ color: lineColor,
526
+ formatter: (params) => line.label ?? formatValue(Number(params.value), format)
527
+ }
528
+ };
529
+ })
530
+ };
531
+ }
532
+ function buildMarkArea(bands, { valueAxis, bandColor }) {
533
+ if (!bands?.length) return void 0;
534
+ const key = valueAxis === "y" ? "yAxis" : "xAxis";
535
+ return {
536
+ silent: true,
537
+ animation: false,
538
+ data: bands.map((band) => [
539
+ {
540
+ [key]: band.from,
541
+ name: band.label,
542
+ itemStyle: { color: band.color ?? bandColor },
543
+ label: {
544
+ show: !!band.label,
545
+ position: "insideTopLeft"
546
+ }
547
+ },
548
+ { [key]: band.to }
549
+ ])
550
+ };
551
+ }
552
+ function withReferenceMarks(series, lines, bands, options) {
553
+ if (!lines?.length && !bands?.length) return series;
554
+ const indexOf = (name) => {
555
+ if (name === void 0) return 0;
556
+ const index = series.findIndex((s) => s.name === name);
557
+ return index === -1 ? 0 : index;
558
+ };
559
+ return series.map((s, index) => {
560
+ const markLine = buildMarkLine(
561
+ lines?.filter((line) => indexOf(line.series) === index),
562
+ options
563
+ );
564
+ const markArea = buildMarkArea(
565
+ bands?.filter((band) => indexOf(band.series) === index),
566
+ options
567
+ );
568
+ if (!markLine && !markArea) return s;
569
+ return { ...s, ...markLine && { markLine }, ...markArea && { markArea } };
570
+ });
571
+ }
572
+ function buildDataZoom(mode, axis, bottomOffset = 0) {
573
+ if (!mode) return void 0;
574
+ const target = axis === "x" ? { xAxisIndex: 0 } : { yAxisIndex: 0 };
575
+ const zooms = [];
576
+ if (mode === true || mode === "inside") zooms.push({ type: "inside", ...target });
577
+ if (mode === true || mode === "slider") {
578
+ zooms.push(
579
+ axis === "x" ? { type: "slider", ...target, bottom: 8 + bottomOffset, height: 20 } : { type: "slider", ...target, right: 8, width: 20 }
580
+ );
581
+ }
582
+ return zooms;
583
+ }
584
+ function withZoomInset(inset, mode, axis) {
585
+ if (mode !== true && mode !== "slider") return inset;
586
+ const edge = axis === "x" ? "bottom" : "right";
587
+ const current = typeof inset?.[edge] === "number" ? inset[edge] : 12;
588
+ return { ...inset, [edge]: current + 36 };
589
+ }
590
+
591
+ // src/utils/stats.ts
592
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
593
+ var clean = (value) => Number(value.toPrecision(12));
594
+ function niceStep(rough) {
595
+ if (!(rough > 0) || !Number.isFinite(rough)) return 1;
596
+ const power = 10 ** Math.floor(Math.log10(rough));
597
+ const fraction = rough / power;
598
+ const nice = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10;
599
+ return clean(nice * power);
600
+ }
601
+ function bucketLabel(start, end, format) {
602
+ const text = (value) => formatValue(value, format);
603
+ if (start === -Infinity && end === Infinity) return "All";
604
+ if (start === -Infinity) return `< ${text(end)}`;
605
+ if (end === Infinity) return `${text(start)}+`;
606
+ return `${text(start)}\u2013${text(end)}`;
607
+ }
608
+ function binValues(values, options = {}) {
609
+ const { bins, binWidth, format } = options;
610
+ const finite = values.filter(isFiniteNumber);
611
+ let edges;
612
+ if (Array.isArray(bins)) {
613
+ edges = bins.map((bin) => ({
614
+ start: bin.start ?? -Infinity,
615
+ end: bin.end ?? Infinity,
616
+ label: bin.label
617
+ }));
618
+ } else {
619
+ if (!finite.length) return [];
620
+ let min = Infinity;
621
+ let max = -Infinity;
622
+ for (const value of finite) {
623
+ if (value < min) min = value;
624
+ if (value > max) max = value;
625
+ }
626
+ if (min === max) {
627
+ edges = [{ start: min, end: max }];
628
+ } else {
629
+ let step;
630
+ let start;
631
+ if (binWidth && binWidth > 0) {
632
+ step = binWidth;
633
+ start = Math.floor(min / step) * step;
634
+ } else if (typeof bins === "number" && bins > 0) {
635
+ step = (max - min) / Math.floor(bins);
636
+ start = min;
637
+ } else {
638
+ const sturges = Math.ceil(Math.log2(finite.length) + 1);
639
+ step = niceStep((max - min) / sturges);
640
+ start = Math.floor(min / step) * step;
641
+ }
642
+ const count = Math.max(1, Math.ceil(clean((max - start) / step)));
643
+ edges = Array.from({ length: count }, (_, index) => ({
644
+ start: clean(start + index * step),
645
+ end: clean(start + (index + 1) * step)
646
+ }));
647
+ }
648
+ }
649
+ const buckets = edges.map(({ start, end, label }) => ({
650
+ label: label ?? bucketLabel(start, end, format),
651
+ start,
652
+ end,
653
+ count: 0
654
+ }));
655
+ const last = buckets.length - 1;
656
+ for (const value of finite) {
657
+ const index = buckets.findIndex(
658
+ (bucket, i) => value >= bucket.start && (value < bucket.end || i === last && value <= bucket.end)
659
+ );
660
+ if (index !== -1) buckets[index].count++;
661
+ }
662
+ return buckets;
663
+ }
664
+ function quantile(sorted, q) {
665
+ if (!sorted.length) return NaN;
666
+ const position = (sorted.length - 1) * q;
667
+ const lower = Math.floor(position);
668
+ const upper = Math.ceil(position);
669
+ if (lower === upper) return sorted[lower];
670
+ return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
671
+ }
672
+ function boxplotStats(values) {
673
+ const sorted = values.filter(isFiniteNumber).sort((a, b) => a - b);
674
+ if (!sorted.length) return null;
675
+ const q1 = quantile(sorted, 0.25);
676
+ const median = quantile(sorted, 0.5);
677
+ const q3 = quantile(sorted, 0.75);
678
+ const iqr = q3 - q1;
679
+ const lowFence = q1 - 1.5 * iqr;
680
+ const highFence = q3 + 1.5 * iqr;
681
+ const inside = sorted.filter((value) => value >= lowFence && value <= highFence);
682
+ const outliers = sorted.filter((value) => value < lowFence || value > highFence);
683
+ return {
684
+ low: inside[0] ?? q1,
685
+ q1,
686
+ median,
687
+ q3,
688
+ high: inside[inside.length - 1] ?? q3,
689
+ outliers,
690
+ count: sorted.length,
691
+ mean: sorted.reduce((sum, value) => sum + value, 0) / sorted.length
692
+ };
693
+ }
694
+ function waterfallLayout(steps) {
695
+ let running = 0;
696
+ return steps.map((step) => {
697
+ let start;
698
+ let end;
699
+ let kind;
700
+ if (step.isTotal === "value") {
701
+ start = 0;
702
+ end = isFiniteNumber(step.value) ? step.value : running;
703
+ kind = "total";
704
+ } else if (step.isTotal) {
705
+ start = 0;
706
+ end = running;
707
+ kind = "total";
708
+ } else {
709
+ const change = isFiniteNumber(step.value) ? step.value : 0;
710
+ start = running;
711
+ end = running + change;
712
+ kind = change < 0 ? "decrease" : "increase";
713
+ }
714
+ running = end;
715
+ const low = Math.min(start, end);
716
+ const high = Math.max(start, end);
717
+ let base = 0;
718
+ let positive = 0;
719
+ let negative = 0;
720
+ if (low >= 0) {
721
+ base = low;
722
+ positive = high - low;
723
+ } else if (high <= 0) {
724
+ base = high;
725
+ negative = low - high;
726
+ } else {
727
+ positive = high;
728
+ negative = low;
729
+ }
730
+ return {
731
+ start,
732
+ end,
733
+ value: kind === "total" ? end : end - start,
734
+ kind,
735
+ base: clean(base),
736
+ positive: clean(positive),
737
+ negative: clean(negative)
738
+ };
739
+ });
740
+ }
741
+ function toPercentStack(series) {
742
+ const length = Math.max(0, ...series.map((s) => s.data.length));
743
+ const totals = Array.from(
744
+ { length },
745
+ (_, index) => series.reduce((sum, s) => {
746
+ const value = s.data[index];
747
+ return sum + (isFiniteNumber(value) ? Math.abs(value) : 0);
748
+ }, 0)
749
+ );
750
+ return series.map((s) => ({
751
+ ...s,
752
+ data: s.data.map((value, index) => {
753
+ if (!isFiniteNumber(value)) return null;
754
+ return totals[index] === 0 ? 0 : clean(value / totals[index] * 100);
755
+ })
756
+ }));
757
+ }
758
+ function pairWithCategories(categories, values, axisType) {
759
+ if (axisType !== "time" && axisType !== "value") return values;
760
+ return values.map((value, index) => [categories[index], value]);
761
+ }
762
+ function resolveChartHeight(height, rows, { rowHeight = 28, chrome = 56, minHeight = 160, maxHeight } = {}) {
763
+ if (height !== "auto") return height;
764
+ let value = Math.max(minHeight, chrome + Math.max(rows, 1) * rowHeight);
765
+ if (maxHeight !== void 0) value = Math.min(value, maxHeight);
766
+ return value;
767
+ }
768
+ function allSeriesEmpty(series) {
769
+ return !series?.length || series.every((s) => !s.data?.some((value) => value != null));
770
+ }
771
+ function trendLine(points) {
772
+ const finite = points.filter(([x, y]) => isFiniteNumber(x) && isFiniteNumber(y));
773
+ const n = finite.length;
774
+ if (n < 2) return null;
775
+ let sumX = 0;
776
+ let sumY = 0;
777
+ let sumXY = 0;
778
+ let sumX2 = 0;
779
+ let minX = Infinity;
780
+ let maxX = -Infinity;
781
+ for (const [x, y] of finite) {
782
+ sumX += x;
783
+ sumY += y;
784
+ sumXY += x * y;
785
+ sumX2 += x * x;
786
+ if (x < minX) minX = x;
787
+ if (x > maxX) maxX = x;
788
+ }
789
+ const denominator = n * sumX2 - sumX * sumX;
790
+ if (minX === maxX || denominator === 0) return null;
791
+ const slope = (n * sumXY - sumX * sumY) / denominator;
792
+ const intercept = (sumY - slope * sumX) / n;
793
+ return [
794
+ [minX, slope * minX + intercept],
795
+ [maxX, slope * maxX + intercept]
796
+ ];
797
+ }
798
+ function computeDelta(current, previous) {
799
+ if (!isFiniteNumber(current) || !isFiniteNumber(previous)) return null;
800
+ const change = clean(current - previous);
801
+ return {
802
+ change,
803
+ ratio: previous === 0 ? null : change / Math.abs(previous),
804
+ direction: change > 0 ? "up" : change < 0 ? "down" : "flat"
805
+ };
806
+ }
807
+
808
+ // src/utils/records.ts
809
+ function read(row, accessor) {
810
+ return typeof accessor === "function" ? accessor(row) : row[accessor];
811
+ }
812
+ function toNumber(value) {
813
+ if (value == null || value === "") return null;
814
+ const number = typeof value === "number" ? value : Number(value);
815
+ return Number.isFinite(number) ? number : null;
816
+ }
817
+ function toLabel(value, nullLabel) {
818
+ return value == null || value === "" ? nullLabel : String(value);
819
+ }
820
+ var Cell = class {
821
+ constructor() {
822
+ this.sum = 0;
823
+ this.count = 0;
824
+ this.min = Infinity;
825
+ this.max = -Infinity;
826
+ this.first = null;
827
+ this.last = null;
828
+ }
829
+ add(value) {
830
+ this.count++;
831
+ if (value === null) return;
832
+ this.sum += value;
833
+ if (value < this.min) this.min = value;
834
+ if (value > this.max) this.max = value;
835
+ if (this.first === null) this.first = value;
836
+ this.last = value;
837
+ }
838
+ result(aggregate, present) {
839
+ switch (aggregate) {
840
+ case "count":
841
+ return this.count;
842
+ case "avg":
843
+ return present ? this.sum / present : null;
844
+ case "min":
845
+ return present ? this.min : null;
846
+ case "max":
847
+ return present ? this.max : null;
848
+ case "first":
849
+ return this.first;
850
+ case "last":
851
+ return this.last;
852
+ default:
853
+ return present ? this.sum : null;
854
+ }
855
+ }
856
+ };
857
+ var Accumulator = class {
858
+ constructor() {
859
+ this.cells = /* @__PURE__ */ new Map();
860
+ }
861
+ add(key, value) {
862
+ let cell = this.cells.get(key);
863
+ if (!cell) {
864
+ cell = Object.assign(new Cell(), { present: 0 });
865
+ this.cells.set(key, cell);
866
+ }
867
+ cell.add(value);
868
+ if (value !== null) cell.present++;
869
+ }
870
+ get(key, aggregate) {
871
+ const cell = this.cells.get(key);
872
+ return cell ? cell.result(aggregate, cell.present) : null;
873
+ }
874
+ };
875
+ function ordered(seen, order) {
876
+ if (!order?.length) return seen;
877
+ const present = new Set(seen);
878
+ const first = order.filter((name) => present.has(name));
879
+ const listed = new Set(first);
880
+ return [...first, ...seen.filter((name) => !listed.has(name))];
881
+ }
882
+ function fromRecords(rows, options) {
883
+ const {
884
+ category,
885
+ value,
886
+ series,
887
+ measures,
888
+ seriesName = "Value",
889
+ aggregate = "sum",
890
+ categoryOrder,
891
+ topN,
892
+ otherLabel,
893
+ seriesOrder,
894
+ nullLabel = "Unknown",
895
+ fill = null,
896
+ colors
897
+ } = options;
898
+ const sort = options.sort ?? (topN !== void 0 ? "total" : "none");
899
+ const categories = [];
900
+ const categorySet = /* @__PURE__ */ new Set();
901
+ const seriesNames = [];
902
+ const seriesSet = /* @__PURE__ */ new Set();
903
+ const cells = new Accumulator();
904
+ const key = (c, s) => `${c}\0${s}`;
905
+ const note = (list, set, name) => {
906
+ if (!set.has(name)) {
907
+ set.add(name);
908
+ list.push(name);
909
+ }
910
+ };
911
+ for (const row of rows ?? []) {
912
+ const c = toLabel(read(row, category), nullLabel);
913
+ note(categories, categorySet, c);
914
+ if (measures) {
915
+ for (const [name, accessor] of Object.entries(measures)) {
916
+ note(seriesNames, seriesSet, name);
917
+ cells.add(key(c, name), toNumber(read(row, accessor)));
918
+ }
919
+ } else {
920
+ const s = series ? toLabel(read(row, series), nullLabel) : seriesName;
921
+ note(seriesNames, seriesSet, s);
922
+ cells.add(key(c, s), value === void 0 ? 1 : toNumber(read(row, value)));
923
+ }
924
+ }
925
+ const names = ordered(seriesNames, seriesOrder);
926
+ const cellValue = (c, s) => cells.get(key(c, s), aggregate);
927
+ const total = (c) => names.reduce((sum, s) => sum + (cellValue(c, s) ?? 0), 0);
928
+ let order = ordered(categories, categoryOrder);
929
+ if (sort === "total" || sort === "total-asc") {
930
+ const totals = new Map(order.map((c) => [c, total(c)]));
931
+ const direction = sort === "total" ? -1 : 1;
932
+ order = [...order].sort((a, b) => direction * (totals.get(a) - totals.get(b)));
933
+ } else if (sort === "category") {
934
+ order = [...order].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
935
+ } else if (typeof sort === "function") {
936
+ order = [...order].sort(sort);
937
+ }
938
+ let rest = [];
939
+ if (topN !== void 0 && order.length > topN) {
940
+ rest = order.slice(topN);
941
+ order = order.slice(0, topN);
942
+ }
943
+ const foldOther = otherLabel !== void 0 && rest.length > 0 && (aggregate === "sum" || aggregate === "count");
944
+ const result = names.map((name) => {
945
+ const data = order.map((c) => cellValue(c, name) ?? fill);
946
+ if (foldOther) {
947
+ const other = rest.reduce((sum, c) => {
948
+ const v = cellValue(c, name);
949
+ return v === null ? sum : (sum ?? 0) + v;
950
+ }, null);
951
+ data.push(other ?? fill);
952
+ }
953
+ return colors?.[name] ? { name, data, color: colors[name] } : { name, data };
954
+ });
955
+ return {
956
+ categories: foldOther ? [...order, otherLabel] : order,
957
+ series: result
958
+ };
959
+ }
960
+ function toSliceData(rows, options) {
961
+ const {
962
+ name,
963
+ value,
964
+ aggregate = "sum",
965
+ dropZero = true,
966
+ sort = "desc",
967
+ topN,
968
+ otherLabel,
969
+ nullLabel = "Unknown",
970
+ colors
971
+ } = options;
972
+ const { categories, series } = fromRecords(rows, {
973
+ category: name,
974
+ value,
975
+ aggregate: value === void 0 ? "count" : aggregate,
976
+ nullLabel
977
+ });
978
+ const values = series[0]?.data ?? [];
979
+ let slices = categories.map((label, index) => ({ name: label, value: values[index] ?? 0 }));
980
+ if (dropZero) slices = slices.filter((slice) => slice.value !== 0);
981
+ if (sort !== "none") {
982
+ const direction = sort === "desc" ? -1 : 1;
983
+ slices = [...slices].sort((a, b) => direction * (a.value - b.value));
984
+ }
985
+ if (topN !== void 0 && slices.length > topN) {
986
+ const rest = slices.slice(topN);
987
+ slices = slices.slice(0, topN);
988
+ if (otherLabel !== void 0) {
989
+ slices.push({ name: otherLabel, value: rest.reduce((sum, slice) => sum + slice.value, 0) });
990
+ }
991
+ }
992
+ return slices.map(
993
+ (slice) => colors?.[slice.name] ? { ...slice, color: colors[slice.name] } : slice
994
+ );
995
+ }
996
+
997
+ // src/utils/tables.ts
998
+ function categoryTable(data, categoryHeader = "Category") {
999
+ return {
1000
+ headers: [categoryHeader, ...data.series.map((s) => s.name)],
1001
+ rows: data.categories.map((category, index) => [
1002
+ category,
1003
+ ...data.series.map((s) => s.data[index] ?? null)
1004
+ ]),
1005
+ columns: ["text", ...data.series.map(() => "value")]
1006
+ };
1007
+ }
1008
+ function sliceTable(slices, nameHeader = "Name") {
1009
+ const total = slices.reduce(
1010
+ (sum, slice) => sum + (Number.isFinite(slice.value) ? slice.value : 0),
1011
+ 0
1012
+ );
1013
+ return {
1014
+ headers: [nameHeader, "Value", "Share"],
1015
+ rows: slices.map((slice) => [
1016
+ slice.name,
1017
+ slice.value,
1018
+ total ? Number((slice.value / total * 100).toFixed(1)) : 0
1019
+ ]),
1020
+ columns: ["text", "value", "percent"]
1021
+ };
1022
+ }
1023
+ function scatterTable(data, xHeader = "X", yHeader = "Y") {
1024
+ return {
1025
+ headers: ["Series", xHeader, yHeader],
1026
+ rows: data.series.flatMap((s) => s.data.map(([x, y]) => [s.name, x, y])),
1027
+ columns: ["text", "number", "value"]
1028
+ };
1029
+ }
1030
+ function radarTable(data, indicators) {
1031
+ return {
1032
+ headers: ["Indicator", ...data.series.map((s) => s.name)],
1033
+ rows: indicators.map((indicator, index) => [
1034
+ indicator.name,
1035
+ ...data.series.map((s) => s.data[index] ?? null)
1036
+ ]),
1037
+ columns: ["text", ...data.series.map(() => "value")]
1038
+ };
1039
+ }
1040
+ function heatmapTable(data, xAxis, yAxis) {
1041
+ const cells = new Map(data.data.map(([x, y, value]) => [`${x},${y}`, value]));
1042
+ return {
1043
+ headers: ["", ...xAxis],
1044
+ rows: yAxis.map((label, y) => [label, ...xAxis.map((_, x) => cells.get(`${x},${y}`) ?? null)]),
1045
+ columns: ["text", ...xAxis.map(() => "value")]
1046
+ };
1047
+ }
1048
+ function treemapTable(nodes) {
1049
+ const rows = [];
1050
+ const visit = (list, path) => {
1051
+ for (const node of list) {
1052
+ const nodePath = [...path, node.name];
1053
+ if (node.children?.length) visit(node.children, nodePath);
1054
+ else rows.push([nodePath.join(" / "), node.value ?? null]);
1055
+ }
1056
+ };
1057
+ visit(nodes, []);
1058
+ return { headers: ["Item", "Value"], rows, columns: ["text", "value"] };
1059
+ }
1060
+ function sankeyTable(data) {
1061
+ return {
1062
+ headers: ["From", "To", "Value"],
1063
+ rows: data.links.map((link) => [link.source, link.target, link.value]),
1064
+ columns: ["text", "text", "value"]
1065
+ };
1066
+ }
1067
+ function candlestickTable(data) {
1068
+ const withVolume = !!data.volume?.length;
1069
+ return {
1070
+ headers: ["Date", "Open", "High", "Low", "Close", ...withVolume ? ["Volume"] : []],
1071
+ rows: data.data.map((item, index) => [
1072
+ data.dates[index] ?? "",
1073
+ item.open,
1074
+ item.high,
1075
+ item.low,
1076
+ item.close,
1077
+ ...withVolume ? [data.volume?.[index] ?? null] : []
1078
+ ]),
1079
+ columns: [
1080
+ "text",
1081
+ "value",
1082
+ "value",
1083
+ "value",
1084
+ "value",
1085
+ ...withVolume ? ["number"] : []
1086
+ ]
1087
+ };
1088
+ }
1089
+ function histogramTable(buckets) {
1090
+ return {
1091
+ headers: ["Range", "Count"],
1092
+ rows: buckets.map((bucket) => [bucket.label, bucket.count]),
1093
+ columns: ["text", "number"]
1094
+ };
1095
+ }
1096
+ function boxPlotTable(groups) {
1097
+ return {
1098
+ headers: ["Group", "Count", "Low", "Q1", "Median", "Q3", "High", "Outliers"],
1099
+ rows: groups.map(
1100
+ ({ name, stats }) => stats ? [
1101
+ name,
1102
+ stats.count,
1103
+ stats.low,
1104
+ stats.q1,
1105
+ stats.median,
1106
+ stats.q3,
1107
+ stats.high,
1108
+ stats.outliers.length
1109
+ ] : [name, 0, null, null, null, null, null, 0]
1110
+ ),
1111
+ columns: ["text", "number", "value", "value", "value", "value", "value", "number"]
1112
+ };
1113
+ }
1114
+ function waterfallTable(steps, bars) {
1115
+ return {
1116
+ headers: ["Step", "Change", "Total"],
1117
+ rows: steps.map((step, index) => [
1118
+ step.name,
1119
+ bars[index]?.kind === "total" ? null : bars[index]?.value ?? null,
1120
+ bars[index]?.end ?? null
1121
+ ]),
1122
+ columns: ["text", "value", "value"]
1123
+ };
1124
+ }
1125
+ function bulletTable(items) {
1126
+ const withTarget = items.some((item) => item.target !== void 0);
1127
+ return {
1128
+ headers: ["Name", "Value", ...withTarget ? ["Target"] : []],
1129
+ rows: items.map((item) => [
1130
+ item.name,
1131
+ item.value,
1132
+ ...withTarget ? [item.target ?? null] : []
1133
+ ]),
1134
+ columns: ["text", "value", ...withTarget ? ["value"] : []]
1135
+ };
1136
+ }
1137
+ function formatTableCell(table, cell, column, format) {
1138
+ if (typeof cell !== "number") return cell ?? "";
1139
+ switch (table.columns?.[column] ?? "value") {
1140
+ case "number":
1141
+ return formatNumber(cell, { maximumFractionDigits: 2 });
1142
+ case "percent":
1143
+ return `${cell}%`;
1144
+ case "text":
1145
+ return String(cell);
1146
+ default:
1147
+ return formatValue(cell, format);
1148
+ }
1149
+ }
1150
+ function csvCell(cell) {
1151
+ if (cell == null) return "";
1152
+ const text = String(cell);
1153
+ return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
1154
+ }
1155
+ function tableToCsv(table, format) {
1156
+ const lines = [table.headers, ...table.rows].map(
1157
+ (row, rowIndex) => row.map(
1158
+ (cell, column) => csvCell(rowIndex > 0 && format ? formatTableCell(table, cell, column, format) : cell)
1159
+ ).join(",")
1160
+ );
1161
+ return lines.join("\r\n");
1162
+ }
1163
+ function chartFilename(title) {
1164
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "chart";
1165
+ }
1166
+ function downloadFile(content, filename, mimeType = "text/plain") {
1167
+ if (typeof document === "undefined") return;
1168
+ const isDataUrl = content.startsWith("data:");
1169
+ const url = isDataUrl ? content : URL.createObjectURL(new Blob([content], { type: `${mimeType};charset=utf-8` }));
1170
+ const link = document.createElement("a");
1171
+ link.href = url;
1172
+ link.download = filename;
1173
+ link.style.display = "none";
1174
+ document.body.appendChild(link);
1175
+ link.click();
1176
+ link.remove();
1177
+ if (!isDataUrl) setTimeout(() => URL.revokeObjectURL(url), 0);
1178
+ }
1179
+ function downloadCsv(table, filename) {
1180
+ downloadFile(
1181
+ `\uFEFF${tableToCsv(table)}`,
1182
+ filename.endsWith(".csv") ? filename : `${filename}.csv`,
1183
+ "text/csv"
1184
+ );
1185
+ }
368
1186
 
369
- export { abbreviateNumber, anchorCenterGraphic, buildAxis, buildDataLabel, buildLegend, buildTooltip, createTooltipFormatter, escapeHtml, formatCurrency, formatDate, formatNumber, formatPercent, formatTooltipValue, generateBarChartDescription, generateDataTable, generateLineChartDescription, generatePieChartDescription, generateScatterChartDescription, getLegendItems, getReplaceMerge, gridInset, hasPointLabels, itemLabelLookup, legendCenter, mergeChartOption, nameLabelLookup, pointLabel, seriesLabelLookup, withPointLabels };
1187
+ export { abbreviateNumber, allSeriesEmpty, anchorCenterGraphic, binValues, boxPlotTable, boxplotStats, buildAxis, buildDataLabel, buildDataZoom, buildLegend, buildMarkArea, buildMarkLine, buildTooltip, bulletTable, candlestickTable, categoryTable, chartFilename, chartFormatters, computeDelta, createTooltipFormatter, downloadCsv, downloadFile, escapeHtml, formatCurrency, formatDate, formatNumber, formatPercent, formatTableCell, formatTooltipValue, formatValue, fromRecords, generateAreaChartDescription, generateBarChartDescription, generateBoxPlotDescription, generateBulletDescription, generateComboChartDescription, generateDataTable, generateDotPlotDescription, generateHistogramDescription, generateLineChartDescription, generatePieChartDescription, generateScatterChartDescription, generateWaterfallDescription, getLegendItems, getReplaceMerge, gridInset, hasPointLabels, heatmapTable, histogramTable, itemLabelLookup, legendCenter, mergeChartOption, nameLabelLookup, niceStep, pairWithCategories, pointLabel, quantile, radarTable, resolveChartHeight, resolveValueFormatter, sankeyTable, scatterTable, seriesLabelLookup, sliceTable, tableToCsv, toPercentStack, toSliceData, treemapTable, trendLine, valueAxisConfig, valueLabelFormatter, waterfallLayout, waterfallTable, withPointLabels, withReferenceMarks, withZoomInset };