@rowsncolumns/charts 28.1.5 → 28.1.7

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 (29) 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 +161 -143
  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 +63 -16
  9. package/dist/cjs/utils/axis-number-format.d.ts +26 -7
  10. package/dist/cjs/utils/axis-number-format.d.ts.map +1 -1
  11. package/dist/cjs/utils/axis-number-format.js +94 -16
  12. package/dist/cjs/utils/overrides.d.ts +3 -3
  13. package/dist/cjs/utils/overrides.d.ts.map +1 -1
  14. package/dist/cjs/utils/overrides.js +32 -15
  15. package/dist/esm/basic-chart/basic-chart.d.ts +1 -1
  16. package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
  17. package/dist/esm/basic-chart/basic-chart.js +75 -53
  18. package/dist/esm/components/editors/chart-editor.d.ts.map +1 -1
  19. package/dist/esm/components/editors/chart-editor.js +5 -1
  20. package/dist/esm/plotly/basic-chart/basic-chart.d.ts +1 -1
  21. package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  22. package/dist/esm/plotly/basic-chart/basic-chart.js +63 -18
  23. package/dist/esm/utils/axis-number-format.d.ts +26 -7
  24. package/dist/esm/utils/axis-number-format.d.ts.map +1 -1
  25. package/dist/esm/utils/axis-number-format.js +83 -13
  26. package/dist/esm/utils/overrides.d.ts +3 -3
  27. package/dist/esm/utils/overrides.d.ts.map +1 -1
  28. package/dist/esm/utils/overrides.js +33 -16
  29. package/package.json +4 -4
@@ -3,7 +3,7 @@ import { getStringifiedColor, useFocusSheet, useRegisterChartActions, useSetSele
3
3
  import { useSubscribeToMultipleRanges } from "@rowsncolumns/spreadsheet-state";
4
4
  import { createDownloadableFile } from "@rowsncolumns/toolkit";
5
5
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, DropdownRightSlot, IconButton, useIsomorphicLayoutEffect, useOnVisibilityChange, } from "@rowsncolumns/ui";
6
- import { debug, isAFormula, isNil } from "@rowsncolumns/utils";
6
+ import { chartPaletteColorAt, debug, expandChartPalette, isAFormula, isNil, } from "@rowsncolumns/utils";
7
7
  import { BarChart, BoxplotChart, CandlestickChart, CustomChart, FunnelChart, HeatmapChart, LineChart, PieChart, RadarChart as EchartsRadarChart, ScatterChart, SunburstChart, TreemapChart as EchartsTreemapChart, } from "echarts/charts";
8
8
  import { DatasetComponent, GridComponent, LegendComponent, RadarComponent, TitleComponent, TooltipComponent, VisualMapComponent, } from "echarts/components";
9
9
  import * as echarts from "echarts/core";
@@ -183,7 +183,7 @@ const defaultLegendConfig = {
183
183
  };
184
184
  /** Category-axis title gap: clears one row of tick labels (#986). */
185
185
  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, }) => {
186
+ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, getFormattingFromRange, onDeleteChart, onUpdateChart, onRequestEdit, onRequestCalculate, isSelected, fieldButtons, locale, }) => {
187
187
  const spec = chart.spec;
188
188
  const explicitChartTitle = chart.spec.title ?? undefined;
189
189
  const hierarchyTitleRef = useRef(explicitChartTitle);
@@ -454,7 +454,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
454
454
  ? "rgba(255,255,255,0.08)"
455
455
  : "rgba(0,0,0,0.06)";
456
456
  const borderColor = isDarkMode ? "rgba(255,255,255,0.3)" : "#FFFFFF";
457
- const regionSeriesLabel = buildSeriesLabelOption(spec.dataLabels);
457
+ const regionSeriesLabel = buildSeriesLabelOption(spec.dataLabels, undefined, locale);
458
458
  const seriesName = seriesEntry?.dataLabel && !isAFormula(seriesEntry.dataLabel)
459
459
  ? seriesEntry.dataLabel
460
460
  : "";
@@ -582,7 +582,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
582
582
  // Spec-level overrides — axis min/max/visibility/textFormat, legend
583
583
  // position/visibility, dataLabel toggles. Same helpers as the
584
584
  // non-bubble layout below.
585
- const bubbleSeriesLabel = buildSeriesLabelOption(spec.dataLabels);
585
+ const bubbleSeriesLabel = buildSeriesLabelOption(spec.dataLabels, undefined, locale);
586
586
  chartRef.current?.setOption({
587
587
  xAxis: [
588
588
  applyAxisOverrides({ ...xAxisConfig }, spec.categoryAxisOptions),
@@ -593,7 +593,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
593
593
  name: spec.verticalAxisTitle ?? "",
594
594
  nameLocation: "middle",
595
595
  nameGap: 45,
596
- }, spec.valueAxisOptions),
596
+ }, spec.valueAxisOptions, undefined, locale),
597
597
  ],
