@rowsncolumns/charts 28.1.7 → 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.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Doughnut geometry shared by the renderers (rowsncolumns/spreadsheet#1064,
3
+ * #1065). Every fraction here is of the doughnut's OUTER radius — Excel's
4
+ * `<c:holeSize val="75"/>` is 0.75 of the outer edge, whatever the chart
5
+ * draws that edge at (echarts: 75% of the chart; plotly: the trace domain).
6
+ */
7
+ /** `doughnutHoleSize` clamped to Excel's 0-90% range; 0.5 when unset. */
8
+ export declare const clampDoughnutHole: (hole: unknown) => number;
9
+ export type DoughnutRing = {
10
+ inner: number;
11
+ outer: number;
12
+ };
13
+ /**
14
+ * Excel draws a doughnut's series as concentric rings, nested from the hole
15
+ * outwards — series 0 innermost — splitting the band between the hole and
16
+ * the outer edge evenly. One ring (the whole band) for a single series.
17
+ */
18
+ export declare const doughnutRings: (hole: number, count: number) => DoughnutRing[];
19
+ //# sourceMappingURL=doughnut.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doughnut.d.ts","sourceRoot":"","sources":["../../../utils/doughnut.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,GAAI,MAAM,OAAO,KAAG,MACiB,CAAC;AAEpE,MAAM,MAAM,YAAY,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG,YAAY,EAOvE,CAAC"}
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /**
3
+ * Doughnut geometry shared by the renderers (rowsncolumns/spreadsheet#1064,
4
+ * #1065). Every fraction here is of the doughnut's OUTER radius — Excel's
5
+ * `<c:holeSize val="75"/>` is 0.75 of the outer edge, whatever the chart
6
+ * draws that edge at (echarts: 75% of the chart; plotly: the trace domain).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.doughnutRings = exports.clampDoughnutHole = void 0;
10
+ /** `doughnutHoleSize` clamped to Excel's 0-90% range; 0.5 when unset. */
11
+ var clampDoughnutHole = function (hole) {
12
+ return typeof hole === "number" ? Math.max(0, Math.min(0.9, hole)) : 0.5;
13
+ };
14
+ exports.clampDoughnutHole = clampDoughnutHole;
15
+ /**
16
+ * Excel draws a doughnut's series as concentric rings, nested from the hole
17
+ * outwards — series 0 innermost — splitting the band between the hole and
18
+ * the outer edge evenly. One ring (the whole band) for a single series.
19
+ */
20
+ var doughnutRings = function (hole, count) {
21
+ var n = Math.max(1, Math.floor(count));
22
+ var band = 1 - hole;
23
+ return Array.from({ length: n }, function (_, k) { return ({
24
+ inner: hole + (band * k) / n,
25
+ outer: hole + (band * (k + 1)) / n,
26
+ }); });
27
+ };
28
+ exports.doughnutRings = doughnutRings;
@@ -9,6 +9,15 @@ export declare const applyLegendOverrides: (legend: LegendComponentOption, opts:
9
9
  */
10
10
  export declare const applyAxisOverrides: (axis: any, opts: any, numberFormatPattern?: string, locale?: string) => any;
11
11
  export declare const dataLabelFormatter: (opts: any, locale?: string) => (params: any) => string;
12
+ /**
13
+ * The data-label options one series renders with: its own
14
+ * `ChartData.dataLabels` when it has them (OOXML's `<c:ser><c:dLbls>`
15
+ * override — `{ show: false }` turns a labelled chart's labels off for
16
+ * that series alone), else the chart-level `ChartSpec.dataLabels`.
17
+ */
18
+ export declare const resolveSeriesDataLabels: <T>(chartLevel: T | null | undefined, series: {
19
+ dataLabels?: T | null;
20
+ } | null | undefined) => T | null | undefined;
12
21
  export declare const buildSeriesLabelOption: (opts: any, seriesType?: string, locale?: string) => {
13
22
  readonly show: true;
14
23
  readonly position: any;
@@ -1 +1 @@
1
- {"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../../utils/overrides.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAWrD,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,KAAK,GAAG,oCASpD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,qBAAqB,EAC7B,MAAM,GAAG,KACR,qBA6CF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,GAAG,EACT,MAAM,GAAG,EACT,sBAAsB,MAAM,EAC5B,SAAS,MAAM,QA4BhB,CAAC;AAgBF,eAAO,MAAM,kBAAkB,GAAI,MAAM,GAAG,EAAE,SAAS,MAAM,MAAM,QAAQ,GAAG,WAiC7E,CAAC;AAgCF,eAAO,MAAM,sBAAsB,GACjC,MAAM,GAAG,EACT,aAAa,MAAM,EACnB,SAAS,MAAM;;;iCApE0D,GAAG;aAiF7E,CAAC"}
1
+ {"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../../utils/overrides.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAWrD,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,KAAK,GAAG,oCASpD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,qBAAqB,EAC7B,MAAM,GAAG,KACR,qBA6CF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,GAAG,EACT,MAAM,GAAG,EACT,sBAAsB,MAAM,EAC5B,SAAS,MAAM,QA4BhB,CAAC;AAgBF,eAAO,MAAM,kBAAkB,GAAI,MAAM,GAAG,EAAE,SAAS,MAAM,MAAM,QAAQ,GAAG,WAiC7E,CAAC;AAgCF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,CAAC,EACvC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EAChC,QAAQ;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,KACnD,CAAC,GAAG,IAAI,GAAG,SAA6C,CAAC;AAE5D,eAAO,MAAM,sBAAsB,GACjC,MAAM,GAAG,EACT,aAAa,MAAM,EACnB,SAAS,MAAM;;;iCA/E0D,GAAG;aA4F7E,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildSeriesLabelOption = exports.dataLabelFormatter = exports.applyAxisOverrides = exports.applyLegendOverrides = exports.textFormatToEchartsTextStyle = void 0;
3
+ exports.buildSeriesLabelOption = exports.resolveSeriesDataLabels = exports.dataLabelFormatter = exports.applyAxisOverrides = exports.applyLegendOverrides = exports.textFormatToEchartsTextStyle = void 0;
4
4
  var tslib_1 = require("tslib");
5
5
  var axis_number_format_1 = require("./axis-number-format");
6
6
  // Map ChartSpec.legend / dataLabels / axisOptions to ECharts option
@@ -183,6 +183,14 @@ var ECHARTS_POINT_DLBL_POS = {
183
183
  right: "right",
184
184
  };
185
185
  var POINT_SERIES_TYPES = new Set(["line", "scatter", "effectScatter"]);
186
+ /**
187
+ * The data-label options one series renders with: its own
188
+ * `ChartData.dataLabels` when it has them (OOXML's `<c:ser><c:dLbls>`
189
+ * override — `{ show: false }` turns a labelled chart's labels off for
190
+ * that series alone), else the chart-level `ChartSpec.dataLabels`.
191
+ */
192
+ var resolveSeriesDataLabels = function (chartLevel, series) { var _a; return (_a = series === null || series === void 0 ? void 0 : series.dataLabels) !== null && _a !== void 0 ? _a : chartLevel; };
193
+ exports.resolveSeriesDataLabels = resolveSeriesDataLabels;
186
194
  var buildSeriesLabelOption = function (opts, seriesType, locale) {
187
195
  var _a;
188
196
  if (!(opts === null || opts === void 0 ? void 0 : opts.show))
@@ -16,15 +16,41 @@ export type RadarSeriesInput = {
16
16
  };
17
17
  export type RadarIndicator = {
18
18
  name: string;
19
+ min: number;
19
20
  max: number;
20
21
  };
22
+ export type RadarAxisScale = {
23
+ min: number;
24
+ max: number;
25
+ };
26
+ /** The value-axis options a radar's common scale reads (`ChartAxisOptions`). */
27
+ export type RadarAxisOptions = {
28
+ min?: number | null;
29
+ max?: number | null;
30
+ visible?: boolean | null;
31
+ } | null | undefined;
32
+ /**
33
+ * The next "nice" axis bound at or above `value` — the top tick a value
34
+ * axis would auto-scale to: the step is 1 / 2 / 5 × a power of ten sized
35
+ * for about five divisions, and the bound is the first multiple of that
36
+ * step at or above the value (9 → 10, 33 → 40, 0.7 → 0.8).
37
+ */
38
+ export declare const niceAxisCeiling: (value: number) => number;
39
+ /**
40
+ * One value scale shared by every radar axis, as Excel draws it: `max` is
41
+ * the spec's `valueAxisOptions.max` when set, else the largest value across
42
+ * all series and categories rounded up to a nice bound (never below 1);
43
+ * `min` is `valueAxisOptions.min` when set (and below `max`), else 0.
44
+ */
45
+ export declare const radarValueScale: (series: RadarSeriesInput[], axisOptions?: RadarAxisOptions) => RadarAxisScale;
21
46
  /**
22
- * Build echarts radar indicators from category labels and series.
23
- * The `max` per indicator is the largest value seen across all series at
24
- * that category (rounded up a touch so the polygon doesn't touch the
25
- * rim). When every series is empty at an index, `max` defaults to 1.
47
+ * Build echarts radar indicators from category labels and series. Every
48
+ * indicator carries the SAME `min` / `max` — the common scale of
49
+ * {@link radarValueScale} — so values on different axes compare, and a
50
+ * single series draws the shape of its data rather than a regular polygon
51
+ * (rowsncolumns/spreadsheet#1066).
26
52
  */
27
- export declare const buildRadarIndicators: (categories: (string | number | boolean | null | undefined)[], series: RadarSeriesInput[]) => RadarIndicator[];
53
+ export declare const buildRadarIndicators: (categories: (string | number | boolean | null | undefined)[], series: RadarSeriesInput[], axisOptions?: RadarAxisOptions) => RadarIndicator[];
28
54
  /**
29
55
  * Shape series into echarts radar data: one entry per series with its
30
56
  * value vector aligned to the indicator order. Missing/non-numeric
@@ -1 +1 @@
1
- {"version":3,"file":"radar.d.ts","sourceRoot":"","sources":["../../../utils/radar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAQ3D;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC/B,YAAY,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EAC5D,QAAQ,gBAAgB,EAAE,KACzB,cAAc,EAahB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,gBAAgB,EAAE,EAC1B,gBAAgB,MAAM,KACrB,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAQzC,CAAC"}
1
+ {"version":3,"file":"radar.d.ts","sourceRoot":"","sources":["../../../utils/radar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAQxE,MAAM,MAAM,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D,gFAAgF;AAChF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1B,GAAG,IAAI,GAAG,SAAS,CAAC;AAErB;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,MAW/C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAC1B,QAAQ,gBAAgB,EAAE,EAC1B,cAAc,gBAAgB,KAC7B,cAaF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC/B,YAAY,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EAC5D,QAAQ,gBAAgB,EAAE,EAC1B,cAAc,gBAAgB,KAC7B,cAAc,EAGhB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,gBAAgB,EAAE,EAC1B,gBAAgB,MAAM,KACrB,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAQzC,CAAC"}
@@ -12,7 +12,7 @@
12
12
  * labels and per-series numeric values.
13
13
  */
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.buildRadarSeriesData = exports.buildRadarIndicators = void 0;
15
+ exports.buildRadarSeriesData = exports.buildRadarIndicators = exports.radarValueScale = exports.niceAxisCeiling = void 0;
16
16
  var tslib_1 = require("tslib");
17
17
  var isFiniteNum = function (v) {
18
18
  return typeof v === "number" && Number.isFinite(v);
@@ -21,36 +21,76 @@ var toNumber = function (v) {
21
21
  return isFiniteNum(v) ? v : 0;
22
22
  };
23
23
  /**
24
- * Build echarts radar indicators from category labels and series.
25
- * The `max` per indicator is the largest value seen across all series at
26
- * that category (rounded up a touch so the polygon doesn't touch the
27
- * rim). When every series is empty at an index, `max` defaults to 1.
24
+ * The next "nice" axis bound at or above `value` — the top tick a value
25
+ * axis would auto-scale to: the step is 1 / 2 / 5 × a power of ten sized
26
+ * for about five divisions, and the bound is the first multiple of that
27
+ * step at or above the value (9 → 10, 33 → 40, 0.7 → 0.8).
28
28
  */
29
- var buildRadarIndicators = function (categories, series) {
30
- return categories.map(function (label, i) {
31
- var e_1, _a;
32
- var max = 0;
33
- try {
34
- for (var series_1 = tslib_1.__values(series), series_1_1 = series_1.next(); !series_1_1.done; series_1_1 = series_1.next()) {
35
- var s = series_1_1.value;
36
- var v = s.values[i];
37
- if (isFiniteNum(v))
38
- max = Math.max(max, v);
39
- }
40
- }
41
- catch (e_1_1) { e_1 = { error: e_1_1 }; }
42
- finally {
29
+ var niceAxisCeiling = function (value) {
30
+ if (!isFiniteNum(value) || value <= 0)
31
+ return 1;
32
+ var rough = value / 5;
33
+ var magnitude = Math.pow(10, Math.floor(Math.log10(rough)));
34
+ var residual = rough / magnitude;
35
+ var step = (residual <= 1 ? 1 : residual <= 2 ? 2 : residual <= 5 ? 5 : 10) *
36
+ magnitude;
37
+ var ceiling = Math.ceil(value / step - 1e-9) * step;
38
+ // Avoid 0.30000000000000004-style bounds from the float arithmetic.
39
+ return Number(ceiling.toPrecision(12));
40
+ };
41
+ exports.niceAxisCeiling = niceAxisCeiling;
42
+ /**
43
+ * One value scale shared by every radar axis, as Excel draws it: `max` is
44
+ * the spec's `valueAxisOptions.max` when set, else the largest value across
45
+ * all series and categories rounded up to a nice bound (never below 1);
46
+ * `min` is `valueAxisOptions.min` when set (and below `max`), else 0.
47
+ */
48
+ var radarValueScale = function (series, axisOptions) {
49
+ var e_1, _a, e_2, _b;
50
+ var largest = 0;
51
+ try {
52
+ for (var series_1 = tslib_1.__values(series), series_1_1 = series_1.next(); !series_1_1.done; series_1_1 = series_1.next()) {
53
+ var s = series_1_1.value;
43
54
  try {
44
- if (series_1_1 && !series_1_1.done && (_a = series_1.return)) _a.call(series_1);
55
+ for (var _c = (e_2 = void 0, tslib_1.__values(s.values)), _d = _c.next(); !_d.done; _d = _c.next()) {
56
+ var v = _d.value;
57
+ if (isFiniteNum(v))
58
+ largest = Math.max(largest, v);
59
+ }
60
+ }
61
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
62
+ finally {
63
+ try {
64
+ if (_d && !_d.done && (_b = _c.return)) _b.call(_c);
65
+ }
66
+ finally { if (e_2) throw e_2.error; }
45
67
  }
46
- finally { if (e_1) throw e_1.error; }
47
68
  }
48
- return {
49
- name: String(label !== null && label !== void 0 ? label : ""),
50
- // Pad by 10% so the outermost ring has breathing room; never 0.
51
- max: max > 0 ? max * 1.1 : 1,
52
- };
53
- });
69
+ }
70
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
71
+ finally {
72
+ try {
73
+ if (series_1_1 && !series_1_1.done && (_a = series_1.return)) _a.call(series_1);
74
+ }
75
+ finally { if (e_1) throw e_1.error; }
76
+ }
77
+ var max = isFiniteNum(axisOptions === null || axisOptions === void 0 ? void 0 : axisOptions.max)
78
+ ? axisOptions.max
79
+ : (0, exports.niceAxisCeiling)(largest);
80
+ var min = isFiniteNum(axisOptions === null || axisOptions === void 0 ? void 0 : axisOptions.min) && axisOptions.min < max ? axisOptions.min : 0;
81
+ return { min: min, max: max };
82
+ };
83
+ exports.radarValueScale = radarValueScale;
84
+ /**
85
+ * Build echarts radar indicators from category labels and series. Every
86
+ * indicator carries the SAME `min` / `max` — the common scale of
87
+ * {@link radarValueScale} — so values on different axes compare, and a
88
+ * single series draws the shape of its data rather than a regular polygon
89
+ * (rowsncolumns/spreadsheet#1066).
90
+ */
91
+ var buildRadarIndicators = function (categories, series, axisOptions) {
92
+ var _a = (0, exports.radarValueScale)(series, axisOptions), min = _a.min, max = _a.max;
93
+ return categories.map(function (label) { return ({ name: String(label !== null && label !== void 0 ? label : ""), min: min, max: max }); });
54
94
  };
55
95
  exports.buildRadarIndicators = buildRadarIndicators;
56
96
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../basic-chart/basic-chart.tsx"],"names":[],"mappings":"AAkEA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAmQrD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAqEnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBA02GjB,CAAC"}
1
+ {"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../basic-chart/basic-chart.tsx"],"names":[],"mappings":"AAkEA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAqQrD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAqEnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBAq6GjB,CAAC"}
@@ -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";
@@ -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, undefined, locale);
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),
@@ -637,7 +639,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
637
639
  const smooth = scatterStyle === "smooth" || scatterStyle === "smoothMarker";
638
640
  // Labels hang off the point symbols: a marker-less line keeps
639
641
  // an invisible 0-size symbol per point when they show (#1049).
640
- const labelsWithoutMarkers = !wantsMarkers && Boolean(spec.dataLabels?.show);
642
+ const labelsWithoutMarkers = !wantsMarkers &&
643
+ Boolean(resolveSeriesDataLabels(spec.dataLabels, seriesEntry)
644
+ ?.show);
641
645
  return wantsLine
642
646
  ? {
643
647
  type: "line",
@@ -678,9 +682,12 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
678
682
  },
679
683
  ...(seriesColor ? { itemStyle: { color: seriesColor } } : {}),
680
684
  };
681
- }).map((s) => bubbleSeriesLabel
682
- ? { ...s, label: { ...(s.label ?? {}), ...bubbleSeriesLabel } }
683
- : s),
685
+ }).map((s, index) => {
686
+ const bubbleSeriesLabel = bubbleSeriesLabelFor(index);
687
+ return bubbleSeriesLabel
688
+ ? { ...s, label: { ...(s.label ?? {}), ...bubbleSeriesLabel } }
689
+ : s;
690
+ }),
684
691
  }, {
685
692
  replaceMerge: [
686
693
  "series",
@@ -1142,7 +1149,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1142
1149
  const values = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
1143
1150
  radarSeriesInputs.push({ name: label, values });
1144
1151
  }
1145
- 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);
1146
1156
  const radarData = buildRadarSeriesData(radarSeriesInputs, indicators.length);
