@rowsncolumns/charts 28.1.10 → 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.
@@ -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";
@@ -104,7 +104,8 @@ const isBubbleChartSpec = (spec) => BUBBLE_CHART_TYPES.has(spec.chartType);
104
104
  const isHierarchyChartSpec = (spec) => HIERARCHY_CHART_TYPES.has(spec.chartType);
105
105
  // Every level keeps the colour its node was given (one solid colour per
106
106
  // top-level category, like Excel — no per-level saturation ramp); thin
107
- // white gaps separate the cells.
107
+ // white gaps separate the cells. Index 0 is echarts' virtual root, 1 the
108
+ // top-level categories, 2+ the levels beneath them.
108
109
  const HIERARCHY_LEVEL_STYLES = [
109
110
  {
110
111
  itemStyle: {
@@ -112,28 +113,55 @@ const HIERARCHY_LEVEL_STYLES = [
112
113
  borderColor: "#ffffff",
113
114
  borderWidth: 0,
114
115
  },
115
- upperLabel: { show: false },
116
116
  },
117
117
  {
118
118
  itemStyle: {
119
- gapWidth: 1,
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
  },
123
130
  },
124
131
  ];
132
+ /** Height of the strip a treemap category name is drawn in (Excel's "Banner"). */
133
+ const TREEMAP_BANNER_HEIGHT = 20;
134
+ /**
135
+ * Excel's treemap `parentLabelLayout`: the top-level category names are
136
+ * drawn in a banner strip (or not at all); the levels between the categories
137
+ * and the leaves are never labelled, so small groups keep their height for
138
+ * the leaf cells (rowsncolumns/spreadsheet#1072). Excel's default
139
+ * `overlapping` writes the name over the group's top-left corner, which an
140
+ * echarts treemap cannot draw (an upper label always reserves its strip and
141
+ * is dropped when the strip is too small for the text), so it draws as a
142
+ * banner.
143
+ */
144
+ const treemapBannerShown = (layout) => layout !== "none";
125
145
  /**
126
146
  * Colour a hierarchy like Excel: each top-level node takes the next
127
147
  * palette colour and every descendant inherits it (#1072).
128
148
  */
129
- const colorHierarchyNodes = (nodes, palette) => nodes.map((node, index) => {
149
+ const colorHierarchyNodes = (nodes, palette, options = {}) => nodes.map((node, index) => {
130
150
  const color = chartPaletteColorAt(palette, index);
131
- const paint = (n) => ({
151
+ const paint = (n, depth) => ({
132
152
  ...n,
133
- itemStyle: { color },
134
- ...(n.children ? { children: n.children.map(paint) } : {}),
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)) } : {}),
135
163
  });
136
- return paint(node);
164
+ return paint(node, 0);
137
165
  });
138
166
  /** Sunburst rings: Excel leaves the centre empty, so even the first level is a ring (#1073). */
139
167
  const SUNBURST_RADIUS = ["15%", "80%"];
@@ -170,17 +198,30 @@ const flattenChartDataValues = (chartData, extractor, hiddenStrategy, skipFirstR
170
198
  });
171
199
  return values;
172
200
  };