598
598
  legend: applyLegendOverrides({
599
599
  ...legend,
@@ -635,15 +635,20 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
635
635
  scatterStyle === "smoothMarker" ||
636
636
  !scatterStyle);
637
637
  const smooth = scatterStyle === "smooth" || scatterStyle === "smoothMarker";
638
+ // Labels hang off the point symbols: a marker-less line keeps
639
+ // an invisible 0-size symbol per point when they show (#1049).
640
+ const labelsWithoutMarkers = !wantsMarkers && Boolean(spec.dataLabels?.show);
638
641
  return wantsLine
639
642
  ? {
640
643
  type: "line",
641
644
  name: entry.name,
642
645
  data: entry.points,
643
- showSymbol: wantsMarkers,
646
+ showSymbol: wantsMarkers || labelsWithoutMarkers,
644
647
  smooth,
645
648
  symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
646
- symbolSize: perMarker?.size ?? 6,
649
+ symbolSize: labelsWithoutMarkers
650
+ ? 0
651
+ : perMarker?.size ?? 6,
647
652
  ...(seriesColor
648
653
  ? {
649
654
  itemStyle: { color: seriesColor },
@@ -715,14 +720,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
715
720
  const value = Array.isArray(params.value)
716
721
  ? params.value[params.value.length - 1]
717
722
  : params.value ?? "";
718
- return `${params.name}: ${formatAxisValue(sizeNumberFormat, value)}`;
723
+ return `${params.name}: ${formatAxisValue(sizeNumberFormat, value, locale)}`;
719
724
  };
720
725
  // Spec-level overrides — treemap/sunburst default to no legend,
721
726
  // but an authored legend (visibility/position) still wins.
722
727
  const hierarchyDataLabels = spec.dataLabels;
723
728
  const hierarchySeriesLabel = buildSeriesLabelOption(hierarchyDataLabels && !hierarchyDataLabels.numberFormatPattern
724
729
  ? { ...hierarchyDataLabels, numberFormatPattern: sizeNumberFormat }
725
- : hierarchyDataLabels);
730
+ : hierarchyDataLabels, undefined, locale);
726
731
  chartRef.current?.setOption({
727
732
  xAxis: [
728
733
  {
@@ -818,7 +823,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
818
823
  const [zMin, zMax] = getValueExtent(contourPoints.filter((point) => typeof point[2] === "number"));
819
824
  // Values format like the cells of the series (row) they came from.
820
825
  const contourFormats = resolveSeriesNumberFormats(spec.series, getFormattingFromRange, spec.hiddenDimensionStrategy);
821
- const contourLabel = buildSeriesLabelOption(spec.dataLabels);
826
+ const contourLabel = buildSeriesLabelOption(spec.dataLabels, undefined, locale);
822
827
  if (!renderedAsContour.current && chartRef.current) {
823
828
  chartRef.current.dispose();
824
829
  chartRef.current = undefined;
@@ -835,7 +840,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
835
840
  const [x, y, z] = (params?.value ?? []);
836
841
  const value = z === "-" || z === undefined
837
842
  ? ""
838
- : formatAxisValue(contourFormats[y], z);
843
+ : formatAxisValue(contourFormats[y], z, locale);
839
844
  return `${seriesNames[y] ?? ""}<br/>${categories[x] ?? ""}: ${value}`;
840
845
  },
841
846
  },
@@ -871,7 +876,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
871
876
  right: 0,
872
877
  top: "center",
873
878
  inRange: { color: [...SURFACE_COLOR_RAMP] },
874
- formatter: (value) => formatAxisValue(contourFormats[0], value),
879
+ formatter: (value) => formatAxisValue(contourFormats[0], value, locale),
875
880
  },
876
881
  ],
877
882
  series: [
@@ -888,7 +893,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
888
893
  const [, y, z] = (params?.value ?? []);
889
894
  return z === "-" || z === undefined
890
895
  ? ""
891
- : formatAxisValue(contourFormats[y], z);
896
+ : formatAxisValue(contourFormats[y], z, locale);
892
897
  },
893
898
  },
894
899
  }
@@ -942,7 +947,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
942
947
  theme.themeColors.accent6,
943
948
  ]);