1147
1157
  const radarStyle = spec.radarStyle ?? "standard";
1148
1158
  const radarColors = radarSeriesInputs.map((_, i) => spec.series?.[i]?.color);
@@ -1168,7 +1178,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1168
1178
  radar: {
1169
1179
  indicator: indicators.length
1170
1180
  ? indicators
1171
- : [{ 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
+ },
1172
1189
  },
1173
1190
  legend: applyLegendOverrides({
1174
1191
  ...legend,
@@ -1788,7 +1805,8 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1788
1805
  // marker-less line with labels (Excel's default "Line" with values
1789
1806
  // shown) keeps an invisible 0-size symbol per point for the label
1790
1807
  // to hang off (#1049).
1791
- const labelsWithoutMarkers = !wantsMarkers && Boolean(spec.dataLabels?.show);
1808
+ const labelsWithoutMarkers = !wantsMarkers &&
1809
+ Boolean(resolveSeriesDataLabels(spec.dataLabels, seriesAny)?.show);
1792
1810
  seriesEntry.showSymbol = wantsMarkers || labelsWithoutMarkers;
1793
1811
  if (wantsMarkers) {
1794
1812
  seriesEntry.symbol =
@@ -2456,19 +2474,37 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2456
2474
  }
2457
2475
  : legendBand.grid;
2458
2476
  // Data labels fall back to the series' axis format when the spec has
2459
- // no explicit `<c:dLbls><c:numFmt>` — Excel's "Linked to source".
2460
- const dataLabelOptions = spec.dataLabels;
2461
- const seriesLabelOptionFor = (s) => buildSeriesLabelOption(dataLabelOptions && !dataLabelOptions.numberFormatPattern
2462
- ? {
2463
- ...dataLabelOptions,
2464
- numberFormatPattern: seriesNumberFormatFor(s),
2465
- }
2466
- : dataLabelOptions, s?.type, locale);
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
+ };
2467
2490
  // For doughnut, surface the inner-radius hole on each pie series.
2468
2491
  // Excel persists `holeSize` as a 0-90 percentage; we normalize to a
2469
2492
  // 0-1 fraction (doughnutHoleSize). Default to 0.5 when unset.
2470
2493
  const isDoughnut = spec.chartType === "doughnut";
2471
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
+ : [];
2472
2508
  // Exploded pie / doughnut: every slice sits in the `select` state,
2473
2509
  // offset outward by the spec's percentage of the pie radius.
2474
2510
  const explosion = isCircularChart && !isOfPie ? resolveExplosion(spec) : 0;
@@ -2519,6 +2555,12 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2519
2555
  if (seriesLabelOption) {
2520
2556
  next.label = { ...(s?.label ?? {}), ...seriesLabelOption };
2521
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
+ }
2522
2564
  // Tooltip values of the value-axis series (a boxplot / candlestick
2523
2565
  // tooltip lists its own statistics and keeps echarts' default).
2524
2566
  const seriesTooltipValueFormatter = VALUE_TOOLTIP_SERIES_TYPES.has(s?.type)
@@ -2547,10 +2589,15 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2547
2589
  }
2548
2590
  }
