@rowsncolumns/charts 28.1.6 → 28.1.8

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 (43) hide show
  1. package/dist/cjs/basic-chart/basic-chart.d.ts +1 -1
  2. package/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -1
  3. package/dist/cjs/basic-chart/basic-chart.js +207 -152
  4. package/dist/cjs/components/editors/chart-editor.d.ts.map +1 -1
  5. package/dist/cjs/components/editors/chart-editor.js +5 -1
  6. package/dist/cjs/plotly/basic-chart/basic-chart.d.ts +1 -1
  7. package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  8. package/dist/cjs/plotly/basic-chart/basic-chart.js +169 -96
  9. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  10. package/dist/cjs/utils/axis-number-format.d.ts +26 -7
  11. package/dist/cjs/utils/axis-number-format.d.ts.map +1 -1
  12. package/dist/cjs/utils/axis-number-format.js +94 -16
  13. package/dist/cjs/utils/doughnut.d.ts +19 -0
  14. package/dist/cjs/utils/doughnut.d.ts.map +1 -0
  15. package/dist/cjs/utils/doughnut.js +28 -0
  16. package/dist/cjs/utils/overrides.d.ts +12 -3
  17. package/dist/cjs/utils/overrides.d.ts.map +1 -1
  18. package/dist/cjs/utils/overrides.js +41 -16
  19. package/dist/cjs/utils/radar.d.ts +31 -5
  20. package/dist/cjs/utils/radar.d.ts.map +1 -1
  21. package/dist/cjs/utils/radar.js +67 -27
  22. package/dist/esm/basic-chart/basic-chart.d.ts +1 -1
  23. package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
  24. package/dist/esm/basic-chart/basic-chart.js +129 -63
  25. package/dist/esm/components/editors/chart-editor.d.ts.map +1 -1
  26. package/dist/esm/components/editors/chart-editor.js +5 -1
  27. package/dist/esm/plotly/basic-chart/basic-chart.d.ts +1 -1
  28. package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  29. package/dist/esm/plotly/basic-chart/basic-chart.js +99 -23
  30. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  31. package/dist/esm/utils/axis-number-format.d.ts +26 -7
  32. package/dist/esm/utils/axis-number-format.d.ts.map +1 -1
  33. package/dist/esm/utils/axis-number-format.js +83 -13
  34. package/dist/esm/utils/doughnut.d.ts +19 -0
  35. package/dist/esm/utils/doughnut.d.ts.map +1 -0
  36. package/dist/esm/utils/doughnut.js +21 -0
  37. package/dist/esm/utils/overrides.d.ts +12 -3
  38. package/dist/esm/utils/overrides.d.ts.map +1 -1
  39. package/dist/esm/utils/overrides.js +40 -16
  40. package/dist/esm/utils/radar.d.ts +31 -5
  41. package/dist/esm/utils/radar.d.ts.map +1 -1
  42. package/dist/esm/utils/radar.js +44 -16
  43. package/package.json +4 -4
