@classic-homes/charts-core 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +272 -218
- package/dist/index.d.cts +42 -2
- package/dist/index.d.ts +42 -2
- package/dist/index.js +270 -219
- package/dist/types/index.d.cts +6 -0
- package/dist/types/index.d.ts +6 -0
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -624,6 +624,237 @@ function mergeChartOption(generated, override) {
|
|
|
624
624
|
return merged;
|
|
625
625
|
}
|
|
626
626
|
|
|
627
|
+
// src/utils/stats.ts
|
|
628
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
629
|
+
var clean = (value) => Number(value.toPrecision(12));
|
|
630
|
+
function niceStep(rough) {
|
|
631
|
+
if (!(rough > 0) || !Number.isFinite(rough)) return 1;
|
|
632
|
+
const power = 10 ** Math.floor(Math.log10(rough));
|
|
633
|
+
const fraction = rough / power;
|
|
634
|
+
const nice = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10;
|
|
635
|
+
return clean(nice * power);
|
|
636
|
+
}
|
|
637
|
+
function bucketLabel(start, end, format) {
|
|
638
|
+
const text = (value) => formatValue(value, format);
|
|
639
|
+
if (start === -Infinity && end === Infinity) return "All";
|
|
640
|
+
if (start === -Infinity) return `< ${text(end)}`;
|
|
641
|
+
if (end === Infinity) return `${text(start)}+`;
|
|
642
|
+
return `${text(start)}\u2013${text(end)}`;
|
|
643
|
+
}
|
|
644
|
+
function binValues(values, options = {}) {
|
|
645
|
+
const { bins, binWidth, format } = options;
|
|
646
|
+
const finite = values.filter(isFiniteNumber);
|
|
647
|
+
let edges;
|
|
648
|
+
if (Array.isArray(bins)) {
|
|
649
|
+
edges = bins.map((bin) => ({
|
|
650
|
+
start: bin.start ?? -Infinity,
|
|
651
|
+
end: bin.end ?? Infinity,
|
|
652
|
+
label: bin.label
|
|
653
|
+
}));
|
|
654
|
+
} else {
|
|
655
|
+
if (!finite.length) return [];
|
|
656
|
+
let min = Infinity;
|
|
657
|
+
let max = -Infinity;
|
|
658
|
+
for (const value of finite) {
|
|
659
|
+
if (value < min) min = value;
|
|
660
|
+
if (value > max) max = value;
|
|
661
|
+
}
|
|
662
|
+
if (min === max) {
|
|
663
|
+
edges = [{ start: min, end: max }];
|
|
664
|
+
} else {
|
|
665
|
+
let step;
|
|
666
|
+
let start;
|
|
667
|
+
if (binWidth && binWidth > 0) {
|
|
668
|
+
step = binWidth;
|
|
669
|
+
start = Math.floor(min / step) * step;
|
|
670
|
+
} else if (typeof bins === "number" && bins > 0) {
|
|
671
|
+
step = (max - min) / Math.floor(bins);
|
|
672
|
+
start = min;
|
|
673
|
+
} else {
|
|
674
|
+
const sturges = Math.ceil(Math.log2(finite.length) + 1);
|
|
675
|
+
step = niceStep((max - min) / sturges);
|
|
676
|
+
start = Math.floor(min / step) * step;
|
|
677
|
+
}
|
|
678
|
+
const count = Math.max(1, Math.ceil(clean((max - start) / step)));
|
|
679
|
+
edges = Array.from({ length: count }, (_, index) => ({
|
|
680
|
+
start: clean(start + index * step),
|
|
681
|
+
end: clean(start + (index + 1) * step)
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
const buckets = edges.map(({ start, end, label }) => ({
|
|
686
|
+
label: label ?? bucketLabel(start, end, format),
|
|
687
|
+
start,
|
|
688
|
+
end,
|
|
689
|
+
count: 0
|
|
690
|
+
}));
|
|
691
|
+
const last = buckets.length - 1;
|
|
692
|
+
for (const value of finite) {
|
|
693
|
+
const index = buckets.findIndex(
|
|
694
|
+
(bucket, i) => value >= bucket.start && (value < bucket.end || i === last && value <= bucket.end)
|
|
695
|
+
);
|
|
696
|
+
if (index !== -1) buckets[index].count++;
|
|
697
|
+
}
|
|
698
|
+
return buckets;
|
|
699
|
+
}
|
|
700
|
+
function quantile(sorted, q) {
|
|
701
|
+
if (!sorted.length) return NaN;
|
|
702
|
+
const position = (sorted.length - 1) * q;
|
|
703
|
+
const lower = Math.floor(position);
|
|
704
|
+
const upper = Math.ceil(position);
|
|
705
|
+
if (lower === upper) return sorted[lower];
|
|
706
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
|
|
707
|
+
}
|
|
708
|
+
function boxplotStats(values) {
|
|
709
|
+
const sorted = values.filter(isFiniteNumber).sort((a, b) => a - b);
|
|
710
|
+
if (!sorted.length) return null;
|
|
711
|
+
const q1 = quantile(sorted, 0.25);
|
|
712
|
+
const median = quantile(sorted, 0.5);
|
|
713
|
+
const q3 = quantile(sorted, 0.75);
|
|
714
|
+
const iqr = q3 - q1;
|
|
715
|
+
const lowFence = q1 - 1.5 * iqr;
|
|
716
|
+
const highFence = q3 + 1.5 * iqr;
|
|
717
|
+
const inside = sorted.filter((value) => value >= lowFence && value <= highFence);
|
|
718
|
+
const outliers = sorted.filter((value) => value < lowFence || value > highFence);
|
|
719
|
+
return {
|
|
720
|
+
low: inside[0] ?? q1,
|
|
721
|
+
q1,
|
|
722
|
+
median,
|
|
723
|
+
q3,
|
|
724
|
+
high: inside[inside.length - 1] ?? q3,
|
|
725
|
+
outliers,
|
|
726
|
+
count: sorted.length,
|
|
727
|
+
mean: sorted.reduce((sum, value) => sum + value, 0) / sorted.length
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function waterfallLayout(steps) {
|
|
731
|
+
let running = 0;
|
|
732
|
+
return steps.map((step) => {
|
|
733
|
+
let start;
|
|
734
|
+
let end;
|
|
735
|
+
let kind;
|
|
736
|
+
if (step.isTotal === "value") {
|
|
737
|
+
start = 0;
|
|
738
|
+
end = isFiniteNumber(step.value) ? step.value : running;
|
|
739
|
+
kind = "total";
|
|
740
|
+
} else if (step.isTotal) {
|
|
741
|
+
start = 0;
|
|
742
|
+
end = running;
|
|
743
|
+
kind = "total";
|
|
744
|
+
} else {
|
|
745
|
+
const change = isFiniteNumber(step.value) ? step.value : 0;
|
|
746
|
+
start = running;
|
|
747
|
+
end = running + change;
|
|
748
|
+
kind = change < 0 ? "decrease" : "increase";
|
|
749
|
+
}
|
|
750
|
+
running = end;
|
|
751
|
+
const low = Math.min(start, end);
|
|
752
|
+
const high = Math.max(start, end);
|
|
753
|
+
let base = 0;
|
|
754
|
+
let positive = 0;
|
|
755
|
+
let negative = 0;
|
|
756
|
+
if (low >= 0) {
|
|
757
|
+
base = low;
|
|
758
|
+
positive = high - low;
|
|
759
|
+
} else if (high <= 0) {
|
|
760
|
+
base = high;
|
|
761
|
+
negative = low - high;
|
|
762
|
+
} else {
|
|
763
|
+
positive = high;
|
|
764
|
+
negative = low;
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
start,
|
|
768
|
+
end,
|
|
769
|
+
value: kind === "total" ? end : end - start,
|
|
770
|
+
kind,
|
|
771
|
+
base: clean(base),
|
|
772
|
+
positive: clean(positive),
|
|
773
|
+
negative: clean(negative)
|
|
774
|
+
};
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
function toPercentStack(series) {
|
|
778
|
+
const length = Math.max(0, ...series.map((s) => s.data.length));
|
|
779
|
+
const totals = Array.from(
|
|
780
|
+
{ length },
|
|
781
|
+
(_, index) => series.reduce((sum, s) => {
|
|
782
|
+
const value = s.data[index];
|
|
783
|
+
return sum + (isFiniteNumber(value) ? Math.abs(value) : 0);
|
|
784
|
+
}, 0)
|
|
785
|
+
);
|
|
786
|
+
return series.map((s) => ({
|
|
787
|
+
...s,
|
|
788
|
+
data: s.data.map((value, index) => {
|
|
789
|
+
if (!isFiniteNumber(value)) return null;
|
|
790
|
+
return totals[index] === 0 ? 0 : clean(value / totals[index] * 100);
|
|
791
|
+
})
|
|
792
|
+
}));
|
|
793
|
+
}
|
|
794
|
+
function stackTotals(series, stacked) {
|
|
795
|
+
const length = Math.max(0, ...series.map((s) => s.data.length));
|
|
796
|
+
const totals = /* @__PURE__ */ new Map();
|
|
797
|
+
for (const s of series) {
|
|
798
|
+
const stack = s.stack ?? (stacked ? "Total" : void 0);
|
|
799
|
+
if (stack === void 0) continue;
|
|
800
|
+
const sums = totals.get(stack) ?? Array.from({ length }, () => null);
|
|
801
|
+
s.data.forEach((value, index) => {
|
|
802
|
+
if (isFiniteNumber(value)) sums[index] = clean((sums[index] ?? 0) + value);
|
|
803
|
+
});
|
|
804
|
+
totals.set(stack, sums);
|
|
805
|
+
}
|
|
806
|
+
return totals;
|
|
807
|
+
}
|
|
808
|
+
function pairWithCategories(categories, values, axisType) {
|
|
809
|
+
if (axisType !== "time" && axisType !== "value") return values;
|
|
810
|
+
return values.map((value, index) => [categories[index], value]);
|
|
811
|
+
}
|
|
812
|
+
function resolveChartHeight(height, rows, { rowHeight = 28, chrome = 56, minHeight = 160, maxHeight } = {}) {
|
|
813
|
+
if (height !== "auto") return height;
|
|
814
|
+
let value = Math.max(minHeight, chrome + Math.max(rows, 1) * rowHeight);
|
|
815
|
+
if (maxHeight !== void 0) value = Math.min(value, maxHeight);
|
|
816
|
+
return value;
|
|
817
|
+
}
|
|
818
|
+
function allSeriesEmpty(series) {
|
|
819
|
+
return !series?.length || series.every((s) => !s.data?.some((value) => value != null));
|
|
820
|
+
}
|
|
821
|
+
function trendLine(points) {
|
|
822
|
+
const finite = points.filter(([x, y]) => isFiniteNumber(x) && isFiniteNumber(y));
|
|
823
|
+
const n = finite.length;
|
|
824
|
+
if (n < 2) return null;
|
|
825
|
+
let sumX = 0;
|
|
826
|
+
let sumY = 0;
|
|
827
|
+
let sumXY = 0;
|
|
828
|
+
let sumX2 = 0;
|
|
829
|
+
let minX = Infinity;
|
|
830
|
+
let maxX = -Infinity;
|
|
831
|
+
for (const [x, y] of finite) {
|
|
832
|
+
sumX += x;
|
|
833
|
+
sumY += y;
|
|
834
|
+
sumXY += x * y;
|
|
835
|
+
sumX2 += x * x;
|
|
836
|
+
if (x < minX) minX = x;
|
|
837
|
+
if (x > maxX) maxX = x;
|
|
838
|
+
}
|
|
839
|
+
const denominator = n * sumX2 - sumX * sumX;
|
|
840
|
+
if (minX === maxX || denominator === 0) return null;
|
|
841
|
+
const slope = (n * sumXY - sumX * sumY) / denominator;
|
|
842
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
843
|
+
return [
|
|
844
|
+
[minX, slope * minX + intercept],
|
|
845
|
+
[maxX, slope * maxX + intercept]
|
|
846
|
+
];
|
|
847
|
+
}
|
|
848
|
+
function computeDelta(current, previous) {
|
|
849
|
+
if (!isFiniteNumber(current) || !isFiniteNumber(previous)) return null;
|
|
850
|
+
const change = clean(current - previous);
|
|
851
|
+
return {
|
|
852
|
+
change,
|
|
853
|
+
ratio: previous === 0 ? null : change / Math.abs(previous),
|
|
854
|
+
direction: change > 0 ? "up" : change < 0 ? "down" : "flat"
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
627
858
|
// src/utils/options.ts
|
|
628
859
|
function toParams(raw, getLabel) {
|
|
629
860
|
const seriesIndex = raw.seriesIndex ?? 0;
|
|
@@ -908,222 +1139,34 @@ function withZoomInset(inset, mode, axis) {
|
|
|
908
1139
|
const current = typeof inset?.[edge] === "number" ? inset[edge] : 12;
|
|
909
1140
|
return { ...inset, [edge]: current + 36 };
|
|
910
1141
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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;
|
|
1142
|
+
function buildStackTotalSeries(categories, series, { stacked, position, format, axisType }) {
|
|
1143
|
+
return [...stackTotals(series, stacked)].map(([stack, totals]) => ({
|
|
1144
|
+
id: `__total:${stack}`,
|
|
1145
|
+
type: "bar",
|
|
1146
|
+
stack,
|
|
1147
|
+
data: pairWithCategories(
|
|
1148
|
+
categories,
|
|
1149
|
+
totals.map((total) => total === null ? null : 0),
|
|
1150
|
+
axisType
|
|
1151
|
+
),
|
|
1152
|
+
silent: true,
|
|
1153
|
+
legendHoverLink: false,
|
|
1154
|
+
tooltip: { show: false },
|
|
1155
|
+
itemStyle: { color: "transparent" },
|
|
1156
|
+
emphasis: { disabled: true },
|
|
1157
|
+
label: {
|
|
1158
|
+
show: true,
|
|
1159
|
+
position,
|
|
1160
|
+
fontWeight: "bold",
|
|
1161
|
+
formatter: (params) => formatValue(totals[params.dataIndex], format)
|
|
1050
1162
|
}
|
|
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
1163
|
}));
|
|
1078
1164
|
}
|
|
1079
|
-
function
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
-
};
|
|
1165
|
+
function withEndLabelInset(inset, labels) {
|
|
1166
|
+
const longest = Math.max(0, ...labels.map((label) => label.length));
|
|
1167
|
+
if (longest === 0) return inset;
|
|
1168
|
+
const current = typeof inset?.right === "number" ? inset.right : 0;
|
|
1169
|
+
return { ...inset, right: current + longest * 7 + 16 };
|
|
1127
1170
|
}
|
|
1128
1171
|
|
|
1129
1172
|
// src/utils/records.ts
|
|
@@ -1290,10 +1333,11 @@ function toSliceData(rows, options) {
|
|
|
1290
1333
|
nullLabel = "Unknown",
|
|
1291
1334
|
colors: colors3
|
|
1292
1335
|
} = options;
|
|
1336
|
+
const effective = value === void 0 ? "count" : aggregate;
|
|
1293
1337
|
const { categories, series } = fromRecords(rows, {
|
|
1294
1338
|
category: name,
|
|
1295
1339
|
value,
|
|
1296
|
-
aggregate:
|
|
1340
|
+
aggregate: effective,
|
|
1297
1341
|
nullLabel
|
|
1298
1342
|
});
|
|
1299
1343
|
const values = series[0]?.data ?? [];
|
|
@@ -1307,7 +1351,12 @@ function toSliceData(rows, options) {
|
|
|
1307
1351
|
const rest = slices.slice(topN);
|
|
1308
1352
|
slices = slices.slice(0, topN);
|
|
1309
1353
|
if (otherLabel !== void 0) {
|
|
1310
|
-
|
|
1354
|
+
const folded = new Set(rest.map((slice) => slice.name));
|
|
1355
|
+
const other = fromRecords(
|
|
1356
|
+
(rows ?? []).filter((row) => folded.has(toLabel(read(row, name), nullLabel))),
|
|
1357
|
+
{ category: () => otherLabel, value, aggregate: effective }
|
|
1358
|
+
).series[0]?.data[0];
|
|
1359
|
+
slices.push({ name: otherLabel, value: other ?? 0 });
|
|
1311
1360
|
}
|
|
1312
1361
|
}
|
|
1313
1362
|
return slices.map(
|
|
@@ -1468,15 +1517,17 @@ function formatTableCell(table, cell, column, format) {
|
|
|
1468
1517
|
return formatValue(cell, format);
|
|
1469
1518
|
}
|
|
1470
1519
|
}
|
|
1471
|
-
|
|
1520
|
+
var FORMULA_START = /^[=+\-@\t\r]/;
|
|
1521
|
+
function csvCell(cell, raw = cell) {
|
|
1472
1522
|
if (cell == null) return "";
|
|
1473
|
-
|
|
1523
|
+
let text = String(cell);
|
|
1524
|
+
if (typeof raw === "string" && FORMULA_START.test(text)) text = `'${text}`;
|
|
1474
1525
|
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
1475
1526
|
}
|
|
1476
1527
|
function tableToCsv(table, format) {
|
|
1477
1528
|
const lines = [table.headers, ...table.rows].map(
|
|
1478
1529
|
(row, rowIndex) => row.map(
|
|
1479
|
-
(cell, column) => csvCell(rowIndex > 0 && format ? formatTableCell(table, cell, column, format) : cell)
|
|
1530
|
+
(cell, column) => csvCell(rowIndex > 0 && format ? formatTableCell(table, cell, column, format) : cell, cell)
|
|
1480
1531
|
).join(",")
|
|
1481
1532
|
);
|
|
1482
1533
|
return lines.join("\r\n");
|
|
@@ -1519,6 +1570,7 @@ exports.buildDataZoom = buildDataZoom;
|
|
|
1519
1570
|
exports.buildLegend = buildLegend;
|
|
1520
1571
|
exports.buildMarkArea = buildMarkArea;
|
|
1521
1572
|
exports.buildMarkLine = buildMarkLine;
|
|
1573
|
+
exports.buildStackTotalSeries = buildStackTotalSeries;
|
|
1522
1574
|
exports.buildTooltip = buildTooltip;
|
|
1523
1575
|
exports.bulletTable = bulletTable;
|
|
1524
1576
|
exports.candlestickTable = candlestickTable;
|
|
@@ -1579,6 +1631,7 @@ exports.sankeyTable = sankeyTable;
|
|
|
1579
1631
|
exports.scatterTable = scatterTable;
|
|
1580
1632
|
exports.seriesLabelLookup = seriesLabelLookup;
|
|
1581
1633
|
exports.sliceTable = sliceTable;
|
|
1634
|
+
exports.stackTotals = stackTotals;
|
|
1582
1635
|
exports.tableToCsv = tableToCsv;
|
|
1583
1636
|
exports.toPercentStack = toPercentStack;
|
|
1584
1637
|
exports.toSliceData = toSliceData;
|
|
@@ -1588,6 +1641,7 @@ exports.valueAxisConfig = valueAxisConfig;
|
|
|
1588
1641
|
exports.valueLabelFormatter = valueLabelFormatter;
|
|
1589
1642
|
exports.waterfallLayout = waterfallLayout;
|
|
1590
1643
|
exports.waterfallTable = waterfallTable;
|
|
1644
|
+
exports.withEndLabelInset = withEndLabelInset;
|
|
1591
1645
|
exports.withPointLabels = withPointLabels;
|
|
1592
1646
|
exports.withReferenceMarks = withReferenceMarks;
|
|
1593
1647
|
exports.withZoomInset = withZoomInset;
|
package/dist/index.d.cts
CHANGED
|
@@ -350,6 +350,32 @@ bottomOffset?: number): AnyObject[] | undefined;
|
|
|
350
350
|
* Grid room for a data zoom slider, added to a `gridInset` result
|
|
351
351
|
*/
|
|
352
352
|
declare function withZoomInset(inset: AnyObject | undefined, mode: boolean | 'inside' | 'slider' | undefined, axis: 'x' | 'y'): AnyObject | undefined;
|
|
353
|
+
interface StackTotalOptions {
|
|
354
|
+
/** The chart's `stacked` prop */
|
|
355
|
+
stacked: boolean | 'percent' | undefined;
|
|
356
|
+
/** Where the label sits: 'right' for horizontal bars, 'top' for vertical ones */
|
|
357
|
+
position: 'right' | 'top';
|
|
358
|
+
/** Formats each total (the resolved value format) */
|
|
359
|
+
format?: (value: number) => string;
|
|
360
|
+
/** The category axis type, for time or value axes that read [x, y] points */
|
|
361
|
+
axisType?: string;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Invisible series that label each stack's total at the end of the bar: a
|
|
365
|
+
* zero-height bar on top of every stack, carrying the total as its label.
|
|
366
|
+
* They are silent and left out of the tooltip; the legend lists only the
|
|
367
|
+
* real series.
|
|
368
|
+
*/
|
|
369
|
+
declare function buildStackTotalSeries(categories: (string | number)[], series: {
|
|
370
|
+
data: (number | null)[];
|
|
371
|
+
stack?: string;
|
|
372
|
+
}[], { stacked, position, format, axisType }: StackTotalOptions): AnyObject[];
|
|
373
|
+
/**
|
|
374
|
+
* Widen the grid's right edge for labels drawn past the end of horizontal
|
|
375
|
+
* bars, which `containLabel` does not reserve room for. Width is estimated
|
|
376
|
+
* from the longest label.
|
|
377
|
+
*/
|
|
378
|
+
declare function withEndLabelInset(inset: AnyObject | undefined, labels: string[]): AnyObject | undefined;
|
|
353
379
|
|
|
354
380
|
/**
|
|
355
381
|
* Data preparation for distribution, waterfall and stacked charts, and
|
|
@@ -417,6 +443,16 @@ declare function waterfallLayout(steps: WaterfallStep[]): WaterfallBar[];
|
|
|
417
443
|
declare function toPercentStack<S extends {
|
|
418
444
|
data: (number | null)[];
|
|
419
445
|
}>(series: S[]): S[];
|
|
446
|
+
/**
|
|
447
|
+
* Each stack's total by category, keyed by stack id. A series stacks under
|
|
448
|
+
* its own `stack` id, or 'Total' when `stacked` is set; unstacked series are
|
|
449
|
+
* left out. Totals are the raw values, also for percent stacks. A category
|
|
450
|
+
* with no values in a stack gets `null`.
|
|
451
|
+
*/
|
|
452
|
+
declare function stackTotals(series: {
|
|
453
|
+
data: (number | null)[];
|
|
454
|
+
stack?: string;
|
|
455
|
+
}[], stacked: boolean | 'percent' | undefined): Map<string, (number | null)[]>;
|
|
420
456
|
/**
|
|
421
457
|
* Pair category labels with values for a time or value x-axis, which reads
|
|
422
458
|
* [x, y] points rather than values by category index
|
|
@@ -536,7 +572,11 @@ interface ToSliceDataOptions<T> {
|
|
|
536
572
|
sort?: 'desc' | 'asc' | 'none';
|
|
537
573
|
/** Keep only the largest N slices */
|
|
538
574
|
topN?: number;
|
|
539
|
-
/**
|
|
575
|
+
/**
|
|
576
|
+
* Fold slices beyond `topN` into one with this label; omit to drop them.
|
|
577
|
+
* The folded slice aggregates the folded records, so with 'avg' it is their
|
|
578
|
+
* average rather than a sum of averages.
|
|
579
|
+
*/
|
|
540
580
|
otherLabel?: string;
|
|
541
581
|
/** Label for a missing name (defaults to 'Unknown') */
|
|
542
582
|
nullLabel?: string;
|
|
@@ -627,4 +667,4 @@ declare function downloadFile(content: string, filename: string, mimeType?: stri
|
|
|
627
667
|
*/
|
|
628
668
|
declare function downloadCsv(table: ChartDataTable, filename: string): void;
|
|
629
669
|
|
|
630
|
-
export { type Accessor, type Aggregate, type AutoHeightOptions, AxisConfig, BarChartData, type BinValuesOptions, BoxPlotStats, type BuildDataLabelOptions, type BuildLegendOptions, type BuildTooltipOptions, BulletItem, CandlestickData, CategoryChartData, CategorySeriesData, ChartDataTable, ChartFormatter, type ChartFormatterOptions, type ChartFormatters, ChartLegendConfig, type Delta, type FromRecordsOptions, HeatmapData, HistogramBin, HistogramBucket, LabelFormatterParams, LegendItem, LineChartData, PieSliceData, type PointLabelLookup, RadarChartData, RadarIndicator, ReferenceBand, ReferenceLine, type ReferenceMarkOptions, SankeyData, ScatterChartData, type ToSliceDataOptions, TooltipFormatterParams, TreemapNode, ValueFormat, type WaterfallBar, WaterfallStep, 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 };
|
|
670
|
+
export { type Accessor, type Aggregate, type AutoHeightOptions, AxisConfig, BarChartData, type BinValuesOptions, BoxPlotStats, type BuildDataLabelOptions, type BuildLegendOptions, type BuildTooltipOptions, BulletItem, CandlestickData, CategoryChartData, CategorySeriesData, ChartDataTable, ChartFormatter, type ChartFormatterOptions, type ChartFormatters, ChartLegendConfig, type Delta, type FromRecordsOptions, HeatmapData, HistogramBin, HistogramBucket, LabelFormatterParams, LegendItem, LineChartData, PieSliceData, type PointLabelLookup, RadarChartData, RadarIndicator, ReferenceBand, ReferenceLine, type ReferenceMarkOptions, SankeyData, ScatterChartData, type StackTotalOptions, type ToSliceDataOptions, TooltipFormatterParams, TreemapNode, ValueFormat, type WaterfallBar, WaterfallStep, abbreviateNumber, allSeriesEmpty, anchorCenterGraphic, binValues, boxPlotTable, boxplotStats, buildAxis, buildDataLabel, buildDataZoom, buildLegend, buildMarkArea, buildMarkLine, buildStackTotalSeries, 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, stackTotals, tableToCsv, toPercentStack, toSliceData, treemapTable, trendLine, valueAxisConfig, valueLabelFormatter, waterfallLayout, waterfallTable, withEndLabelInset, withPointLabels, withReferenceMarks, withZoomInset };
|
package/dist/index.d.ts
CHANGED
|
@@ -350,6 +350,32 @@ bottomOffset?: number): AnyObject[] | undefined;
|
|
|
350
350
|
* Grid room for a data zoom slider, added to a `gridInset` result
|
|
351
351
|
*/
|
|
352
352
|
declare function withZoomInset(inset: AnyObject | undefined, mode: boolean | 'inside' | 'slider' | undefined, axis: 'x' | 'y'): AnyObject | undefined;
|
|
353
|
+
interface StackTotalOptions {
|
|
354
|
+
/** The chart's `stacked` prop */
|
|
355
|
+
stacked: boolean | 'percent' | undefined;
|
|
356
|
+
/** Where the label sits: 'right' for horizontal bars, 'top' for vertical ones */
|
|
357
|
+
position: 'right' | 'top';
|
|
358
|
+
/** Formats each total (the resolved value format) */
|
|
359
|
+
format?: (value: number) => string;
|
|
360
|
+
/** The category axis type, for time or value axes that read [x, y] points */
|
|
361
|
+
axisType?: string;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Invisible series that label each stack's total at the end of the bar: a
|
|
365
|
+
* zero-height bar on top of every stack, carrying the total as its label.
|
|
366
|
+
* They are silent and left out of the tooltip; the legend lists only the
|
|
367
|
+
* real series.
|
|
368
|
+
*/
|
|
369
|
+
declare function buildStackTotalSeries(categories: (string | number)[], series: {
|
|
370
|
+
data: (number | null)[];
|
|
371
|
+
stack?: string;
|
|
372
|
+
}[], { stacked, position, format, axisType }: StackTotalOptions): AnyObject[];
|
|
373
|
+
/**
|
|
374
|
+
* Widen the grid's right edge for labels drawn past the end of horizontal
|
|
375
|
+
* bars, which `containLabel` does not reserve room for. Width is estimated
|
|
376
|
+
* from the longest label.
|
|
377
|
+
*/
|
|
378
|
+
declare function withEndLabelInset(inset: AnyObject | undefined, labels: string[]): AnyObject | undefined;
|
|
353
379
|
|
|
354
380
|
/**
|
|
355
381
|
* Data preparation for distribution, waterfall and stacked charts, and
|
|
@@ -417,6 +443,16 @@ declare function waterfallLayout(steps: WaterfallStep[]): WaterfallBar[];
|
|
|
417
443
|
declare function toPercentStack<S extends {
|
|
418
444
|
data: (number | null)[];
|
|
419
445
|
}>(series: S[]): S[];
|
|
446
|
+
/**
|
|
447
|
+
* Each stack's total by category, keyed by stack id. A series stacks under
|
|
448
|
+
* its own `stack` id, or 'Total' when `stacked` is set; unstacked series are
|
|
449
|
+
* left out. Totals are the raw values, also for percent stacks. A category
|
|
450
|
+
* with no values in a stack gets `null`.
|
|
451
|
+
*/
|
|
452
|
+
declare function stackTotals(series: {
|
|
453
|
+
data: (number | null)[];
|
|
454
|
+
stack?: string;
|
|
455
|
+
}[], stacked: boolean | 'percent' | undefined): Map<string, (number | null)[]>;
|
|
420
456
|
/**
|
|
421
457
|
* Pair category labels with values for a time or value x-axis, which reads
|
|
422
458
|
* [x, y] points rather than values by category index
|
|
@@ -536,7 +572,11 @@ interface ToSliceDataOptions<T> {
|
|
|
536
572
|
sort?: 'desc' | 'asc' | 'none';
|
|
537
573
|
/** Keep only the largest N slices */
|
|
538
574
|
topN?: number;
|
|
539
|
-
/**
|
|
575
|
+
/**
|
|
576
|
+
* Fold slices beyond `topN` into one with this label; omit to drop them.
|
|
577
|
+
* The folded slice aggregates the folded records, so with 'avg' it is their
|
|
578
|
+
* average rather than a sum of averages.
|
|
579
|
+
*/
|
|
540
580
|
otherLabel?: string;
|
|
541
581
|
/** Label for a missing name (defaults to 'Unknown') */
|
|
542
582
|
nullLabel?: string;
|
|
@@ -627,4 +667,4 @@ declare function downloadFile(content: string, filename: string, mimeType?: stri
|
|
|
627
667
|
*/
|
|
628
668
|
declare function downloadCsv(table: ChartDataTable, filename: string): void;
|
|
629
669
|
|
|
630
|
-
export { type Accessor, type Aggregate, type AutoHeightOptions, AxisConfig, BarChartData, type BinValuesOptions, BoxPlotStats, type BuildDataLabelOptions, type BuildLegendOptions, type BuildTooltipOptions, BulletItem, CandlestickData, CategoryChartData, CategorySeriesData, ChartDataTable, ChartFormatter, type ChartFormatterOptions, type ChartFormatters, ChartLegendConfig, type Delta, type FromRecordsOptions, HeatmapData, HistogramBin, HistogramBucket, LabelFormatterParams, LegendItem, LineChartData, PieSliceData, type PointLabelLookup, RadarChartData, RadarIndicator, ReferenceBand, ReferenceLine, type ReferenceMarkOptions, SankeyData, ScatterChartData, type ToSliceDataOptions, TooltipFormatterParams, TreemapNode, ValueFormat, type WaterfallBar, WaterfallStep, 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 };
|
|
670
|
+
export { type Accessor, type Aggregate, type AutoHeightOptions, AxisConfig, BarChartData, type BinValuesOptions, BoxPlotStats, type BuildDataLabelOptions, type BuildLegendOptions, type BuildTooltipOptions, BulletItem, CandlestickData, CategoryChartData, CategorySeriesData, ChartDataTable, ChartFormatter, type ChartFormatterOptions, type ChartFormatters, ChartLegendConfig, type Delta, type FromRecordsOptions, HeatmapData, HistogramBin, HistogramBucket, LabelFormatterParams, LegendItem, LineChartData, PieSliceData, type PointLabelLookup, RadarChartData, RadarIndicator, ReferenceBand, ReferenceLine, type ReferenceMarkOptions, SankeyData, ScatterChartData, type StackTotalOptions, type ToSliceDataOptions, TooltipFormatterParams, TreemapNode, ValueFormat, type WaterfallBar, WaterfallStep, abbreviateNumber, allSeriesEmpty, anchorCenterGraphic, binValues, boxPlotTable, boxplotStats, buildAxis, buildDataLabel, buildDataZoom, buildLegend, buildMarkArea, buildMarkLine, buildStackTotalSeries, 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, stackTotals, tableToCsv, toPercentStack, toSliceData, treemapTable, trendLine, valueAxisConfig, valueLabelFormatter, waterfallLayout, waterfallTable, withEndLabelInset, withPointLabels, withReferenceMarks, withZoomInset };
|
package/dist/index.js
CHANGED
|
@@ -303,6 +303,237 @@ function mergeChartOption(generated, override) {
|
|
|
303
303
|
return merged;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
// src/utils/stats.ts
|
|
307
|
+
var isFiniteNumber = (value) => typeof value === "number" && Number.isFinite(value);
|
|
308
|
+
var clean = (value) => Number(value.toPrecision(12));
|
|
309
|
+
function niceStep(rough) {
|
|
310
|
+
if (!(rough > 0) || !Number.isFinite(rough)) return 1;
|
|
311
|
+
const power = 10 ** Math.floor(Math.log10(rough));
|
|
312
|
+
const fraction = rough / power;
|
|
313
|
+
const nice = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10;
|
|
314
|
+
return clean(nice * power);
|
|
315
|
+
}
|
|
316
|
+
function bucketLabel(start, end, format) {
|
|
317
|
+
const text = (value) => formatValue(value, format);
|
|
318
|
+
if (start === -Infinity && end === Infinity) return "All";
|
|
319
|
+
if (start === -Infinity) return `< ${text(end)}`;
|
|
320
|
+
if (end === Infinity) return `${text(start)}+`;
|
|
321
|
+
return `${text(start)}\u2013${text(end)}`;
|
|
322
|
+
}
|
|
323
|
+
function binValues(values, options = {}) {
|
|
324
|
+
const { bins, binWidth, format } = options;
|
|
325
|
+
const finite = values.filter(isFiniteNumber);
|
|
326
|
+
let edges;
|
|
327
|
+
if (Array.isArray(bins)) {
|
|
328
|
+
edges = bins.map((bin) => ({
|
|
329
|
+
start: bin.start ?? -Infinity,
|
|
330
|
+
end: bin.end ?? Infinity,
|
|
331
|
+
label: bin.label
|
|
332
|
+
}));
|
|
333
|
+
} else {
|
|
334
|
+
if (!finite.length) return [];
|
|
335
|
+
let min = Infinity;
|
|
336
|
+
let max = -Infinity;
|
|
337
|
+
for (const value of finite) {
|
|
338
|
+
if (value < min) min = value;
|
|
339
|
+
if (value > max) max = value;
|
|
340
|
+
}
|
|
341
|
+
if (min === max) {
|
|
342
|
+
edges = [{ start: min, end: max }];
|
|
343
|
+
} else {
|
|
344
|
+
let step;
|
|
345
|
+
let start;
|
|
346
|
+
if (binWidth && binWidth > 0) {
|
|
347
|
+
step = binWidth;
|
|
348
|
+
start = Math.floor(min / step) * step;
|
|
349
|
+
} else if (typeof bins === "number" && bins > 0) {
|
|
350
|
+
step = (max - min) / Math.floor(bins);
|
|
351
|
+
start = min;
|
|
352
|
+
} else {
|
|
353
|
+
const sturges = Math.ceil(Math.log2(finite.length) + 1);
|
|
354
|
+
step = niceStep((max - min) / sturges);
|
|
355
|
+
start = Math.floor(min / step) * step;
|
|
356
|
+
}
|
|
357
|
+
const count = Math.max(1, Math.ceil(clean((max - start) / step)));
|
|
358
|
+
edges = Array.from({ length: count }, (_, index) => ({
|
|
359
|
+
start: clean(start + index * step),
|
|
360
|
+
end: clean(start + (index + 1) * step)
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
const buckets = edges.map(({ start, end, label }) => ({
|
|
365
|
+
label: label ?? bucketLabel(start, end, format),
|
|
366
|
+
start,
|
|
367
|
+
end,
|
|
368
|
+
count: 0
|
|
369
|
+
}));
|
|
370
|
+
const last = buckets.length - 1;
|
|
371
|
+
for (const value of finite) {
|
|
372
|
+
const index = buckets.findIndex(
|
|
373
|
+
(bucket, i) => value >= bucket.start && (value < bucket.end || i === last && value <= bucket.end)
|
|
374
|
+
);
|
|
375
|
+
if (index !== -1) buckets[index].count++;
|
|
376
|
+
}
|
|
377
|
+
return buckets;
|
|
378
|
+
}
|
|
379
|
+
function quantile(sorted, q) {
|
|
380
|
+
if (!sorted.length) return NaN;
|
|
381
|
+
const position = (sorted.length - 1) * q;
|
|
382
|
+
const lower = Math.floor(position);
|
|
383
|
+
const upper = Math.ceil(position);
|
|
384
|
+
if (lower === upper) return sorted[lower];
|
|
385
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
|
|
386
|
+
}
|
|
387
|
+
function boxplotStats(values) {
|
|
388
|
+
const sorted = values.filter(isFiniteNumber).sort((a, b) => a - b);
|
|
389
|
+
if (!sorted.length) return null;
|
|
390
|
+
const q1 = quantile(sorted, 0.25);
|
|
391
|
+
const median = quantile(sorted, 0.5);
|
|
392
|
+
const q3 = quantile(sorted, 0.75);
|
|
393
|
+
const iqr = q3 - q1;
|
|
394
|
+
const lowFence = q1 - 1.5 * iqr;
|
|
395
|
+
const highFence = q3 + 1.5 * iqr;
|
|
396
|
+
const inside = sorted.filter((value) => value >= lowFence && value <= highFence);
|
|
397
|
+
const outliers = sorted.filter((value) => value < lowFence || value > highFence);
|
|
398
|
+
return {
|
|
399
|
+
low: inside[0] ?? q1,
|
|
400
|
+
q1,
|
|
401
|
+
median,
|
|
402
|
+
q3,
|
|
403
|
+
high: inside[inside.length - 1] ?? q3,
|
|
404
|
+
outliers,
|
|
405
|
+
count: sorted.length,
|
|
406
|
+
mean: sorted.reduce((sum, value) => sum + value, 0) / sorted.length
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function waterfallLayout(steps) {
|
|
410
|
+
let running = 0;
|
|
411
|
+
return steps.map((step) => {
|
|
412
|
+
let start;
|
|
413
|
+
let end;
|
|
414
|
+
let kind;
|
|
415
|
+
if (step.isTotal === "value") {
|
|
416
|
+
start = 0;
|
|
417
|
+
end = isFiniteNumber(step.value) ? step.value : running;
|
|
418
|
+
kind = "total";
|
|
419
|
+
} else if (step.isTotal) {
|
|
420
|
+
start = 0;
|
|
421
|
+
end = running;
|
|
422
|
+
kind = "total";
|
|
423
|
+
} else {
|
|
424
|
+
const change = isFiniteNumber(step.value) ? step.value : 0;
|
|
425
|
+
start = running;
|
|
426
|
+
end = running + change;
|
|
427
|
+
kind = change < 0 ? "decrease" : "increase";
|
|
428
|
+
}
|
|
429
|
+
running = end;
|
|
430
|
+
const low = Math.min(start, end);
|
|
431
|
+
const high = Math.max(start, end);
|
|
432
|
+
let base = 0;
|
|
433
|
+
let positive = 0;
|
|
434
|
+
let negative = 0;
|
|
435
|
+
if (low >= 0) {
|
|
436
|
+
base = low;
|
|
437
|
+
positive = high - low;
|
|
438
|
+
} else if (high <= 0) {
|
|
439
|
+
base = high;
|
|
440
|
+
negative = low - high;
|
|
441
|
+
} else {
|
|
442
|
+
positive = high;
|
|
443
|
+
negative = low;
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
start,
|
|
447
|
+
end,
|
|
448
|
+
value: kind === "total" ? end : end - start,
|
|
449
|
+
kind,
|
|
450
|
+
base: clean(base),
|
|
451
|
+
positive: clean(positive),
|
|
452
|
+
negative: clean(negative)
|
|
453
|
+
};
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
function toPercentStack(series) {
|
|
457
|
+
const length = Math.max(0, ...series.map((s) => s.data.length));
|
|
458
|
+
const totals = Array.from(
|
|
459
|
+
{ length },
|
|
460
|
+
(_, index) => series.reduce((sum, s) => {
|
|
461
|
+
const value = s.data[index];
|
|
462
|
+
return sum + (isFiniteNumber(value) ? Math.abs(value) : 0);
|
|
463
|
+
}, 0)
|
|
464
|
+
);
|
|
465
|
+
return series.map((s) => ({
|
|
466
|
+
...s,
|
|
467
|
+
data: s.data.map((value, index) => {
|
|
468
|
+
if (!isFiniteNumber(value)) return null;
|
|
469
|
+
return totals[index] === 0 ? 0 : clean(value / totals[index] * 100);
|
|
470
|
+
})
|
|
471
|
+
}));
|
|
472
|
+
}
|
|
473
|
+
function stackTotals(series, stacked) {
|
|
474
|
+
const length = Math.max(0, ...series.map((s) => s.data.length));
|
|
475
|
+
const totals = /* @__PURE__ */ new Map();
|
|
476
|
+
for (const s of series) {
|
|
477
|
+
const stack = s.stack ?? (stacked ? "Total" : void 0);
|
|
478
|
+
if (stack === void 0) continue;
|
|
479
|
+
const sums = totals.get(stack) ?? Array.from({ length }, () => null);
|
|
480
|
+
s.data.forEach((value, index) => {
|
|
481
|
+
if (isFiniteNumber(value)) sums[index] = clean((sums[index] ?? 0) + value);
|
|
482
|
+
});
|
|
483
|
+
totals.set(stack, sums);
|
|
484
|
+
}
|
|
485
|
+
return totals;
|
|
486
|
+
}
|
|
487
|
+
function pairWithCategories(categories, values, axisType) {
|
|
488
|
+
if (axisType !== "time" && axisType !== "value") return values;
|
|
489
|
+
return values.map((value, index) => [categories[index], value]);
|
|
490
|
+
}
|
|
491
|
+
function resolveChartHeight(height, rows, { rowHeight = 28, chrome = 56, minHeight = 160, maxHeight } = {}) {
|
|
492
|
+
if (height !== "auto") return height;
|
|
493
|
+
let value = Math.max(minHeight, chrome + Math.max(rows, 1) * rowHeight);
|
|
494
|
+
if (maxHeight !== void 0) value = Math.min(value, maxHeight);
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
function allSeriesEmpty(series) {
|
|
498
|
+
return !series?.length || series.every((s) => !s.data?.some((value) => value != null));
|
|
499
|
+
}
|
|
500
|
+
function trendLine(points) {
|
|
501
|
+
const finite = points.filter(([x, y]) => isFiniteNumber(x) && isFiniteNumber(y));
|
|
502
|
+
const n = finite.length;
|
|
503
|
+
if (n < 2) return null;
|
|
504
|
+
let sumX = 0;
|
|
505
|
+
let sumY = 0;
|
|
506
|
+
let sumXY = 0;
|
|
507
|
+
let sumX2 = 0;
|
|
508
|
+
let minX = Infinity;
|
|
509
|
+
let maxX = -Infinity;
|
|
510
|
+
for (const [x, y] of finite) {
|
|
511
|
+
sumX += x;
|
|
512
|
+
sumY += y;
|
|
513
|
+
sumXY += x * y;
|
|
514
|
+
sumX2 += x * x;
|
|
515
|
+
if (x < minX) minX = x;
|
|
516
|
+
if (x > maxX) maxX = x;
|
|
517
|
+
}
|
|
518
|
+
const denominator = n * sumX2 - sumX * sumX;
|
|
519
|
+
if (minX === maxX || denominator === 0) return null;
|
|
520
|
+
const slope = (n * sumXY - sumX * sumY) / denominator;
|
|
521
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
522
|
+
return [
|
|
523
|
+
[minX, slope * minX + intercept],
|
|
524
|
+
[maxX, slope * maxX + intercept]
|
|
525
|
+
];
|
|
526
|
+
}
|
|
527
|
+
function computeDelta(current, previous) {
|
|
528
|
+
if (!isFiniteNumber(current) || !isFiniteNumber(previous)) return null;
|
|
529
|
+
const change = clean(current - previous);
|
|
530
|
+
return {
|
|
531
|
+
change,
|
|
532
|
+
ratio: previous === 0 ? null : change / Math.abs(previous),
|
|
533
|
+
direction: change > 0 ? "up" : change < 0 ? "down" : "flat"
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
306
537
|
// src/utils/options.ts
|
|
307
538
|
function toParams(raw, getLabel) {
|
|
308
539
|
const seriesIndex = raw.seriesIndex ?? 0;
|
|
@@ -587,222 +818,34 @@ function withZoomInset(inset, mode, axis) {
|
|
|
587
818
|
const current = typeof inset?.[edge] === "number" ? inset[edge] : 12;
|
|
588
819
|
return { ...inset, [edge]: current + 36 };
|
|
589
820
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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;
|
|
821
|
+
function buildStackTotalSeries(categories, series, { stacked, position, format, axisType }) {
|
|
822
|
+
return [...stackTotals(series, stacked)].map(([stack, totals]) => ({
|
|
823
|
+
id: `__total:${stack}`,
|
|
824
|
+
type: "bar",
|
|
825
|
+
stack,
|
|
826
|
+
data: pairWithCategories(
|
|
827
|
+
categories,
|
|
828
|
+
totals.map((total) => total === null ? null : 0),
|
|
829
|
+
axisType
|
|
830
|
+
),
|
|
831
|
+
silent: true,
|
|
832
|
+
legendHoverLink: false,
|
|
833
|
+
tooltip: { show: false },
|
|
834
|
+
itemStyle: { color: "transparent" },
|
|
835
|
+
emphasis: { disabled: true },
|
|
836
|
+
label: {
|
|
837
|
+
show: true,
|
|
838
|
+
position,
|
|
839
|
+
fontWeight: "bold",
|
|
840
|
+
formatter: (params) => formatValue(totals[params.dataIndex], format)
|
|
729
841
|
}
|
|
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
842
|
}));
|
|
757
843
|
}
|
|
758
|
-
function
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
};
|
|
844
|
+
function withEndLabelInset(inset, labels) {
|
|
845
|
+
const longest = Math.max(0, ...labels.map((label) => label.length));
|
|
846
|
+
if (longest === 0) return inset;
|
|
847
|
+
const current = typeof inset?.right === "number" ? inset.right : 0;
|
|
848
|
+
return { ...inset, right: current + longest * 7 + 16 };
|
|
806
849
|
}
|
|
807
850
|
|
|
808
851
|
// src/utils/records.ts
|
|
@@ -969,10 +1012,11 @@ function toSliceData(rows, options) {
|
|
|
969
1012
|
nullLabel = "Unknown",
|
|
970
1013
|
colors
|
|
971
1014
|
} = options;
|
|
1015
|
+
const effective = value === void 0 ? "count" : aggregate;
|
|
972
1016
|
const { categories, series } = fromRecords(rows, {
|
|
973
1017
|
category: name,
|
|
974
1018
|
value,
|
|
975
|
-
aggregate:
|
|
1019
|
+
aggregate: effective,
|
|
976
1020
|
nullLabel
|
|
977
1021
|
});
|
|
978
1022
|
const values = series[0]?.data ?? [];
|
|
@@ -986,7 +1030,12 @@ function toSliceData(rows, options) {
|
|
|
986
1030
|
const rest = slices.slice(topN);
|
|
987
1031
|
slices = slices.slice(0, topN);
|
|
988
1032
|
if (otherLabel !== void 0) {
|
|
989
|
-
|
|
1033
|
+
const folded = new Set(rest.map((slice) => slice.name));
|
|
1034
|
+
const other = fromRecords(
|
|
1035
|
+
(rows ?? []).filter((row) => folded.has(toLabel(read(row, name), nullLabel))),
|
|
1036
|
+
{ category: () => otherLabel, value, aggregate: effective }
|
|
1037
|
+
).series[0]?.data[0];
|
|
1038
|
+
slices.push({ name: otherLabel, value: other ?? 0 });
|
|
990
1039
|
}
|
|
991
1040
|
}
|
|
992
1041
|
return slices.map(
|
|
@@ -1147,15 +1196,17 @@ function formatTableCell(table, cell, column, format) {
|
|
|
1147
1196
|
return formatValue(cell, format);
|
|
1148
1197
|
}
|
|
1149
1198
|
}
|
|
1150
|
-
|
|
1199
|
+
var FORMULA_START = /^[=+\-@\t\r]/;
|
|
1200
|
+
function csvCell(cell, raw = cell) {
|
|
1151
1201
|
if (cell == null) return "";
|
|
1152
|
-
|
|
1202
|
+
let text = String(cell);
|
|
1203
|
+
if (typeof raw === "string" && FORMULA_START.test(text)) text = `'${text}`;
|
|
1153
1204
|
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
1154
1205
|
}
|
|
1155
1206
|
function tableToCsv(table, format) {
|
|
1156
1207
|
const lines = [table.headers, ...table.rows].map(
|
|
1157
1208
|
(row, rowIndex) => row.map(
|
|
1158
|
-
(cell, column) => csvCell(rowIndex > 0 && format ? formatTableCell(table, cell, column, format) : cell)
|
|
1209
|
+
(cell, column) => csvCell(rowIndex > 0 && format ? formatTableCell(table, cell, column, format) : cell, cell)
|
|
1159
1210
|
).join(",")
|
|
1160
1211
|
);
|
|
1161
1212
|
return lines.join("\r\n");
|
|
@@ -1184,4 +1235,4 @@ function downloadCsv(table, filename) {
|
|
|
1184
1235
|
);
|
|
1185
1236
|
}
|
|
1186
1237
|
|
|
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 };
|
|
1238
|
+
export { abbreviateNumber, allSeriesEmpty, anchorCenterGraphic, binValues, boxPlotTable, boxplotStats, buildAxis, buildDataLabel, buildDataZoom, buildLegend, buildMarkArea, buildMarkLine, buildStackTotalSeries, 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, stackTotals, tableToCsv, toPercentStack, toSliceData, treemapTable, trendLine, valueAxisConfig, valueLabelFormatter, waterfallLayout, waterfallTable, withEndLabelInset, withPointLabels, withReferenceMarks, withZoomInset };
|
package/dist/types/index.d.cts
CHANGED
|
@@ -816,6 +816,12 @@ interface BarChartProps extends BaseChartProps, LegendChartProps, LabelChartProp
|
|
|
816
816
|
stacked?: boolean | 'percent';
|
|
817
817
|
/** Show value labels on bars */
|
|
818
818
|
showValues?: boolean;
|
|
819
|
+
/**
|
|
820
|
+
* Label each stack's total at the end of the bar, formatted with
|
|
821
|
+
* `valueFormat`. Applies to stacked series (`stacked` or a series `stack`
|
|
822
|
+
* id); percent stacks show the raw total.
|
|
823
|
+
*/
|
|
824
|
+
showTotal?: boolean;
|
|
819
825
|
/** Click event handler */
|
|
820
826
|
onClick?: (params: DataPointEventParams) => void;
|
|
821
827
|
/** Hover event handler */
|
package/dist/types/index.d.ts
CHANGED
|
@@ -816,6 +816,12 @@ interface BarChartProps extends BaseChartProps, LegendChartProps, LabelChartProp
|
|
|
816
816
|
stacked?: boolean | 'percent';
|
|
817
817
|
/** Show value labels on bars */
|
|
818
818
|
showValues?: boolean;
|
|
819
|
+
/**
|
|
820
|
+
* Label each stack's total at the end of the bar, formatted with
|
|
821
|
+
* `valueFormat`. Applies to stacked series (`stacked` or a series `stack`
|
|
822
|
+
* id); percent stacks show the raw total.
|
|
823
|
+
*/
|
|
824
|
+
showTotal?: boolean;
|
|
819
825
|
/** Click event handler */
|
|
820
826
|
onClick?: (params: DataPointEventParams) => void;
|
|
821
827
|
/** Hover event handler */
|