944
949
  const seriesColorAt = (index) => spec.series?.[index]?.color ??
945
- threeDPalette[index % threeDPalette.length];
950
+ chartPaletteColorAt(threeDPalette, index);
946
951
  // Entering 3-D from a 2-D render: the cartesian option doesn't
947
952
  // merge into a grid3D one, so re-initialize the echarts instance
948
953
  // from scratch (theme options are re-applied after setOption).
@@ -980,7 +985,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
980
985
  : seriesNames[Math.round(value[1] ?? 0)] ?? params?.seriesName ?? "";
981
986
  const valueLabel = spec.stackedType === "PERCENT_STACKED" && stackedBar3D
982
987
  ? `${Math.round((value[2] ?? 0) * 10) / 10}%`
983
- : formatAxisValue(threeDFormats[seriesIndex], value[2]);
988
+ : formatAxisValue(threeDFormats[seriesIndex], value[2], locale);
984
989
  return `${seriesLabel ? `${seriesLabel}<br/>` : ""}${categoryLabel}: ${valueLabel}`;
985
990
  };
986
991
  let threeDSeries;
@@ -1152,11 +1157,11 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1152
1157
  ...radarDataLabels,
1153
1158
  numberFormatPattern: radarFormats[seriesIndex],
1154
1159
  }
1155
- : radarDataLabels);
1160
+ : radarDataLabels, undefined, locale);
1156
1161
  const radarTooltipFormatter = (params) => {
1157
1162
  const values = Array.isArray(params?.value) ? params.value : [];
1158
1163
  const pattern = radarFormats[params?.dataIndex ?? 0];
1159
- const lines = indicators.map((indicator, i) => `${indicator.name}: ${formatAxisValue(pattern, values[i])}`);
1164
+ const lines = indicators.map((indicator, i) => `${indicator.name}: ${formatAxisValue(pattern, values[i], locale)}`);
1160
1165
  return [params?.name ?? "", ...lines].filter(Boolean).join("<br/>");
1161
1166
  };
1162
1167
  chartRef.current?.setOption({
@@ -1230,7 +1235,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1230
1235
  const stockDataLabels = spec.dataLabels;
1231
1236
  const stockSeriesLabelFor = (role) => buildSeriesLabelOption(stockDataLabels && !stockDataLabels.numberFormatPattern
1232
1237
  ? { ...stockDataLabels, numberFormatPattern: stockFormatFor(role) }
1233
- : stockDataLabels);
1238
+ : stockDataLabels, undefined, locale);
1234
1239
  // V-variants (VHLC / VOHLC) carry a volume series that renders as
1235
1240
  // a column sub-pane under the price plot: two grids stacked
1236
1241
  // vertically (price ~70% of the plot, volume below on its own
@@ -1295,10 +1300,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1295
1300
  value: p.value,
1296
1301
  itemStyle: { color: p.up ? upColor : downColor },
1297
1302
  })),
1298
- ...(tooltipValueFormatter(stockFormatFor("volume"))
1303
+ ...(tooltipValueFormatter(stockFormatFor("volume"), locale)
1299
1304
  ? {
1300
1305
  tooltip: {
1301
- valueFormatter: tooltipValueFormatter(stockFormatFor("volume")),
1306
+ valueFormatter: tooltipValueFormatter(stockFormatFor("volume"), locale),
1302
1307
  },
1303
1308
  }
1304
1309
  : {}),
@@ -1326,7 +1331,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1326
1331
  nameLocation: "middle",
1327
1332
  nameGap: 50,
1328
1333
  gridIndex: 0,
1329
- }, spec.valueAxisOptions),
1334
+ }, spec.valueAxisOptions, undefined, locale),
1330
1335
  ...(hasVolume ? [volumeYAxis] : []),
1331
1336
  ],
1332
1337
  legend: applyLegendOverrides({ ...legend, show: false }, spec.legend),
@@ -1344,10 +1349,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1344
1349
  borderColor: upColor,
1345
1350
  borderColor0: downColor,
1346
1351
  },
1347
- ...(tooltipValueFormatter(stockFormatFor("close"))
1352
+ ...(tooltipValueFormatter(stockFormatFor("close"), locale)
1348
1353
  ? {
1349
1354
  tooltip: {
1350
- valueFormatter: tooltipValueFormatter(stockFormatFor("close")),
1355
+ valueFormatter: tooltipValueFormatter(stockFormatFor("close"), locale),
1351
1356
  },
1352
1357
  }
1353
1358
  : {}),
@@ -1385,7 +1390,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1385
1390
  nameLocation: "middle",
1386
1391
  nameGap: 50,
1387
1392
  gridIndex: 0,
1388
- }, spec.valueAxisOptions),
1393
+ }, spec.valueAxisOptions, undefined, locale),
1389
1394
  ...(hasVolume ? [volumeYAxis] : []),