@@ -27,7 +27,8 @@ import { buildHierarchyTree, extractHierarchyRows, getHierarchyHeaderTitle, } fr
27
27
  import { 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
- import { applyAxisOverrides, applyLegendOverrides, buildSeriesLabelOption, } from "../utils/overrides";
30
+ import { clampDoughnutHole, doughnutRings } from "../utils/doughnut";
31
+ import { applyAxisOverrides, applyLegendOverrides, buildSeriesLabelOption, resolveSeriesDataLabels, } from "../utils/overrides";
31
32
  import { buildRadarIndicators, buildRadarSeriesData } from "../utils/radar";
32
33
  import { buildRegionMapData, buildSequentialColorRamp, isRegionMapSpec, resolveGeoScope, warnOnceForUnmatchedRegions, } from "../utils/region-map";
33
34
  import { loadRegionMapGeo } from "../utils/region-map-loader";
@@ -183,7 +184,7 @@ const defaultLegendConfig = {
183
184
  };
184
185
  /** Category-axis title gap: clears one row of tick labels (#986). */
185
186
  const CATEGORY_AXIS_NAME_GAP = categoryAxisNameGap();
186
- export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, getFormattingFromRange, onDeleteChart, onUpdateChart, onRequestEdit, onRequestCalculate, isSelected, fieldButtons, }) => {
187
+ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, getFormattingFromRange, onDeleteChart, onUpdateChart, onRequestEdit, onRequestCalculate, isSelected, fieldButtons, locale, }) => {
187
188
  const spec = chart.spec;
188
189
  const explicitChartTitle = chart.spec.title ?? undefined;
189
190
  const hierarchyTitleRef = useRef(explicitChartTitle);
@@ -454,7 +455,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
454
455
  ? "rgba(255,255,255,0.08)"
455
456
  : "rgba(0,0,0,0.06)";
456
457
  const borderColor = isDarkMode ? "rgba(255,255,255,0.3)" : "#FFFFFF";
457
- const regionSeriesLabel = buildSeriesLabelOption(spec.dataLabels);
458
+ const regionSeriesLabel = buildSeriesLabelOption(spec.dataLabels, undefined, locale);
458
459
  const seriesName = seriesEntry?.dataLabel && !isAFormula(seriesEntry.dataLabel)
459
460
  ? seriesEntry.dataLabel
460
461
  : "";
@@ -581,8 +582,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
581
582
  };
582
583
  // Spec-level overrides — axis min/max/visibility/textFormat, legend
583
584
  // position/visibility, dataLabel toggles. Same helpers as the
584
- // non-bubble layout below.
585
- const bubbleSeriesLabel = buildSeriesLabelOption(spec.dataLabels);
585
+ // non-bubble layout below. A series' own `dataLabels` replace the
586
+ // chart-level ones for it (#1059).
587
+ const bubbleSeriesLabelFor = (index) => buildSeriesLabelOption(resolveSeriesDataLabels(spec.dataLabels, spec.series?.[index]), undefined, locale);
586
588
  chartRef.current?.setOption({
587
589
  xAxis: [
588
590
  applyAxisOverrides({ ...xAxisConfig }, spec.categoryAxisOptions),
@@ -593,7 +595,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
593
595
  name: spec.verticalAxisTitle ?? "",
594
596
  nameLocation: "middle",
595
597
  nameGap: 45,
596
- }, spec.valueAxisOptions),
598
+ }, spec.valueAxisOptions, undefined, locale),
597
599
  ],