2549
2591
  if (isDoughnut && next.type === "pie") {
2550
- const inner = typeof doughnutHoleSize === "number"
2551
- ? Math.round(Math.max(0, Math.min(0.9, doughnutHoleSize)) * 100)
2552
- : 50;
2553
- 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)];
2554
2601
  }
2555
2602
  return next;
2556
2603
  });
@@ -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;AAuZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AA0FnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBA00EjB,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"}
@@ -17,6 +17,8 @@ import { computeErrorBars, resolveCustomErrorBarValues, } from "../../utils/erro
17
17
  import { plotlyMulticategoryX, resolveCategoryLevels, } from "../../utils/category-levels";
18
18
  import { gradientToPlotly } from "../../utils/gradient";
19
19
  import { BAND_GAP, legendLabelsFrom, measureTextWidth, reservePlotlyLegendBand, } from "../../utils/legend-layout";
20
+ import { clampDoughnutHole, doughnutRings } from "../../utils/doughnut";
21
+ import { resolveSeriesDataLabels } from "../../utils/overrides";
20
22
  import { plotlyTitleText } from "../../utils/plotly-title";
21
23
  import { explosionToPlotlyPull, resolveExplosion, } from "../../utils/pie-explosion";
22
24
  import { resolvePlotlyClickSelection, wasDragged, } from "../../utils/plotly-click";