1390
1395
  ],
1391
1396
  legend: applyLegendOverrides({
@@ -1402,15 +1407,15 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1402
1407
  name: s.name,
1403
1408
  data: s.values,
1404
1409
  showSymbol: false,
1405
- itemStyle: { color: stockColors[i % stockColors.length] },
1406
- lineStyle: { color: stockColors[i % stockColors.length] },
1410
+ itemStyle: { color: chartPaletteColorAt(stockColors, i) },
1411
+ lineStyle: { color: chartPaletteColorAt(stockColors, i) },
1407
1412
  ...(stockSeriesLabelFor(s.role)
1408
1413
  ? { label: stockSeriesLabelFor(s.role) }
1409
1414
  : {}),
1410
- ...(tooltipValueFormatter(stockFormatFor(s.role))
1415
+ ...(tooltipValueFormatter(stockFormatFor(s.role), locale)
1411
1416
  ? {
1412
1417
  tooltip: {
1413
- valueFormatter: tooltipValueFormatter(stockFormatFor(s.role)),
1418
+ valueFormatter: tooltipValueFormatter(stockFormatFor(s.role), locale),
1414
1419
  },
1415
1420
  }
1416
1421
  : {}),
@@ -1688,7 +1693,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1688
1693
  theme.themeColors.accent5,
1689
1694
  theme.themeColors.accent6,
1690
1695
  ]);
1691
- const paletteColorAt = (index) => seriesPalette[index % seriesPalette.length];
1696
+ const paletteColorAt = (index) => chartPaletteColorAt(seriesPalette, index);
1692
1697
  for (const [specSeriesIndex, series] of (spec.series ?? []).entries()) {
1693
1698
  const dataLabel = !isNil(series.dataLabel) && isAFormula(series.dataLabel)
1694
1699
  ? coerceCalculatedLabel(await onRequestCalculate(series.dataLabel, position.sheetId ?? sheetId, position.overlayPosition.anchorCell.rowIndex, position.overlayPosition.anchorCell.columnIndex))
@@ -1779,26 +1784,38 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1779
1784
  const wantsMarkers = markerSymbol === "none"
1780
1785
  ? false
1781
1786
  : Boolean(spec.showMarkers) || !isNil(markerSymbol);
1782
- seriesEntry.showSymbol = wantsMarkers;
1787
+ // echarts draws a line's data labels on its point symbols, so a
1788
+ // marker-less line with labels (Excel's default "Line" with values
1789
+ // shown) keeps an invisible 0-size symbol per point for the label
1790
+ // to hang off (#1049).
1791
+ const labelsWithoutMarkers = !wantsMarkers && Boolean(spec.dataLabels?.show);
1792
+ seriesEntry.showSymbol = wantsMarkers || labelsWithoutMarkers;
1783
1793
  if (wantsMarkers) {
1784
1794
  seriesEntry.symbol =
1785
1795
  mapMarkerSymbolToEcharts(markerSymbol) ?? "circle";
1786
1796
  seriesEntry.symbolSize = seriesAny.marker?.size ?? 6;
1787
1797
  }
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 };
1798
+ else {
1799
+ if (labelsWithoutMarkers) {
1800
+ seriesEntry.symbol = "circle";
1801
+ seriesEntry.symbolSize = 0;
1802
+ }
1803
+ if (Array.isArray(data)) {
1804
+ // A marker-less line draws nothing for a point with a gap on both
1805
+ // sides (a data row followed by a blank row), so that row vanishes
1806
+ // from the chart. Excel plots an isolated marker there: keep the
1807
+ // series symbol-less and mark only those points.
1808
+ const isolated = isolatedPointIndexes(data, connectsAcrossEmptyCells(spec, lineLike));
1809
+ if (isolated.length) {
1810
+ seriesEntry.showSymbol = true;
1811
+ if (!labelsWithoutMarkers)
1812
+ seriesEntry.symbol = "none";
1813
+ const marked = [...data];
1814
+ for (const i of isolated) {
1815
+ marked[i] = { value: marked[i], symbol: "circle", symbolSize: 6 };
1816
+ }
1817
+ seriesEntry.data = marked;
1800
1818
  }
1801
- seriesEntry.data = marked;
1802
1819
  }
1803
1820
  }
1804
1821
  }
@@ -1969,7 +1986,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1969
1986
  theme.themeColors.accent5,
