@rowsncolumns/charts 28.1.11 → 28.1.12
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/cjs/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/basic-chart/basic-chart.js +212 -159
- package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/plotly/basic-chart/basic-chart.js +122 -95
- package/dist/cjs/utils/hierarchy.d.ts +6 -0
- package/dist/cjs/utils/hierarchy.d.ts.map +1 -1
- package/dist/cjs/utils/hierarchy.js +23 -1
- package/dist/cjs/utils/histogram.d.ts +42 -3
- package/dist/cjs/utils/histogram.d.ts.map +1 -1
- package/dist/cjs/utils/histogram.js +193 -5
- package/dist/cjs/utils/overrides.d.ts +1 -1
- package/dist/cjs/utils/overrides.d.ts.map +1 -1
- package/dist/cjs/utils/overrides.js +44 -39
- package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/basic-chart/basic-chart.js +106 -27
- package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/plotly/basic-chart/basic-chart.js +45 -6
- package/dist/esm/utils/hierarchy.d.ts +6 -0
- package/dist/esm/utils/hierarchy.d.ts.map +1 -1
- package/dist/esm/utils/hierarchy.js +21 -0
- package/dist/esm/utils/histogram.d.ts +42 -3
- package/dist/esm/utils/histogram.d.ts.map +1 -1
- package/dist/esm/utils/histogram.js +171 -4
- package/dist/esm/utils/overrides.d.ts +1 -1
- package/dist/esm/utils/overrides.d.ts.map +1 -1
- package/dist/esm/utils/overrides.js +4 -2
- package/package.json +2 -2
|
@@ -23,12 +23,12 @@ import { applyEmptyCellsPolicy, connectsAcrossEmptyCells, isolatedPointIndexes,
|
|
|
23
23
|
import { getEchartsGlState, loadEchartsGl, } from "../utils/echarts-gl-loader";
|
|
24
24
|
import { echartsGroupedCategoryAxes, resolveCategoryLevels, } from "../utils/category-levels";
|
|
25
25
|
import { gradientToEcharts } from "../utils/gradient";
|
|
26
|
-
import { buildHierarchyTree, extractHierarchyRows, getHierarchyHeaderTitle, } from "../utils/hierarchy";
|
|
27
|
-
import { binHistogram, computePareto, isParetoSpec, } from "../utils/histogram";
|
|
26
|
+
import { buildHierarchyTree, contrastTextColor, extractHierarchyRows, getHierarchyHeaderTitle, } from "../utils/hierarchy";
|
|
27
|
+
import { aggregateByCategory, binHistogram, computePareto, isParetoSpec, } from "../utils/histogram";
|
|
28
28
|
import { categoryAxisNameGap, legendLabelsFrom, reserveEchartsLegendBand, } from "../utils/legend-layout";
|
|
29
29
|
import { clampSecondPieSize, isOfPieChartSpec, otherSliceHalfAngle, resolveOfPieType, splitOfPie, } from "../utils/of-pie";
|
|
30
30
|
import { clampDoughnutHole, doughnutRings } from "../utils/doughnut";
|
|
31
|
-
import { applyAxisOverrides, applyLegendOverrides, buildSeriesLabelOption, resolveSeriesDataLabels, } from "../utils/overrides";
|
|
31
|
+
import { applyAxisOverrides, dataLabelFormatter, applyLegendOverrides, buildSeriesLabelOption, resolveSeriesDataLabels, } from "../utils/overrides";
|
|
32
32
|
import { buildRadarIndicators, buildRadarSeriesData } from "../utils/radar";
|
|
33
33
|
import { buildRegionMapData, buildSequentialColorRamp, isRegionMapSpec, resolveGeoScope, warnOnceForUnmatchedRegions, } from "../utils/region-map";
|
|
34
34
|
import { loadRegionMapGeo } from "../utils/region-map-loader";
|
|
@@ -116,7 +116,14 @@ const HIERARCHY_LEVEL_STYLES = [
|
|
|
116
116
|
},
|
|
117
117
|
{
|
|
118
118
|
itemStyle: {
|
|
119
|
-
gapWidth:
|
|
119
|
+
gapWidth: 2,
|
|
120
|
+
borderColor: "#ffffff",
|
|
121
|
+
borderWidth: 1,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
itemStyle: {
|
|
126
|
+
gapWidth: 0,
|
|
120
127
|
borderColor: "#ffffff",
|
|
121
128
|
borderWidth: 1,
|
|
122
129
|
},
|
|
@@ -139,14 +146,22 @@ const treemapBannerShown = (layout) => layout !== "none";
|
|
|
139
146
|
* Colour a hierarchy like Excel: each top-level node takes the next
|
|
140
147
|
* palette colour and every descendant inherits it (#1072).
|
|
141
148
|
*/
|
|
142
|
-
const colorHierarchyNodes = (nodes, palette) => nodes.map((node, index) => {
|
|
149
|
+
const colorHierarchyNodes = (nodes, palette, options = {}) => nodes.map((node, index) => {
|
|
143
150
|
const color = chartPaletteColorAt(palette, index);
|
|
144
|
-
const paint = (n) => ({
|
|
151
|
+
const paint = (n, depth) => ({
|
|
145
152
|
...n,
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
// A top-level node's "border" is what echarts paints behind its
|
|
154
|
+
// banner strip: fill it with the category colour and write the name
|
|
155
|
+
// in a contrasting colour, like Excel's banner (#1086).
|
|
156
|
+
itemStyle: depth === 0 && options.banner
|
|
157
|
+
? { color, borderColor: color }
|
|
158
|
+
: { color },
|
|
159
|
+
...(depth === 0 && options.banner
|
|
160
|
+
? { upperLabel: { color: contrastTextColor(color) } }
|
|
161
|
+
: {}),
|
|
162
|
+
...(n.children ? { children: n.children.map((c) => paint(c, depth + 1)) } : {}),
|
|
148
163
|
});
|
|
149
|
-
return paint(node);
|
|
164
|
+
return paint(node, 0);
|
|
150
165
|
});
|
|
151
166
|
/** Sunburst rings: Excel leaves the centre empty, so even the first level is a ring (#1073). */
|
|
152
167
|
const SUNBURST_RADIUS = ["15%", "80%"];
|
|
@@ -834,13 +849,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
834
849
|
theme.themeColors.accent5,
|
|
835
850
|
theme.themeColors.accent6,
|
|
836
851
|
]);
|
|
837
|
-
const
|
|
852
|
+
const isTreemap = spec.chartType === "treemap";
|
|
853
|
+
const banner = isTreemap && treemapBannerShown(spec.parentLabelLayout);
|
|
854
|
+
const hierarchyData = colorHierarchyNodes(buildHierarchyTree(hierarchyRows), hierarchyPalette, { banner });
|
|
838
855
|
const rootName = hierarchyData.length === 1 ? hierarchyData[0]?.name ?? "" : "";
|
|
839
856
|
const drillDown = Boolean(spec.drillDown);
|
|
840
857
|
const seriesDisplayName = explicitChartTitle ?? headerTitle ?? rootName ?? "";
|
|
841
858
|
const hierarchyDepth = getHierarchyDepth(hierarchyData);
|
|
842
|
-
const isTreemap = spec.chartType === "treemap";
|
|
843
|
-
const banner = isTreemap && treemapBannerShown(spec.parentLabelLayout);
|
|
844
859
|
const levels = buildHierarchyLevels(hierarchyDepth, {
|
|
845
860
|
treemap: isTreemap,
|
|
846
861
|
banner,
|
|
@@ -873,12 +888,45 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
873
888
|
: params.value ?? "";
|
|
874
889
|
return `${params.name}: ${formatAxisValue(sizeNumberFormat, value, locale)}`;
|
|
875
890
|
};
|
|
876
|
-
//
|
|
877
|
-
//
|
|
891
|
+
// Data labels: a treemap block's label always sits in its bottom-left
|
|
892
|
+
// corner (the bar placements mean nothing on a block) with each part
|
|
893
|
+
// — name, value — on its own line; `{ show: false }` turns the labels
|
|
894
|
+
// off entirely (#1086). Without `dataLabels` the block shows its name.
|
|
878
895
|
const hierarchyDataLabels = spec.dataLabels;
|
|
879
|
-
const
|
|
896
|
+
const hierarchyLabelOpts = hierarchyDataLabels && !hierarchyDataLabels.numberFormatPattern
|
|
880
897
|
? { ...hierarchyDataLabels, numberFormatPattern: sizeNumberFormat }
|
|
881
|
-
: hierarchyDataLabels
|
|
898
|
+
: hierarchyDataLabels;
|
|
899
|
+
const hierarchyLabel = hierarchyLabelOpts?.show === false
|
|
900
|
+
? { show: false }
|
|
901
|
+
: hierarchyLabelOpts?.show
|
|
902
|
+
? {
|
|
903
|
+
show: true,
|
|
904
|
+
formatter: dataLabelFormatter(hierarchyLabelOpts, locale, isTreemap ? "\n" : ": "),
|
|
905
|
+
...(isTreemap ? { position: "insideBottomLeft" } : {}),
|
|
906
|
+
}
|
|
907
|
+
: {
|
|
908
|
+
show: true,
|
|
909
|
+
formatter: "{b}",
|
|
910
|
+
...(isTreemap ? { position: "insideBottomLeft" } : {}),
|
|
911
|
+
};
|
|
912
|
+
// Reserve a band for the legend (and the title) so neither paints
|
|
913
|
+
// over the blocks (#1086), as the cartesian layout does.
|
|
914
|
+
const hierarchyBand = reserveEchartsLegendBand({
|
|
915
|
+
position: spec.legend?.position,
|
|
916
|
+
visible: legendShown && hierarchyData.length > 0,
|
|
917
|
+
labels: hierarchyData.map((node) => node.name),
|
|
918
|
+
legendFontSize: hierarchyLegend.textStyle?.fontSize ??
|
|
919
|
+
spec.legend?.textFormat?.fontSize ??
|
|
920
|
+
12,
|
|
921
|
+
legendItemWidth: defaultLegendConfig.itemWidth,
|
|
922
|
+
width: width ?? 0,
|
|
923
|
+
height: height ?? 0,
|
|
924
|
+
hasTitle: Boolean(explicitChartTitle ?? headerTitle),
|
|
925
|
+
titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
|
|
926
|
+
hasBottomAxisTitle: false,
|
|
927
|
+
bottomAxisNameGap: 0,
|
|
928
|
+
hasLeftAxisTitle: false,
|
|
929
|
+
});
|
|
882
930
|
chartRef.current?.setOption({
|
|
883
931
|
xAxis: [
|
|
884
932
|
{
|
|
@@ -894,6 +942,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
894
942
|
],
|
|
895
943
|
legend: {
|
|
896
944
|
...hierarchyLegend,
|
|
945
|
+
...hierarchyBand.legend,
|
|
897
946
|
data: hierarchyData.map((node) => node.name),
|
|
898
947
|
selectedMode: false,
|
|
899
948
|
},
|
|
@@ -913,16 +962,22 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
913
962
|
name: seriesDisplayName,
|
|
914
963
|
rootName: rootName || seriesDisplayName || "",
|
|
915
964
|
data: hierarchyData,
|
|
916
|
-
label:
|
|
917
|
-
show: true,
|
|
918
|
-
formatter: "{b}",
|
|
919
|
-
...(isTreemap ? { position: "insideBottomLeft" } : {}),
|
|
920
|
-
...(hierarchySeriesLabel ?? {}),
|
|
921
|
-
},
|
|
965
|
+
label: hierarchyLabel,
|
|
922
966
|
// Only `levels[1]` (the categories) may carry a banner; see
|
|
923
967
|
// buildHierarchyLevels.
|
|
924
968
|
upperLabel: isTreemap ? { show: false } : undefined,
|
|
925
969
|
levels,
|
|
970
|
+
...(isTreemap
|
|
971
|
+
? {
|
|
972
|
+
// Excel has no wheel zoom / drag pan on a treemap; the
|
|
973
|
+
// sheet keeps scrolling under the pointer (#1086).
|
|
974
|
+
roam: drillDown,
|
|
975
|
+
left: hierarchyBand.grid.left,
|
|
976
|
+
right: hierarchyBand.grid.right,
|
|
977
|
+
top: hierarchyBand.grid.top,
|
|
978
|
+
bottom: hierarchyBand.grid.bottom,
|
|
979
|
+
}
|
|
980
|
+
: {}),
|
|
926
981
|
// Excel has no drill-down on either chart: zooming into a
|
|
927
982
|
// branch on click (and the treemap breadcrumb) is opt-in.
|
|
928
983
|
nodeClick: drillDown
|
|
@@ -2418,7 +2473,21 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2418
2473
|
}
|
|
2419
2474
|
else if (spec.chartType === "histogram") {
|
|
2420
2475
|
const rawValues = flattenChartDataValues(spec.series, getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
|
|
2421
|
-
|
|
2476
|
+
// A domain names a category column: Excel's "By Category" histogram
|
|
2477
|
+
// / Pareto over categories aggregates the values per category (sum
|
|
2478
|
+
// unless the series says otherwise) instead of binning them (#1087).
|
|
2479
|
+
const histogramDomainSource = spec.domains?.[0]?.sources?.[0];
|
|
2480
|
+
const histogramCategories = histogramDomainSource
|
|
2481
|
+
? (getDomainValuesFromRange(histogramDomainSource, spec.hiddenDimensionStrategy) ?? [])
|
|
2482
|
+
: [];
|
|
2483
|
+
let { labels, counts } = histogramCategories.length
|
|
2484
|
+
? aggregateByCategory(histogramCategories, rawValues, spec.series?.[0]?.aggregateType ?? "SUM")
|
|
2485
|
+
: binHistogram(rawValues, spec.binCount, {
|
|
2486
|
+
binSize: spec.binSize,
|
|
2487
|
+
intervalClosed: spec.intervalClosed,
|
|
2488
|
+
overflow: spec.overflow,
|
|
2489
|
+
underflow: spec.underflow,
|
|
2490
|
+
});
|
|
2422
2491
|
// Pareto — a histogram whose bins are sorted descending with a
|
|
2423
2492
|
// cumulative-% line overlaid on a secondary 0-100% axis. The
|
|
2424
2493
|
// authored type arrives as `sourceChartType: "PARETO"` on an
|
|
@@ -2448,7 +2517,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2448
2517
|
type: "bar",
|
|
2449
2518
|
name: spec.series?.[0]?.dataLabel ?? "",
|
|
2450
2519
|
data: counts,
|
|
2451
|
-
|
|
2520
|
+
// Excel's histogram columns touch unless `gapWidth` says otherwise
|
|
2521
|
+
// (#1087); the gap maps like a column chart's.
|
|
2522
|
+
barCategoryGap: gapWidthToBarCategoryGap(spec.gapWidth ?? 0, { seriesCount: 1 }) ??
|
|
2523
|
+
"0%",
|
|
2452
2524
|
};
|
|
2453
2525
|
const histogramColor = spec.series?.[0]?.color;
|
|
2454
2526
|
if (histogramColor) {
|
|
@@ -2458,14 +2530,21 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2458
2530
|
if (isPareto) {
|
|
2459
2531
|
// Cumulative-% overlay on the secondary axis (yAxisIndex 1);
|
|
2460
2532
|
// the matching 0-100% axis element is appended with the other
|
|
2461
|
-
// value axes below.
|
|
2533
|
+
// value axes below. Excel draws the Pareto line without markers.
|
|
2534
|
+
const paretoLineColor = spec.paretoLineColor;
|
|
2462
2535
|
chartSeries.push({
|
|
2463
2536
|
type: "line",
|
|
2464
2537
|
name: "Cumulative %",
|
|
2465
2538
|
data: cumulativePercents,
|
|
2466
2539
|
yAxisIndex: 1,
|
|
2467
|
-
|
|
2468
|
-
|
|
2540
|
+
showSymbol: false,
|
|
2541
|
+
symbol: "none",
|
|
2542
|
+
...(paretoLineColor
|
|
2543
|
+
? {
|
|
2544
|
+
itemStyle: { color: paretoLineColor },
|
|
2545
|
+
lineStyle: { color: paretoLineColor },
|
|
2546
|
+
}
|
|
2547
|
+
: {}),
|
|
2469
2548
|
});
|
|
2470
2549
|
}
|
|
2471
2550
|
legend.data = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/basic-chart.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/basic-chart.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA+ZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAmGnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBA4nFjB,CAAC"}
|
|
@@ -25,7 +25,7 @@ import { resolvePlotlyClickSelection, wasDragged, } from "../../utils/plotly-cli
|
|
|
25
25
|
import { buildPlotlySurfaceTrace } from "../../utils/plotly-surface";
|
|
26
26
|
import { normalize3DSourceType, } from "../../utils/three-d";
|
|
27
27
|
import { extractHierarchyRows, getHierarchyHeaderTitle, hierarchyNodeColors, toPlotlyHierarchy, } from "../../utils/hierarchy";
|
|
28
|
-
import { binHistogram, computePareto, isParetoSpec, } from "../../utils/histogram";
|
|
28
|
+
import { aggregateByCategory, binHistogram, computePareto, hasExcelBinning, isParetoSpec, } from "../../utils/histogram";
|
|
29
29
|
import { clampSecondPieSize, isOfPieChartSpec, otherSliceHalfAngle, resolveOfPieType, splitOfPie, } from "../../utils/of-pie";
|
|
30
30
|
import { buildRadarIndicators } from "../../utils/radar";
|
|
31
31
|
import { buildSequentialColorRamp, isRegionMapSpec, resolveGeoScope, WORLD_GEO_SCOPE, } from "../../utils/region-map";
|
|
@@ -851,9 +851,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
851
851
|
...(isSunburst ? { line: { width: 1 } } : {}),
|
|
852
852
|
},
|
|
853
853
|
name: seriesName,
|
|
854
|
-
|
|
854
|
+
// `dataLabels: { show: false }` turns the labels off (#1086).
|
|
855
|
+
textinfo: spec.dataLabels?.show === false ? "none" : "label+value",
|
|
855
856
|
branchvalues: "total",
|
|
856
|
-
...(isSunburst
|
|
857
|
+
...(isSunburst || spec.dataLabels?.show === false
|
|
857
858
|
? {}
|
|
858
859
|
: {
|
|
859
860
|
textposition: "bottom left",
|
|
@@ -1619,12 +1620,29 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1619
1620
|
if (effectiveChartType === "histogram") {
|
|
1620
1621
|
const rawValues = seriesValuesOf(seriesData);
|
|
1621
1622
|
const binCount = spec.binCount;
|
|
1623
|
+
const binningOptions = {
|
|
1624
|
+
binSize: spec.binSize,
|
|
1625
|
+
intervalClosed: spec.intervalClosed,
|
|
1626
|
+
overflow: spec.overflow,
|
|
1627
|
+
underflow: spec.underflow,
|
|
1628
|
+
};
|
|
1629
|
+
// A domain names a category column: aggregate per category (Excel's
|
|
1630
|
+
// "By Category" / Pareto over categories) instead of binning
|
|
1631
|
+
// (#1087). Excel binning options also pre-bin with the shared util,
|
|
1632
|
+
// since plotly's native histogram cannot close intervals on the
|
|
1633
|
+
// right or add overflow bins.
|
|
1634
|
+
const categories = domainData;
|
|
1635
|
+
const prebinned = categories.length
|
|
1636
|
+
? aggregateByCategory(categories, rawValues, series.aggregateType ?? "SUM")
|
|
1637
|
+
: hasExcelBinning(binningOptions)
|
|
1638
|
+
? binHistogram(rawValues, binCount, binningOptions)
|
|
1639
|
+
: null;
|
|
1622
1640
|
if (isPareto) {
|
|
1623
1641
|
// Plotly's native histogram trace bins in axis order and
|
|
1624
1642
|
// can't reorder bins, so Pareto pre-bins with the shared
|
|
1625
1643
|
// util, sorts descending, and renders a bar trace plus the
|
|
1626
1644
|
// cumulative-% line pointed at the y2 overlay axis.
|
|
1627
|
-
const { labels, counts, cumulativePercents } = computePareto(binHistogram(rawValues, binCount));
|
|
1645
|
+
const { labels, counts, cumulativePercents } = computePareto(prebinned ?? binHistogram(rawValues, binCount));
|
|
1628
1646
|
const barTrace = {
|
|
1629
1647
|
type: "bar",
|
|
1630
1648
|
x: labels,
|
|
@@ -1638,16 +1656,34 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1638
1656
|
effectiveChartType,
|
|
1639
1657
|
});
|
|
1640
1658
|
traces.push(barTrace);
|
|
1659
|
+
const paretoLineColor = spec.paretoLineColor;
|
|
1660
|
+
// Excel draws the Pareto line without markers.
|
|
1641
1661
|
traces.push({
|
|
1642
1662
|
type: "scatter",
|
|
1643
|
-
mode: "lines
|
|
1663
|
+
mode: "lines",
|
|
1644
1664
|
x: labels,
|
|
1645
1665
|
y: cumulativePercents,
|
|
1646
1666
|
name: "Cumulative %",
|
|
1647
1667
|
yaxis: "y2",
|
|
1668
|
+
...(paretoLineColor ? { line: { color: paretoLineColor } } : {}),
|
|
1648
1669
|
});
|
|
1649
1670
|
hasSecondaryAxis = true;
|
|
1650
1671
|
}
|
|
1672
|
+
else if (prebinned) {
|
|
1673
|
+
const barTrace = {
|
|
1674
|
+
type: "bar",
|
|
1675
|
+
x: prebinned.labels,
|
|
1676
|
+
y: prebinned.counts,
|
|
1677
|
+
name: dataLabel,
|
|
1678
|
+
};
|
|
1679
|
+
applyTraceColor(barTrace, series.color ?? undefined, {
|
|
1680
|
+
noFill: series.noFill,
|
|
1681
|
+
dataPointColors: series.dataPointColors,
|
|
1682
|
+
seriesIndex: currentSeriesIndex,
|
|
1683
|
+
effectiveChartType,
|
|
1684
|
+
});
|
|
1685
|
+
traces.push(barTrace);
|
|
1686
|
+
}
|
|
1651
1687
|
else {
|
|
1652
1688
|
const trace = {
|
|
1653
1689
|
type: "histogram",
|
|
@@ -2131,7 +2167,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2131
2167
|
// gapWidth=150 (default) → ~0.4 bargap.
|
|
2132
2168
|
...(typeof spec.gapWidth === "number"
|
|
2133
2169
|
? { bargap: Math.max(0, Math.min(0.95, spec.gapWidth / 400)) }
|
|
2134
|
-
:
|
|
2170
|
+
: spec.chartType === "histogram"
|
|
2171
|
+
? // Excel's histogram columns touch by default (#1087).
|
|
2172
|
+
{ bargap: 0 }
|
|
2173
|
+
: {}),
|
|
2135
2174
|
font: {
|
|
2136
2175
|
family: "Untitled Sans, apple-system, sans-serif",
|
|
2137
2176
|
size: 12,
|
|
@@ -46,5 +46,11 @@ export declare const toPlotlyHierarchy: (rows: HierarchyRow[]) => PlotlyHierarch
|
|
|
46
46
|
* Returns the colour per node of `toPlotlyHierarchy`'s flat arrays.
|
|
47
47
|
*/
|
|
48
48
|
export declare const hierarchyNodeColors: (hierarchy: Pick<PlotlyHierarchy, "ids" | "parents">, palette: string[]) => string[];
|
|
49
|
+
/**
|
|
50
|
+
* Black or white, whichever reads on `background` (a `#rgb` / `#rrggbb`
|
|
51
|
+
* colour; anything else gets white) — for a treemap banner filled with
|
|
52
|
+
* its category's colour (rowsncolumns/spreadsheet#1086).
|
|
53
|
+
*/
|
|
54
|
+
export declare const contrastTextColor: (background: string | undefined) => string;
|
|
49
55
|
export {};
|
|
50
56
|
//# sourceMappingURL=hierarchy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hierarchy.d.ts","sourceRoot":"","sources":["../../../utils/hierarchy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,SAAS,EACT,UAAU,EACV,YAAY,EACb,MAAM,2BAA2B,CAAC;AAEnC,KAAK,SAAS,GAAG,CACf,KAAK,EAAE,UAAU,EACjB,cAAc,CAAC,EAAE,SAAS,CAAC,yBAAyB,CAAC,KAClD,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG;IACzB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,wBAAwB,EAAE,SAAS,CAAC;IACpC,wBAAwB,EAAE,SAAS,CAAC;IACpC,uBAAuB,CAAC,EAAE,SAAS,CAAC,yBAAyB,CAAC,CAAC;CAChE,CAAC;AA0CF,eAAO,MAAM,uBAAuB,GAClC,MAAM,YAAY,EAClB,SAAS,cAAc,KACtB,MAAM,GAAG,SAgBX,CAAC;AAyCF,eAAO,MAAM,oBAAoB,GAC/B,MAAM,YAAY,EAClB,SAAS,cAAc,KACtB,YAAY,EAiFd,CAAC;AAiDF,eAAO,MAAM,kBAAkB,GAAI,MAAM,YAAY,EAAE,KAAG,aAAa,EAmDtE,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,4BAA4B;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,6EAA6E;IAC7E,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,yCAAyC;IACzC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,oCAAoC;IACpC,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,MAAM,YAAY,EAAE,KAAG,eAwBxD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,WAAW,IAAI,CAAC,eAAe,EAAE,KAAK,GAAG,SAAS,CAAC,EACnD,SAAS,MAAM,EAAE,KAChB,MAAM,EAYR,CAAC"}
|
|
1
|
+
{"version":3,"file":"hierarchy.d.ts","sourceRoot":"","sources":["../../../utils/hierarchy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,SAAS,EACT,UAAU,EACV,YAAY,EACb,MAAM,2BAA2B,CAAC;AAEnC,KAAK,SAAS,GAAG,CACf,KAAK,EAAE,UAAU,EACjB,cAAc,CAAC,EAAE,SAAS,CAAC,yBAAyB,CAAC,KAClD,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG;IACzB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,wBAAwB,EAAE,SAAS,CAAC;IACpC,wBAAwB,EAAE,SAAS,CAAC;IACpC,uBAAuB,CAAC,EAAE,SAAS,CAAC,yBAAyB,CAAC,CAAC;CAChE,CAAC;AA0CF,eAAO,MAAM,uBAAuB,GAClC,MAAM,YAAY,EAClB,SAAS,cAAc,KACtB,MAAM,GAAG,SAgBX,CAAC;AAyCF,eAAO,MAAM,oBAAoB,GAC/B,MAAM,YAAY,EAClB,SAAS,cAAc,KACtB,YAAY,EAiFd,CAAC;AAiDF,eAAO,MAAM,kBAAkB,GAAI,MAAM,YAAY,EAAE,KAAG,aAAa,EAmDtE,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,4BAA4B;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,6EAA6E;IAC7E,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,yCAAyC;IACzC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,oCAAoC;IACpC,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,MAAM,YAAY,EAAE,KAAG,eAwBxD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,WAAW,IAAI,CAAC,eAAe,EAAE,KAAK,GAAG,SAAS,CAAC,EACnD,SAAS,MAAM,EAAE,KAChB,MAAM,EAYR,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,GAAG,SAAS,KAAG,MAelE,CAAC"}
|
|
@@ -266,3 +266,24 @@ export const hierarchyNodeColors = (hierarchy, palette) => {
|
|
|
266
266
|
return color;
|
|
267
267
|
});
|
|
268
268
|
};
|
|
269
|
+
/**
|
|
270
|
+
* Black or white, whichever reads on `background` (a `#rgb` / `#rrggbb`
|
|
271
|
+
* colour; anything else gets white) — for a treemap banner filled with
|
|
272
|
+
* its category's colour (rowsncolumns/spreadsheet#1086).
|
|
273
|
+
*/
|
|
274
|
+
export const contrastTextColor = (background) => {
|
|
275
|
+
const hex = (background ?? "").trim().replace(/^#/, "");
|
|
276
|
+
const full = hex.length === 3
|
|
277
|
+
? hex
|
|
278
|
+
.split("")
|
|
279
|
+
.map((c) => c + c)
|
|
280
|
+
.join("")
|
|
281
|
+
: hex;
|
|
282
|
+
if (!/^[0-9a-f]{6}$/i.test(full))
|
|
283
|
+
return "#ffffff";
|
|
284
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
285
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
286
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
287
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
288
|
+
return luminance > 0.6 ? "#000000" : "#ffffff";
|
|
289
|
+
};
|
|
@@ -39,8 +39,47 @@ export declare const isParetoSpec: (spec: {
|
|
|
39
39
|
*/
|
|
40
40
|
export declare const computePareto: (bins: HistogramBins) => ParetoData;
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
42
|
+
* Excel's `<cx:binning>` options (rowsncolumns/spreadsheet#1087). Any of
|
|
43
|
+
* them present switches the histogram to Excel's conventions: intervals
|
|
44
|
+
* closed on the right by default and automatic bins by Excel's rule.
|
|
44
45
|
*/
|
|
45
|
-
export
|
|
46
|
+
export type HistogramBinningOptions = {
|
|
47
|
+
/** Fixed bin width (`<cx:binSize>`); wins over `binCount`. */
|
|
48
|
+
binSize?: number | null;
|
|
49
|
+
/** Which side of an interval is closed (`intervalClosed`); Excel's default is `r`. */
|
|
50
|
+
intervalClosed?: "l" | "r" | null;
|
|
51
|
+
/** Values above this collect in a final `> overflow` bin. */
|
|
52
|
+
overflow?: number | null;
|
|
53
|
+
/** Values below this collect in a first `< underflow` bin. */
|
|
54
|
+
underflow?: number | null;
|
|
55
|
+
};
|
|
56
|
+
/** True when the spec authored any Excel binning option. */
|
|
57
|
+
export declare const hasExcelBinning: (options: HistogramBinningOptions | null | undefined) => boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Excel's automatic bin width: Scott's normal reference rule,
|
|
60
|
+
* 3.5 · σ / ∛n with the sample standard deviation, rounded up to two
|
|
61
|
+
* significant digits. For the 20 values 12 … 95 of #1087 (σ ≈ 23.86) that
|
|
62
|
+
* is 30.77 → 31, i.e. Excel's three bins [12, 43], (43, 74], (74, 105].
|
|
63
|
+
*/
|
|
64
|
+
export declare const excelAutoBinWidth: (numeric: number[]) => number;
|
|
65
|
+
/**
|
|
66
|
+
* Bin a set of numeric values into equal-width bins.
|
|
67
|
+
*
|
|
68
|
+
* Default (no Excel options): `binCount` bins over [min, max], closed on
|
|
69
|
+
* the left — or Sturges' rule, ceil(log2(n) + 1), when `binCount` is
|
|
70
|
+
* omitted. With Excel binning options (#1087): bins start at the minimum
|
|
71
|
+
* (or the underflow bound), are `binSize` wide — else (max − min) /
|
|
72
|
+
* `binCount`, else Excel's automatic width — closed on the right unless
|
|
73
|
+
* `intervalClosed` is `l`, and values beyond `overflow` / `underflow`
|
|
74
|
+
* collect in their own end bins.
|
|
75
|
+
*/
|
|
76
|
+
export declare const binHistogram: (values: (number | null | undefined)[], binCount?: number | null, options?: HistogramBinningOptions | null) => HistogramBins;
|
|
77
|
+
export type HistogramAggregateType = "SUM" | "AVERAGE" | "COUNT" | "MAX" | "MIN" | "MEDIAN";
|
|
78
|
+
/**
|
|
79
|
+
* Excel's histogram "By Category" / Pareto over categories
|
|
80
|
+
* (`<cx:aggregation/>`, #1087): one column per distinct category in order
|
|
81
|
+
* of first appearance, its value the aggregate (sum by default) of the
|
|
82
|
+
* series values of that category — instead of binning the values.
|
|
83
|
+
*/
|
|
84
|
+
export declare const aggregateByCategory: (categories: (string | number | null | undefined)[], values: (number | null | undefined)[], aggregateType?: HistogramAggregateType | null | undefined) => HistogramBins;
|
|
46
85
|
//# sourceMappingURL=histogram.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"histogram.d.ts","sourceRoot":"","sources":["../../../utils/histogram.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,gDAAgD;IAChD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC,KAAG,OAGgD,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,aAAa,KAAG,UAmBnD,CAAC;
|
|
1
|
+
{"version":3,"file":"histogram.d.ts","sourceRoot":"","sources":["../../../utils/histogram.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,gDAAgD;IAChD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,mEAAmE;IACnE,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,EAAE,CAAC;CAC9B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC,KAAG,OAGgD,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,aAAa,KAAG,UAmBnD,CAAC;AAUF;;;;GAIG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,sFAAsF;IACtF,cAAc,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;IAClC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,8DAA8D;IAC9D,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAKF,4DAA4D;AAC5D,eAAO,MAAM,eAAe,GAC1B,SAAS,uBAAuB,GAAG,IAAI,GAAG,SAAS,KAClD,OAQA,CAAC;AASJ;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,SAAS,MAAM,EAAE,KAAG,MASrD,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,GACvB,QAAQ,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EACrC,WAAW,MAAM,GAAG,IAAI,EACxB,UAAU,uBAAuB,GAAG,IAAI,KACvC,aA4CF,CAAC;AAiFF,MAAM,MAAM,sBAAsB,GAC9B,KAAK,GACL,SAAS,GACT,OAAO,GACP,KAAK,GACL,KAAK,GACL,QAAQ,CAAC;AAEb;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,YAAY,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EAClD,QAAQ,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EACrC,gBAAe,sBAAsB,GAAG,IAAI,GAAG,SAAiB,KAC/D,aA0CF,CAAC"}
|
|
@@ -45,17 +45,58 @@ export const computePareto = (bins) => {
|
|
|
45
45
|
const formatBinBound = (value) => {
|
|
46
46
|
if (!Number.isFinite(value))
|
|
47
47
|
return String(value);
|
|
48
|
+
if (value === 0)
|
|
49
|
+
return "0";
|
|
48
50
|
const abs = Math.abs(value);
|
|
49
51
|
if (abs >= 1000 || abs < 0.01)
|
|
50
52
|
return value.toExponential(2);
|
|
51
53
|
return Number.parseFloat(value.toFixed(2)).toString();
|
|
52
54
|
};
|
|
55
|
+
const isFinite_ = (v) => typeof v === "number" && Number.isFinite(v);
|
|
56
|
+
/** True when the spec authored any Excel binning option. */
|
|
57
|
+
export const hasExcelBinning = (options) => Boolean(options &&
|
|
58
|
+
(isFinite_(options.binSize) ||
|
|
59
|
+
options.intervalClosed === "l" ||
|
|
60
|
+
options.intervalClosed === "r" ||
|
|
61
|
+
isFinite_(options.overflow) ||
|
|
62
|
+
isFinite_(options.underflow)));
|
|
63
|
+
/** Round up to two significant digits (31 for 30.77, 0.038 for 0.0377). */
|
|
64
|
+
const ceilToTwoSignificant = (value) => {
|
|
65
|
+
if (!(value > 0))
|
|
66
|
+
return value;
|
|
67
|
+
const magnitude = 10 ** (Math.floor(Math.log10(value)) - 1);
|
|
68
|
+
return Math.ceil(value / magnitude - 1e-9) * magnitude;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Excel's automatic bin width: Scott's normal reference rule,
|
|
72
|
+
* 3.5 · σ / ∛n with the sample standard deviation, rounded up to two
|
|
73
|
+
* significant digits. For the 20 values 12 … 95 of #1087 (σ ≈ 23.86) that
|
|
74
|
+
* is 30.77 → 31, i.e. Excel's three bins [12, 43], (43, 74], (74, 105].
|
|
75
|
+
*/
|
|
76
|
+
export const excelAutoBinWidth = (numeric) => {
|
|
77
|
+
const n = numeric.length;
|
|
78
|
+
if (n < 2)
|
|
79
|
+
return 1;
|
|
80
|
+
const mean = numeric.reduce((sum, v) => sum + v, 0) / n;
|
|
81
|
+
const variance = numeric.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (n - 1);
|
|
82
|
+
const sigma = Math.sqrt(variance);
|
|
83
|
+
if (!(sigma > 0))
|
|
84
|
+
return 1;
|
|
85
|
+
return ceilToTwoSignificant((3.5 * sigma) / Math.cbrt(n));
|
|
86
|
+
};
|
|
53
87
|
/**
|
|
54
|
-
* Bin a set of numeric values into
|
|
55
|
-
*
|
|
88
|
+
* Bin a set of numeric values into equal-width bins.
|
|
89
|
+
*
|
|
90
|
+
* Default (no Excel options): `binCount` bins over [min, max], closed on
|
|
91
|
+
* the left — or Sturges' rule, ceil(log2(n) + 1), when `binCount` is
|
|
92
|
+
* omitted. With Excel binning options (#1087): bins start at the minimum
|
|
93
|
+
* (or the underflow bound), are `binSize` wide — else (max − min) /
|
|
94
|
+
* `binCount`, else Excel's automatic width — closed on the right unless
|
|
95
|
+
* `intervalClosed` is `l`, and values beyond `overflow` / `underflow`
|
|
96
|
+
* collect in their own end bins.
|
|
56
97
|
*/
|
|
57
|
-
export const binHistogram = (values, binCount) => {
|
|
58
|
-
const numeric = values.filter(
|
|
98
|
+
export const binHistogram = (values, binCount, options) => {
|
|
99
|
+
const numeric = values.filter(isFinite_);
|
|
59
100
|
if (numeric.length === 0) {
|
|
60
101
|
return { labels: [], counts: [] };
|
|
61
102
|
}
|
|
@@ -67,6 +108,9 @@ export const binHistogram = (values, binCount) => {
|
|
|
67
108
|
if (v > max)
|
|
68
109
|
max = v;
|
|
69
110
|
}
|
|
111
|
+
if (hasExcelBinning(options)) {
|
|
112
|
+
return binExcel(numeric, min, max, binCount, options);
|
|
113
|
+
}
|
|
70
114
|
const sturges = Math.max(1, Math.ceil(Math.log2(numeric.length) + 1));
|
|
71
115
|
const bins = typeof binCount === "number" && binCount > 0
|
|
72
116
|
? Math.floor(binCount)
|
|
@@ -93,3 +137,126 @@ export const binHistogram = (values, binCount) => {
|
|
|
93
137
|
}
|
|
94
138
|
return { labels, counts };
|
|
95
139
|
};
|
|
140
|
+
const binExcel = (numeric, min, max, binCount, options) => {
|
|
141
|
+
const rightClosed = options.intervalClosed !== "l";
|
|
142
|
+
const underflow = isFinite_(options.underflow) ? options.underflow : null;
|
|
143
|
+
const overflow = isFinite_(options.overflow) ? options.overflow : null;
|
|
144
|
+
// The regular bins span the values between the end bins.
|
|
145
|
+
const inRange = numeric.filter((v) => (underflow === null || v >= underflow) &&
|
|
146
|
+
(overflow === null || v <= overflow));
|
|
147
|
+
const start = underflow ?? (inRange.length ? Math.min(...inRange) : min);
|
|
148
|
+
const end = overflow ?? (inRange.length ? Math.max(...inRange) : max);
|
|
149
|
+
const span = end - start;
|
|
150
|
+
let width;
|
|
151
|
+
if (isFinite_(options.binSize) && options.binSize > 0) {
|
|
152
|
+
width = options.binSize;
|
|
153
|
+
}
|
|
154
|
+
else if (typeof binCount === "number" && binCount > 0) {
|
|
155
|
+
width = span > 0 ? span / Math.floor(binCount) : 1;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
width = excelAutoBinWidth(inRange.length ? inRange : numeric);
|
|
159
|
+
}
|
|
160
|
+
// Enough bins to reach the last value; a right-closed layout needs one
|
|
161
|
+
// more when the span is an exact multiple (the boundary value belongs to
|
|
162
|
+
// the bin below, so the top bin stays).
|
|
163
|
+
let bins = span > 0 ? Math.ceil(span / width - 1e-9) : 1;
|
|
164
|
+
bins = Math.max(1, bins);
|
|
165
|
+
const counts = new Array(bins).fill(0);
|
|
166
|
+
const edges = Array.from({ length: bins + 1 }, (_, i) => start + width * i);
|
|
167
|
+
let under = 0;
|
|
168
|
+
let over = 0;
|
|
169
|
+
for (const v of numeric) {
|
|
170
|
+
if (underflow !== null && v < underflow) {
|
|
171
|
+
under++;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (overflow !== null && v > overflow) {
|
|
175
|
+
over++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
let idx;
|
|
179
|
+
if (rightClosed) {
|
|
180
|
+
// (lo, hi]: a value on a boundary belongs to the bin below it; the
|
|
181
|
+
// first bin also takes its own lower bound.
|
|
182
|
+
idx = Math.ceil((v - start) / width - 1e-9) - 1;
|
|
183
|
+
if (idx < 0)
|
|
184
|
+
idx = 0;
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
idx = Math.floor((v - start) / width + 1e-9);
|
|
188
|
+
}
|
|
189
|
+
if (idx >= bins)
|
|
190
|
+
idx = bins - 1;
|
|
191
|
+
counts[idx]++;
|
|
192
|
+
}
|
|
193
|
+
const labels = edges.slice(0, -1).map((lo, i) => {
|
|
194
|
+
const hi = edges[i + 1];
|
|
195
|
+
if (rightClosed) {
|
|
196
|
+
const opening = i === 0 ? "[" : "(";
|
|
197
|
+
return `${opening}${formatBinBound(lo)}, ${formatBinBound(hi)}]`;
|
|
198
|
+
}
|
|
199
|
+
const closing = i === bins - 1 ? "]" : ")";
|
|
200
|
+
return `[${formatBinBound(lo)}, ${formatBinBound(hi)}${closing}`;
|
|
201
|
+
});
|
|
202
|
+
if (underflow !== null) {
|
|
203
|
+
labels.unshift(`< ${formatBinBound(underflow)}`);
|
|
204
|
+
counts.unshift(under);
|
|
205
|
+
}
|
|
206
|
+
if (overflow !== null) {
|
|
207
|
+
labels.push(`> ${formatBinBound(overflow)}`);
|
|
208
|
+
counts.push(over);
|
|
209
|
+
}
|
|
210
|
+
return { labels, counts };
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* Excel's histogram "By Category" / Pareto over categories
|
|
214
|
+
* (`<cx:aggregation/>`, #1087): one column per distinct category in order
|
|
215
|
+
* of first appearance, its value the aggregate (sum by default) of the
|
|
216
|
+
* series values of that category — instead of binning the values.
|
|
217
|
+
*/
|
|
218
|
+
export const aggregateByCategory = (categories, values, aggregateType = "SUM") => {
|
|
219
|
+
const groups = new Map();
|
|
220
|
+
const order = [];
|
|
221
|
+
const count = Math.max(categories.length, values.length);
|
|
222
|
+
for (let i = 0; i < count; i++) {
|
|
223
|
+
const raw = categories[i];
|
|
224
|
+
const label = raw === null || raw === undefined ? "" : `${raw}`.trim();
|
|
225
|
+
if (label === "")
|
|
226
|
+
continue;
|
|
227
|
+
if (!groups.has(label)) {
|
|
228
|
+
groups.set(label, []);
|
|
229
|
+
order.push(label);
|
|
230
|
+
}
|
|
231
|
+
const v = values[i];
|
|
232
|
+
if (isFinite_(v))
|
|
233
|
+
groups.get(label).push(v);
|
|
234
|
+
}
|
|
235
|
+
const reduce = (nums) => {
|
|
236
|
+
if (nums.length === 0)
|
|
237
|
+
return 0;
|
|
238
|
+
switch (aggregateType ?? "SUM") {
|
|
239
|
+
case "COUNT":
|
|
240
|
+
return nums.length;
|
|
241
|
+
case "AVERAGE":
|
|
242
|
+
return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
243
|
+
case "MAX":
|
|
244
|
+
return Math.max(...nums);
|
|
245
|
+
case "MIN":
|
|
246
|
+
return Math.min(...nums);
|
|
247
|
+
case "MEDIAN": {
|
|
248
|
+
const sorted = [...nums].sort((a, b) => a - b);
|
|
249
|
+
const mid = Math.floor(sorted.length / 2);
|
|
250
|
+
return sorted.length % 2
|
|
251
|
+
? sorted[mid]
|
|
252
|
+
: (sorted[mid - 1] + sorted[mid]) / 2;
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
return nums.reduce((a, b) => a + b, 0);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
return {
|
|
259
|
+
labels: order,
|
|
260
|
+
counts: order.map((label) => reduce(groups.get(label) ?? [])),
|
|
261
|
+
};
|
|
262
|
+
};
|
|
@@ -8,7 +8,7 @@ export declare const applyLegendOverrides: (legend: LegendComponentOption, opts:
|
|
|
8
8
|
* becomes the tick-label formatter.
|
|
9
9
|
*/
|
|
10
10
|
export declare const applyAxisOverrides: (axis: any, opts: any, numberFormatPattern?: string, locale?: string) => any;
|
|
11
|
-
export declare const dataLabelFormatter: (opts: any, locale?: string) => (params: any) => string;
|
|
11
|
+
export declare const dataLabelFormatter: (opts: any, locale?: string, joiner?: string) => (params: any) => string;
|
|
12
12
|
/**
|
|
13
13
|
* The data-label options one series renders with: its own
|
|
14
14
|
* `ChartData.dataLabels` when it has them (OOXML's `<c:ser><c:dLbls>`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../../utils/overrides.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAWrD,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,KAAK,GAAG,oCASpD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,qBAAqB,EAC7B,MAAM,GAAG,KACR,qBA6CF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,GAAG,EACT,MAAM,GAAG,EACT,sBAAsB,MAAM,EAC5B,SAAS,MAAM,QA4BhB,CAAC;AAgBF,eAAO,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../../utils/overrides.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAWrD,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,KAAK,GAAG,oCASpD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,qBAAqB,EAC7B,MAAM,GAAG,KACR,qBA6CF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,GAAG,EACT,MAAM,GAAG,EACT,sBAAsB,MAAM,EAC5B,SAAS,MAAM,QA4BhB,CAAC;AAgBF,eAAO,MAAM,kBAAkB,GAC7B,MAAM,GAAG,EACT,SAAS,MAAM,EAEf,eAAa,MACT,QAAQ,GAAG,WAoDhB,CAAC;AAgCF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,CAAC,EACvC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EAChC,QAAQ;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,KACnD,CAAC,GAAG,IAAI,GAAG,SAA6C,CAAC;AAE5D,eAAO,MAAM,sBAAsB,GACjC,MAAM,GAAG,EACT,aAAa,MAAM,EACnB,SAAS,MAAM;;;iCAlGH,GAAG;aA+GhB,CAAC"}
|