598
600
  legend: applyLegendOverrides({
599
601
  ...legend,
@@ -635,15 +637,22 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
635
637
  scatterStyle === "smoothMarker" ||
636
638
  !scatterStyle);
637
639
  const smooth = scatterStyle === "smooth" || scatterStyle === "smoothMarker";
640
+ // Labels hang off the point symbols: a marker-less line keeps
641
+ // an invisible 0-size symbol per point when they show (#1049).
642
+ const labelsWithoutMarkers = !wantsMarkers &&
643
+ Boolean(resolveSeriesDataLabels(spec.dataLabels, seriesEntry)
644
+ ?.show);
638
645
  return wantsLine
639
646
  ? {
640
647
  type: "line",
641
648
  name: entry.name,
642
649
  data: entry.points,
643
- showSymbol: wantsMarkers,
650
+ showSymbol: wantsMarkers || labelsWithoutMarkers,
644
651
  smooth,
645
652
  symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
646
- symbolSize: perMarker?.size ?? 6,
653
+ symbolSize: labelsWithoutMarkers
654
+ ? 0
655
+ : perMarker?.size ?? 6,
647
656
  ...(seriesColor
648
657
  ? {
649
658
  itemStyle: { color: seriesColor },
@@ -673,9 +682,12 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
673
682
  },
674
683
  ...(seriesColor ? { itemStyle: { color: seriesColor } } : {}),
675
684
  };
676
- }).map((s) => bubbleSeriesLabel
677
- ? { ...s, label: { ...(s.label ?? {}), ...bubbleSeriesLabel } }
678
- : s),
685
+ }).map((s, index) => {
686
+ const bubbleSeriesLabel = bubbleSeriesLabelFor(index);
687
+ return bubbleSeriesLabel
688
+ ? { ...s, label: { ...(s.label ?? {}), ...bubbleSeriesLabel } }
689
+ : s;
690
+ }),
679
691
  }, {
680
692
  replaceMerge: [
681
693
  "series",
@@ -715,14 +727,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
715
727
  const value = Array.isArray(params.value)
716
728
  ? params.value[params.value.length - 1]
717
729
  : params.value ?? "";
718
- return `${params.name}: ${formatAxisValue(sizeNumberFormat, value)}`;
730
+ return `${params.name}: ${formatAxisValue(sizeNumberFormat, value, locale)}`;
719
731
  };
720
732
  // Spec-level overrides — treemap/sunburst default to no legend,
721
733
  // but an authored legend (visibility/position) still wins.
722
734
  const hierarchyDataLabels = spec.dataLabels;
723
735
  const hierarchySeriesLabel = buildSeriesLabelOption(hierarchyDataLabels && !hierarchyDataLabels.numberFormatPattern
724
736
  ? { ...hierarchyDataLabels, numberFormatPattern: sizeNumberFormat }
725
- : hierarchyDataLabels);
737
+ : hierarchyDataLabels, undefined, locale);
726
738
  chartRef.current?.setOption({
727
739
  xAxis: [
728
740
  {
@@ -818,7 +830,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
818
830
  const [zMin, zMax] = getValueExtent(contourPoints.filter((point) => typeof point[2] === "number"));
819
831
  // Values format like the cells of the series (row) they came from.
820
832
  const contourFormats = resolveSeriesNumberFormats(spec.series, getFormattingFromRange, spec.hiddenDimensionStrategy);
821
- const contourLabel = buildSeriesLabelOption(spec.dataLabels);
833
+ const contourLabel = buildSeriesLabelOption(spec.dataLabels, undefined, locale);
822
834
  if (!renderedAsContour.current && chartRef.current) {
823
835
  chartRef.current.dispose();
824
836
  chartRef.current = undefined;
@@ -835,7 +847,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
835
847
  const [x, y, z] = (params?.value ?? []);
836
848
  const value = z === "-" || z === undefined
837
849
  ? ""
838
- : formatAxisValue(contourFormats[y], z);
850
+ : formatAxisValue(contourFormats[y], z, locale);
839
851
  return `${seriesNames[y] ?? ""}<br/>${categories[x] ?? ""}: ${value}`;
840
852
  },
841
853
  },
@@ -871,7 +883,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
871
883
  right: 0,
872
884
  top: "center",
873
885
  inRange: { color: [...SURFACE_COLOR_RAMP] },
874
- formatter: (value) => formatAxisValue(contourFormats[0], value),
886
+ formatter: (value) => formatAxisValue(contourFormats[0], value, locale),
875
887
  },
876
888
  ],
877
889
  series: [
@@ -888,7 +900,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
888
900
  const [, y, z] = (params?.value ?? []);
889
901
  return z === "-" || z === undefined
890
902
  ? ""
891
- : formatAxisValue(contourFormats[y], z);
903
+ : formatAxisValue(contourFormats[y], z, locale);
892
904
  },
893
905
  },
894
906
  }
@@ -980,7 +992,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
980
992
  : seriesNames[Math.round(value[1] ?? 0)] ?? params?.seriesName ?? "";
981
993
  const valueLabel = spec.stackedType === "PERCENT_STACKED" && stackedBar3D
982
994
  ? `${Math.round((value[2] ?? 0) * 10) / 10}%`
983
- : formatAxisValue(threeDFormats[seriesIndex], value[2]);
995
+ : formatAxisValue(threeDFormats[seriesIndex], value[2], locale);
984
996
  return `${seriesLabel ? `${seriesLabel}<br/>` : ""}${categoryLabel}: ${valueLabel}`;
985
997
  };
986
998
  let threeDSeries;
@@ -1137,7 +1149,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1137
1149
  const values = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
1138
1150
  radarSeriesInputs.push({ name: label, values });
1139
1151
  }