1970
1987
  theme.themeColors.accent6,
1971
1988
  ]);
1972
- const paletteAt = (index) => ofPiePalette[index % ofPiePalette.length];
1989
+ const paletteAt = (index) => chartPaletteColorAt(ofPiePalette, index);
1973
1990
  chartSeries.push({
1974
1991
  type: "pie",
1975
1992
  name: seriesEntry?.dataLabel ?? "",
@@ -2389,7 +2406,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2389
2406
  trigger: isCircularChart ? "item" : "axis",
2390
2407
  formatter: isCircularChart
2391
2408
  ? primaryAxisNumberFormat
2392
- ? circularTooltipFormatter(primaryAxisNumberFormat)
2409
+ ? circularTooltipFormatter(primaryAxisNumberFormat, locale)
2393
2410
  : "{b}: {c} ({d}%)"
2394
2411
  : undefined,
2395
2412
  };
@@ -2446,7 +2463,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2446
2463
  ...dataLabelOptions,
2447
2464
  numberFormatPattern: seriesNumberFormatFor(s),
2448
2465
  }
2449
- : dataLabelOptions);
2466
+ : dataLabelOptions, s?.type, locale);
2450
2467
  // For doughnut, surface the inner-radius hole on each pie series.
2451
2468
  // Excel persists `holeSize` as a 0-90 percentage; we normalize to a
2452
2469
  // 0-1 fraction (doughnutHoleSize). Default to 0.5 when unset.
@@ -2505,7 +2522,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2505
2522
  // Tooltip values of the value-axis series (a boxplot / candlestick
2506
2523
  // tooltip lists its own statistics and keeps echarts' default).
2507
2524
  const seriesTooltipValueFormatter = VALUE_TOOLTIP_SERIES_TYPES.has(s?.type)
2508
- ? valueAxisLabelFormatter(seriesNumberFormatFor(s))
2525
+ ? valueAxisLabelFormatter(seriesNumberFormatFor(s), locale)
2509
2526
  : undefined;
2510
2527
  if (seriesTooltipValueFormatter && s?.tooltip?.show !== false) {
2511
2528
  next.tooltip = {
@@ -2518,7 +2535,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2518
2535
  if (pieNumberFormat && s?.tooltip?.show !== false) {
2519
2536
  next.tooltip = {
2520
2537
  ...(s?.tooltip ?? {}),
2521
- formatter: circularTooltipFormatter(pieNumberFormat),
2538
+ formatter: circularTooltipFormatter(pieNumberFormat, locale),
2522
2539
  };
2523
2540
  }
2524
2541
  if (explosionOffset > 0 && Array.isArray(next.data)) {
@@ -2584,7 +2601,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2584
2601
  applyAxisOverrides({
2585
2602
  ...valueAxisBase,
2586
2603
  name: spec.horizontalAxisTitle ?? "",
2587
- }, spec.valueAxisOptions, primaryAxisNumberFormat),
2604
+ }, spec.valueAxisOptions, primaryAxisNumberFormat, locale),
2588
2605
  ];
2589
2606
  }
2590
2607
  const base = chartDomains.length > 0
@@ -2671,8 +2688,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2671
2688
  ? s.data.map((d) => d !== null && typeof d === "object" ? d.value : d)
2672
2689
  : []),
2673
2690
  pattern: primaryAxisNumberFormat,
2691
+ locale,
2674
2692
  }),
2675
- }, spec.valueAxisOptions, primaryAxisNumberFormat),
2693
+ }, spec.valueAxisOptions, primaryAxisNumberFormat, locale),
2676
2694
  ];
2677
2695
  if (hasSecondaryAxis) {
2678
2696
  axes.push(applyAxisOverrides({
@@ -2687,7 +2705,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2687
2705
  axisLabel: { formatter: "{value}%" },
2688
2706
  }
2689
2707
  : {}),
2690
- }, spec.secondaryValueAxisOptions, secondaryAxisNumberFormat));
2708
+ }, spec.secondaryValueAxisOptions, secondaryAxisNumberFormat, locale));
2691
2709
  }
2692
2710
  return axes;
2693
2711
  })(),
@@ -2782,14 +2800,17 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2782
2800
  color: textColor,
2783
2801
  },
2784
2802
  },