@@ -882,7 +884,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
882
884
  }
883
885
  // buildRadarIndicators keeps category/value alignment consistent
884
886
  // with the echarts renderer (and validates we have axes).
885
- const indicators = buildRadarIndicators(categories, radarInputs);
887
+ // One value scale for every axis (Excel), from the spec's value-axis
888
+ // min / max when set (#1066).
889
+ const radarAxisOptions = spec.valueAxisOptions;
890
+ const indicators = buildRadarIndicators(categories, radarInputs, radarAxisOptions);
886
891
  for (let i = 0; i < radarInputs.length; i++) {
887
892
  const input = radarInputs[i];
888
893
  const seriesColor = spec.series?.[i]?.color ?? undefined;
@@ -898,7 +903,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
898
903
  ...(seriesColor ? { line: { color: seriesColor } } : {}),
899
904
  });
900
905
  }
901
- const maxR = indicators.reduce((m, ind) => Math.max(m, ind.max), 0);
906
+ const radarScale = indicators[0] ?? { min: 0, max: 1 };
902
907
  setPlotData(radarTraces);
903
908
  setLayout({
904
909
  title: { text: plotlyTitleText(title, spec.titleTextFormat) },
@@ -908,8 +913,8 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
908
913
  showlegend: radarTraces.length > 1,
909
914
  polar: {
910
915
  radialaxis: {
911
- visible: true,
912
- range: [0, maxR > 0 ? maxR : 1],
916
+ visible: radarAxisOptions?.visible !== false,
917
+ range: [radarScale.min, radarScale.max],
913
918
  },
914
919
  },
915
920
  margin: { l: 50, r: 50, t: 50, b: 50 },
@@ -2044,8 +2049,26 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2044
2049
  : {}),
2045
2050
  };