173
- const buildHierarchyLevels = (maxDepth) => {
201
+ const buildHierarchyLevels = (maxDepth, options) => {
174
202
  if (maxDepth <= 0)
175
203
  return undefined;
176
204
  const levels = [];
177
- for (let i = 0; i < maxDepth; i++) {
205
+ // One entry per depth, the virtual root included, so every level's upper
206
+ // label is explicit: the categories' banner on level 1 only.
207
+ for (let i = 0; i <= maxDepth; i++) {
178
208
  const baseStyle = HIERARCHY_LEVEL_STYLES[Math.min(i, HIERARCHY_LEVEL_STYLES.length - 1)];
179
209
  levels.push({
180
210
  ...baseStyle,
181
211
  itemStyle: {
182
212
  ...baseStyle.itemStyle,
183
213
  },
214
+ ...(options.treemap
215
+ ? {
216
+ upperLabel: i === 1 && options.banner
217
+ ? {
218
+ show: true,
219
+ height: TREEMAP_BANNER_HEIGHT,
220
+ padding: [0, 0, 0, 4],
221
+ }
222
+ : { show: false },
223
+ }
224
+ : {}),
184
225
  });
185
226
  }
186
227
  return levels;
@@ -808,12 +849,35 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
808
849
  theme.themeColors.accent5,
809
850
  theme.themeColors.accent6,
810
851
  ]);
811
- const hierarchyData = colorHierarchyNodes(buildHierarchyTree(hierarchyRows), hierarchyPalette);
852
+ const isTreemap = spec.chartType === "treemap";
853
+ const banner = isTreemap && treemapBannerShown(spec.parentLabelLayout);
854
+ const hierarchyData = colorHierarchyNodes(buildHierarchyTree(hierarchyRows), hierarchyPalette, { banner });
812
855
  const rootName = hierarchyData.length === 1 ? hierarchyData[0]?.name ?? "" : "";
813
856
  const drillDown = Boolean(spec.drillDown);
814
857
  const seriesDisplayName = explicitChartTitle ?? headerTitle ?? rootName ?? "";
815
858
  const hierarchyDepth = getHierarchyDepth(hierarchyData);
816
- const levels = buildHierarchyLevels(hierarchyDepth);
859
+ const levels = buildHierarchyLevels(hierarchyDepth, {
860
+ treemap: isTreemap,
861
+ banner,
862
+ });
863
+ // Excel lists the top-level categories with their colours in the
864
+ // legend. An echarts legend cannot address treemap nodes, so each
865
+ // category gets a data-less placeholder series carrying its name and
866
+ // colour; `selectedMode: false` keeps the swatches from toggling
867
+ // anything.
868
+ const hierarchyLegend = applyLegendOverrides({ ...defaultLegendConfig, show: false }, spec.legend);
869
+ const legendShown = hierarchyLegend.show !== false;
870
+ const categoryLegendSeries = legendShown
871
+ ? hierarchyData.map((node) => ({
872
+ type: "bar",
873
+ id: `rnc-hierarchy-legend-${node.id ?? node.name}`,
874
+ name: node.name,
875
+ data: [],
876
+ silent: true,
877
+ itemStyle: { color: node.itemStyle?.color },
878
+ tooltip: { show: false },
879
+ }))
880
+ : [];
817
881
  // Sizes format like the cells they were read from (Excel's "Linked
818
882
  // to source"), unless the data labels carry their own pattern.
819
883
  const sizeNumberFormat = resolveSourceNumberFormat(spec.sizeData?.[0]
@@ -824,12 +888,45 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
824
888
  : params.value ?? "";
825
889
  return `${params.name}: ${formatAxisValue(sizeNumberFormat, value, locale)}`;
826
890
  };
827
- // Spec-level overrides — treemap/sunburst default to no legend,
828
- // but an authored legend (visibility/position) still wins.
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.
829
895
  const hierarchyDataLabels = spec.dataLabels;
830
- const hierarchySeriesLabel = buildSeriesLabelOption(hierarchyDataLabels && !hierarchyDataLabels.numberFormatPattern
896
+ const hierarchyLabelOpts = hierarchyDataLabels && !hierarchyDataLabels.numberFormatPattern
831
897
  ? { ...hierarchyDataLabels, numberFormatPattern: sizeNumberFormat }
832
- : hierarchyDataLabels, undefined, locale);
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
+ });
833
930
  chartRef.current?.setOption({
834
931
  xAxis: [
835
932
  {
@@ -843,7 +940,12 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
843
940
  show: false,
844
941
  },
845
942
  ],
846
- legend: applyLegendOverrides({ ...defaultLegendConfig, show: false }, spec.legend),
943
+ legend: {
944
+ ...hierarchyLegend,
945
+ ...hierarchyBand.legend,
946
+ data: hierarchyData.map((node) => node.name),
947
+ selectedMode: false,
948
+ },
847
949
  grid: {
848
950
  left: 0,
849
951
  right: 0,
@@ -860,21 +962,22 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
860
962
  name: seriesDisplayName,
861
963
  rootName: rootName || seriesDisplayName || "",
862
964
  data: hierarchyData,
863
- label: {
864
- show: true,
865
- formatter: "{b}",
866
- ...(spec.chartType === "treemap"
867
- ? { position: "insideBottomLeft" }
868
- : {}),
869
- ...(hierarchySeriesLabel ?? {}),
870
- },
871
- upperLabel: spec.chartType === "treemap"
965
+ label: hierarchyLabel,
966
+ // Only `levels[1]` (the categories) may carry a banner; see
967
+ // buildHierarchyLevels.
968
+ upperLabel: isTreemap ? { show: false } : undefined,
969
+ levels,
970
+ ...(isTreemap
872
971
  ? {
873
- show: true,
874
- padding: [0, 0, 0, 4],
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,
875
979
  }
876
- : undefined,
877
- levels,
980
+ : {}),
878
981
  // Excel has no drill-down on either chart: zooming into a
879
982
  // branch on click (and the treemap breadcrumb) is opt-in.
880
983
  nodeClick: drillDown
@@ -882,10 +985,11 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
882
985
  ? "zoomToNode"
883
986
  : "rootToNode"
884
987
  : false,
885
- ...(spec.chartType === "treemap"
988
+ ...(isTreemap
886
989
  ? { breadcrumb: { show: drillDown } }
887
990
  : { radius: [...SUNBURST_RADIUS] }),
888
991
  },
992
+ ...categoryLegendSeries,
889
993
  ],
890
994
  }, {
891
995
  replaceMerge: [
@@ -898,7 +1002,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
898
1002
  ],
899
1003
  });