2785
- color: getChartPalette(chart.spec, [
2803
+ // Unrolled through Excel's luminance variations: echarts cycles this
2804
+ // list over pie slices and over series, and the seventh must not
2805
+ // repeat the first (rowsncolumns/spreadsheet#1045).
2806
+ color: expandChartPalette(getChartPalette(chart.spec, [
2786
2807
  theme.themeColors.accent1,
2787
2808
  theme.themeColors.accent2,
2788
2809
  theme.themeColors.accent3,
2789
2810
  theme.themeColors.accent4,
2790
2811
  theme.themeColors.accent5,
2791
2812
  theme.themeColors.accent6,
2792
- ]),
2813
+ ])),
2793
2814
  // Excel's Format Chart Area fill; `transparent` (echarts' own
2794
2815
  // default) so clearing the fill undoes an earlier setOption.
2795
2816
  backgroundColor: chart.spec.backgroundColor ?? "transparent",
@@ -2832,6 +2853,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2832
2853
  getDomainValuesFromRange,
2833
2854
  getSeriesValuesFromRange,
2834
2855
  getFormattingFromRange,
2856
+ locale,
2835
2857
  glUpgradeTick,
2836
2858
  ]);
2837
2859
  // 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":"AA6BA,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,sBA4zEjB,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;AAuZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AA0FnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBA00EjB,CAAC"}
@@ -2,10 +2,10 @@ import { DotsVerticalIcon } from "@rowsncolumns/icons";
2
2
  import { getStringifiedColor, useFocusSheet, useRegisterChartActions, useSetSelectedChartSeries, } from "@rowsncolumns/spreadsheet";
3
3
  import { useSubscribeToSheet } from "@rowsncolumns/spreadsheet-state";
4
4
  import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuTrigger, IconButton, useIsomorphicLayoutEffect, useOnVisibilityChange, } from "@rowsncolumns/ui";
5
- import { debug, isAFormula, isNil, ssfFormat } from "@rowsncolumns/utils";
5
+ import { chartPaletteColorAt, debug, expandChartPalette, isAFormula, isNil, ssfFormat, } from "@rowsncolumns/utils";
6
6
  import React, { useCallback, useMemo, useRef, useState } from "react";
7
7
  import { aggregateSeriesByDomain, getDomainValuesForAxis, sanitizeDomainValues, shouldAggregateSeries, } from "../../utils/aggregation";
8
- import { formatAxisValue, numberFormatToPlotlyTicks, resolveSeriesNumberFormats, resolveValueAxisNumberFormat, valueAxisNameGap, } from "../../utils/axis-number-format";
8
+ import { formatAxisValue, numberFormatToPlotlyTicks, plotlySeparators, resolveSeriesNumberFormats, resolveValueAxisNumberFormat, valueAxisNameGap, } from "../../utils/axis-number-format";
9
9
  import { computeBoxplotSummary } from "../../utils/boxplot";
10
10
  import { createBubbleSizeScale, prepareBubbleSeriesData, toLabelString, } from "../../utils/bubble";
11
11
  import { canSwitchRowColumn, cellLabelGetterFromRangeReader, switchRowColumn, } from "../../utils/chart-orientation";
@@ -179,14 +179,44 @@ const PLOTLY_DLBL_POS_BAR = {
179
179
  insideEnd: "inside",
180
180
  outsideEnd: "outside",
181
181
  bestFit: "outside",
182
+ above: "outside",
183
+ below: "outside",
184
+ left: "outside",
185
+ right: "outside",
182
186
  };
