@rowsncolumns/charts 28.1.8 → 28.1.10

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 (47) hide show
  1. package/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -1
  2. package/dist/cjs/basic-chart/basic-chart.js +483 -417
  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 +315 -184
  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/tsconfig.cjs.tsbuildinfo +1 -1
  10. package/dist/cjs/utils/bubble.d.ts +34 -1
  11. package/dist/cjs/utils/bubble.d.ts.map +1 -1
  12. package/dist/cjs/utils/bubble.js +39 -13
  13. package/dist/cjs/utils/echarts-overlays.d.ts +46 -0
  14. package/dist/cjs/utils/echarts-overlays.d.ts.map +1 -0
  15. package/dist/cjs/utils/echarts-overlays.js +134 -0
  16. package/dist/cjs/utils/hierarchy.d.ts +23 -1
  17. package/dist/cjs/utils/hierarchy.d.ts.map +1 -1
  18. package/dist/cjs/utils/hierarchy.js +179 -55
  19. package/dist/cjs/utils/overrides.d.ts.map +1 -1
  20. package/dist/cjs/utils/overrides.js +9 -2
  21. package/dist/cjs/utils/trendline.d.ts +7 -1
  22. package/dist/cjs/utils/trendline.d.ts.map +1 -1
  23. package/dist/cjs/utils/trendline.js +82 -18
  24. package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
  25. package/dist/esm/basic-chart/basic-chart.js +320 -239
  26. package/dist/esm/basic-chart/helpers.d.ts.map +1 -1
  27. package/dist/esm/basic-chart/helpers.js +2 -0
  28. package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  29. package/dist/esm/plotly/basic-chart/basic-chart.js +235 -58
  30. package/dist/esm/plotly/basic-chart/helpers.d.ts.map +1 -1
  31. package/dist/esm/plotly/basic-chart/helpers.js +2 -0
  32. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  33. package/dist/esm/utils/bubble.d.ts +34 -1
  34. package/dist/esm/utils/bubble.d.ts.map +1 -1
  35. package/dist/esm/utils/bubble.js +35 -11
  36. package/dist/esm/utils/echarts-overlays.d.ts +46 -0
  37. package/dist/esm/utils/echarts-overlays.d.ts.map +1 -0
  38. package/dist/esm/utils/echarts-overlays.js +135 -0
  39. package/dist/esm/utils/hierarchy.d.ts +23 -1
  40. package/dist/esm/utils/hierarchy.d.ts.map +1 -1
  41. package/dist/esm/utils/hierarchy.js +168 -50
  42. package/dist/esm/utils/overrides.d.ts.map +1 -1
  43. package/dist/esm/utils/overrides.js +9 -2
  44. package/dist/esm/utils/trendline.d.ts +7 -1
  45. package/dist/esm/utils/trendline.d.ts.map +1 -1
  46. package/dist/esm/utils/trendline.js +67 -14
  47. package/package.json +3 -3