2046
2051
  // Phase 5.1: apply data labels per trace, and set the doughnut hole
2047
- // when chartType === "doughnut".
2052
+ // when chartType === "doughnut". A series' own `dataLabels` replace the
2053
+ // chart-level ones for its trace (#1059).
2048
2054
  const dlOpts = spec.dataLabels;
2055
+ // Excel nests a doughnut's series as concentric rings, series 0
2056
+ // innermost (#1065): each pie trace is scaled into a centred sub-domain
2057
+ // whose radius is its ring's outer edge, with the hole at the inner one.
2058
+ const doughnutSeriesOrder = spec.chartType === "doughnut"
2059
+ ? traces
2060
+ .filter((t) => t?.type === "pie")
2061
+ .map((t) => t?.meta?.rncSeriesIndex)
2062
+ .filter((i) => typeof i === "number")
2063
+ .sort((a, b) => a - b)
2064
+ : [];
2065
+ const doughnutRingSet = spec.chartType === "doughnut"
2066
+ ? doughnutRings(clampDoughnutHole(spec.doughnutHoleSize), doughnutSeriesOrder.length)
2067
+ : [];
2068
+ const traceDataLabels = (trace) => {
2069
+ const specIndex = trace?.meta?.rncSeriesIndex;
2070
+ return resolveSeriesDataLabels(dlOpts, typeof specIndex === "number" ? spec.series?.[specIndex] : undefined);
2071
+ };
2049
2072
  const doughnutHole = spec.doughnutHoleSize;