900
1004
  previousChartType.current = spec.chartType;
901
- previousSeriesCount.current = 1;
1005
+ previousSeriesCount.current = 1 + categoryLegendSeries.length;
902
1006
  return;
903
1007
  }
904
1008
  else {
@@ -2369,7 +2473,21 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2369
2473
  }
2370
2474
  else if (spec.chartType === "histogram") {
2371
2475
  const rawValues = flattenChartDataValues(spec.series, getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
2372
- let { labels, counts } = binHistogram(rawValues, spec.binCount);
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
+ });
2373
2491
  // Pareto — a histogram whose bins are sorted descending with a
2374
2492
  // cumulative-% line overlaid on a secondary 0-100% axis. The
2375
2493
  // authored type arrives as `sourceChartType: "PARETO"` on an
@@ -2399,7 +2517,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2399
2517
  type: "bar",
2400
2518
  name: spec.series?.[0]?.dataLabel ?? "",
2401
2519
  data: counts,
2402
- barCategoryGap: "1%",
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%",
2403
2524
  };
2404
2525
  const histogramColor = spec.series?.[0]?.color;
2405
2526
  if (histogramColor) {
@@ -2409,14 +2530,21 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2409
2530
  if (isPareto) {
2410
2531
  // Cumulative-% overlay on the secondary axis (yAxisIndex 1);
2411
2532
  // the matching 0-100% axis element is appended with the other
2412
- // value axes below.
2533
+ // value axes below. Excel draws the Pareto line without markers.
2534
+ const paretoLineColor = spec.paretoLineColor;
2413
2535
  chartSeries.push({
2414
2536
  type: "line",
2415
2537
  name: "Cumulative %",
2416
2538
  data: cumulativePercents,
2417
2539
  yAxisIndex: 1,
2418
- symbol: "circle",
2419
- symbolSize: 6,
2540
+ showSymbol: false,
2541
+ symbol: "none",
2542
+ ...(paretoLineColor
2543
+ ? {
2544
+ itemStyle: { color: paretoLineColor },
2545
+ lineStyle: { color: paretoLineColor },
2546
+ }
2547
+ : {}),
2420
2548
  });
2421
2549
  }
2422
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;AA6ZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAmGnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBAgkFjB,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,28 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
851
851
  ...(isSunburst ? { line: { width: 1 } } : {}),
852
852
  },
853
853
  name: seriesName,