1140
- const indicators = buildRadarIndicators(categories, radarSeriesInputs);
1152
+ // One value scale for every axis (Excel), from the spec's value-axis
1153
+ // min / max when set (#1066).
1154
+ const radarAxisOptions = spec.valueAxisOptions;
1155
+ const indicators = buildRadarIndicators(categories, radarSeriesInputs, radarAxisOptions);
1141
1156
  const radarData = buildRadarSeriesData(radarSeriesInputs, indicators.length);
1142
1157
  const radarStyle = spec.radarStyle ?? "standard";
1143
1158
  const radarColors = radarSeriesInputs.map((_, i) => spec.series?.[i]?.color);
@@ -1152,18 +1167,25 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1152
1167
  ...radarDataLabels,
1153
1168
  numberFormatPattern: radarFormats[seriesIndex],
1154
1169
  }
1155
- : radarDataLabels);
1170
+ : radarDataLabels, undefined, locale);
1156
1171
  const radarTooltipFormatter = (params) => {
1157
1172
  const values = Array.isArray(params?.value) ? params.value : [];
1158
1173
  const pattern = radarFormats[params?.dataIndex ?? 0];
1159
- const lines = indicators.map((indicator, i) => `${indicator.name}: ${formatAxisValue(pattern, values[i])}`);
1174
+ const lines = indicators.map((indicator, i) => `${indicator.name}: ${formatAxisValue(pattern, values[i], locale)}`);
1160
1175
  return [params?.name ?? "", ...lines].filter(Boolean).join("<br/>");
1161
1176
  };
1162
1177
  chartRef.current?.setOption({
1163
1178
  radar: {
1164
1179
  indicator: indicators.length
1165
1180
  ? indicators
1166
- : [{ name: "", max: 1 }],
1181
+ : [{ name: "", min: 0, max: 1 }],
1182
+ // The common scale's tick labels along one axis, as Excel draws
1183
+ // them (echarts labels the first indicator axis only); hidden
1184
+ // with the spec's value axis.
1185
+ axisLabel: {
1186
+ show: radarAxisOptions?.visible !== false,
1187
+ formatter: (value) => formatAxisValue(radarFormats[0], value, locale),
1188
+ },
1167
1189
  },
1168
1190
  legend: applyLegendOverrides({
1169
1191
  ...legend,
@@ -1230,7 +1252,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1230
1252
  const stockDataLabels = spec.dataLabels;
1231
1253
  const stockSeriesLabelFor = (role) => buildSeriesLabelOption(stockDataLabels && !stockDataLabels.numberFormatPattern
1232
1254
  ? { ...stockDataLabels, numberFormatPattern: stockFormatFor(role) }
1233
- : stockDataLabels);
1255
+ : stockDataLabels, undefined, locale);
1234
1256
  // V-variants (VHLC / VOHLC) carry a volume series that renders as
1235
1257
  // a column sub-pane under the price plot: two grids stacked
1236
1258
  // vertically (price ~70% of the plot, volume below on its own
@@ -1295,10 +1317,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1295
1317
  value: p.value,
1296
1318
  itemStyle: { color: p.up ? upColor : downColor },
1297
1319
  })),
1298
- ...(tooltipValueFormatter(stockFormatFor("volume"))
1320
+ ...(tooltipValueFormatter(stockFormatFor("volume"), locale)
1299
1321
  ? {
1300
1322
  tooltip: {
1301
- valueFormatter: tooltipValueFormatter(stockFormatFor("volume")),
1323
+ valueFormatter: tooltipValueFormatter(stockFormatFor("volume"), locale),
1302
1324
  },
1303
1325
  }
1304
1326
  : {}),
@@ -1326,7 +1348,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1326
1348
  nameLocation: "middle",
1327
1349
  nameGap: 50,
1328
1350
  gridIndex: 0,
1329
- }, spec.valueAxisOptions),
1351
+ }, spec.valueAxisOptions, undefined, locale),
1330
1352
  ...(hasVolume ? [volumeYAxis] : []),
1331
1353
  ],
1332
1354
  legend: applyLegendOverrides({ ...legend, show: false }, spec.legend),
@@ -1344,10 +1366,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1344
1366
  borderColor: upColor,
1345
1367
  borderColor0: downColor,
1346
1368
  },