183
- const applyDataLabelsToTracePlotly = (trace, opts, chartType, sourceNumberFormat) => {
187
+ // A scatter trace (line / area / scatter) places its text with the compass
188
+ // positions, and only draws it when `mode` carries `text`. Excel's line
189
+ // placements are centre / left / right / above / below; the bar-only ones
190
+ // land on Excel's default for a line, above the point (#1049).
191
+ const PLOTLY_DLBL_POS_POINT = {
192
+ center: "middle center",
193
+ inside: "middle center",
194
+ outside: "top center",
195
+ insideEnd: "top center",
196
+ outsideEnd: "top center",
197
+ bestFit: "top center",
198
+ above: "top center",
199
+ below: "bottom center",
200
+ left: "middle left",
201
+ right: "middle right",
202
+ };
203
+ const withTextMode = (mode) => {
204
+ const base = typeof mode === "string" && mode ? mode : "lines+markers";
205
+ return base.split("+").includes("text") ? base : `${base}+text`;
206
+ };
207
+ const applyDataLabelsToTracePlotly = (trace, opts, chartType, sourceNumberFormat, locale) => {
184
208
  if (!opts?.show)
185
209
  return trace;
186
210
  const next = { ...trace };
187
211
  const isCircular = chartType === "pie" || chartType === "doughnut";
188
- const posMap = isCircular ? PLOTLY_DLBL_POS_PIE : PLOTLY_DLBL_POS_BAR;
189
- const position = opts.position ? posMap[opts.position] ?? "inside" : "inside";
212
+ const isPoint = !isCircular && next.type === "scatter";
213
+ const posMap = isCircular
214
+ ? PLOTLY_DLBL_POS_PIE
215
+ : isPoint
216
+ ? PLOTLY_DLBL_POS_POINT
217
+ : PLOTLY_DLBL_POS_BAR;
218
+ const fallback = isPoint ? "top center" : "inside";
219
+ const position = opts.position ? posMap[opts.position] ?? fallback : fallback;
190
220
  if (isCircular) {
191
221
  // Plotly's pie trace uses `textinfo` to control which fields render.
192
222
  const parts = [];
@@ -204,7 +234,7 @@ const applyDataLabelsToTracePlotly = (trace, opts, chartType, sourceNumberFormat
204
234
  const pattern = opts.numberFormatPattern ?? sourceNumberFormat;
205
235
  if (pattern) {
206
236
  const values = Array.isArray(next.values) ? next.values : [];
207
- next.text = values.map((v) => formatAxisValue(pattern, v));
237
+ next.text = values.map((v) => formatAxisValue(pattern, v, locale));
208
238
  next.texttemplate = next.textinfo
209
239
  .split("+")
210
240
  .map((part) => (part === "value" ? "%{text}" : `%{${part}}`))
@@ -223,9 +253,13 @@ const applyDataLabelsToTracePlotly = (trace, opts, chartType, sourceNumberFormat
223
253
  const values = next.orientation === "h" ? next.x : next.y;
224
254
  const vals = Array.isArray(values) ? values : [];
225
255
  next.text = pattern
226
- ? vals.map((v) => (v === null || v === undefined ? "" : ssfFormat(pattern, v)))
227
- : values;
256
+ ? vals.map((v) => v === null || v === undefined ? "" : ssfFormat(pattern, v, locale))
257
+ : locale
258
+ ? vals.map((v) => formatAxisValue(undefined, v, locale))
259
+ : values;
228
260
  next.textposition = position;
261
+ if (isPoint)
262
+ next.mode = withTextMode(next.mode);
229
263
  }
230
264
  return next;
231
265
  };
@@ -345,7 +379,7 @@ const addAlphaToColor = (color, alpha) => {
345
379
  }
346
380
  return color;
347
381
  };
348
- export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, getFormattingFromRange, onDeleteChart, onUpdateChart, onRequestEdit, onRequestCalculate, isSelected, fieldButtons, }) => {
382
+ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, getFormattingFromRange, onDeleteChart, onUpdateChart, onRequestEdit, onRequestCalculate, isSelected, fieldButtons, locale, }) => {
349
383
  const spec = chart.spec;
350
384
  const hierarchyTitleRef = useRef(undefined);
351
385
  const explicitChartTitle = chart.spec.title ?? undefined;
@@ -525,7 +559,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
525
559
  : point.value[0]);
526
560
  const yValues = entry.points.map((point) => point.value[1]);
527
561
  const labelValuesForSeries = entry.points.map((point) => point.name);
528
- const seriesColor = spec.series[index]?.color ?? palette[index % palette.length];
562
+ const seriesColor = spec.series[index]?.color ?? chartPaletteColorAt(palette, index);
529
563
  const perSeriesMarker = spec.series[index]?.marker ?? undefined;
530
564
  if (isScatter) {
531
565
  // Per-series marker.symbol = "none" → pure line (no markers).
@@ -1144,7 +1178,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1144
1178
  const effectiveColor = options?.gradientColor ??
1145
1179
  seriesColor ??
1146
1180
  (options?.seriesIndex !== undefined
1147
- ? themeColorPalette[options.seriesIndex % themeColorPalette.length]
1181
+ ? chartPaletteColorAt(themeColorPalette, options.seriesIndex)
1148
1182
  : undefined);
1149
1183
  // Per-data-point color overrides — Gantt charts use `<c:dPt>` to
1150
1184
  // give every task its own color. Build a per-point color array
@@ -1166,7 +1200,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1166
1200
  // with (or the series' own colour when one was authored).
1167
1201
  const existing = Array.isArray(target.marker?.colors)
1168
1202
  ? target.marker.colors
1169
- : themeColorPalette;
1203
+ : expandChartPalette(themeColorPalette);
1170
1204
  const labelCount = Array.isArray(target.labels)
1171
1205
  ? target.labels.length
1172
1206
  : 0;
@@ -1584,7 +1618,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1584
1618
  const ofPieType = resolveOfPieType(spec.ofPieType);
1585
1619
  const secondScale = clampSecondPieSize(spec.secondPieSize) / 100;
1586
1620
  const halfAngle = otherSliceHalfAngle(otherTotal, total);
1587
- const paletteAt = (index) => themeColorPalette[index % themeColorPalette.length];
1621
+ const paletteAt = (index) => chartPaletteColorAt(themeColorPalette, index);
1588
1622
  // Layout in paper coordinates (0-1 across the plot area
1589
1623
  // inside the margins). Main pie on the left, secondary plot
1590
1624
  // on the right.
@@ -1714,7 +1748,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1714
1748
  labels: pieData.labels,
1715
1749
  name: dataLabel,
1716
1750
  marker: {
1717
- colors: themeColorPalette,
1751
+ // Unrolled through Excel's luminance variations so slice 7 does
1752
+ // not repeat slice 1 (rowsncolumns/spreadsheet#1045).
1753
+ colors: expandChartPalette(themeColorPalette),
1718
1754
  },
1719
1755
  };