854
- textinfo: "label+value",
854
+ // `dataLabels: { show: false }` turns the labels off (#1086).
855
+ textinfo: spec.dataLabels?.show === false ? "none" : "label+value",
855
856
  branchvalues: "total",
856
- ...(isSunburst ? {} : { textposition: "bottom left" }),
857
+ ...(isSunburst || spec.dataLabels?.show === false
858
+ ? {}
859
+ : {
860
+ textposition: "bottom left",
861
+ // Excel labels a treemap's top-level categories (in a header
862
+ // strip, or not at all with parentLabelLayout "none") and its
863
+ // leaves; the levels in between are never labelled (#1072).
864
+ text: hierarchyData.labels.map(() => ""),
865
+ texttemplate: hierarchyData.ids.map((id, i) => {
866
+ const isLeaf = !hierarchyData.parents.includes(id);
867
+ if (isLeaf)
868
+ return "%{label}<br>%{value}";
869
+ const isTopLevel = hierarchyData.depths[i] === 0;
870
+ return isTopLevel &&
871
+ spec.parentLabelLayout !== "none"
872
+ ? "%{label}"
873
+ : "%{text}";
874
+ }),
875
+ }),
857
876
  };
858
877
  const layoutForHierarchy = {
859
878
  title: {
@@ -1601,12 +1620,29 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1601
1620
  if (effectiveChartType === "histogram") {
1602
1621
  const rawValues = seriesValuesOf(seriesData);
1603
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;
1604
1640
  if (isPareto) {
1605
1641
  // Plotly's native histogram trace bins in axis order and
1606
1642
  // can't reorder bins, so Pareto pre-bins with the shared
1607
1643
  // util, sorts descending, and renders a bar trace plus the
1608
1644
  // cumulative-% line pointed at the y2 overlay axis.
1609
- const { labels, counts, cumulativePercents } = computePareto(binHistogram(rawValues, binCount));
1645
+ const { labels, counts, cumulativePercents } = computePareto(prebinned ?? binHistogram(rawValues, binCount));
1610
1646
  const barTrace = {
1611
1647
  type: "bar",
1612
1648
  x: labels,
@@ -1620,16 +1656,34 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1620
1656
  effectiveChartType,
1621
1657
  });
1622
1658
  traces.push(barTrace);
1659
+ const paretoLineColor = spec.paretoLineColor;
1660
+ // Excel draws the Pareto line without markers.
1623
1661
  traces.push({
1624
1662
  type: "scatter",
1625
- mode: "lines+markers",
1663
+ mode: "lines",
1626
1664
  x: labels,
1627
1665
  y: cumulativePercents,
1628
1666
  name: "Cumulative %",
1629
1667
  yaxis: "y2",
1668
+ ...(paretoLineColor ? { line: { color: paretoLineColor } } : {}),
1630
1669
  });
1631
1670
  hasSecondaryAxis = true;
1632
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
+ }
1633
1687
  else {
1634
1688
  const trace = {
1635
1689
  type: "histogram",
@@ -2113,7 +2167,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2113
2167
  // gapWidth=150 (default) → ~0.4 bargap.
2114
2168
  ...(typeof spec.gapWidth === "number"
2115
2169
  ? { bargap: Math.max(0, Math.min(0.95, spec.gapWidth / 400)) }
2116
- : {}),
2170
+ : spec.chartType === "histogram"
2171
+ ? // Excel's histogram columns touch by default (#1087).
2172
+ { bargap: 0 }
2173
+ : {}),
2117
2174
  font: {
2118
2175
  family: "Untitled Sans, apple-system, sans-serif",
2119
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
- * Bin a set of numeric values into `binCount` equal-width bins.
43
- * When `binCount` is omitted, uses Sturges' rule: ceil(log2(n) + 1).
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 declare const binHistogram: (values: (number | null | undefined)[], binCount?: number | null) => HistogramBins;
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;AASF;;;GAGG;AACH,eAAO,MAAM,YAAY,GACvB,QAAQ,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EACrC,WAAW,MAAM,GAAG,IAAI,KACvB,aA0CF,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"}