@rowsncolumns/charts 28.1.11 → 28.1.13

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.
Files changed (35) hide show
  1. package/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -1
  2. package/dist/cjs/basic-chart/basic-chart.js +462 -286
  3. package/dist/cjs/basic-chart/helpers.d.ts.map +1 -1
  4. package/dist/cjs/basic-chart/helpers.js +2 -0
  5. package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  6. package/dist/cjs/plotly/basic-chart/basic-chart.js +198 -149
  7. package/dist/cjs/plotly/basic-chart/helpers.d.ts.map +1 -1
  8. package/dist/cjs/plotly/basic-chart/helpers.js +2 -0
  9. package/dist/cjs/utils/hierarchy.d.ts +6 -0
  10. package/dist/cjs/utils/hierarchy.d.ts.map +1 -1
  11. package/dist/cjs/utils/hierarchy.js +23 -1
  12. package/dist/cjs/utils/histogram.d.ts +42 -3
  13. package/dist/cjs/utils/histogram.d.ts.map +1 -1
  14. package/dist/cjs/utils/histogram.js +193 -5
  15. package/dist/cjs/utils/overrides.d.ts +1 -1
  16. package/dist/cjs/utils/overrides.d.ts.map +1 -1
  17. package/dist/cjs/utils/overrides.js +44 -39
  18. package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
  19. package/dist/esm/basic-chart/basic-chart.js +297 -109
  20. package/dist/esm/basic-chart/helpers.d.ts.map +1 -1
  21. package/dist/esm/basic-chart/helpers.js +2 -0
  22. package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  23. package/dist/esm/plotly/basic-chart/basic-chart.js +132 -70
  24. package/dist/esm/plotly/basic-chart/helpers.d.ts.map +1 -1
  25. package/dist/esm/plotly/basic-chart/helpers.js +2 -0
  26. package/dist/esm/utils/hierarchy.d.ts +6 -0
  27. package/dist/esm/utils/hierarchy.d.ts.map +1 -1
  28. package/dist/esm/utils/hierarchy.js +21 -0
  29. package/dist/esm/utils/histogram.d.ts +42 -3
  30. package/dist/esm/utils/histogram.d.ts.map +1 -1
  31. package/dist/esm/utils/histogram.js +171 -4
  32. package/dist/esm/utils/overrides.d.ts +1 -1
  33. package/dist/esm/utils/overrides.d.ts.map +1 -1
  34. package/dist/esm/utils/overrides.js +4 -2
  35. 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: 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
  },
@@ -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
- itemStyle: { color },
147
- ...(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)) } : {}),
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%"];
@@ -759,6 +774,27 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
759
774
  resolveRange: (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy),
760
775
  });
761
776
  });
777
+ const bubbleLegend = applyLegendOverrides({
778
+ ...legend,
779
+ show: preparedSeries.length > 1,
780
+ data: preparedSeries.map((entry) => entry.name),
781
+ }, spec.legend);
782
+ const bubbleBand = reserveEchartsLegendBand({
783
+ position: spec.legend?.position,
784
+ visible: bubbleLegend.show !== false,
785
+ labels: preparedSeries.map((entry) => entry.name),
786
+ legendFontSize: bubbleLegend.textStyle?.fontSize ??
787
+ spec.legend?.textFormat?.fontSize ??
788
+ 12,
789
+ legendItemWidth: defaultLegendConfig.itemWidth,
790
+ width: width ?? 0,
791
+ height: height ?? 0,
792
+ hasTitle: Boolean(explicitChartTitle),
793
+ titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
794
+ hasBottomAxisTitle: Boolean(spec.horizontalAxisTitle),
795
+ bottomAxisNameGap: 30,
796
+ hasLeftAxisTitle: Boolean(spec.verticalAxisTitle),
797
+ });
762
798
  chartRef.current?.setOption({
763
799
  xAxis: [
764
800
  applyAxisOverrides({ ...xAxisConfig }, spec.categoryAxisOptions),
@@ -784,17 +820,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
784
820
  ]
785
821
  : []),
786
822
  ],