2050
2073
  // Data labels are linked to each series' OWN source cells (Excel's
2051
2074
  // `<c:dLbls><c:numFmt sourceLinked="1">`); the axis format only steps
@@ -2064,8 +2087,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2064
2087
  const piePull = isOfPie ? undefined : explosionToPlotlyPull(resolveExplosion(spec));
2065
2088
  const formattedTraces = traces.map((trace) => {
2066
2089
  const pattern = traceNumberFormat(trace);
2067
- let next = applyDataLabelsToTracePlotly(trace, dlOpts, spec.chartType, pattern, locale);
2090
+ let next = applyDataLabelsToTracePlotly(trace, traceDataLabels(trace), spec.chartType, pattern, locale);
2068
2091
  if (next.type === "pie") {
2092
+ // Plotly's pie prints every slice's percentage by default; Excel
2093
+ // draws nothing until data labels are on (#1062).
2094
+ if (!traceDataLabels(trace)?.show) {
2095
+ next = { ...next, textinfo: "none" };
2096
+ }
2069
2097
  // Hover values format like the slice's source cell.
2070
2098
  if (pattern && !next.text) {
2071
2099
  const values = Array.isArray(next.values) ? next.values : [];
@@ -2108,12 +2136,17 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2108
2136
  // (doughnutHoleSize); clamp like the echarts renderer and default
2109
2137
  // to 0.5 when unset.
2110
2138
  if (spec.chartType === "doughnut" && next.type === "pie") {
2111
- next = {
2112
- ...next,
2113
- hole: typeof doughnutHole === "number"
2114
- ? Math.max(0, Math.min(0.9, doughnutHole))
2115
- : 0.5,
2116
- };
2139
+ const rank = doughnutSeriesOrder.indexOf(next.meta?.rncSeriesIndex);
2140
+ const ring = doughnutRingSet[rank] ?? doughnutRings(clampDoughnutHole(doughnutHole), 1)[0];
2141
+ // Plotly's `hole` is a fraction of the trace's own radius.
2142
+ next = { ...next, hole: ring.inner / ring.outer };
2143
+ if (doughnutRingSet.length > 1) {
2144
+ const half = ring.outer / 2;
2145
+ next = {
2146
+ ...next,
2147
+ domain: { x: [0.5 - half, 0.5 + half], y: [0.5 - half, 0.5 + half] },
2148
+ };
2149
+ }
2117
2150
  }
2118
2151
  return next;
2119
2152
  });
@@ -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/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/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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Doughnut geometry shared by the renderers (rowsncolumns/spreadsheet#1064,
3
+ * #1065). Every fraction here is of the doughnut's OUTER radius — Excel's
4
+ * `<c:holeSize val="75"/>` is 0.75 of the outer edge, whatever the chart
5
+ * draws that edge at (echarts: 75% of the chart; plotly: the trace domain).
6
+ */
7
+ /** `doughnutHoleSize` clamped to Excel's 0-90% range; 0.5 when unset. */
8
+ export declare const clampDoughnutHole: (hole: unknown) => number;
9
+ export type DoughnutRing = {
10
+ inner: number;
11
+ outer: number;
12
+ };
13
+ /**
14
+ * Excel draws a doughnut's series as concentric rings, nested from the hole
15
+ * outwards — series 0 innermost — splitting the band between the hole and
16
+ * the outer edge evenly. One ring (the whole band) for a single series.
17
+ */
18
+ export declare const doughnutRings: (hole: number, count: number) => DoughnutRing[];
19
+ //# sourceMappingURL=doughnut.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doughnut.d.ts","sourceRoot":"","sources":["../../../utils/doughnut.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,GAAI,MAAM,OAAO,KAAG,MACiB,CAAC;AAEpE,MAAM,MAAM,YAAY,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG,YAAY,EAOvE,CAAC"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Doughnut geometry shared by the renderers (rowsncolumns/spreadsheet#1064,
3
+ * #1065). Every fraction here is of the doughnut's OUTER radius — Excel's
4
+ * `<c:holeSize val="75"/>` is 0.75 of the outer edge, whatever the chart
5
+ * draws that edge at (echarts: 75% of the chart; plotly: the trace domain).
6
+ */
7
+ /** `doughnutHoleSize` clamped to Excel's 0-90% range; 0.5 when unset. */
8
+ export const clampDoughnutHole = (hole) => typeof hole === "number" ? Math.max(0, Math.min(0.9, hole)) : 0.5;
9
+ /**
10
+ * Excel draws a doughnut's series as concentric rings, nested from the hole
11
+ * outwards — series 0 innermost — splitting the band between the hole and
12
+ * the outer edge evenly. One ring (the whole band) for a single series.
13
+ */
14
+ export const doughnutRings = (hole, count) => {
15
+ const n = Math.max(1, Math.floor(count));
16
+ const band = 1 - hole;
17
+ return Array.from({ length: n }, (_, k) => ({
18
+ inner: hole + (band * k) / n,
19
+ outer: hole + (band * (k + 1)) / n,
20
+ }));
21
+ };