@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.cjs CHANGED
@@ -284,7 +284,12 @@ var lightChartColors = {
284
284
  axisPointerLabel: themeTokens.semanticColors.light.mutedForeground,
285
285
  divider: themeTokens.colors.gray[600],
286
286
  success: themeTokens.semanticColors.light.success,
287
- destructive: themeTokens.semanticColors.light.destructive
287
+ destructive: themeTokens.semanticColors.light.destructive,
288
+ warning: themeTokens.semanticColors.light.warning,
289
+ reference: themeTokens.semanticColors.light.mutedForeground,
290
+ referenceBand: "rgba(102, 102, 102, 0.08)",
291
+ surface: themeTokens.semanticColors.light.card,
292
+ ranges: [themeTokens.colors.gray[300], themeTokens.colors.gray[200], themeTokens.colors.gray[100]]
288
293
  };
289
294
  var darkChartColors = {
290
295
  series: darkTheme.color,
@@ -294,7 +299,12 @@ var darkChartColors = {
294
299
  axisPointerLabel: themeTokens.colors.gray[600],
295
300
  divider: themeTokens.colors.gray[700],
296
301
  success: themeTokens.semanticColors.dark.success,
297
- destructive: themeTokens.semanticColors.dark.destructive
302
+ destructive: themeTokens.semanticColors.dark.destructive,
303
+ warning: themeTokens.semanticColors.dark.warning,
304
+ reference: themeTokens.semanticColors.dark.mutedForeground,
305
+ referenceBand: "rgba(255, 255, 255, 0.06)",
306
+ surface: themeTokens.semanticColors.dark.card,
307
+ ranges: [themeTokens.colors.gray[600], themeTokens.colors.gray[700], themeTokens.colors.gray[800]]
298
308
  };
299
309
  function getChartColors(themeName) {
300
310
  return themeName === THEME_DARK ? darkChartColors : lightChartColors;
@@ -312,17 +322,58 @@ function colorWithAlpha(color, alpha) {
312
322
  }
313
323
 
314
324
  // src/utils/format.ts
315
- function formatNumber(value, options) {
316
- return new Intl.NumberFormat("en-US", options).format(value);
325
+ var numberFormats = /* @__PURE__ */ new Map();
326
+ function numberFormat(locale, options) {
327
+ const key = `${locale}|${options ? JSON.stringify(options) : ""}`;
328
+ let format = numberFormats.get(key);
329
+ if (!format) {
330
+ format = new Intl.NumberFormat(locale, options);
331
+ numberFormats.set(key, format);
332
+ }
333
+ return format;
334
+ }
335
+ function formatNumber(value, options, locale = "en-US") {
336
+ return numberFormat(locale, options).format(value);
337
+ }
338
+ var PRESETS = {
339
+ number: { maximumFractionDigits: 2 },
340
+ integer: { maximumFractionDigits: 0 },
341
+ compact: { notation: "compact", maximumFractionDigits: 1 },
342
+ currency: { style: "currency", currency: "USD", maximumFractionDigits: 0 },
343
+ currencyCents: { style: "currency", currency: "USD" },
344
+ percent: { style: "percent", maximumFractionDigits: 1 },
345
+ percentValue: { style: "percent", maximumFractionDigits: 1 }
346
+ };
347
+ function resolveValueFormatter(format, locale = "en-US") {
348
+ if (format === void 0) return void 0;
349
+ if (typeof format === "function") return format;
350
+ if (format === "days") {
351
+ return (value) => {
352
+ const days = Math.round(value);
353
+ return `${numberFormat(locale).format(days)} ${Math.abs(days) === 1 ? "day" : "days"}`;
354
+ };
355
+ }
356
+ if (format === "percentValue") {
357
+ const intl2 = numberFormat(locale, PRESETS.percentValue);
358
+ return (value) => intl2.format(value / 100);
359
+ }
360
+ const intl = numberFormat(locale, typeof format === "string" ? PRESETS[format] : format);
361
+ return (value) => intl.format(value);
362
+ }
363
+ function formatValue(value, formatter, empty = "") {
364
+ if (value == null) return empty;
365
+ if (typeof value === "string") return value;
366
+ if (!Number.isFinite(value)) return empty;
367
+ return formatter ? formatter(value) : formatNumber(value, { maximumFractionDigits: 2 });
317
368
  }
318
369
  function formatCurrency(value, currency = "USD", locale = "en-US") {
319
- return new Intl.NumberFormat(locale, {
370
+ return numberFormat(locale, {
320
371
  style: "currency",
321
372
  currency
322
373
  }).format(value);
323
374
  }
324
375
  function formatPercent(value, decimals = 1, locale = "en-US") {
325
- return new Intl.NumberFormat(locale, {
376
+ return numberFormat(locale, {
326
377
  style: "percent",
327
378
  minimumFractionDigits: decimals,
328
379
  maximumFractionDigits: decimals
@@ -374,52 +425,80 @@ function createTooltipFormatter(options = {}) {
374
425
  }
375
426
 
376
427
  // src/utils/accessibility.ts
377
- function generateLineChartDescription(title, data) {
428
+ var isNumber = (value) => typeof value === "number" && Number.isFinite(value);
429
+ function seriesList(names) {
430
+ return names.join(", ");
431
+ }
432
+ function describeTrends(title, kind, data, format) {
378
433
  const seriesCount = data.series.length;
379
- const categoryCount = data.categories.length;
380
- const seriesNames = data.series.map((s) => s.name).join(", ");
381
- let description = `${title}. Line chart with ${categoryCount} data points`;
434
+ let description = `${title}. ${kind} chart with ${data.categories.length} data points`;
382
435
  if (seriesCount > 1) {
383
- description += ` across ${seriesCount} series: ${seriesNames}`;
436
+ description += ` across ${seriesCount} series: ${seriesList(data.series.map((s) => s.name))}`;
384
437
  }
385
438
  data.series.forEach((series) => {
386
- if (series.data.length >= 2) {
387
- const first = series.data[0];
388
- const last = series.data[series.data.length - 1];
439
+ const present = series.data.filter(isNumber);
440
+ if (present.length >= 2) {
441
+ const first = present[0];
442
+ const last = present[present.length - 1];
389
443
  const trend = last > first ? "increasing" : last < first ? "decreasing" : "stable";
390
- description += `. ${series.name} shows ${trend} trend from ${first} to ${last}`;
444
+ description += `. ${series.name} shows ${trend} trend from ${formatValue(first, format)} to ${formatValue(last, format)}`;
391
445
  }
392
446
  });
447
+ const missing = data.series.reduce(
448
+ (sum, series) => sum + series.data.filter((value) => !isNumber(value)).length,
449
+ 0
450
+ );
451
+ if (missing > 0) description += `. ${missing} values are missing`;
393
452
  return description;
394
453
  }
395
- function generateBarChartDescription(title, data) {
454
+ function generateLineChartDescription(title, data, format) {
455
+ return describeTrends(title, "Line", data, format);
456
+ }
457
+ function generateAreaChartDescription(title, data, format) {
458
+ return describeTrends(title, "Area", data, format);
459
+ }
460
+ function generateComboChartDescription(title, data, format) {
461
+ return describeTrends(title, "Combined bar and line", data, format);
462
+ }
463
+ function extremes(data) {
464
+ let max;
465
+ let min;
466
+ data.series.forEach((series) => {
467
+ series.data.forEach((value, index) => {
468
+ if (!isNumber(value)) return;
469
+ const category = data.categories[index] ?? "";
470
+ if (!max || value > max.value) max = { value, category };
471
+ if (!min || value < min.value) min = { value, category };
472
+ });
473
+ });
474
+ return { max, min };
475
+ }
476
+ function generateBarChartDescription(title, data, format) {
396
477
  const categoryCount = data.categories.length;
397
478
  const seriesCount = data.series.length;
398
479
  let description = `${title}. Bar chart with ${categoryCount} categories`;
399
480
  if (seriesCount > 1) {
400
481
  description += ` and ${seriesCount} series`;
401
482
  }
402
- let maxValue = -Infinity;
403
- let maxCategory = "";
404
- let minValue = Infinity;
405
- let minCategory = "";
406
- data.series.forEach((series) => {
407
- series.data.forEach((value, index) => {
408
- if (value > maxValue) {
409
- maxValue = value;
410
- maxCategory = data.categories[index];
411
- }
412
- if (value < minValue) {
413
- minValue = value;
414
- minCategory = data.categories[index];
415
- }
416
- });
417
- });
418
- description += `. Highest value is ${maxValue} for ${maxCategory}. Lowest value is ${minValue} for ${minCategory}`;
483
+ const { max, min } = extremes(data);
484
+ if (max && min) {
485
+ description += `. Highest value is ${formatValue(max.value, format)} for ${max.category}. Lowest value is ${formatValue(min.value, format)} for ${min.category}`;
486
+ }
487
+ return description;
488
+ }
489
+ function generateDotPlotDescription(title, data, format) {
490
+ let description = `${title}. Dot plot with ${data.categories.length} rows`;
491
+ if (data.series.length > 1) {
492
+ description += ` comparing ${seriesList(data.series.map((s) => s.name))}`;
493
+ }
494
+ const { max, min } = extremes(data);
495
+ if (max && min) {
496
+ description += `. Highest value is ${formatValue(max.value, format)} for ${max.category}. Lowest value is ${formatValue(min.value, format)} for ${min.category}`;
497
+ }
419
498
  return description;
420
499
  }
421
500
  function generatePieChartDescription(title, data, isDonut = false) {
422
- const total = data.reduce((sum, slice) => sum + slice.value, 0);
501
+ const total = data.reduce((sum, slice) => sum + (isNumber(slice.value) ? slice.value : 0), 0);
423
502
  const chartType = isDonut ? "Donut" : "Pie";
424
503
  let description = `${title}. ${chartType} chart with ${data.length} segments`;
425
504
  if (total === 0) {
@@ -444,6 +523,44 @@ function generateScatterChartDescription(title, data) {
444
523
  }
445
524
  return description;
446
525
  }
526
+ function generateHistogramDescription(title, buckets, total) {
527
+ let description = `${title}. Histogram of ${total} values in ${buckets.length} buckets`;
528
+ const peak = buckets.reduce(
529
+ (best, bucket) => !best || bucket.count > best.count ? bucket : best,
530
+ void 0
531
+ );
532
+ if (peak && peak.count > 0) {
533
+ description += `. The most common range is ${peak.label} with ${peak.count} values`;
534
+ }
535
+ return description;
536
+ }
537
+ function generateBoxPlotDescription(title, groups, format) {
538
+ let description = `${title}. Box plot of ${groups.length} groups`;
539
+ const parts = groups.filter((group) => group.stats).slice(0, 5).map(
540
+ ({ name, stats }) => `${name}: median ${formatValue(stats.median, format)}, middle half ${formatValue(stats.q1, format)} to ${formatValue(stats.q3, format)}`
541
+ );
542
+ if (parts.length) description += `. ${parts.join("; ")}`;
543
+ return description;
544
+ }
545
+ function generateWaterfallDescription(title, steps, final, format) {
546
+ const changes = steps.filter((step) => !step.isTotal);
547
+ const increases = changes.filter((step) => step.value > 0).length;
548
+ const decreases = changes.filter((step) => step.value < 0).length;
549
+ return `${title}. Waterfall chart with ${steps.length} steps: ${increases} increases and ${decreases} decreases, ending at ${formatValue(final, format)}`;
550
+ }
551
+ function generateBulletDescription(title, items, format) {
552
+ const withTarget = items.filter((item) => isNumber(item.target));
553
+ let description = `${title}. Bullet chart with ${items.length} rows`;
554
+ if (withTarget.length) {
555
+ const met = withTarget.filter((item) => item.value >= item.target).length;
556
+ description += `. ${met} of ${withTarget.length} meet their target`;
557
+ }
558
+ const parts = items.slice(0, 5).map(
559
+ (item) => `${item.name} ${formatValue(item.value, format)}${isNumber(item.target) ? ` of ${formatValue(item.target, format)}` : ""}`
560
+ );
561
+ if (parts.length) description += `. ${parts.join(", ")}`;
562
+ return description;
563
+ }
447
564
  function generateDataTable(headers, rows) {
448
565
  const headerRow = headers.map((h) => `<th scope="col">${escapeHtml(h)}</th>`).join("");
449
566
  const bodyRows = rows.map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`).join("");
@@ -603,7 +720,14 @@ function getLegendItems(names, palette, overrides, visible) {
603
720
  function buildAxis(base, config) {
604
721
  if (!config) return base;
605
722
  const axis = { ...base };
606
- const { show, name, nameLocation, nameGap, formatter, rotate, min, max, interval } = config;
723
+ const { type, show, name, nameLocation, nameGap, formatter, rotate, min, max, interval } = config;
724
+ if (type !== void 0) {
725
+ axis.type = type;
726
+ if (type !== "category") {
727
+ delete axis.data;
728
+ delete axis.boundaryGap;
729
+ }
730
+ }
607
731
  if (show !== void 0) axis.show = show;
608
732
  if (name !== void 0) {
609
733
  axis.name = name;
@@ -676,43 +800,794 @@ function formatTooltipValue(value, valueFormatter) {
676
800
  if (valueFormatter && typeof value === "number") return valueFormatter(value);
677
801
  return String(value ?? "");
678
802
  }
803
+ function valueLabelFormatter(format) {
804
+ return (params) => {
805
+ if (params.label != null && params.label !== "") return params.label;
806
+ const value = Array.isArray(params.value) ? params.value[params.value.length - 1] : params.value;
807
+ return formatValue(value, format);
808
+ };
809
+ }
810
+ function valueAxisConfig(config, format) {
811
+ if (!format || config?.formatter !== void 0) return config;
812
+ return { ...config, formatter: (value) => format(Number(value)) };
813
+ }
814
+ function chartFormatters({
815
+ valueFormat,
816
+ locale,
817
+ tooltipValueFormatter,
818
+ labelFormatter
819
+ }) {
820
+ const format = resolveValueFormatter(valueFormat, locale);
821
+ return {
822
+ format,
823
+ tooltip: tooltipValueFormatter ?? format,
824
+ label: labelFormatter ?? (format ? valueLabelFormatter(format) : void 0),
825
+ axis: (config) => valueAxisConfig(config, format)
826
+ };
827
+ }
828
+ var STATISTICS = ["average", "median", "min", "max"];
829
+ function buildMarkLine(lines, { valueAxis, format, color, inverse = false }) {
830
+ if (!lines?.length) return void 0;
831
+ return {
832
+ silent: true,
833
+ symbol: "none",
834
+ animation: false,
835
+ data: lines.map((line) => {
836
+ const statistic = STATISTICS.includes(line.value);
837
+ const lineColor = line.color ?? color;
838
+ const position = valueAxis === "y" ? "insideEndTop" : inverse ? "start" : "end";
839
+ return {
840
+ ...statistic ? { type: line.value, valueDim: valueAxis } : { [valueAxis === "y" ? "yAxis" : "xAxis"]: line.value },
841
+ name: line.label,
842
+ lineStyle: { type: line.style ?? "dashed", color: lineColor, width: 1.5 },
843
+ label: {
844
+ show: true,
845
+ position,
846
+ color: lineColor,
847
+ formatter: (params) => line.label ?? formatValue(Number(params.value), format)
848
+ }
849
+ };
850
+ })
851
+ };
852
+ }
853
+ function buildMarkArea(bands, { valueAxis, bandColor }) {
854
+ if (!bands?.length) return void 0;
855
+ const key = valueAxis === "y" ? "yAxis" : "xAxis";
856
+ return {
857
+ silent: true,
858
+ animation: false,
859
+ data: bands.map((band) => [
860
+ {
861
+ [key]: band.from,
862
+ name: band.label,
863
+ itemStyle: { color: band.color ?? bandColor },
864
+ label: {
865
+ show: !!band.label,
866
+ position: "insideTopLeft"
867
+ }
868
+ },
869
+ { [key]: band.to }
870
+ ])
871
+ };
872
+ }
873
+ function withReferenceMarks(series, lines, bands, options) {
874
+ if (!lines?.length && !bands?.length) return series;
875
+ const indexOf = (name) => {
876
+ if (name === void 0) return 0;
877
+ const index = series.findIndex((s) => s.name === name);
878
+ return index === -1 ? 0 : index;
879
+ };
880
+ return series.map((s, index) => {
881
+ const markLine = buildMarkLine(
882
+ lines?.filter((line) => indexOf(line.series) === index),
883
+ options
884
+ );
885
+ const markArea = buildMarkArea(
886
+ bands?.filter((band) => indexOf(band.series) === index),
887
+ options
888
+ );
889
+ if (!markLine && !markArea) return s;
890
+ return { ...s, ...markLine && { markLine }, ...markArea && { markArea } };
891
+ });
892
+ }
893
+ function buildDataZoom(mode, axis, bottomOffset = 0) {
894
+ if (!mode) return void 0;
895
+ const target = axis === "x" ? { xAxisIndex: 0 } : { yAxisIndex: 0 };
896
+ const zooms = [];
897
+ if (mode === true || mode === "inside") zooms.push({ type: "inside", ...target });
898
+ if (mode === true || mode === "slider") {
899
+ zooms.push(
900
+ axis === "x" ? { type: "slider", ...target, bottom: 8 + bottomOffset, height: 20 } : { type: "slider", ...target, right: 8, width: 20 }
901
+ );
902
+ }
903
+ return zooms;
904
+ }
905
+ function withZoomInset(inset, mode, axis) {
906
+ if (mode !== true && mode !== "slider") return inset;
907
+ const edge = axis === "x" ? "bottom" : "right";
908
+ const current = typeof inset?.[edge] === "number" ? inset[edge] : 12;
909
+ return { ...inset, [edge]: current + 36 };
910
+ }
911
+
912
+ // src/utils/stats.ts
913
+ var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
914
+ var clean = (value) => Number(value.toPrecision(12));
915
+ function niceStep(rough) {
916
+ if (!(rough > 0) || !Number.isFinite(rough)) return 1;
917
+ const power = 10 ** Math.floor(Math.log10(rough));
918
+ const fraction = rough / power;
919
+ const nice = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10;
920
+ return clean(nice * power);
921
+ }
922
+ function bucketLabel(start, end, format) {
923
+ const text = (value) => formatValue(value, format);
924
+ if (start === -Infinity && end === Infinity) return "All";
925
+ if (start === -Infinity) return `< ${text(end)}`;
926
+ if (end === Infinity) return `${text(start)}+`;
927
+ return `${text(start)}\u2013${text(end)}`;
928
+ }
929
+ function binValues(values, options = {}) {
930
+ const { bins, binWidth, format } = options;
931
+ const finite = values.filter(isFiniteNumber);
932
+ let edges;
933
+ if (Array.isArray(bins)) {
934
+ edges = bins.map((bin) => ({
935
+ start: bin.start ?? -Infinity,
936
+ end: bin.end ?? Infinity,
937
+ label: bin.label
938
+ }));
939
+ } else {
940
+ if (!finite.length) return [];
941
+ let min = Infinity;
942
+ let max = -Infinity;
943
+ for (const value of finite) {
944
+ if (value < min) min = value;
945
+ if (value > max) max = value;
946
+ }
947
+ if (min === max) {
948
+ edges = [{ start: min, end: max }];
949
+ } else {
950
+ let step;
951
+ let start;
952
+ if (binWidth && binWidth > 0) {
953
+ step = binWidth;
954
+ start = Math.floor(min / step) * step;
955
+ } else if (typeof bins === "number" && bins > 0) {
956
+ step = (max - min) / Math.floor(bins);
957
+ start = min;
958
+ } else {
959
+ const sturges = Math.ceil(Math.log2(finite.length) + 1);
960
+ step = niceStep((max - min) / sturges);
961
+ start = Math.floor(min / step) * step;
962
+ }
963
+ const count = Math.max(1, Math.ceil(clean((max - start) / step)));
964
+ edges = Array.from({ length: count }, (_, index) => ({
965
+ start: clean(start + index * step),
966
+ end: clean(start + (index + 1) * step)
967
+ }));
968
+ }
969
+ }
970
+ const buckets = edges.map(({ start, end, label }) => ({
971
+ label: label ?? bucketLabel(start, end, format),
972
+ start,
973
+ end,
974
+ count: 0
975
+ }));
976
+ const last = buckets.length - 1;
977
+ for (const value of finite) {
978
+ const index = buckets.findIndex(
979
+ (bucket, i) => value >= bucket.start && (value < bucket.end || i === last && value <= bucket.end)
980
+ );
981
+ if (index !== -1) buckets[index].count++;
982
+ }
983
+ return buckets;
984
+ }
985
+ function quantile(sorted, q) {
986
+ if (!sorted.length) return NaN;
987
+ const position = (sorted.length - 1) * q;
988
+ const lower = Math.floor(position);
989
+ const upper = Math.ceil(position);
990
+ if (lower === upper) return sorted[lower];
991
+ return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
992
+ }
993
+ function boxplotStats(values) {
994
+ const sorted = values.filter(isFiniteNumber).sort((a, b) => a - b);
995
+ if (!sorted.length) return null;
996
+ const q1 = quantile(sorted, 0.25);
997
+ const median = quantile(sorted, 0.5);
998
+ const q3 = quantile(sorted, 0.75);
999
+ const iqr = q3 - q1;
1000
+ const lowFence = q1 - 1.5 * iqr;
1001
+ const highFence = q3 + 1.5 * iqr;
1002
+ const inside = sorted.filter((value) => value >= lowFence && value <= highFence);
1003
+ const outliers = sorted.filter((value) => value < lowFence || value > highFence);
1004
+ return {
1005
+ low: inside[0] ?? q1,
1006
+ q1,
1007
+ median,
1008
+ q3,
1009
+ high: inside[inside.length - 1] ?? q3,
1010
+ outliers,
1011
+ count: sorted.length,
1012
+ mean: sorted.reduce((sum, value) => sum + value, 0) / sorted.length
1013
+ };
1014
+ }
1015
+ function waterfallLayout(steps) {
1016
+ let running = 0;
1017
+ return steps.map((step) => {
1018
+ let start;
1019
+ let end;
1020
+ let kind;
1021
+ if (step.isTotal === "value") {
1022
+ start = 0;
1023
+ end = isFiniteNumber(step.value) ? step.value : running;
1024
+ kind = "total";
1025
+ } else if (step.isTotal) {
1026
+ start = 0;
1027
+ end = running;
1028
+ kind = "total";
1029
+ } else {
1030
+ const change = isFiniteNumber(step.value) ? step.value : 0;
1031
+ start = running;
1032
+ end = running + change;
1033
+ kind = change < 0 ? "decrease" : "increase";
1034
+ }
1035
+ running = end;
1036
+ const low = Math.min(start, end);
1037
+ const high = Math.max(start, end);
1038
+ let base = 0;
1039
+ let positive = 0;
1040
+ let negative = 0;
1041
+ if (low >= 0) {
1042
+ base = low;
1043
+ positive = high - low;
1044
+ } else if (high <= 0) {
1045
+ base = high;
1046
+ negative = low - high;
1047
+ } else {
1048
+ positive = high;
1049
+ negative = low;
1050
+ }
1051
+ return {
1052
+ start,
1053
+ end,
1054
+ value: kind === "total" ? end : end - start,
1055
+ kind,
1056
+ base: clean(base),
1057
+ positive: clean(positive),
1058
+ negative: clean(negative)
1059
+ };
1060
+ });
1061
+ }
1062
+ function toPercentStack(series) {
1063
+ const length = Math.max(0, ...series.map((s) => s.data.length));
1064
+ const totals = Array.from(
1065
+ { length },
1066
+ (_, index) => series.reduce((sum, s) => {
1067
+ const value = s.data[index];
1068
+ return sum + (isFiniteNumber(value) ? Math.abs(value) : 0);
1069
+ }, 0)
1070
+ );
1071
+ return series.map((s) => ({
1072
+ ...s,
1073
+ data: s.data.map((value, index) => {
1074
+ if (!isFiniteNumber(value)) return null;
1075
+ return totals[index] === 0 ? 0 : clean(value / totals[index] * 100);
1076
+ })
1077
+ }));
1078
+ }
1079
+ function pairWithCategories(categories, values, axisType) {
1080
+ if (axisType !== "time" && axisType !== "value") return values;
1081
+ return values.map((value, index) => [categories[index], value]);
1082
+ }
1083
+ function resolveChartHeight(height, rows, { rowHeight = 28, chrome = 56, minHeight = 160, maxHeight } = {}) {
1084
+ if (height !== "auto") return height;
1085
+ let value = Math.max(minHeight, chrome + Math.max(rows, 1) * rowHeight);
1086
+ if (maxHeight !== void 0) value = Math.min(value, maxHeight);
1087
+ return value;
1088
+ }
1089
+ function allSeriesEmpty(series) {
1090
+ return !series?.length || series.every((s) => !s.data?.some((value) => value != null));
1091
+ }
1092
+ function trendLine(points) {
1093
+ const finite = points.filter(([x, y]) => isFiniteNumber(x) && isFiniteNumber(y));
1094
+ const n = finite.length;
1095
+ if (n < 2) return null;
1096
+ let sumX = 0;
1097
+ let sumY = 0;
1098
+ let sumXY = 0;
1099
+ let sumX2 = 0;
1100
+ let minX = Infinity;
1101
+ let maxX = -Infinity;
1102
+ for (const [x, y] of finite) {
1103
+ sumX += x;
1104
+ sumY += y;
1105
+ sumXY += x * y;
1106
+ sumX2 += x * x;
1107
+ if (x < minX) minX = x;
1108
+ if (x > maxX) maxX = x;
1109
+ }
1110
+ const denominator = n * sumX2 - sumX * sumX;
1111
+ if (minX === maxX || denominator === 0) return null;
1112
+ const slope = (n * sumXY - sumX * sumY) / denominator;
1113
+ const intercept = (sumY - slope * sumX) / n;
1114
+ return [
1115
+ [minX, slope * minX + intercept],
1116
+ [maxX, slope * maxX + intercept]
1117
+ ];
1118
+ }
1119
+ function computeDelta(current, previous) {
1120
+ if (!isFiniteNumber(current) || !isFiniteNumber(previous)) return null;
1121
+ const change = clean(current - previous);
1122
+ return {
1123
+ change,
1124
+ ratio: previous === 0 ? null : change / Math.abs(previous),
1125
+ direction: change > 0 ? "up" : change < 0 ? "down" : "flat"
1126
+ };
1127
+ }
1128
+
1129
+ // src/utils/records.ts
1130
+ function read(row, accessor) {
1131
+ return typeof accessor === "function" ? accessor(row) : row[accessor];
1132
+ }
1133
+ function toNumber(value) {
1134
+ if (value == null || value === "") return null;
1135
+ const number = typeof value === "number" ? value : Number(value);
1136
+ return Number.isFinite(number) ? number : null;
1137
+ }
1138
+ function toLabel(value, nullLabel) {
1139
+ return value == null || value === "" ? nullLabel : String(value);
1140
+ }
1141
+ var Cell = class {
1142
+ constructor() {
1143
+ this.sum = 0;
1144
+ this.count = 0;
1145
+ this.min = Infinity;
1146
+ this.max = -Infinity;
1147
+ this.first = null;
1148
+ this.last = null;
1149
+ }
1150
+ add(value) {
1151
+ this.count++;
1152
+ if (value === null) return;
1153
+ this.sum += value;
1154
+ if (value < this.min) this.min = value;
1155
+ if (value > this.max) this.max = value;
1156
+ if (this.first === null) this.first = value;
1157
+ this.last = value;
1158
+ }
1159
+ result(aggregate, present) {
1160
+ switch (aggregate) {
1161
+ case "count":
1162
+ return this.count;
1163
+ case "avg":
1164
+ return present ? this.sum / present : null;
1165
+ case "min":
1166
+ return present ? this.min : null;
1167
+ case "max":
1168
+ return present ? this.max : null;
1169
+ case "first":
1170
+ return this.first;
1171
+ case "last":
1172
+ return this.last;
1173
+ default:
1174
+ return present ? this.sum : null;
1175
+ }
1176
+ }
1177
+ };
1178
+ var Accumulator = class {
1179
+ constructor() {
1180
+ this.cells = /* @__PURE__ */ new Map();
1181
+ }
1182
+ add(key, value) {
1183
+ let cell = this.cells.get(key);
1184
+ if (!cell) {
1185
+ cell = Object.assign(new Cell(), { present: 0 });
1186
+ this.cells.set(key, cell);
1187
+ }
1188
+ cell.add(value);
1189
+ if (value !== null) cell.present++;
1190
+ }
1191
+ get(key, aggregate) {
1192
+ const cell = this.cells.get(key);
1193
+ return cell ? cell.result(aggregate, cell.present) : null;
1194
+ }
1195
+ };
1196
+ function ordered(seen, order) {
1197
+ if (!order?.length) return seen;
1198
+ const present = new Set(seen);
1199
+ const first = order.filter((name) => present.has(name));
1200
+ const listed = new Set(first);
1201
+ return [...first, ...seen.filter((name) => !listed.has(name))];
1202
+ }
1203
+ function fromRecords(rows, options) {
1204
+ const {
1205
+ category,
1206
+ value,
1207
+ series,
1208
+ measures,
1209
+ seriesName = "Value",
1210
+ aggregate = "sum",
1211
+ categoryOrder,
1212
+ topN,
1213
+ otherLabel,
1214
+ seriesOrder,
1215
+ nullLabel = "Unknown",
1216
+ fill = null,
1217
+ colors: colors3
1218
+ } = options;
1219
+ const sort = options.sort ?? (topN !== void 0 ? "total" : "none");
1220
+ const categories = [];
1221
+ const categorySet = /* @__PURE__ */ new Set();
1222
+ const seriesNames = [];
1223
+ const seriesSet = /* @__PURE__ */ new Set();
1224
+ const cells = new Accumulator();
1225
+ const key = (c, s) => `${c}\0${s}`;
1226
+ const note = (list, set, name) => {
1227
+ if (!set.has(name)) {
1228
+ set.add(name);
1229
+ list.push(name);
1230
+ }
1231
+ };
1232
+ for (const row of rows ?? []) {
1233
+ const c = toLabel(read(row, category), nullLabel);
1234
+ note(categories, categorySet, c);
1235
+ if (measures) {
1236
+ for (const [name, accessor] of Object.entries(measures)) {
1237
+ note(seriesNames, seriesSet, name);
1238
+ cells.add(key(c, name), toNumber(read(row, accessor)));
1239
+ }
1240
+ } else {
1241
+ const s = series ? toLabel(read(row, series), nullLabel) : seriesName;
1242
+ note(seriesNames, seriesSet, s);
1243
+ cells.add(key(c, s), value === void 0 ? 1 : toNumber(read(row, value)));
1244
+ }
1245
+ }
1246
+ const names = ordered(seriesNames, seriesOrder);
1247
+ const cellValue = (c, s) => cells.get(key(c, s), aggregate);
1248
+ const total = (c) => names.reduce((sum, s) => sum + (cellValue(c, s) ?? 0), 0);
1249
+ let order = ordered(categories, categoryOrder);
1250
+ if (sort === "total" || sort === "total-asc") {
1251
+ const totals = new Map(order.map((c) => [c, total(c)]));
1252
+ const direction = sort === "total" ? -1 : 1;
1253
+ order = [...order].sort((a, b) => direction * (totals.get(a) - totals.get(b)));
1254
+ } else if (sort === "category") {
1255
+ order = [...order].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
1256
+ } else if (typeof sort === "function") {
1257
+ order = [...order].sort(sort);
1258
+ }
1259
+ let rest = [];
1260
+ if (topN !== void 0 && order.length > topN) {
1261
+ rest = order.slice(topN);
1262
+ order = order.slice(0, topN);
1263
+ }
1264
+ const foldOther = otherLabel !== void 0 && rest.length > 0 && (aggregate === "sum" || aggregate === "count");
1265
+ const result = names.map((name) => {
1266
+ const data = order.map((c) => cellValue(c, name) ?? fill);
1267
+ if (foldOther) {
1268
+ const other = rest.reduce((sum, c) => {
1269
+ const v = cellValue(c, name);
1270
+ return v === null ? sum : (sum ?? 0) + v;
1271
+ }, null);
1272
+ data.push(other ?? fill);
1273
+ }
1274
+ return colors3?.[name] ? { name, data, color: colors3[name] } : { name, data };
1275
+ });
1276
+ return {
1277
+ categories: foldOther ? [...order, otherLabel] : order,
1278
+ series: result
1279
+ };
1280
+ }
1281
+ function toSliceData(rows, options) {
1282
+ const {
1283
+ name,
1284
+ value,
1285
+ aggregate = "sum",
1286
+ dropZero = true,
1287
+ sort = "desc",
1288
+ topN,
1289
+ otherLabel,
1290
+ nullLabel = "Unknown",
1291
+ colors: colors3
1292
+ } = options;
1293
+ const { categories, series } = fromRecords(rows, {
1294
+ category: name,
1295
+ value,
1296
+ aggregate: value === void 0 ? "count" : aggregate,
1297
+ nullLabel
1298
+ });
1299
+ const values = series[0]?.data ?? [];
1300
+ let slices = categories.map((label, index) => ({ name: label, value: values[index] ?? 0 }));
1301
+ if (dropZero) slices = slices.filter((slice) => slice.value !== 0);
1302
+ if (sort !== "none") {
1303
+ const direction = sort === "desc" ? -1 : 1;
1304
+ slices = [...slices].sort((a, b) => direction * (a.value - b.value));
1305
+ }
1306
+ if (topN !== void 0 && slices.length > topN) {
1307
+ const rest = slices.slice(topN);
1308
+ slices = slices.slice(0, topN);
1309
+ if (otherLabel !== void 0) {
1310
+ slices.push({ name: otherLabel, value: rest.reduce((sum, slice) => sum + slice.value, 0) });
1311
+ }
1312
+ }
1313
+ return slices.map(
1314
+ (slice) => colors3?.[slice.name] ? { ...slice, color: colors3[slice.name] } : slice
1315
+ );
1316
+ }
1317
+
1318
+ // src/utils/tables.ts
1319
+ function categoryTable(data, categoryHeader = "Category") {
1320
+ return {
1321
+ headers: [categoryHeader, ...data.series.map((s) => s.name)],
1322
+ rows: data.categories.map((category, index) => [
1323
+ category,
1324
+ ...data.series.map((s) => s.data[index] ?? null)
1325
+ ]),
1326
+ columns: ["text", ...data.series.map(() => "value")]
1327
+ };
1328
+ }
1329
+ function sliceTable(slices, nameHeader = "Name") {
1330
+ const total = slices.reduce(
1331
+ (sum, slice) => sum + (Number.isFinite(slice.value) ? slice.value : 0),
1332
+ 0
1333
+ );
1334
+ return {
1335
+ headers: [nameHeader, "Value", "Share"],
1336
+ rows: slices.map((slice) => [
1337
+ slice.name,
1338
+ slice.value,
1339
+ total ? Number((slice.value / total * 100).toFixed(1)) : 0
1340
+ ]),
1341
+ columns: ["text", "value", "percent"]
1342
+ };
1343
+ }
1344
+ function scatterTable(data, xHeader = "X", yHeader = "Y") {
1345
+ return {
1346
+ headers: ["Series", xHeader, yHeader],
1347
+ rows: data.series.flatMap((s) => s.data.map(([x, y]) => [s.name, x, y])),
1348
+ columns: ["text", "number", "value"]
1349
+ };
1350
+ }
1351
+ function radarTable(data, indicators) {
1352
+ return {
1353
+ headers: ["Indicator", ...data.series.map((s) => s.name)],
1354
+ rows: indicators.map((indicator, index) => [
1355
+ indicator.name,
1356
+ ...data.series.map((s) => s.data[index] ?? null)
1357
+ ]),
1358
+ columns: ["text", ...data.series.map(() => "value")]
1359
+ };
1360
+ }
1361
+ function heatmapTable(data, xAxis, yAxis) {
1362
+ const cells = new Map(data.data.map(([x, y, value]) => [`${x},${y}`, value]));
1363
+ return {
1364
+ headers: ["", ...xAxis],
1365
+ rows: yAxis.map((label, y) => [label, ...xAxis.map((_, x) => cells.get(`${x},${y}`) ?? null)]),
1366
+ columns: ["text", ...xAxis.map(() => "value")]
1367
+ };
1368
+ }
1369
+ function treemapTable(nodes) {
1370
+ const rows = [];
1371
+ const visit = (list, path) => {
1372
+ for (const node of list) {
1373
+ const nodePath = [...path, node.name];
1374
+ if (node.children?.length) visit(node.children, nodePath);
1375
+ else rows.push([nodePath.join(" / "), node.value ?? null]);
1376
+ }
1377
+ };
1378
+ visit(nodes, []);
1379
+ return { headers: ["Item", "Value"], rows, columns: ["text", "value"] };
1380
+ }
1381
+ function sankeyTable(data) {
1382
+ return {
1383
+ headers: ["From", "To", "Value"],
1384
+ rows: data.links.map((link) => [link.source, link.target, link.value]),
1385
+ columns: ["text", "text", "value"]
1386
+ };
1387
+ }
1388
+ function candlestickTable(data) {
1389
+ const withVolume = !!data.volume?.length;
1390
+ return {
1391
+ headers: ["Date", "Open", "High", "Low", "Close", ...withVolume ? ["Volume"] : []],
1392
+ rows: data.data.map((item, index) => [
1393
+ data.dates[index] ?? "",
1394
+ item.open,
1395
+ item.high,
1396
+ item.low,
1397
+ item.close,
1398
+ ...withVolume ? [data.volume?.[index] ?? null] : []
1399
+ ]),
1400
+ columns: [
1401
+ "text",
1402
+ "value",
1403
+ "value",
1404
+ "value",
1405
+ "value",
1406
+ ...withVolume ? ["number"] : []
1407
+ ]
1408
+ };
1409
+ }
1410
+ function histogramTable(buckets) {
1411
+ return {
1412
+ headers: ["Range", "Count"],
1413
+ rows: buckets.map((bucket) => [bucket.label, bucket.count]),
1414
+ columns: ["text", "number"]
1415
+ };
1416
+ }
1417
+ function boxPlotTable(groups) {
1418
+ return {
1419
+ headers: ["Group", "Count", "Low", "Q1", "Median", "Q3", "High", "Outliers"],
1420
+ rows: groups.map(
1421
+ ({ name, stats }) => stats ? [
1422
+ name,
1423
+ stats.count,
1424
+ stats.low,
1425
+ stats.q1,
1426
+ stats.median,
1427
+ stats.q3,
1428
+ stats.high,
1429
+ stats.outliers.length
1430
+ ] : [name, 0, null, null, null, null, null, 0]
1431
+ ),
1432
+ columns: ["text", "number", "value", "value", "value", "value", "value", "number"]
1433
+ };
1434
+ }
1435
+ function waterfallTable(steps, bars) {
1436
+ return {
1437
+ headers: ["Step", "Change", "Total"],
1438
+ rows: steps.map((step, index) => [
1439
+ step.name,
1440
+ bars[index]?.kind === "total" ? null : bars[index]?.value ?? null,
1441
+ bars[index]?.end ?? null
1442
+ ]),
1443
+ columns: ["text", "value", "value"]
1444
+ };
1445
+ }
1446
+ function bulletTable(items) {
1447
+ const withTarget = items.some((item) => item.target !== void 0);
1448
+ return {
1449
+ headers: ["Name", "Value", ...withTarget ? ["Target"] : []],
1450
+ rows: items.map((item) => [
1451
+ item.name,
1452
+ item.value,
1453
+ ...withTarget ? [item.target ?? null] : []
1454
+ ]),
1455
+ columns: ["text", "value", ...withTarget ? ["value"] : []]
1456
+ };
1457
+ }
1458
+ function formatTableCell(table, cell, column, format) {
1459
+ if (typeof cell !== "number") return cell ?? "";
1460
+ switch (table.columns?.[column] ?? "value") {
1461
+ case "number":
1462
+ return formatNumber(cell, { maximumFractionDigits: 2 });
1463
+ case "percent":
1464
+ return `${cell}%`;
1465
+ case "text":
1466
+ return String(cell);
1467
+ default:
1468
+ return formatValue(cell, format);
1469
+ }
1470
+ }
1471
+ function csvCell(cell) {
1472
+ if (cell == null) return "";
1473
+ const text = String(cell);
1474
+ return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
1475
+ }
1476
+ function tableToCsv(table, format) {
1477
+ const lines = [table.headers, ...table.rows].map(
1478
+ (row, rowIndex) => row.map(
1479
+ (cell, column) => csvCell(rowIndex > 0 && format ? formatTableCell(table, cell, column, format) : cell)
1480
+ ).join(",")
1481
+ );
1482
+ return lines.join("\r\n");
1483
+ }
1484
+ function chartFilename(title) {
1485
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "chart";
1486
+ }
1487
+ function downloadFile(content, filename, mimeType = "text/plain") {
1488
+ if (typeof document === "undefined") return;
1489
+ const isDataUrl = content.startsWith("data:");
1490
+ const url = isDataUrl ? content : URL.createObjectURL(new Blob([content], { type: `${mimeType};charset=utf-8` }));
1491
+ const link = document.createElement("a");
1492
+ link.href = url;
1493
+ link.download = filename;
1494
+ link.style.display = "none";
1495
+ document.body.appendChild(link);
1496
+ link.click();
1497
+ link.remove();
1498
+ if (!isDataUrl) setTimeout(() => URL.revokeObjectURL(url), 0);
1499
+ }
1500
+ function downloadCsv(table, filename) {
1501
+ downloadFile(
1502
+ `\uFEFF${tableToCsv(table)}`,
1503
+ filename.endsWith(".csv") ? filename : `${filename}.csv`,
1504
+ "text/csv"
1505
+ );
1506
+ }
679
1507
 
680
1508
  exports.THEME_DARK = THEME_DARK;
681
1509
  exports.THEME_LIGHT = THEME_LIGHT;
682
1510
  exports.abbreviateNumber = abbreviateNumber;
1511
+ exports.allSeriesEmpty = allSeriesEmpty;
683
1512
  exports.anchorCenterGraphic = anchorCenterGraphic;
1513
+ exports.binValues = binValues;
1514
+ exports.boxPlotTable = boxPlotTable;
1515
+ exports.boxplotStats = boxplotStats;
684
1516
  exports.buildAxis = buildAxis;
685
1517
  exports.buildDataLabel = buildDataLabel;
1518
+ exports.buildDataZoom = buildDataZoom;
686
1519
  exports.buildLegend = buildLegend;
1520
+ exports.buildMarkArea = buildMarkArea;
1521
+ exports.buildMarkLine = buildMarkLine;
687
1522
  exports.buildTooltip = buildTooltip;
1523
+ exports.bulletTable = bulletTable;
1524
+ exports.candlestickTable = candlestickTable;
1525
+ exports.categoryTable = categoryTable;
1526
+ exports.chartFilename = chartFilename;
1527
+ exports.chartFormatters = chartFormatters;
688
1528
  exports.colorWithAlpha = colorWithAlpha;
1529
+ exports.computeDelta = computeDelta;
689
1530
  exports.createTooltipFormatter = createTooltipFormatter;
690
1531
  exports.darkChartColors = darkChartColors;
691
1532
  exports.darkTheme = darkTheme;
1533
+ exports.downloadCsv = downloadCsv;
1534
+ exports.downloadFile = downloadFile;
692
1535
  exports.escapeHtml = escapeHtml;
693
1536
  exports.formatCurrency = formatCurrency;
694
1537
  exports.formatDate = formatDate;
695
1538
  exports.formatNumber = formatNumber;
696
1539
  exports.formatPercent = formatPercent;
1540
+ exports.formatTableCell = formatTableCell;
697
1541
  exports.formatTooltipValue = formatTooltipValue;
1542
+ exports.formatValue = formatValue;
1543
+ exports.fromRecords = fromRecords;
1544
+ exports.generateAreaChartDescription = generateAreaChartDescription;
698
1545
  exports.generateBarChartDescription = generateBarChartDescription;
1546
+ exports.generateBoxPlotDescription = generateBoxPlotDescription;
1547
+ exports.generateBulletDescription = generateBulletDescription;
1548
+ exports.generateComboChartDescription = generateComboChartDescription;
699
1549
  exports.generateDarkTheme = generateDarkTheme;
700
1550
  exports.generateDataTable = generateDataTable;
1551
+ exports.generateDotPlotDescription = generateDotPlotDescription;
1552
+ exports.generateHistogramDescription = generateHistogramDescription;
701
1553
  exports.generateLightTheme = generateLightTheme;
702
1554
  exports.generateLineChartDescription = generateLineChartDescription;
703
1555
  exports.generatePieChartDescription = generatePieChartDescription;
704
1556
  exports.generateScatterChartDescription = generateScatterChartDescription;
1557
+ exports.generateWaterfallDescription = generateWaterfallDescription;
705
1558
  exports.getChartColors = getChartColors;
706
1559
  exports.getLegendItems = getLegendItems;
707
1560
  exports.getReplaceMerge = getReplaceMerge;
708
1561
  exports.gridInset = gridInset;
709
1562
  exports.hasPointLabels = hasPointLabels;
1563
+ exports.heatmapTable = heatmapTable;
1564
+ exports.histogramTable = histogramTable;
710
1565
  exports.itemLabelLookup = itemLabelLookup;
711
1566
  exports.legendCenter = legendCenter;
712
1567
  exports.lightChartColors = lightChartColors;
713
1568
  exports.lightTheme = lightTheme;
714
1569
  exports.mergeChartOption = mergeChartOption;
715
1570
  exports.nameLabelLookup = nameLabelLookup;
1571
+ exports.niceStep = niceStep;
1572
+ exports.pairWithCategories = pairWithCategories;
716
1573
  exports.pointLabel = pointLabel;
1574
+ exports.quantile = quantile;
1575
+ exports.radarTable = radarTable;
1576
+ exports.resolveChartHeight = resolveChartHeight;
1577
+ exports.resolveValueFormatter = resolveValueFormatter;
1578
+ exports.sankeyTable = sankeyTable;
1579
+ exports.scatterTable = scatterTable;
717
1580
  exports.seriesLabelLookup = seriesLabelLookup;
1581
+ exports.sliceTable = sliceTable;
1582
+ exports.tableToCsv = tableToCsv;
1583
+ exports.toPercentStack = toPercentStack;
1584
+ exports.toSliceData = toSliceData;
1585
+ exports.treemapTable = treemapTable;
1586
+ exports.trendLine = trendLine;
1587
+ exports.valueAxisConfig = valueAxisConfig;
1588
+ exports.valueLabelFormatter = valueLabelFormatter;
1589
+ exports.waterfallLayout = waterfallLayout;
1590
+ exports.waterfallTable = waterfallTable;
718
1591
  exports.withPointLabels = withPointLabels;
1592
+ exports.withReferenceMarks = withReferenceMarks;
1593
+ exports.withZoomInset = withZoomInset;