787
- legend: applyLegendOverrides({
788
- ...legend,
789
- show: preparedSeries.length > 1,
790
- data: preparedSeries.map((entry) => entry.name),
791
- }, spec.legend),
792
- grid: {
793
- left: "3%",
794
- right: "4%",
795
- bottom: "3%",
796
- containLabel: true,
797
- },
823
+ legend: { ...bubbleLegend, ...bubbleBand.legend },
824
+ // The plot shrinks to leave the legend its own band, as the
825
+ // category charts do (#1089).
826
+ grid: bubbleBand.grid,
798
827
  tooltip: {
799
828
  trigger: "item",
800
829
  formatter: tooltipFormatter,
@@ -834,13 +863,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
834
863
  theme.themeColors.accent5,
835
864
  theme.themeColors.accent6,
836
865
  ]);
837
- const hierarchyData = colorHierarchyNodes(buildHierarchyTree(hierarchyRows), hierarchyPalette);
866
+ const isTreemap = spec.chartType === "treemap";
867
+ const banner = isTreemap && treemapBannerShown(spec.parentLabelLayout);
868
+ const hierarchyData = colorHierarchyNodes(buildHierarchyTree(hierarchyRows), hierarchyPalette, { banner });
838
869
  const rootName = hierarchyData.length === 1 ? hierarchyData[0]?.name ?? "" : "";
839
870
  const drillDown = Boolean(spec.drillDown);
840
871
  const seriesDisplayName = explicitChartTitle ?? headerTitle ?? rootName ?? "";
841
872
  const hierarchyDepth = getHierarchyDepth(hierarchyData);