1720
1756
  // Ties the trace back to spec.series for click selection and
@@ -1831,6 +1867,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1831
1867
  : valueAxisNameGap({
1832
1868
  values: primaryValues,
1833
1869
  pattern: primaryAxisNumberFormat,
1870
+ locale,
1834
1871
  fontSize: axisFontSize,
1835
1872
  minimum: 0,
1836
1873
  }) -
@@ -1846,6 +1883,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1846
1883
  ? trace.y
1847
1884
  : []),
1848
1885
  pattern: secondaryAxisNumberFormat,
1886
+ locale,
1849
1887
  fontSize: axisFontSize,
1850
1888
  minimum: 0,
1851
1889
  }) -
@@ -2026,14 +2064,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2026
2064
  const piePull = isOfPie ? undefined : explosionToPlotlyPull(resolveExplosion(spec));
2027
2065
  const formattedTraces = traces.map((trace) => {
2028
2066
  const pattern = traceNumberFormat(trace);
2029
- let next = applyDataLabelsToTracePlotly(trace, dlOpts, spec.chartType, pattern);
2067
+ let next = applyDataLabelsToTracePlotly(trace, dlOpts, spec.chartType, pattern, locale);
2030
2068
  if (next.type === "pie") {
2031
2069
  // Hover values format like the slice's source cell.
2032
2070
  if (pattern && !next.text) {
2033
2071
  const values = Array.isArray(next.values) ? next.values : [];
2034
2072
  next = {
2035
2073
  ...next,
2036
- text: values.map((v) => formatAxisValue(pattern, v)),
2074
+ text: values.map((v) => formatAxisValue(pattern, v, locale)),
2037
2075
  hoverinfo: "label+text+percent+name",
2038
2076
  };
2039
2077
  }
@@ -2060,7 +2098,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2060
2098
  if (seriesPattern && Array.isArray(values)) {
2061
2099
  next = {
2062
2100
  ...next,
2063
- customdata: values.map((v) => formatAxisValue(seriesPattern, v)),
2101
+ customdata: values.map((v) => formatAxisValue(seriesPattern, v, locale)),
2064
2102
  hovertemplate: `${horizontal ? "%{y}" : "%{x}"}<br>%{customdata}<extra>%{fullData.name}</extra>`,
2065
2103
  };
2066
2104
  }
@@ -2102,6 +2140,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2102
2140
  getDomainValuesFromRange,
2103
2141
  getSeriesValuesFromRange,
2104
2142
  getFormattingFromRange,
2143
+ locale,
2105
2144
  ]);
2106
2145
  // Update theme
2107
2146
  useIsomorphicLayoutEffect(() => {
@@ -2139,7 +2178,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2139
2178
  scrollZoom: false,
2140
2179
  doubleClick: false,
2141
2180
  };
2142
- const plotLayout = useMemo(() => ({ ...layout, dragmode: false }), [layout]);
2181
+ // Ticks and hover values plotly formats itself use the sheet's separators.
2182
+ const separators = useMemo(() => plotlySeparators(locale), [locale]);
2183
+ const plotLayout = useMemo(() => ({
2184
+ ...layout,
2185
+ dragmode: false,
2186
+ ...(separators ? { separators } : {}),
2187
+ }), [layout, separators]);
2143
2188
  // Excel's "Rounded Corners" chart frame (`<c:roundedCorners val="1"/>`
2144
2189
  // at the chartSpace root). Default per OOXML §21.2.2.183 is square,
2145
2190
  // so only opt in when the workbook explicitly set the flag.