1347
- ...(tooltipValueFormatter(stockFormatFor("close"))
1369
+ ...(tooltipValueFormatter(stockFormatFor("close"), locale)
1348
1370
  ? {
1349
1371
  tooltip: {
1350
- valueFormatter: tooltipValueFormatter(stockFormatFor("close")),
1372
+ valueFormatter: tooltipValueFormatter(stockFormatFor("close"), locale),
1351
1373
  },
1352
1374
  }
1353
1375
  : {}),
@@ -1385,7 +1407,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1385
1407
  nameLocation: "middle",
1386
1408
  nameGap: 50,
1387
1409
  gridIndex: 0,
1388
- }, spec.valueAxisOptions),
1410
+ }, spec.valueAxisOptions, undefined, locale),
1389
1411
  ...(hasVolume ? [volumeYAxis] : []),
1390
1412
  ],
1391
1413
  legend: applyLegendOverrides({
@@ -1407,10 +1429,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1407
1429
  ...(stockSeriesLabelFor(s.role)
1408
1430
  ? { label: stockSeriesLabelFor(s.role) }
1409
1431
  : {}),
1410
- ...(tooltipValueFormatter(stockFormatFor(s.role))
1432
+ ...(tooltipValueFormatter(stockFormatFor(s.role), locale)
1411
1433
  ? {
1412
1434
  tooltip: {
1413
- valueFormatter: tooltipValueFormatter(stockFormatFor(s.role)),
1435
+ valueFormatter: tooltipValueFormatter(stockFormatFor(s.role), locale),
1414
1436
  },
1415
1437
  }
1416
1438
  : {}),
@@ -1779,26 +1801,39 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1779
1801
  const wantsMarkers = markerSymbol === "none"
1780
1802
  ? false
1781
1803
  : Boolean(spec.showMarkers) || !isNil(markerSymbol);
1782
- seriesEntry.showSymbol = wantsMarkers;
1804
+ // echarts draws a line's data labels on its point symbols, so a
1805
+ // marker-less line with labels (Excel's default "Line" with values
1806
+ // shown) keeps an invisible 0-size symbol per point for the label
1807
+ // to hang off (#1049).
1808
+ const labelsWithoutMarkers = !wantsMarkers &&
1809
+ Boolean(resolveSeriesDataLabels(spec.dataLabels, seriesAny)?.show);
1810
+ seriesEntry.showSymbol = wantsMarkers || labelsWithoutMarkers;
1783
1811
  if (wantsMarkers) {
1784
1812
  seriesEntry.symbol =
1785
1813
  mapMarkerSymbolToEcharts(markerSymbol) ?? "circle";
1786
1814
  seriesEntry.symbolSize = seriesAny.marker?.size ?? 6;
1787
1815
  }
1788
- else if (Array.isArray(data)) {
1789
- // A marker-less line draws nothing for a point with a gap on both
1790
- // sides (a data row followed by a blank row), so that row vanishes
1791
- // from the chart. Excel plots an isolated marker there: keep the
1792
- // series symbol-less and mark only those points.
1793
- const isolated = isolatedPointIndexes(data, connectsAcrossEmptyCells(spec, lineLike));
1794
- if (isolated.length) {
1795
- seriesEntry.showSymbol = true;
1796
- seriesEntry.symbol = "none";
1797
- const marked = [...data];
1798
- for (const i of isolated) {
1799
- marked[i] = { value: marked[i], symbol: "circle", symbolSize: 6 };
1816
+ else {
1817
+ if (labelsWithoutMarkers) {
1818
+ seriesEntry.symbol = "circle";
1819
+ seriesEntry.symbolSize = 0;
1820
+ }
1821
+ if (Array.isArray(data)) {
1822
+ // A marker-less line draws nothing for a point with a gap on both
1823
+ // sides (a data row followed by a blank row), so that row vanishes
1824
+ // from the chart. Excel plots an isolated marker there: keep the
1825
+ // series symbol-less and mark only those points.
1826
+ const isolated = isolatedPointIndexes(data, connectsAcrossEmptyCells(spec, lineLike));
1827
+ if (isolated.length) {
1828
+ seriesEntry.showSymbol = true;
1829
+ if (!labelsWithoutMarkers)
1830
+ seriesEntry.symbol = "none";
1831
+ const marked = [...data];
1832
+ for (const i of isolated) {
1833
+ marked[i] = { value: marked[i], symbol: "circle", symbolSize: 6 };
1834
+ }
1835
+ seriesEntry.data = marked;
1800
1836
  }
1801
- seriesEntry.data = marked;
1802
1837
  }
1803
1838
  }
1804
1839
  }
@@ -2389,7 +2424,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2389
2424
  trigger: isCircularChart ? "item" : "axis",
2390
2425
  formatter: isCircularChart
2391
2426
  ? primaryAxisNumberFormat
2392
- ? circularTooltipFormatter(primaryAxisNumberFormat)
2427
+ ? circularTooltipFormatter(primaryAxisNumberFormat, locale)
2393
2428
  : "{b}: {c} ({d}%)"
2394
2429
  : undefined,
2395
2430
  };
@@ -2439,19 +2474,37 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2439
2474
  }
2440
2475
  : legendBand.grid;
2441
2476
  // Data labels fall back to the series' axis format when the spec has
2442
- // no explicit `<c:dLbls><c:numFmt>` — Excel's "Linked to source".
2443
- const dataLabelOptions = spec.dataLabels;
2444
- const seriesLabelOptionFor = (s) => buildSeriesLabelOption(dataLabelOptions && !dataLabelOptions.numberFormatPattern
2445
- ? {
2446
- ...dataLabelOptions,
2447
- numberFormatPattern: seriesNumberFormatFor(s),
2448
- }
2449
- : dataLabelOptions);
2477
+ // no explicit `<c:dLbls><c:numFmt>` — Excel's "Linked to source". A
2478
+ // series' own `dataLabels` replace the chart-level ones for it (#1059).
2479
+ const chartDataLabels = spec.dataLabels;
2480
+ const seriesLabelOptionFor = (s) => {
2481
+ const specIndex = chartSeriesSpecIndex(s?.id);
2482
+ const dataLabelOptions = resolveSeriesDataLabels(chartDataLabels, specIndex === undefined ? undefined : spec.series?.[specIndex]);
2483
+ return buildSeriesLabelOption(dataLabelOptions && !dataLabelOptions.numberFormatPattern
2484
+ ? {
2485
+ ...dataLabelOptions,
2486
+ numberFormatPattern: seriesNumberFormatFor(s),
2487
+ }
2488
+ : dataLabelOptions, s?.type, locale);
2489
+ };
2450
2490
  // For doughnut, surface the inner-radius hole on each pie series.
2451
2491
  // Excel persists `holeSize` as a 0-90 percentage; we normalize to a
2452
2492
  // 0-1 fraction (doughnutHoleSize). Default to 0.5 when unset.
2453
2493
  const isDoughnut = spec.chartType === "doughnut";
2454
2494
  const doughnutHoleSize = spec.doughnutHoleSize;
2495
+ // Excel nests a doughnut's series as concentric rings, series 0
2496
+ // innermost (#1065): the ring each pie series draws is its rank among
2497
+ // the spec-tagged pie series.
2498
+ const doughnutSeriesOrder = isDoughnut
2499
+ ? chartSeries
2500
+ .filter((s) => s?.type === "pie")
2501
+ .map((s) => chartSeriesSpecIndex(s?.id))
2502
+ .filter((i) => i !== undefined)
2503
+ .sort((a, b) => a - b)
2504
+ : [];
2505
+ const doughnutRingSet = isDoughnut
2506
+ ? doughnutRings(clampDoughnutHole(doughnutHoleSize), doughnutSeriesOrder.length)
2507
+ : [];
2455
2508
  // Exploded pie / doughnut: every slice sits in the `select` state,
2456
2509
  // offset outward by the spec's percentage of the pie radius.
2457
2510
  const explosion = isCircularChart && !isOfPie ? resolveExplosion(spec) : 0;
@@ -2502,10 +2555,16 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2502
2555
  if (seriesLabelOption) {
2503
2556
  next.label = { ...(s?.label ?? {}), ...seriesLabelOption };
2504
2557
  }
2558
+ else if (s?.type === "pie") {
2559
+ // echarts labels every pie slice by default (category name on a
2560
+ // leader line); Excel draws nothing until data labels are on (#1062).
2561
+ next.label = { ...(s?.label ?? {}), show: false };
2562
+ next.labelLine = { ...(s?.labelLine ?? {}), show: false };
2563
+ }
2505
2564
  // Tooltip values of the value-axis series (a boxplot / candlestick
2506
2565
  // tooltip lists its own statistics and keeps echarts' default).
2507
2566
  const seriesTooltipValueFormatter = VALUE_TOOLTIP_SERIES_TYPES.has(s?.type)
2508
- ? valueAxisLabelFormatter(seriesNumberFormatFor(s))
2567
+ ? valueAxisLabelFormatter(seriesNumberFormatFor(s), locale)
2509
2568
  : undefined;
2510
2569
  if (seriesTooltipValueFormatter && s?.tooltip?.show !== false) {
2511
2570
  next.tooltip = {
@@ -2518,7 +2577,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2518
2577
  if (pieNumberFormat && s?.tooltip?.show !== false) {
2519
2578
  next.tooltip = {
2520
2579
  ...(s?.tooltip ?? {}),
2521
- formatter: circularTooltipFormatter(pieNumberFormat),
2580
+ formatter: circularTooltipFormatter(pieNumberFormat, locale),
2522
2581
  };
2523
2582
  }
2524
2583
  if (explosionOffset > 0 && Array.isArray(next.data)) {
@@ -2530,10 +2589,15 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2530
2589
  }
2531
2590
  }
2532
2591
  if (isDoughnut && next.type === "pie") {
2533
- const inner = typeof doughnutHoleSize === "number"
2534
- ? Math.round(Math.max(0, Math.min(0.9, doughnutHoleSize)) * 100)
2535
- : 50;
2536
- next.radius = [`${inner}%`, "75%"];
2592
+ // The hole is a fraction of the OUTER radius (Excel's `<c:holeSize>`),
2593
+ // and the doughnut's outer edge sits at 75% of the chart — so every
2594
+ // ring edge is its fraction of 75%, not of the whole chart (#1064:
2595
+ // Excel's default 0.75 used to meet the outer edge, an empty ring).
2596
+ const rank = doughnutSeriesOrder.indexOf(chartSeriesSpecIndex(s?.id));
2597
+ const ring = doughnutRingSet[rank] ??
2598
+ doughnutRings(clampDoughnutHole(doughnutHoleSize), 1)[0];
2599
+ const pct = (fraction) => `${Math.round(fraction * 75 * 100) / 100}%`;
2600
+ next.radius = [pct(ring.inner), pct(ring.outer)];
2537
2601
  }
2538
2602
  return next;
2539
2603
  });
@@ -2584,7 +2648,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2584
2648
  applyAxisOverrides({
2585
2649
  ...valueAxisBase,
2586
2650
  name: spec.horizontalAxisTitle ?? "",
2587
- }, spec.valueAxisOptions, primaryAxisNumberFormat),
2651
+ }, spec.valueAxisOptions, primaryAxisNumberFormat, locale),
2588
2652
  ];