842
- const isTreemap = spec.chartType === "treemap";
843
- const banner = isTreemap && treemapBannerShown(spec.parentLabelLayout);
844
873
  const levels = buildHierarchyLevels(hierarchyDepth, {
845
874
  treemap: isTreemap,
846
875
  banner,
@@ -873,12 +902,45 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
873
902
  : params.value ?? "";
874
903
  return `${params.name}: ${formatAxisValue(sizeNumberFormat, value, locale)}`;
875
904
  };
876
- // Spec-level overrides — treemap/sunburst default to no legend,
877
- // but an authored legend (visibility/position) still wins.
905
+ // Data labels: a treemap block's label always sits in its bottom-left
906
+ // corner (the bar placements mean nothing on a block) with each part
907
+ // — name, value — on its own line; `{ show: false }` turns the labels
908
+ // off entirely (#1086). Without `dataLabels` the block shows its name.
878
909
  const hierarchyDataLabels = spec.dataLabels;
879
- const hierarchySeriesLabel = buildSeriesLabelOption(hierarchyDataLabels && !hierarchyDataLabels.numberFormatPattern
910
+ const hierarchyLabelOpts = hierarchyDataLabels && !hierarchyDataLabels.numberFormatPattern
880
911
  ? { ...hierarchyDataLabels, numberFormatPattern: sizeNumberFormat }
881
- : hierarchyDataLabels, undefined, locale);
912
+ : hierarchyDataLabels;
913
+ const hierarchyLabel = hierarchyLabelOpts?.show === false
914
+ ? { show: false }
915
+ : hierarchyLabelOpts?.show
916
+ ? {
917
+ show: true,
918
+ formatter: dataLabelFormatter(hierarchyLabelOpts, locale, isTreemap ? "\n" : ": "),
919
+ ...(isTreemap ? { position: "insideBottomLeft" } : {}),
920
+ }
921
+ : {
922
+ show: true,
923
+ formatter: "{b}",
924
+ ...(isTreemap ? { position: "insideBottomLeft" } : {}),
925
+ };
926
+ // Reserve a band for the legend (and the title) so neither paints
927
+ // over the blocks (#1086), as the cartesian layout does.
928
+ const hierarchyBand = reserveEchartsLegendBand({
929
+ position: spec.legend?.position,
930
+ visible: legendShown && hierarchyData.length > 0,
931
+ labels: hierarchyData.map((node) => node.name),
932
+ legendFontSize: hierarchyLegend.textStyle?.fontSize ??
933
+ spec.legend?.textFormat?.fontSize ??
934
+ 12,
935
+ legendItemWidth: defaultLegendConfig.itemWidth,
936
+ width: width ?? 0,
937
+ height: height ?? 0,
938
+ hasTitle: Boolean(explicitChartTitle ?? headerTitle),
939
+ titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
940
+ hasBottomAxisTitle: false,
941
+ bottomAxisNameGap: 0,
942
+ hasLeftAxisTitle: false,
943
+ });
882
944
  chartRef.current?.setOption({
883
945
  xAxis: [
884
946
  {
@@ -894,6 +956,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
894
956
  ],
895
957
  legend: {
896
958
  ...hierarchyLegend,
959
+ ...hierarchyBand.legend,
897
960
  data: hierarchyData.map((node) => node.name),
898
961
  selectedMode: false,
899
962
  },
@@ -913,16 +976,22 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
913
976
  name: seriesDisplayName,
914
977
  rootName: rootName || seriesDisplayName || "",
915
978
  data: hierarchyData,
916
- label: {
917
- show: true,
918
- formatter: "{b}",
919
- ...(isTreemap ? { position: "insideBottomLeft" } : {}),
920
- ...(hierarchySeriesLabel ?? {}),
921
- },
979
+ label: hierarchyLabel,
922
980
  // Only `levels[1]` (the categories) may carry a banner; see
923
981
  // buildHierarchyLevels.
924
982
  upperLabel: isTreemap ? { show: false } : undefined,
925
983
  levels,
984
+ ...(isTreemap
985
+ ? {
986
+ // Excel has no wheel zoom / drag pan on a treemap; the
987
+ // sheet keeps scrolling under the pointer (#1086).
988
+ roam: drillDown,
989
+ left: hierarchyBand.grid.left,
990
+ right: hierarchyBand.grid.right,
991
+ top: hierarchyBand.grid.top,
992
+ bottom: hierarchyBand.grid.bottom,
993
+ }
994
+ : {}),
926
995
  // Excel has no drill-down on either chart: zooming into a
927
996
  // branch on click (and the treemap breadcrumb) is opt-in.
928
997
  nodeClick: drillDown
@@ -1330,8 +1399,44 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1330
1399
  const lines = indicators.map((indicator, i) => `${indicator.name}: ${formatAxisValue(pattern, values[i], locale)}`);
1331
1400
  return [params?.name ?? "", ...lines].filter(Boolean).join("<br/>");
1332
1401
  };
1402
+ const radarLegend = applyLegendOverrides({
1403
+ ...legend,
1404
+ show: radarData.length > 1,
1405
+ data: radarData.map((d) => d.name),
1406
+ }, spec.legend);
1407
+ // The circle moves and shrinks to leave the legend (and the title)
1408
+ // their own band instead of being drawn over (#1089).
1409
+ const radarBand = reserveEchartsLegendBand({
1410
+ position: spec.legend?.position,
1411
+ visible: radarLegend.show !== false,
1412
+ labels: radarData.map((d) => d.name),
1413
+ legendFontSize: radarLegend.textStyle?.fontSize ??
1414
+ spec.legend?.textFormat?.fontSize ??
1415
+ 12,
1416
+ legendItemWidth: defaultLegendConfig.itemWidth,
1417
+ width: width ?? 0,
1418
+ height: height ?? 0,
1419
+ hasTitle: Boolean(explicitChartTitle),
1420
+ titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
1421
+ hasBottomAxisTitle: false,
1422
+ bottomAxisNameGap: 0,
1423
+ hasLeftAxisTitle: false,
1424
+ });
1425
+ const radarPlotWidth = Math.max(0, (width ?? 0) - radarBand.grid.left - radarBand.grid.right);
1426
+ const radarPlotHeight = Math.max(0, (height ?? 0) - radarBand.grid.top - radarBand.grid.bottom);
1427
+ // Leave ~30px around the circle for the axis names.
1428
+ const radarGeometry = radarPlotWidth > 0 && radarPlotHeight > 0
1429
+ ? {
1430
+ center: [
1431
+ radarBand.grid.left + radarPlotWidth / 2,
1432
+ radarBand.grid.top + radarPlotHeight / 2,
1433
+ ],
1434
+ radius: Math.max(10, Math.min(radarPlotWidth, radarPlotHeight) / 2 - 30),
1435
+ }
1436
+ : {};
1333
1437
  chartRef.current?.setOption({
1334
1438
  radar: {
1439
+ ...radarGeometry,
1335
1440
  // The common scale's tick labels along ONE axis, as Excel draws
1336
1441
  // them: echarts labels every indicator axis, so each indicator
1337
1442
  // carries its own `axisLabel.show` (an indicator's own options win
@@ -1350,11 +1455,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1350
1455
  formatter: (value) => formatAxisValue(radarFormats[0], value, locale),
1351
1456
  },
1352
1457
  },
1353
- legend: applyLegendOverrides({
1354
- ...legend,
1355
- show: radarData.length > 1,
1356
- data: radarData.map((d) => d.name),
1357
- }, spec.legend),
1458
+ legend: { ...radarLegend, ...radarBand.legend },
1358
1459
  tooltip: { trigger: "item", formatter: radarTooltipFormatter },
1359
1460
  series: [
1360
1461
  {
@@ -2301,6 +2402,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2301
2402
  top: 60,
2302
2403
  bottom: 20,
2303
2404
  };
2405
+ // `dataLabels: { show: false }` hides the values inside the bars; the
2406
+ // stage names on the left stay (#1091).
2407
+ const funnelLabelsOff = resolveSeriesDataLabels(spec.dataLabels, seriesEntry)?.show ===
2408
+ false;
2304
2409
  chartSeries.push({
2305
2410
  type: "funnel",
2306
2411
  id: chartSeriesId(0, 0),
@@ -2310,11 +2415,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2310
2415
  itemStyle: {
2311
2416
  color: (params) => stageColors.get(params.dataIndex) ?? funnelColor,
2312
2417
  },
2313
- label: {
2314
- show: true,
2315
- position: "inside",
2316
- formatter: (params) => formatAxisValue(undefined, params.value, locale),
2317
- },
2418
+ label: funnelLabelsOff
2419
+ ? { show: false }
2420
+ : {
2421
+ show: true,
2422
+ position: "inside",
2423
+ formatter: (params) => formatAxisValue(undefined, params.value, locale),
2424
+ },
2318
2425
  });
2319
2426
  // The stage names sit to the left of the bars, where Excel's
2320
2427
  // category axis puts them: an invisible twin funnel carries them.
@@ -2331,49 +2438,49 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2331
2438
  labelLine: { show: false },
2332
2439
  z: 1,
2333
2440
  });
2334
- legend.data = [];
2441
+ // The legend lists the series (shown when `legend.position` says so).
2442
+ legend.data = seriesEntry?.dataLabel ? [seriesEntry.dataLabel] : [];
2335
2443
  }
2336
2444
  else if (spec.chartType === "boxplot") {
2337
2445
  const quartileMethod = spec.quartileMethod;
2338
- const showOutliers = spec.showOutliers;
2446
+ const showOutliers = spec.showOutliers !== false;
2339
2447
  const showMeanMarker = spec.showMeanMarker === true;
2340
- const boxStats = [];
2341
- const boxLabels = [];
2342
- // [categoryIndex, value] pairs for the outlier / mean overlay
2343
- // scatter series (only populated when the options are on).
2344
- const outlierPoints = [];
2345
- const meanPoints = [];
2346
- for (const seriesEntry of spec.series ?? []) {
2347
- const label = seriesEntry.dataLabel && !isAFormula(seriesEntry.dataLabel)
2348
- ? seriesEntry.dataLabel
2349
- : "";
2350
- const rawValues = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
2351
- const summary = computeBoxplotSummary(rawValues, { quartileMethod });
2352
- if (summary) {
2353
- const boxIndex = boxStats.length;
2354
- // `showOutliers === true` clips the whiskers to the most
2355
- // extreme data points inside the 1.5×IQR fences and plots
2356
- // the points beyond them (Excel's "show outlier points").
2357
- // Otherwise the whiskers span the full min/max — today's
2358
- // rendering.
2359
- const lo = showOutliers === true ? summary.lowerWhisker : summary.min;
2360
- const hi = showOutliers === true ? summary.upperWhisker : summary.max;
2361
- boxStats.push([lo, summary.q1, summary.median, summary.q3, hi]);
2362
- boxLabels.push(label);
2363
- if (showOutliers === true) {
2364
- for (const outlier of summary.outliers) {
2365
- outlierPoints.push([boxIndex, outlier]);
2366
- }
2367
- }
2368
- if (showMeanMarker) {
2369
- meanPoints.push([boxIndex, summary.mean]);
2370
- }
2448
+ const showInnerPoints = spec.showInnerPoints === true;
2449
+ const showMeanLine = spec.showMeanLine === true;
2450
+ // A category domain groups every series by its distinct values (in
2451
+ // order of first appearance): one box per category per series, as
2452
+ // Excel draws it. Without a domain each series is one box on its own
2453
+ // slot (#1092).
2454
+ const boxDomainSource = spec.domains?.[0]?.sources?.[0];
2455
+ const boxCategories = boxDomainSource
2456
+ ? (getDomainValuesFromRange(boxDomainSource, spec.hiddenDimensionStrategy) ?? []).map((v) => (v === null || v === undefined ? "" : `${v}`.trim()))
2457
+ : [];
2458
+ const grouped = boxCategories.some((c) => c !== "");
2459
+ const groups = [];
2460
+ if (grouped) {
2461
+ for (const c of boxCategories) {
2462
+ if (c !== "" && !groups.includes(c))
2463
+ groups.push(c);
2371
2464
  }
2372
2465
  }
2466
+ const boxSeriesSpecs = spec.series ?? [];
2467
+ const slots = grouped ? groups : boxSeriesSpecs.map((s, i) => s.dataLabel && !isAFormula(s.dataLabel) ? s.dataLabel : `Series ${i + 1}`);
2468
+ // Excel sizes the boxes like columns: gapWidth is a percentage of the
2469
+ // box width (default 150). echarts' boxplot only clamps its width
2470
+ // (`boxWidth`), so the clamp is the box width that gap allows in the
2471
+ // plot's category band.
2472
+ const boxGap = typeof spec.gapWidth === "number" ? Math.max(0, spec.gapWidth) : 150;
2473
+ const boxPlotWidth = Math.max(0, (width ?? 0) * 0.85 - 40);
2474
+ const boxBand = slots.length ? boxPlotWidth / slots.length : boxPlotWidth;
2475
+ const boxWidthMax = Math.max(2, Math.round(boxBand / (Math.max(1, boxSeriesSpecs.length) + boxGap / 100)));
2476
+ const boxCount = Math.max(1, boxSeriesSpecs.length);
2477
+ // Points of series i sit at that series' sub-slot inside the band
2478
+ // (echarts spreads a category's boxes over ~80% of it).
2479
+ const slotX = (slotIndex, seriesIndex) => slotIndex + ((seriesIndex + 0.5) / boxCount - 0.5) * 0.8;
2373
2480
  chartDomains.length = 0;
2374
2481
  chartDomains.push({
2375
2482
  type: "category",
2376
- data: boxLabels,
2483
+ data: slots,
2377
2484
  show: showAxis,
2378
2485
  axisLine: { show: true },
2379
2486
  axisTick: { show: true },
@@ -2383,42 +2490,113 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2383
2490
  nameGap: 30,
2384
2491
  });
2385
2492
  chartSeries.length = 0;
2386
- const boxSeries = {
2387
- type: "boxplot",
2388
- name: "",
2389
- data: boxStats,
2390
- };
2391
- const boxColor = spec.series?.[0]?.color;
2392
- if (boxColor) {
2393
- boxSeries.itemStyle = { color: boxColor, borderColor: boxColor };
2394
- }
2395
- chartSeries.push(boxSeries);
2396
- if (outlierPoints.length > 0) {
2397
- chartSeries.push({
2398
- type: "scatter",
2399
- name: "",
2400
- data: outlierPoints,
2401
- symbolSize: 6,
2402
- ...(boxColor ? { itemStyle: { color: boxColor } } : {}),
2403
- tooltip: { show: false },
2493
+ const boxLegend = [];
2494
+ boxSeriesSpecs.forEach((seriesEntry, seriesIndex) => {
2495
+ const label = seriesEntry.dataLabel && !isAFormula(seriesEntry.dataLabel)
2496
+ ? seriesEntry.dataLabel
2497
+ : "";
2498
+ const rawValues = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
2499
+ const color = seriesEntry.color ?? paletteColorAt(seriesIndex);
2500
+ const boxData = [];
2501
+ const outlierPoints = [];
2502
+ const innerPoints = [];
2503
+ const meanPoints = [];
2504
+ const meanLine = [];
2505
+ const slotValues = grouped
2506
+ ? groups.map((g) => rawValues.filter((_, i) => boxCategories[i] === g))
2507
+ : slots.map((_, i) => (i === seriesIndex ? rawValues : []));
2508
+ slotValues.forEach((values, slotIndex) => {
2509
+ const summary = computeBoxplotSummary(values, { quartileMethod });
2510
+ if (!summary) {
2511
+ boxData.push([]);
2512
+ meanLine.push(null);
2513
+ return;
2514
+ }
2515
+ // Whiskers stop at the most extreme points inside the 1.5×IQR
2516
+ // fences; hiding the outliers hides the points only (Excel).
2517
+ boxData.push([
2518
+ summary.lowerWhisker,
2519
+ summary.q1,
2520
+ summary.median,
2521
+ summary.q3,
2522
+ summary.upperWhisker,
2523
+ ]);
2524
+ meanLine.push(summary.mean);
2525
+ const x = slotX(slotIndex, seriesIndex);
2526
+ if (showOutliers) {
2527
+ for (const outlier of summary.outliers)
2528
+ outlierPoints.push([x, outlier]);
2529
+ }
2530
+ if (showInnerPoints) {
2531
+ for (const v of values) {
2532
+ if (v >= summary.lowerWhisker && v <= summary.upperWhisker) {
2533
+ innerPoints.push([x, v]);
2534
+ }
2535
+ }
2536
+ }
2537
+ if (showMeanMarker)
2538
+ meanPoints.push([x, summary.mean]);
2404
2539
  });
2405
- }
2406
- if (meanPoints.length > 0) {
2540
+ if (label)
2541
+ boxLegend.push(label);
2407
2542
  chartSeries.push({
2543
+ type: "boxplot",
2544
+ id: chartSeriesId(seriesIndex, 0),
2545
+ name: label,
2546
+ data: boxData,
2547
+ boxWidth: [2, boxWidthMax],
2548
+ itemStyle: { color, borderColor: color },
2549
+ });
2550
+ const pointSeries = (name, points, extra) => ({
2408
2551
  type: "scatter",
2409
- name: "",
2410
- data: meanPoints,
2411
- symbol: "diamond",
2412
- symbolSize: 9,
2413
- ...(boxColor ? { itemStyle: { color: boxColor } } : {}),
2552
+ name,
2553
+ data: points,
2554
+ itemStyle: { color },
2414
2555
  tooltip: { show: false },
2556
+ ...extra,
2415
2557
  });
2416
- }
2417
- legend.data = [];
2558
+ if (outlierPoints.length > 0) {
2559
+ chartSeries.push(pointSeries("", outlierPoints, { symbolSize: 6 }));
2560
+ }
2561
+ if (innerPoints.length > 0) {
2562
+ chartSeries.push(pointSeries("", innerPoints, { symbolSize: 4, itemStyle: { color, opacity: 0.6 } }));
2563
+ }
2564
+ if (meanPoints.length > 0) {
2565
+ chartSeries.push(pointSeries("", meanPoints, { symbol: "diamond", symbolSize: 9 }));
2566
+ }
2567
+ if (showMeanLine && meanLine.some((m) => m !== null)) {
2568
+ chartSeries.push({
2569
+ type: "line",
2570
+ name: "",
2571
+ data: meanLine.map((m, slotIndex) => m === null ? null : [slotX(slotIndex, seriesIndex), m]),
2572
+ showSymbol: false,
2573
+ lineStyle: { color, type: "dashed", width: 1 },
2574
+ itemStyle: { color },
2575
+ tooltip: { show: false },
2576
+ z: 3,
2577
+ });
2578
+ }
2579
+ });
2580
+ // The legend lists the series (shown when `legend.position` says so).
2581
+ legend.data = boxLegend;
2418
2582
  }
2419
2583
  else if (spec.chartType === "histogram") {
2420
2584
  const rawValues = flattenChartDataValues(spec.series, getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
2421
- let { labels, counts } = binHistogram(rawValues, spec.binCount);
2585
+ // A domain names a category column: Excel's "By Category" histogram
2586
+ // / Pareto over categories aggregates the values per category (sum
2587
+ // unless the series says otherwise) instead of binning them (#1087).
2588
+ const histogramDomainSource = spec.domains?.[0]?.sources?.[0];
2589
+ const histogramCategories = histogramDomainSource
2590
+ ? (getDomainValuesFromRange(histogramDomainSource, spec.hiddenDimensionStrategy) ?? [])
2591
+ : [];
2592
+ let { labels, counts } = histogramCategories.length
2593
+ ? aggregateByCategory(histogramCategories, rawValues, spec.series?.[0]?.aggregateType ?? "SUM")
2594
+ : binHistogram(rawValues, spec.binCount, {
2595
+ binSize: spec.binSize,
2596
+ intervalClosed: spec.intervalClosed,
2597
+ overflow: spec.overflow,
2598
+ underflow: spec.underflow,
2599
+ });
2422
2600
  // Pareto — a histogram whose bins are sorted descending with a
2423
2601
  // cumulative-% line overlaid on a secondary 0-100% axis. The
2424
2602
  // authored type arrives as `sourceChartType: "PARETO"` on an
@@ -2448,7 +2626,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2448
2626
  type: "bar",
2449
2627
  name: spec.series?.[0]?.dataLabel ?? "",
2450
2628
  data: counts,
2451
- barCategoryGap: "1%",
2629
+ // Excel's histogram columns touch unless `gapWidth` says otherwise
2630
+ // (#1087); the gap maps like a column chart's.
2631
+ barCategoryGap: gapWidthToBarCategoryGap(spec.gapWidth ?? 0, { seriesCount: 1 }) ??
2632
+ "0%",
2452
2633
  };
2453
2634
  const histogramColor = spec.series?.[0]?.color;
2454
2635
  if (histogramColor) {
@@ -2458,14 +2639,21 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2458
2639
  if (isPareto) {
2459
2640
  // Cumulative-% overlay on the secondary axis (yAxisIndex 1);
2460
2641
  // the matching 0-100% axis element is appended with the other
2461
- // value axes below.
2642
+ // value axes below. Excel draws the Pareto line without markers.
2643
+ const paretoLineColor = spec.paretoLineColor;
2462
2644
  chartSeries.push({
2463
2645
  type: "line",
2464
2646
  name: "Cumulative %",
2465
2647
  data: cumulativePercents,
2466
2648
  yAxisIndex: 1,
2467
- symbol: "circle",
2468
- symbolSize: 6,
2649
+ showSymbol: false,
2650
+ symbol: "none",
2651
+ ...(paretoLineColor
2652
+ ? {
2653
+ itemStyle: { color: paretoLineColor },
2654
+ lineStyle: { color: paretoLineColor },
2655
+ }
2656
+ : {}),
2469
2657
  });
2470
2658
  }
2471
2659
  legend.data = [];
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,sBAAsB,GACjC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,yGA6DzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SAuBX,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,GAAG,SAAS,KACnD,OAAgD,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,aAAa,CAAC,OAAO,CAAC,KAC7B,MAAM,EAmBR,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;aASzC,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAiBjE,CAAC"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,sBAAsB,GACjC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,yGA6DzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SAuBX,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,GAAG,SAAS,KACnD,OAAgD,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,aAAa,CAAC,OAAO,CAAC,KAC7B,MAAM,EAmBR,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;aASzC,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAmBjE,CAAC"}
@@ -121,6 +121,8 @@ export const showXAxis = (type) => {
121
121
  type === "histogram" ||
122
122
  // A waterfall's steps are categories (Opening, Sales, …) (#1070).
123
123
  type === "waterfall" ||
124
+ // A box & whisker's slots are categories (or the series names) (#1092).
125
+ type === "boxplot" ||
124
126
  // Stock is line-derived and lives on a category x-axis. Radar is
125
127
  // axis-less (it uses a polar `radar` indicator instead).
126
128
  type === "stock") {
@@ -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,sBAilFjB,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;AA8ZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAmGnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBAkqFjB,CAAC"}