@@ -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,YAejE,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,YAiBjE,CAAC"}
@@ -119,6 +119,8 @@ export const showXAxis = (type) => {
119
119
  type === "column" ||
120
120
  type === "bubble" ||
121
121
  type === "histogram" ||
122
+ // A waterfall's steps are categories (Opening, Sales, …) (#1070).
123
+ type === "waterfall" ||
122
124
  // Stock is line-derived and lives on a category x-axis. Radar is
123
125
  // axis-less (it uses a polar `radar` indicator instead).
124
126
  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;AAyZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AA0FnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBAq3EjB,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;AA6ZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAmGnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBAgkFjB,CAAC"}
@@ -18,13 +18,13 @@ import { plotlyMulticategoryX, resolveCategoryLevels, } from "../../utils/catego
18
18
  import { gradientToPlotly } from "../../utils/gradient";
19
19
  import { BAND_GAP, legendLabelsFrom, measureTextWidth, reservePlotlyLegendBand, } from "../../utils/legend-layout";
20
20
  import { clampDoughnutHole, doughnutRings } from "../../utils/doughnut";
21
- import { resolveSeriesDataLabels } from "../../utils/overrides";
21
+ import { dataLabelFormatter, resolveSeriesDataLabels, } from "../../utils/overrides";
22
22
  import { plotlyTitleText } from "../../utils/plotly-title";
23
23
  import { explosionToPlotlyPull, resolveExplosion, } from "../../utils/pie-explosion";
24
24
  import { resolvePlotlyClickSelection, wasDragged, } from "../../utils/plotly-click";
25
25
  import { buildPlotlySurfaceTrace } from "../../utils/plotly-surface";
26
26
  import { normalize3DSourceType, } from "../../utils/three-d";
27
- import { extractHierarchyRows, getHierarchyHeaderTitle, toPlotlyHierarchy, } from "../../utils/hierarchy";
27
+ import { extractHierarchyRows, getHierarchyHeaderTitle, hierarchyNodeColors, toPlotlyHierarchy, } from "../../utils/hierarchy";
28
28
  import { binHistogram, computePareto, isParetoSpec, } from "../../utils/histogram";
29
29
  import { clampSecondPieSize, isOfPieChartSpec, otherSliceHalfAngle, resolveOfPieType, splitOfPie, } from "../../utils/of-pie";
30
30
  import { buildRadarIndicators } from "../../utils/radar";
@@ -339,6 +339,13 @@ const sanitizeChartValue = (value) => {
339
339
  * (Excel): every range's points in sequence. A single range passes through
340
340
  * untouched so the common path renders exactly as before.
341
341
  */
342
+ /**
343
+ * The plain value array of a series' data. (`"values" in seriesData` is
344
+ * true for an ARRAY too — `Array.prototype.values` — which handed the
345
+ * waterfall / funnel / histogram / boxplot branches that method instead
346
+ * of the numbers.)
347
+ */
348
+ const seriesValuesOf = (seriesData) => Array.isArray(seriesData) ? seriesData : seriesData.values;
342
349
  const mergeSeriesData = (parts) => {
343
350
  if (parts.length === 1)
344
351
  return parts[0];
@@ -389,6 +396,11 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
389
396
  // The plotted `<div>` plotly hands back on init — what `Plotly.toImage`
390
397
  // rasterises for Copy as Picture and the print preview.
391
398
  const graphDivRef = useRef(null);
399
+ // Plotly zooms a treemap / sunburst into the clicked branch by default;
400
+ // Excel has no drill-down, so the click handler cancels it unless the
401
+ // spec opts in (`drillDown`).
402
+ const drillDownRef = useRef(false);
403
+ drillDownRef.current = Boolean(spec.drillDown);
392
404
  const focusSheet = useFocusSheet();
393
405
  // Excel selects a data series (or one data point) when it is clicked
394
406
  // inside the chart and the grid highlights its source cells. The plot
@@ -516,8 +528,16 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
516
528
  sizeValues: sharedSizeValues,
517
529
  series: seriesInputs,
518
530
  });
519
- const sizeScale = createBubbleSizeScale(sizeValues);
531
+ // Bubble diameters scale from zero (area by default, width when
532
+ // `sizeRepresents` says so) and the largest bubble is a fraction of
533
+ // the plot area, which the margins below leave of the chart (#1068).
534
+ const sizeScale = createBubbleSizeScale(sizeValues, {
535
+ sizeRepresents: spec.sizeRepresents,
536
+ bubbleScale: spec.bubbleScale,
537
+ plotMinSide: Math.min((width ?? 0) - 80, (height ?? 0) - 100),
538
+ });
520
539
  const hasStringX = categories.length > 0;
540
+ const hasBubbleSecondaryAxis = (spec.series ?? []).some((seriesEntry) => seriesEntry?.useSecondaryAxis);
521
541
  const palette = getChartPalette(spec, [
522
542
  theme.themeColors.accent1,
523
543
  theme.themeColors.accent2,
@@ -553,16 +573,56 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
553
573
  const lineShapeFor = (style) => style === "smooth" || style === "smoothMarker"
554
574
  ? "spline"
555
575
  : "linear";
556
- const traces = preparedSeries.map((entry, index) => {
576
+ const traces = [];
577
+ preparedSeries.forEach((entry, index) => {
557
578
  const xValues = entry.points.map((point) => hasStringX
558
579
  ? point.xLabel ??
559
580
  categories[Math.round(point.value[0])] ??
560
581
  point.value[0]
561
582
  : point.value[0]);
562
583
  const yValues = entry.points.map((point) => point.value[1]);
563
- const labelValuesForSeries = entry.points.map((point) => point.name);
564
- const seriesColor = spec.series[index]?.color ?? chartPaletteColorAt(palette, index);
565
- const perSeriesMarker = spec.series[index]?.marker ?? undefined;
584
+ const rawSizes = entry.points.map((point) => point.value[2]);
585
+ const seriesSpec = spec.series[index];
586
+ const seriesColor = seriesSpec?.color ?? chartPaletteColorAt(palette, index);
587
+ const perSeriesMarker = seriesSpec?.marker ?? undefined;
588
+ // Per-point colours (`<c:dPt>`) key on the point's source row
589
+ // (`BubbleSeriesPoint.index` — points without a Y are skipped).
590
+ const pointColorOverrides = new Map();
591
+ for (const dp of seriesSpec?.dataPointColors ?? []) {
592
+ if (typeof dp?.idx === "number" && typeof dp?.color === "string") {
593
+ pointColorOverrides.set(dp.idx, dp.color);
594
+ }
595
+ }
596
+ const pointColors = (base) => pointColorOverrides.size
597
+ ? entry.points.map((point) => pointColorOverrides.get(point.index) ?? base)
598
+ : base;
599
+ // Hover reads the bubble label and size from `customdata`, leaving
600
+ // `text` to the data labels.
601
+ const customdata = entry.points.map((point) => [point.name, point.value[2]]);
602
+ const onSecondary = seriesSpec?.useSecondaryAxis ? { yaxis: "y2" } : {};
603
+ // This series' data labels: Y value by default, the bubble's size
604
+ // with `showBubbleSize`, the bubble label with `showCategoryName`.
605
+ const labelOpts = resolveSeriesDataLabels(spec.dataLabels, seriesSpec);
606
+ const labelText = labelOpts?.show
607
+ ? entry.points.map((point) => dataLabelFormatter(labelOpts, locale)({
608
+ value: point.value,
609
+ name: point.name,
610
+ seriesName: entry.name,
611
+ data: point,
612
+ }))
613
+ : undefined;
614
+ const labelPosition = labelOpts?.show
615
+ ? (labelOpts.position && PLOTLY_DLBL_POS_POINT[labelOpts.position]) ??
616
+ "top center"
617
+ : undefined;
618
+ const withLabels = (trace) => labelText
619
+ ? {
620
+ ...trace,
621
+ mode: withTextMode(trace.mode),
622
+ text: labelText,
623
+ textposition: labelPosition,
624
+ }
625
+ : trace;
566
626
  if (isScatter) {
567
627
  // Per-series marker.symbol = "none" → pure line (no markers).
568
628
  // Per-series marker.size overrides the default. Falls back to
@@ -574,14 +634,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
574
634
  : plotlyModeForScatterStyle(scatterStyle);
575
635
  const markerSize = perSeriesMarker?.size ?? 6;
576
636
  const markerColor = perSeriesMarker?.color ?? seriesColor;
577
- return {
637
+ traces.push(withLabels({
578
638
  type: "scatter",
579
639
  mode,
580
640
  x: xValues,
581
641
  y: yValues,
582
- text: labelValuesForSeries,
642
+ customdata,
583
643
  name: entry.name,
584
- hovertemplate: "%{text}<br>X: %{x}<br>Y: %{y}<extra></extra>",
644
+ hovertemplate: "%{customdata[0]}<br>X: %{x}<br>Y: %{y}<extra></extra>",
585
645
  line: {
586
646
  shape: lineShapeFor(scatterStyle),
587
647
  color: seriesColor,
@@ -592,31 +652,82 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
592
652
  : {
593
653
  marker: {
594
654
  size: markerSize,
595
- color: markerColor,
655
+ color: pointColors(markerColor),
596
656
  symbol: mapMarkerSymbolToPlotly(symbol),
597
657
  },
598
658
  }),
599
- };
659
+ ...onSecondary,
660
+ }));
661
+ }
662
+ else {
663
+ // Bubble — third dimension scaled into marker diameter.
664
+ const scaledSizes = rawSizes.map((value) => sizeScale(value));
665
+ traces.push(withLabels({
666
+ type: "scatter",
667
+ mode: "markers",
668
+ x: xValues,
669
+ y: yValues,
670
+ customdata,
671
+ name: entry.name,
672
+ hovertemplate: "%{customdata[0]}<br>X: %{x}<br>Y: %{y}<br>Size: %{customdata[1]}<extra></extra>",
673
+ marker: {
674
+ size: scaledSizes,
675
+ sizemode: "diameter",
676
+ color: pointColors(seriesColor),
677
+ opacity: 0.7,
678
+ },
679
+ ...onSecondary,
680
+ }));
681
+ }
682
+ // Trendlines on the real (x, y) points — a regression of Y on X,
683
+ // not on the point index — and native error bars (#1067).
684
+ const numericX = entry.points.map((point) => point.value[0]);
685
+ for (const tl of seriesSpec?.trendlines ?? []) {
686
+ const result = computeTrendline(yValues, {
687
+ type: tl.type,
688
+ order: tl.order,
689
+ period: tl.period,
690
+ forecastForward: tl.forecastForward,
691
+ forecastBackward: tl.forecastBackward,
692
+ }, numericX);
693
+ if (!result || result.points.length < 2)
694
+ continue;
695
+ const tlName = tl.name ??
696
+ `${entry.name ? entry.name + " " : ""}${tl.type ?? "linear"} trend`;
697
+ traces.push({
698
+ type: "scatter",
699
+ mode: "lines",
700
+ name: tlName,
701
+ x: result.points.map((p) => hasStringX
702
+ ? categories[Math.round(p.x)] ?? p.x
703
+ : p.x),
704
+ y: result.points.map((p) => p.y),
705
+ line: { color: tl.color ?? seriesColor, dash: "dash", width: 2 },
706
+ hoverinfo: "skip",
707
+ ...onSecondary,
708
+ });
709
+ }
710
+ if (seriesSpec?.errorBars) {
711
+ const direction = seriesSpec.errorBars.direction ?? "y";
712
+ // An X error bar sizes from the X values (a percentage or
713
+ // deviation of X).
714
+ const magnitudes = computeErrorBars(direction === "x" ? numericX : yValues, resolveCustomErrorBarValues(seriesSpec.errorBars, (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy)));
715
+ if (magnitudes) {
716
+ const symmetric = magnitudes.plus.every((plus, i) => plus === magnitudes.minus[i]);
717
+ const errorConfig = {
718
+ type: "data",
719
+ symmetric,
720
+ array: magnitudes.plus,
721
+ ...(symmetric ? {} : { arrayminus: magnitudes.minus }),
722
+ color: seriesColor,
723
+ ...(seriesSpec.errorBars.noEndCap ? { width: 0 } : {}),
724
+ };
725
+ const target = traces[traces.length - 1 - (seriesSpec.trendlines?.length ?? 0)];
726
+ if (target) {
727
+ target[direction === "x" ? "error_x" : "error_y"] = errorConfig;
728
+ }
729
+ }
600
730
  }
601
- // Bubble — third dimension scaled into marker diameter.
602
- const rawSizes = entry.points.map((point) => point.value[2]);
603
- const scaledSizes = rawSizes.map((value) => sizeScale(value));
604
- return {
605
- type: "scatter",
606
- mode: "markers",
607
- x: xValues,
608
- y: yValues,
609
- text: labelValuesForSeries,
610
- customdata: rawSizes,
611
- name: entry.name,
612
- hovertemplate: "%{text}<br>X: %{x}<br>Y: %{y}<br>Size: %{customdata}<extra></extra>",
613
- marker: {
614
- size: scaledSizes,
615
- sizemode: "diameter",
616
- color: seriesColor,
617
- opacity: 0.7,
618
- },
619
- };
620
731
  });
621
732
  // Resolve text formats: chart-authored values (via OOXML <c:txPr>)
622
733
  // override the theme-derived defaults. `spec.titleTextFormat`,
@@ -636,7 +747,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
636
747
  x: 0.02,
637
748
  xanchor: "left",
638
749
  },
639
- xaxis: {
750
+ // Axis min / max / reverse / log / visibility from the spec
751
+ // (`categoryAxisOptions` is the X axis, as in the echarts
752
+ // renderer).
753
+ xaxis: applyAxisOverridesPlotly({
640
754
  title: {
641
755
  text: spec.horizontalAxisTitle ?? "",
642
756
  font: { color: xAxisFmt?.color ?? textColor },
@@ -648,8 +762,8 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
648
762
  ...(xAxisFmt?.fontSize ? { size: xAxisFmt.fontSize } : {}),
649
763
  },
650
764
  type: hasStringX ? "category" : "linear",
651
- },
652
- yaxis: {
765
+ }, spec.categoryAxisOptions),
766
+ yaxis: applyAxisOverridesPlotly({
653
767
  title: {
654
768
  text: spec.verticalAxisTitle ?? "",
655
769
  font: { color: yAxisFmt?.color ?? textColor },
@@ -660,7 +774,20 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
660
774
  ...(yAxisFmt?.fontFamily ? { family: yAxisFmt.fontFamily } : {}),
661
775
  ...(yAxisFmt?.fontSize ? { size: yAxisFmt.fontSize } : {}),
662
776
  },
663
- },
777
+ }, spec.valueAxisOptions),
778
+ // Secondary value axis on the right for the series that opt in
779
+ // (`useSecondaryAxis`), as the category layout has (#1067).
780
+ ...(hasBubbleSecondaryAxis
781
+ ? {
782
+ yaxis2: applyAxisOverridesPlotly({
783
+ overlaying: "y",
784
+ side: "right",
785
+ showgrid: false,
786
+ automargin: true,
787
+ tickfont: { color: yAxisFmt?.color ?? textColor },
788
+ }, spec.secondaryValueAxisOptions),
789
+ }
790
+ : {}),
664
791
  legend: {
665
792
  font: {
666
793
  color: legendFmt?.color ?? textColor,
@@ -671,7 +798,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
671
798
  paper_bgcolor: bgColor,
672
799
  plot_bgcolor: bgColor,
673
800
  showlegend: preparedSeries.length > 1,
674
- margin: { l: 60, r: 20, t: 50, b: 50 },
801
+ margin: { l: 60, r: hasBubbleSecondaryAxis ? 60 : 20, t: 50, b: 50 },
675
802
  };
676
803
  setPlotData(traces);
677
804
  setLayout(bubbleLayout);
@@ -693,14 +820,40 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
693
820
  const seriesName = hierarchyData.labels.length === 1
694
821
  ? hierarchyData.labels[0] ?? ""
695
822
  : explicitChartTitle ?? headerTitle ?? "";
823
+ // One solid colour per top-level category, inherited by its branch
824
+ // (#1072); nodes reference each other by id so a leaf name may
825
+ // repeat under different parents.
826
+ const hierarchyPalette = getChartPalette(spec, [
827
+ theme.themeColors.accent1,
828
+ theme.themeColors.accent2,
829
+ theme.themeColors.accent3,
830
+ theme.themeColors.accent4,
831
+ theme.themeColors.accent5,
832
+ theme.themeColors.accent6,
833
+ ]);
834
+ const nodeColors = hierarchyNodeColors(hierarchyData, hierarchyPalette);
835
+ const isSunburst = spec.chartType === "sunburst";
836
+ // Excel's sunburst leaves the centre empty — every level, the first
837
+ // included, is a ring (#1073). Plotly draws the top level as a ring
838
+ // only around a root node, so an invisible one holds the centre.
839
+ const SUNBURST_ROOT_ID = "__rnc_sunburst_root__";
840
+ const rootTotal = hierarchyData.values.reduce((sum, value, i) => (hierarchyData.parents[i] === "" ? sum + value : sum), 0);
696
841
  const hierarchyTrace = {
697
842
  type: chartType,
698
- labels: hierarchyData.labels,
699
- parents: hierarchyData.parents,
700
- values: hierarchyData.values,
843
+ labels: isSunburst ? ["", ...hierarchyData.labels] : hierarchyData.labels,
844
+ ids: isSunburst ? [SUNBURST_ROOT_ID, ...hierarchyData.ids] : hierarchyData.ids,
845
+ parents: isSunburst
846
+ ? ["", ...hierarchyData.parents.map((parent) => parent || SUNBURST_ROOT_ID)]
847
+ : hierarchyData.parents,
848
+ values: isSunburst ? [rootTotal, ...hierarchyData.values] : hierarchyData.values,
849
+ marker: {
850
+ colors: isSunburst ? ["rgba(0,0,0,0)", ...nodeColors] : nodeColors,
851
+ ...(isSunburst ? { line: { width: 1 } } : {}),
852
+ },
701
853
  name: seriesName,
702
854
  textinfo: "label+value",
703
855
  branchvalues: "total",
856
+ ...(isSunburst ? {} : { textposition: "bottom left" }),
704
857
  };
705
858
  const layoutForHierarchy = {
706
859
  title: {
@@ -1190,7 +1343,8 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1190
1343
  // by category index, falling back to the series color for points
1191
1344
  // without an override.
1192
1345
  const isPie = chartTypeForStyling === "pie";
1193
- if ((isBarOrColumn || isPie) &&
1346
+ const isFunnel = chartTypeForStyling === "funnel";
1347
+ if ((isBarOrColumn || isPie || isFunnel) &&
1194
1348
  Array.isArray(options?.dataPointColors) &&
1195
1349
  options?.dataPointColors.length) {
1196
1350
  const overrides = new Map();
@@ -1445,9 +1599,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1445
1599
  : {}),
1446
1600
  };
1447
1601
  if (effectiveChartType === "histogram") {
1448
- const rawValues = "values" in seriesData
1449
- ? seriesData.values
1450
- : seriesData;
1602
+ const rawValues = seriesValuesOf(seriesData);
1451
1603
  const binCount = spec.binCount;
1452
1604
  if (isPareto) {
1453
1605
  // Plotly's native histogram trace bins in axis order and
@@ -1497,9 +1649,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1497
1649
  }
1498
1650
  }
1499
1651
  else if (effectiveChartType === "boxplot") {
1500
- const rawValues = "values" in seriesData
1501
- ? seriesData.values
1502
- : seriesData;
1652
+ const rawValues = seriesValuesOf(seriesData);
1503
1653
  const quartileMethod = spec.quartileMethod;
1504
1654
  const showOutliers = spec.showOutliers;
1505
1655
  const showMeanMarker = spec.showMeanMarker === true;
@@ -1566,37 +1716,51 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1566
1716
  traces.push(trace);
1567
1717
  }
1568
1718
  else if (effectiveChartType === "waterfall") {
1569
- const rawValues = "values" in seriesData
1570
- ? seriesData.values
1571
- : seriesData;
1719
+ const rawValues = seriesValuesOf(seriesData);
1572
1720
  // Plotly's native waterfall draws connector lines itself and
1573
1721
  // renders "total" measures from the zero baseline with its
1574
1722
  // own distinct fill.
1575
1723
  const totalIndices = new Set(spec.totalIndices ?? []);
1724
+ // Increases take the series colour, decreases its negative colour
1725
+ // and totals the theme's third accent, as the echarts renderer and
1726
+ // Excel's default waterfall do (plotly's own defaults are green /
1727
+ // red / blue). Plotly paints a waterfall by measure, not per bar, so
1728
+ // `dataPointColors` cannot recolour one step here.
1729
+ const increaseColor = series.color ?? chartPaletteColorAt(themeColorPalette, currentSeriesIndex);
1576
1730
  const trace = {
1577
1731
  type: "waterfall",
1578
1732
  x: categoriesFor(series),
1579
1733
  y: rawValues.map((v) => (v ?? null)),
1580
1734
  measure: rawValues.map((_, i) => totalIndices.has(i) ? "total" : "relative"),
1581
1735
  name: dataLabel,
1736
+ increasing: { marker: { color: increaseColor } },
1737
+ decreasing: {
1738
+ marker: {
1739
+ color: series.negativeColor ?? theme.themeColors.accent2 ?? increaseColor,
1740
+ },
1741
+ },
1742
+ totals: { marker: { color: theme.themeColors.accent3 ?? increaseColor } },
1743
+ connector: {
1744
+ line: {
1745
+ color: isDarkMode ? "rgba(255,255,255,0.45)" : "#999999",
1746
+ width: 1,
1747
+ },
1748
+ },
1582
1749
  };
1583
- applyTraceColor(trace, series.color ?? undefined, {
1584
- noFill: series.noFill,
1585
- dataPointColors: series.dataPointColors,
1586
- seriesIndex: currentSeriesIndex,
1587
- effectiveChartType,
1588
- });
1589
1750
  traces.push(trace);
1590
1751
  }
1591
1752
  else if (effectiveChartType === "funnel") {
1592
- const rawValues = "values" in seriesData
1593
- ? seriesData.values
1594
- : seriesData;
1753
+ const rawValues = seriesValuesOf(seriesData);
1754
+ // Horizontal bars with the stage names on the category axis to
1755
+ // the left; the series colour (per-stage `<c:dPt>` overrides) and
1756
+ // the values inside the bars follow below (#1069).
1595
1757
  const trace = {
1596
1758
  type: "funnel",
1759
+ orientation: "h",
1597
1760
  y: categoriesFor(series),
1598
1761
  x: rawValues.map((v) => (v ?? null)),
1599
1762
  name: dataLabel,
1763
+ textinfo: "none",
1600
1764
  };
1601
1765
  applyTraceColor(trace, series.color ?? undefined, {
1602
1766
  noFill: series.noFill,
@@ -2088,6 +2252,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2088
2252
  const formattedTraces = traces.map((trace) => {
2089
2253
  const pattern = traceNumberFormat(trace);
2090
2254
  let next = applyDataLabelsToTracePlotly(trace, traceDataLabels(trace), spec.chartType, pattern, locale);
2255
+ if (next.type === "funnel") {
2256
+ // The formatted values ride on `text`; a funnel prints its own
2257
+ // `value` unless told to draw the text (or nothing).
2258
+ next = {
2259
+ ...next,
2260
+ textinfo: traceDataLabels(trace)?.show ? "text" : "none",
2261
+ };
2262
+ }
2091
2263
  if (next.type === "pie") {
2092
2264
  // Plotly's pie prints every slice's percentage by default; Excel
2093
2265
  // draws nothing until data labels are on (#1062).
@@ -2278,5 +2450,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2278
2450
  React.createElement("div", { className: "flex-1 rnc-plotly-chart-container", onPointerDownCapture: handlePointerDownCapture },
2279
2451
  React.createElement(Plot, { data: plotData, layout: plotLayout, config: config, onClick: handlePlotClick, onInitialized: (_figure, graphDiv) => {
2280
2452
  graphDivRef.current = graphDiv;
2453
+ const plotDiv = graphDiv;
2454
+ // Returning false from these handlers keeps the clicked branch
2455
+ // from becoming the new root.
2456
+ plotDiv.on?.("plotly_treemapclick", () => drillDownRef.current);
2457
+ plotDiv.on?.("plotly_sunburstclick", () => drillDownRef.current);
2281
2458
  }, style: { width: "100%", height: "100%" } }))));
2282
2459
  };
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,KACvC,MAmEF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;;;;;;;;CAgBzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SA0BX,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAejE,CAAC"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,KACvC,MAmEF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;;;;;;;;CAgBzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SA0BX,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAiBjE,CAAC"}
@@ -101,6 +101,8 @@ export const showXAxis = (type) => {
101
101
  type === "column" ||
102
102
  type === "bubble" ||
103
103
  type === "histogram" ||
104
+ // A waterfall's steps are categories (Opening, Sales, …) (#1070).
105
+ type === "waterfall" ||
104
106
  // Stock is line-derived on a category x-axis. Radar is polar and
105
107
  // does not use the cartesian x-axis.
106
108
  type === "stock") {
@@ -1 +1 @@
1
- {"root":["../../config.ts","../../echarts-gl.d.ts","../../geo-modules.d.ts","../../index.ts","../../plotly-dist.d.ts","../../types.ts","../../vitest.config.ts","../../atoms/editing-chart.atom.ts","../../atoms/pivot-chart.atom.ts","../../basic-chart/basic-chart.tsx","../../basic-chart/helpers.ts","../../basic-chart/index.tsx","../../components/dialogs/chart-editor-dialog.tsx","../../components/dialogs/pivot-chart-dialog.tsx","../../components/editors/chart-editor.tsx","../../components/editors/chart-range-input.tsx","../../hooks/use-chart-editor.ts","../../hooks/use-charts.ts","../../hooks/use-on-create-chart.ts","../../hooks/use-on-delete-chart.ts","../../hooks/use-on-move-chart-to-sheet.ts","../../hooks/use-on-move-chart.ts","../../hooks/use-on-resize-chart.ts","../../hooks/use-on-update-chart.ts","../../interface/create-chart.ts","../../interface/delete-chart.ts","../../interface/move-chart-to-sheet.ts","../../interface/move-chart.ts","../../interface/resize-chart.ts","../../interface/shift-chart-ranges.ts","../../interface/update-chart.ts","../../plotly/plot.ts","../../plotly/basic-chart/basic-chart.tsx","../../plotly/basic-chart/helpers.ts","../../plotly/basic-chart/index.tsx","../../utils/aggregation.ts","../../utils/axis-number-format.ts","../../utils/bar-layout.ts","../../utils/boxplot.ts","../../utils/bubble.ts","../../utils/category-levels.ts","../../utils/chart-image.ts","../../utils/chart-orientation.ts","../../utils/chart-spec-utils.ts","../../utils/chart-styles.ts","../../utils/doughnut.ts","../../utils/echarts-gl-loader.ts","../../utils/empty-cells.ts","../../utils/error-bars.ts","../../utils/gradient.ts","../../utils/hierarchy.ts","../../utils/histogram.ts","../../utils/legend-layout.ts","../../utils/of-pie.ts","../../utils/overrides.ts","../../utils/pie-explosion.ts","../../utils/plotly-click.ts","../../utils/plotly-surface.ts","../../utils/plotly-title.ts","../../utils/radar.ts","../../utils/region-map-loader.ts","../../utils/region-map.ts","../../utils/series-click.ts","../../utils/stock.ts","../../utils/three-d.ts","../../utils/trendline.ts","../../utils/waterfall.ts","../../utils/webgl.ts"],"version":"5.8.3"}
1
+ {"root":["../../config.ts","../../echarts-gl.d.ts","../../geo-modules.d.ts","../../index.ts","../../plotly-dist.d.ts","../../types.ts","../../vitest.config.ts","../../atoms/editing-chart.atom.ts","../../atoms/pivot-chart.atom.ts","../../basic-chart/basic-chart.tsx","../../basic-chart/helpers.ts","../../basic-chart/index.tsx","../../components/dialogs/chart-editor-dialog.tsx","../../components/dialogs/pivot-chart-dialog.tsx","../../components/editors/chart-editor.tsx","../../components/editors/chart-range-input.tsx","../../hooks/use-chart-editor.ts","../../hooks/use-charts.ts","../../hooks/use-on-create-chart.ts","../../hooks/use-on-delete-chart.ts","../../hooks/use-on-move-chart-to-sheet.ts","../../hooks/use-on-move-chart.ts","../../hooks/use-on-resize-chart.ts","../../hooks/use-on-update-chart.ts","../../interface/create-chart.ts","../../interface/delete-chart.ts","../../interface/move-chart-to-sheet.ts","../../interface/move-chart.ts","../../interface/resize-chart.ts","../../interface/shift-chart-ranges.ts","../../interface/update-chart.ts","../../plotly/plot.ts","../../plotly/basic-chart/basic-chart.tsx","../../plotly/basic-chart/helpers.ts","../../plotly/basic-chart/index.tsx","../../utils/aggregation.ts","../../utils/axis-number-format.ts","../../utils/bar-layout.ts","../../utils/boxplot.ts","../../utils/bubble.ts","../../utils/category-levels.ts","../../utils/chart-image.ts","../../utils/chart-orientation.ts","../../utils/chart-spec-utils.ts","../../utils/chart-styles.ts","../../utils/doughnut.ts","../../utils/echarts-gl-loader.ts","../../utils/echarts-overlays.ts","../../utils/empty-cells.ts","../../utils/error-bars.ts","../../utils/gradient.ts","../../utils/hierarchy.ts","../../utils/histogram.ts","../../utils/legend-layout.ts","../../utils/of-pie.ts","../../utils/overrides.ts","../../utils/pie-explosion.ts","../../utils/plotly-click.ts","../../utils/plotly-surface.ts","../../utils/plotly-title.ts","../../utils/radar.ts","../../utils/region-map-loader.ts","../../utils/region-map.ts","../../utils/series-click.ts","../../utils/stock.ts","../../utils/three-d.ts","../../utils/trendline.ts","../../utils/waterfall.ts","../../utils/webgl.ts"],"version":"5.8.3"}
@@ -1,6 +1,33 @@
1
1
  export declare const toLabelString: (value: unknown) => string;
2
2
  export declare const toNumericValue: (value: unknown) => number | null;
3
- export declare const createBubbleSizeScale: (values: number[]) => (value: number) => number;
3
+ export type BubbleSizeOptions = {
4
+ /**
5
+ * Whether a size value sets the bubble's area (Excel's default,
6
+ * diameter ∝ √value) or its width (diameter ∝ value).
7
+ */
8
+ sizeRepresents?: "area" | "width" | null;
9
+ /** Percent of the default size, 0-300 (Excel's `<c:bubbleScale>`). */
10
+ bubbleScale?: number | null;
11
+ /**
12
+ * The plot area's smaller side in pixels — the largest bubble's
13
+ * diameter is a fraction of it, so bubbles grow with the chart.
14
+ */
15
+ plotMinSide?: number | null;
16
+ };
17
+ /** The largest bubble's diameter at bubbleScale 100: a quarter of the plot area's smaller side. */
18
+ export declare const BUBBLE_MAX_DIAMETER_FRACTION = 0.25;
19
+ /**
20
+ * Pixel diameter per size value, scaled from ZERO like Excel: the largest
21
+ * absolute value gets the maximum diameter and every other bubble's area
22
+ * (or width) is its share of that value. Scaling from the minimum value
23
+ * instead made the ratio between bubbles depend on the spread of the
24
+ * data (100 and 101 drew as the smallest and largest bubble), and a
25
+ * linear diameter made the perceived area grow with the square of the
26
+ * value (rowsncolumns/spreadsheet#1068).
27
+ */
28
+ export declare const createBubbleSizeScale: (values: number[], options?: BubbleSizeOptions) => (value: number) => number;
29
+ /** The diameter of the largest bubble for these options. */
30
+ export declare const bubbleMaxDiameter: (options?: BubbleSizeOptions) => number;
4
31
  export type BubbleSeriesInput = {
5
32
  name?: string | null;
6
33
  values: (string | number | boolean | null | undefined)[];
@@ -23,6 +50,12 @@ export type BubbleSeriesPoint = {
23
50
  value: [number, number, number];
24
51
  name: string;
25
52
  xLabel?: string;
53
+ /**
54
+ * The point's row in the source ranges. Points with no Y are skipped,
55
+ * so this is not the point's position in the series — per-point
56
+ * colours (`dataPointColors[].idx`) key on it.
57
+ */
58
+ index: number;
26
59
  };
27
60
  export type PreparedBubbleSeries = {
28
61
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"bubble.d.ts","sourceRoot":"","sources":["../../../utils/bubble.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,MACU,CAAC;AAE1D,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAYxD,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,MAAM,EAAE,aAQrC,MAAM,WAOtB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IACzD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IAC3D;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;CAC/D,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iBAAiB,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;;OAKG;IACH,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IAC1D,WAAW,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IAC9D;;OAEG;IACH,UAAU,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IAC7D,MAAM,EAAE,iBAAiB,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAwCF,eAAO,MAAM,uBAAuB,GAAI,+CAKrC,uBAAuB,KAAG,kBAsF5B,CAAC"}
1
+ {"version":3,"file":"bubble.d.ts","sourceRoot":"","sources":["../../../utils/bubble.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,MACU,CAAC;AAE1D,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAYxD,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACzC,sEAAsE;IACtE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,CAAC;AAEF,mGAAmG;AACnG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAMjD;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,MAAM,EAAE,EAChB,UAAS,iBAAsB,MAMvB,OAAO,MAAM,WAQtB,CAAC;AAEF,4DAA4D;AAC5D,eAAO,MAAM,iBAAiB,GAAI,UAAS,iBAAsB,WAUhE,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IACzD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IAC3D;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;CAC/D,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,iBAAiB,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;;OAKG;IACH,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IAC1D,WAAW,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IAC9D;;OAEG;IACH,UAAU,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IAC7D,MAAM,EAAE,iBAAiB,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB,CAAC;AAwCF,eAAO,MAAM,uBAAuB,GAAI,+CAKrC,uBAAuB,KAAG,kBAuF5B,CAAC"}
@@ -12,22 +12,45 @@ export const toNumericValue = (value) => {
12
12
  }
13
13
  return null;
14
14
  };
15
- export const createBubbleSizeScale = (values) => {
15
+ /** The largest bubble's diameter at bubbleScale 100: a quarter of the plot area's smaller side. */
16
+ export const BUBBLE_MAX_DIAMETER_FRACTION = 0.25;
17
+ /** Fallback plot side when the chart has no measured size yet. */
18
+ const DEFAULT_PLOT_MIN_SIDE = 240;
19
+ /** A positive size never collapses below this diameter. */
20
+ const MIN_BUBBLE_DIAMETER = 2;
21
+ /**
22
+ * Pixel diameter per size value, scaled from ZERO like Excel: the largest
23
+ * absolute value gets the maximum diameter and every other bubble's area
24
+ * (or width) is its share of that value. Scaling from the minimum value
25
+ * instead made the ratio between bubbles depend on the spread of the
26
+ * data (100 and 101 drew as the smallest and largest bubble), and a
27
+ * linear diameter made the perceived area grow with the square of the
28
+ * value (rowsncolumns/spreadsheet#1068).
29
+ */
30
+ export const createBubbleSizeScale = (values, options = {}) => {
31
+ const maxDiameter = bubbleMaxDiameter(options);
16
32
  const valid = values.filter((value) => Number.isFinite(value));
17
- if (valid.length === 0) {
18
- return () => 24;
19
- }
20
- const min = Math.min(...valid);
21
- const max = Math.max(...valid);
22
- const range = max - min || 1;
33
+ const maxAbs = valid.reduce((max, value) => Math.max(max, Math.abs(value)), 0);
34
+ const byWidth = options.sizeRepresents === "width";
23
35
  return (value) => {
24
- if (!Number.isFinite(value)) {
25
- return 24;
36
+ if (!Number.isFinite(value) || maxAbs === 0) {
37
+ return maxDiameter;
26
38
  }
27
- const ratio = (value - min) / range;
28
- return 12 + ratio * 42;
39
+ const share = Math.abs(value) / maxAbs;
40
+ const diameter = maxDiameter * (byWidth ? share : Math.sqrt(share));
41
+ return share > 0 ? Math.max(MIN_BUBBLE_DIAMETER, diameter) : 0;
29
42
  };
30
43
  };
44
+ /** The diameter of the largest bubble for these options. */
45
+ export const bubbleMaxDiameter = (options = {}) => {
46
+ const plotMinSide = typeof options.plotMinSide === "number" && options.plotMinSide > 0
47
+ ? options.plotMinSide
48
+ : DEFAULT_PLOT_MIN_SIDE;
49
+ const scale = typeof options.bubbleScale === "number" && Number.isFinite(options.bubbleScale)
50
+ ? Math.min(300, Math.max(0, options.bubbleScale)) / 100
51
+ : 1;
52
+ return plotMinSide * BUBBLE_MAX_DIAMETER_FRACTION * scale;
53
+ };
31
54
  /**
32
55
  * Build the X-coordinate index for a series. Numeric values map to
33
56
  * their own coordinate; string values become categorical (deduped into
@@ -107,6 +130,7 @@ export const prepareBubbleSeriesData = ({ xValues, labelValues, sizeValues, seri
107
130
  value: [coordinate.coordinate, y, sizeValue],
108
131
  name: label,
109
132
  xLabel: coordinateLabel,
133
+ index: i,
110
134
  });
111
135
  }
112
136
  const name = serie.name && serie.name.trim()