2589
2653
  }
2590
2654
  const base = chartDomains.length > 0
@@ -2671,8 +2735,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2671
2735
  ? s.data.map((d) => d !== null && typeof d === "object" ? d.value : d)
2672
2736
  : []),
2673
2737
  pattern: primaryAxisNumberFormat,
2738
+ locale,
2674
2739
  }),
2675
- }, spec.valueAxisOptions, primaryAxisNumberFormat),
2740
+ }, spec.valueAxisOptions, primaryAxisNumberFormat, locale),
2676
2741
  ];
2677
2742
  if (hasSecondaryAxis) {
2678
2743
  axes.push(applyAxisOverrides({
@@ -2687,7 +2752,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2687
2752
  axisLabel: { formatter: "{value}%" },
2688
2753
  }
2689
2754
  : {}),
2690
- }, spec.secondaryValueAxisOptions, secondaryAxisNumberFormat));
2755
+ }, spec.secondaryValueAxisOptions, secondaryAxisNumberFormat, locale));
2691
2756
  }
2692
2757
  return axes;
2693
2758
  })(),
@@ -2835,6 +2900,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2835
2900
  getDomainValuesFromRange,
2836
2901
  getSeriesValuesFromRange,
2837
2902
  getFormattingFromRange,
2903
+ locale,
2838
2904
  glUpgradeTick,
2839
2905
  ]);
2840
2906
  // Resize chart
@@ -1 +1 @@
1
- {"version":3,"file":"chart-editor.d.ts","sourceRoot":"","sources":["../../../../components/editors/chart-editor.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,aAAa,EAGd,MAAM,2BAA2B,CAAC;AAuBnC,OAAO,KAAkB,MAAM,OAAO,CAAC;AAMvC,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,+BAA+B,CAAC;AA4CvC,KAAK,gBAAgB,GAAG;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACtC;;;;OAIG;IACH,kBAAkB,CAAC,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IAChD;;;OAGG;IACH,iBAAiB,CAAC,EAAE,oBAAoB,CAAC;CAC1C,CAAC;AAuBF,eAAO,MAAM,WAAW,GAAI,sEAMzB,gBAAgB,sBAo6DlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"chart-editor.d.ts","sourceRoot":"","sources":["../../../../components/editors/chart-editor.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,aAAa,EAGd,MAAM,2BAA2B,CAAC;AAuBnC,OAAO,KAAkB,MAAM,OAAO,CAAC;AAMvC,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,+BAA+B,CAAC;AA4CvC,KAAK,gBAAgB,GAAG;IACtB,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACtC;;;;OAIG;IACH,kBAAkB,CAAC,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IAChD;;;OAGG;IACH,iBAAiB,CAAC,EAAE,oBAAoB,CAAC;CAC1C,CAAC;AAuBF,eAAO,MAAM,WAAW,GAAI,sEAMzB,gBAAgB,sBAw6DlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -622,7 +622,11 @@ export const ChartEditor = ({ chart, sheetId, onSubmit, onRequestMoveChart, getF
622
622
  React.createElement("option", { value: "outside" }, "Outside"),
623
623
  React.createElement("option", { value: "insideEnd" }, "Inside end"),
624
624
  React.createElement("option", { value: "outsideEnd" }, "Outside end"),
625
- React.createElement("option", { value: "bestFit" }, "Best fit")),
625
+ React.createElement("option", { value: "bestFit" }, "Best fit"),
626
+ React.createElement("option", { value: "above" }, "Above"),
627
+ React.createElement("option", { value: "below" }, "Below"),
628
+ React.createElement("option", { value: "left" }, "Left"),
629
+ React.createElement("option", { value: "right" }, "Right")),
626
630
  React.createElement("label", { className: "font-system text-xs" }, "Number format pattern"),
627
631
  React.createElement(Input, { value: values?.spec.dataLabels?.numberFormatPattern ?? "", type: "text", onChange: (e) => setFieldValue("spec.dataLabels.numberFormatPattern", e.target.value || undefined), className: "text-xs w-32", placeholder: "#,##0.00" }))) : null)),
628
632
  basicSpec && showChartOptions ? (React.createElement(Section, { title: "Chart options" },
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
2
  import type { EChartComponentProps } from "../../types";
3
3
  export type BasicChartProps = EChartComponentProps;
4
- export declare const BasicChart: ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, getFormattingFromRange, onDeleteChart, onUpdateChart, onRequestEdit, onRequestCalculate, isSelected, fieldButtons, }: BasicChartProps) => React.JSX.Element;
4
+ export declare const BasicChart: ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, getFormattingFromRange, onDeleteChart, onUpdateChart, onRequestEdit, onRequestCalculate, isSelected, fieldButtons, locale, }: BasicChartProps) => React.JSX.Element;
5
5
  //# sourceMappingURL=basic-chart.d.ts.map
@@ -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;AAgXxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AA0FnD,eAAO,MAAM,UAAU,GAAI,wOAiBxB,eAAe,sBA8zEjB,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;AAyZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AA0FnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBAq3EjB,CAAC"}