@rowsncolumns/charts 28.1.12 → 28.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,KACvC,MAmEF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;;;;;;;;CAgBzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SA0BX,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAiBjE,CAAC"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,KACvC,MAmEF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;;;;;;;;CAgBzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SA0BX,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAmBjE,CAAC"}
@@ -109,6 +109,8 @@ var showXAxis = function (type) {
109
109
  type === "histogram" ||
110
110
  // A waterfall's steps are categories (Opening, Sales, …) (#1070).
111
111
  type === "waterfall" ||
112
+ // A box & whisker's slots are categories (or the series names) (#1092).
113
+ type === "boxplot" ||
112
114
  // Stock is line-derived on a category x-axis. Radar is polar and
113
115
  // does not use the cartesian x-axis.
114
116
  type === "stock") {
@@ -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;AAsTrD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAiGnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBA8kHjB,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;AAsTrD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAiGnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBAgtHjB,CAAC"}
@@ -774,6 +774,27 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
774
774
  resolveRange: (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy),
775
775
  });
776
776
  });
777
+ const bubbleLegend = applyLegendOverrides({
778
+ ...legend,
779
+ show: preparedSeries.length > 1,
780
+ data: preparedSeries.map((entry) => entry.name),
781
+ }, spec.legend);
782
+ const bubbleBand = reserveEchartsLegendBand({
783
+ position: spec.legend?.position,
784
+ visible: bubbleLegend.show !== false,
785
+ labels: preparedSeries.map((entry) => entry.name),
786
+ legendFontSize: bubbleLegend.textStyle?.fontSize ??
787
+ spec.legend?.textFormat?.fontSize ??
788
+ 12,
789
+ legendItemWidth: defaultLegendConfig.itemWidth,
790
+ width: width ?? 0,
791
+ height: height ?? 0,
792
+ hasTitle: Boolean(explicitChartTitle),
793
+ titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
794
+ hasBottomAxisTitle: Boolean(spec.horizontalAxisTitle),
795
+ bottomAxisNameGap: 30,
796
+ hasLeftAxisTitle: Boolean(spec.verticalAxisTitle),
797
+ });
777
798
  chartRef.current?.setOption({
778
799
  xAxis: [
779
800
  applyAxisOverrides({ ...xAxisConfig }, spec.categoryAxisOptions),
@@ -799,17 +820,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
799
820
  ]
800
821
  : []),
801
822
  ],
802
- legend: applyLegendOverrides({
803
- ...legend,
804
- show: preparedSeries.length > 1,
805
- data: preparedSeries.map((entry) => entry.name),
806
- }, spec.legend),
807
- grid: {
808
- left: "3%",
809
- right: "4%",
810
- bottom: "3%",
811
- containLabel: true,
812
- },
823
+ legend: { ...bubbleLegend, ...bubbleBand.legend },
824
+ // The plot shrinks to leave the legend its own band, as the
825
+ // category charts do (#1089).
826
+ grid: bubbleBand.grid,
813
827
  tooltip: {
814
828
  trigger: "item",
815
829
  formatter: tooltipFormatter,
@@ -1385,8 +1399,44 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1385
1399
  const lines = indicators.map((indicator, i) => `${indicator.name}: ${formatAxisValue(pattern, values[i], locale)}`);
1386
1400
  return [params?.name ?? "", ...lines].filter(Boolean).join("<br/>");
1387
1401
  };
1402
+ const radarLegend = applyLegendOverrides({
1403
+ ...legend,
1404
+ show: radarData.length > 1,
1405
+ data: radarData.map((d) => d.name),
1406
+ }, spec.legend);
1407
+ // The circle moves and shrinks to leave the legend (and the title)
1408
+ // their own band instead of being drawn over (#1089).
1409
+ const radarBand = reserveEchartsLegendBand({
1410
+ position: spec.legend?.position,
1411
+ visible: radarLegend.show !== false,
1412
+ labels: radarData.map((d) => d.name),
1413
+ legendFontSize: radarLegend.textStyle?.fontSize ??
1414
+ spec.legend?.textFormat?.fontSize ??
1415
+ 12,
1416
+ legendItemWidth: defaultLegendConfig.itemWidth,
1417
+ width: width ?? 0,
1418
+ height: height ?? 0,
1419
+ hasTitle: Boolean(explicitChartTitle),
1420
+ titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
1421
+ hasBottomAxisTitle: false,
1422
+ bottomAxisNameGap: 0,
1423
+ hasLeftAxisTitle: false,
1424
+ });
1425
+ const radarPlotWidth = Math.max(0, (width ?? 0) - radarBand.grid.left - radarBand.grid.right);
1426
+ const radarPlotHeight = Math.max(0, (height ?? 0) - radarBand.grid.top - radarBand.grid.bottom);
1427
+ // Leave ~30px around the circle for the axis names.
1428
+ const radarGeometry = radarPlotWidth > 0 && radarPlotHeight > 0
1429
+ ? {
1430
+ center: [
1431
+ radarBand.grid.left + radarPlotWidth / 2,
1432
+ radarBand.grid.top + radarPlotHeight / 2,
1433
+ ],
1434
+ radius: Math.max(10, Math.min(radarPlotWidth, radarPlotHeight) / 2 - 30),
1435
+ }
1436
+ : {};
1388
1437
  chartRef.current?.setOption({
1389
1438
  radar: {
1439
+ ...radarGeometry,
1390
1440
  // The common scale's tick labels along ONE axis, as Excel draws
1391
1441
  // them: echarts labels every indicator axis, so each indicator
1392
1442
  // carries its own `axisLabel.show` (an indicator's own options win
@@ -1405,11 +1455,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1405
1455
  formatter: (value) => formatAxisValue(radarFormats[0], value, locale),
1406
1456
  },
1407
1457
  },
1408
- legend: applyLegendOverrides({
1409
- ...legend,
1410
- show: radarData.length > 1,
1411
- data: radarData.map((d) => d.name),
1412
- }, spec.legend),
1458
+ legend: { ...radarLegend, ...radarBand.legend },
1413
1459
  tooltip: { trigger: "item", formatter: radarTooltipFormatter },
1414
1460
  series: [
1415
1461
  {
@@ -2356,6 +2402,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2356
2402
  top: 60,
2357
2403
  bottom: 20,
2358
2404
  };
2405
+ // `dataLabels: { show: false }` hides the values inside the bars; the
2406
+ // stage names on the left stay (#1091).
2407
+ const funnelLabelsOff = resolveSeriesDataLabels(spec.dataLabels, seriesEntry)?.show ===
2408
+ false;
2359
2409
  chartSeries.push({
2360
2410
  type: "funnel",
2361
2411
  id: chartSeriesId(0, 0),
@@ -2365,11 +2415,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2365
2415
  itemStyle: {
2366
2416
  color: (params) => stageColors.get(params.dataIndex) ?? funnelColor,
2367
2417
  },
2368
- label: {
2369
- show: true,
2370
- position: "inside",
2371
- formatter: (params) => formatAxisValue(undefined, params.value, locale),
2372
- },
2418
+ label: funnelLabelsOff
2419
+ ? { show: false }
2420
+ : {
2421
+ show: true,
2422
+ position: "inside",
2423
+ formatter: (params) => formatAxisValue(undefined, params.value, locale),
2424
+ },
2373
2425
  });
2374
2426
  // The stage names sit to the left of the bars, where Excel's
2375
2427
  // category axis puts them: an invisible twin funnel carries them.
@@ -2386,49 +2438,49 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2386
2438
  labelLine: { show: false },
2387
2439
  z: 1,
2388
2440
  });
2389
- legend.data = [];
2441
+ // The legend lists the series (shown when `legend.position` says so).
2442
+ legend.data = seriesEntry?.dataLabel ? [seriesEntry.dataLabel] : [];
2390
2443
  }
2391
2444
  else if (spec.chartType === "boxplot") {
2392
2445
  const quartileMethod = spec.quartileMethod;
2393
- const showOutliers = spec.showOutliers;
2446
+ const showOutliers = spec.showOutliers !== false;
2394
2447
  const showMeanMarker = spec.showMeanMarker === true;
2395
- const boxStats = [];
2396
- const boxLabels = [];
2397
- // [categoryIndex, value] pairs for the outlier / mean overlay
2398
- // scatter series (only populated when the options are on).
2399
- const outlierPoints = [];
2400
- const meanPoints = [];
2401
- for (const seriesEntry of spec.series ?? []) {
2402
- const label = seriesEntry.dataLabel && !isAFormula(seriesEntry.dataLabel)
2403
- ? seriesEntry.dataLabel
2404
- : "";
2405
- const rawValues = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
2406
- const summary = computeBoxplotSummary(rawValues, { quartileMethod });
2407
- if (summary) {
2408
- const boxIndex = boxStats.length;
2409
- // `showOutliers === true` clips the whiskers to the most
2410
- // extreme data points inside the 1.5×IQR fences and plots
2411
- // the points beyond them (Excel's "show outlier points").
2412
- // Otherwise the whiskers span the full min/max — today's
2413
- // rendering.
2414
- const lo = showOutliers === true ? summary.lowerWhisker : summary.min;
2415
- const hi = showOutliers === true ? summary.upperWhisker : summary.max;
2416
- boxStats.push([lo, summary.q1, summary.median, summary.q3, hi]);
2417
- boxLabels.push(label);
2418
- if (showOutliers === true) {
2419
- for (const outlier of summary.outliers) {
2420
- outlierPoints.push([boxIndex, outlier]);
2421
- }
2422
- }
2423
- if (showMeanMarker) {
2424
- meanPoints.push([boxIndex, summary.mean]);
2425
- }
2448
+ const showInnerPoints = spec.showInnerPoints === true;
2449
+ const showMeanLine = spec.showMeanLine === true;
2450
+ // A category domain groups every series by its distinct values (in
2451
+ // order of first appearance): one box per category per series, as
2452
+ // Excel draws it. Without a domain each series is one box on its own
2453
+ // slot (#1092).
2454
+ const boxDomainSource = spec.domains?.[0]?.sources?.[0];
2455
+ const boxCategories = boxDomainSource
2456
+ ? (getDomainValuesFromRange(boxDomainSource, spec.hiddenDimensionStrategy) ?? []).map((v) => (v === null || v === undefined ? "" : `${v}`.trim()))
2457
+ : [];
2458
+ const grouped = boxCategories.some((c) => c !== "");
2459
+ const groups = [];
2460
+ if (grouped) {
2461
+ for (const c of boxCategories) {
2462
+ if (c !== "" && !groups.includes(c))
2463
+ groups.push(c);
2426
2464
  }
2427
2465
  }
2466
+ const boxSeriesSpecs = spec.series ?? [];
2467
+ const slots = grouped ? groups : boxSeriesSpecs.map((s, i) => s.dataLabel && !isAFormula(s.dataLabel) ? s.dataLabel : `Series ${i + 1}`);
2468
+ // Excel sizes the boxes like columns: gapWidth is a percentage of the
2469
+ // box width (default 150). echarts' boxplot only clamps its width
2470
+ // (`boxWidth`), so the clamp is the box width that gap allows in the
2471
+ // plot's category band.
2472
+ const boxGap = typeof spec.gapWidth === "number" ? Math.max(0, spec.gapWidth) : 150;
2473
+ const boxPlotWidth = Math.max(0, (width ?? 0) * 0.85 - 40);
2474
+ const boxBand = slots.length ? boxPlotWidth / slots.length : boxPlotWidth;
2475
+ const boxWidthMax = Math.max(2, Math.round(boxBand / (Math.max(1, boxSeriesSpecs.length) + boxGap / 100)));
2476
+ const boxCount = Math.max(1, boxSeriesSpecs.length);
2477
+ // Points of series i sit at that series' sub-slot inside the band
2478
+ // (echarts spreads a category's boxes over ~80% of it).
2479
+ const slotX = (slotIndex, seriesIndex) => slotIndex + ((seriesIndex + 0.5) / boxCount - 0.5) * 0.8;
2428
2480
  chartDomains.length = 0;
2429
2481
  chartDomains.push({
2430
2482
  type: "category",
2431
- data: boxLabels,
2483
+ data: slots,
2432
2484
  show: showAxis,
2433
2485
  axisLine: { show: true },
2434
2486
  axisTick: { show: true },
@@ -2438,38 +2490,95 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2438
2490
  nameGap: 30,
2439
2491
  });
2440
2492
  chartSeries.length = 0;
2441
- const boxSeries = {
2442
- type: "boxplot",
2443
- name: "",
2444
- data: boxStats,
2445
- };
2446
- const boxColor = spec.series?.[0]?.color;
2447
- if (boxColor) {
2448
- boxSeries.itemStyle = { color: boxColor, borderColor: boxColor };
2449
- }
2450
- chartSeries.push(boxSeries);
2451
- if (outlierPoints.length > 0) {
2452
- chartSeries.push({
2453
- type: "scatter",
2454
- name: "",
2455
- data: outlierPoints,
2456
- symbolSize: 6,
2457
- ...(boxColor ? { itemStyle: { color: boxColor } } : {}),
2458
- tooltip: { show: false },
2493
+ const boxLegend = [];
2494
+ boxSeriesSpecs.forEach((seriesEntry, seriesIndex) => {
2495
+ const label = seriesEntry.dataLabel && !isAFormula(seriesEntry.dataLabel)
2496
+ ? seriesEntry.dataLabel
2497
+ : "";
2498
+ const rawValues = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
2499
+ const color = seriesEntry.color ?? paletteColorAt(seriesIndex);
2500
+ const boxData = [];
2501
+ const outlierPoints = [];
2502
+ const innerPoints = [];
2503
+ const meanPoints = [];
2504
+ const meanLine = [];
2505
+ const slotValues = grouped
2506
+ ? groups.map((g) => rawValues.filter((_, i) => boxCategories[i] === g))
2507
+ : slots.map((_, i) => (i === seriesIndex ? rawValues : []));
2508
+ slotValues.forEach((values, slotIndex) => {
2509
+ const summary = computeBoxplotSummary(values, { quartileMethod });
2510
+ if (!summary) {
2511
+ boxData.push([]);
2512
+ meanLine.push(null);
2513
+ return;
2514
+ }
2515
+ // Whiskers stop at the most extreme points inside the 1.5×IQR
2516
+ // fences; hiding the outliers hides the points only (Excel).
2517
+ boxData.push([
2518
+ summary.lowerWhisker,
2519
+ summary.q1,
2520
+ summary.median,
2521
+ summary.q3,
2522
+ summary.upperWhisker,
2523
+ ]);
2524
+ meanLine.push(summary.mean);
2525
+ const x = slotX(slotIndex, seriesIndex);
2526
+ if (showOutliers) {
2527
+ for (const outlier of summary.outliers)
2528
+ outlierPoints.push([x, outlier]);
2529
+ }
2530
+ if (showInnerPoints) {
2531
+ for (const v of values) {
2532
+ if (v >= summary.lowerWhisker && v <= summary.upperWhisker) {
2533
+ innerPoints.push([x, v]);
2534
+ }
2535
+ }
2536
+ }
2537
+ if (showMeanMarker)
2538
+ meanPoints.push([x, summary.mean]);
2459
2539
  });
2460
- }
2461
- if (meanPoints.length > 0) {
2540
+ if (label)
2541
+ boxLegend.push(label);
2462
2542
  chartSeries.push({
2543
+ type: "boxplot",
2544
+ id: chartSeriesId(seriesIndex, 0),
2545
+ name: label,
2546
+ data: boxData,
2547
+ boxWidth: [2, boxWidthMax],
2548
+ itemStyle: { color, borderColor: color },
2549
+ });
2550
+ const pointSeries = (name, points, extra) => ({
2463
2551
  type: "scatter",
2464
- name: "",
2465
- data: meanPoints,
2466
- symbol: "diamond",
2467
- symbolSize: 9,
2468
- ...(boxColor ? { itemStyle: { color: boxColor } } : {}),
2552
+ name,
2553
+ data: points,
2554
+ itemStyle: { color },
2469
2555
  tooltip: { show: false },
2556
+ ...extra,
2470
2557
  });
2471
- }
2472
- legend.data = [];
2558
+ if (outlierPoints.length > 0) {
2559
+ chartSeries.push(pointSeries("", outlierPoints, { symbolSize: 6 }));
2560
+ }
2561
+ if (innerPoints.length > 0) {
2562
+ chartSeries.push(pointSeries("", innerPoints, { symbolSize: 4, itemStyle: { color, opacity: 0.6 } }));
2563
+ }
2564
+ if (meanPoints.length > 0) {
2565
+ chartSeries.push(pointSeries("", meanPoints, { symbol: "diamond", symbolSize: 9 }));
2566
+ }
2567
+ if (showMeanLine && meanLine.some((m) => m !== null)) {
2568
+ chartSeries.push({
2569
+ type: "line",
2570
+ name: "",
2571
+ data: meanLine.map((m, slotIndex) => m === null ? null : [slotX(slotIndex, seriesIndex), m]),
2572
+ showSymbol: false,
2573
+ lineStyle: { color, type: "dashed", width: 1 },
2574
+ itemStyle: { color },
2575
+ tooltip: { show: false },
2576
+ z: 3,
2577
+ });
2578
+ }
2579
+ });
2580
+ // The legend lists the series (shown when `legend.position` says so).
2581
+ legend.data = boxLegend;
2473
2582
  }
2474
2583
  else if (spec.chartType === "histogram") {
2475
2584
  const rawValues = flattenChartDataValues(spec.series, getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,sBAAsB,GACjC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,yGA6DzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SAuBX,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,GAAG,SAAS,KACnD,OAAgD,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,aAAa,CAAC,OAAO,CAAC,KAC7B,MAAM,EAmBR,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;aASzC,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAiBjE,CAAC"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,sBAAsB,GACjC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,yGA6DzC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,GACnC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SAuBX,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,GAAG,SAAS,KACnD,OAAgD,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,aAAa,CAAC,OAAO,CAAC,KAC7B,MAAM,EAmBR,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;aASzC,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAmBjE,CAAC"}
@@ -121,6 +121,8 @@ export const showXAxis = (type) => {
121
121
  type === "histogram" ||
122
122
  // A waterfall's steps are categories (Opening, Sales, …) (#1070).
123
123
  type === "waterfall" ||
124
+ // A box & whisker's slots are categories (or the series names) (#1092).
125
+ type === "boxplot" ||
124
126
  // Stock is line-derived and lives on a category x-axis. Radar is
125
127
  // axis-less (it uses a polar `radar` indicator instead).
126
128
  type === "stock") {
@@ -1 +1 @@
1
- {"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/basic-chart.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA+ZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAmGnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBA4nFjB,CAAC"}
1
+ {"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/basic-chart.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA8ZxD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAmGnD,eAAO,MAAM,UAAU,GAAI,gPAkBxB,eAAe,sBAkqFjB,CAAC"}
@@ -6,7 +6,6 @@ import { chartPaletteColorAt, debug, expandChartPalette, isAFormula, isNil, ssfF
6
6
  import React, { useCallback, useMemo, useRef, useState } from "react";
7
7
  import { aggregateSeriesByDomain, getDomainValuesForAxis, sanitizeDomainValues, shouldAggregateSeries, } from "../../utils/aggregation";
8
8
  import { formatAxisValue, numberFormatToPlotlyTicks, plotlySeparators, resolveSeriesNumberFormats, resolveValueAxisNumberFormat, valueAxisNameGap, } from "../../utils/axis-number-format";
9
- import { computeBoxplotSummary } from "../../utils/boxplot";
10
9
  import { createBubbleSizeScale, prepareBubbleSeriesData, toLabelString, } from "../../utils/bubble";
11
10
  import { canSwitchRowColumn, cellLabelGetterFromRangeReader, switchRowColumn, } from "../../utils/chart-orientation";
12
11
  import { dataUrlToBlob } from "../../utils/chart-image";
@@ -736,6 +735,29 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
736
735
  const xAxisFmt = spec.categoryAxisOptions?.textFormat;
737
736
  const yAxisFmt = spec.valueAxisOptions?.textFormat;
738
737
  const legendFmt = spec.legend?.textFormat;
738
+ const bubbleLegendOverrides = applyLegendOverridesPlotly({
739
+ font: {
740
+ color: legendFmt?.color ?? textColor,
741
+ ...(legendFmt?.fontFamily ? { family: legendFmt.fontFamily } : {}),
742
+ ...(legendFmt?.fontSize ? { size: legendFmt.fontSize } : {}),
743
+ },
744
+ }, spec.legend);
745
+ const bubbleBand = reservePlotlyLegendBand({
746
+ position: spec.legend?.position,
747
+ visible: (bubbleLegendOverrides.showlegend ?? preparedSeries.length > 1) !==
748
+ false,
749
+ labels: preparedSeries.map((entry) => entry.name),
750
+ legendFontSize: legendFmt?.fontSize ?? 12,
751
+ width: Math.max(0, (width ?? 0) - 2),
752
+ height: Math.max(0, (height ?? 0) - 2),
753
+ hasTitle: Boolean(title),
754
+ titleFontSize: titleFmt?.fontSize ?? 16,
755
+ hasBottomAxisTitle: Boolean(spec.horizontalAxisTitle),
756
+ hasLeftAxisTitle: Boolean(spec.verticalAxisTitle),
757
+ leftAxisDepth: 60,
758
+ rightAxisDepth: hasBubbleSecondaryAxis ? 60 : 0,
759
+ axisFontSize: 12,
760
+ });
739
761
  const bubbleLayout = {
740
762
  title: {
741
763
  text: plotlyTitleText(title, spec.titleTextFormat),
@@ -789,16 +811,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
789
811
  }
790
812
  : {}),
791
813
  legend: {
792
- font: {
793
- color: legendFmt?.color ?? textColor,
794
- ...(legendFmt?.fontFamily ? { family: legendFmt.fontFamily } : {}),
795
- ...(legendFmt?.fontSize ? { size: legendFmt.fontSize } : {}),
796
- },
814
+ ...bubbleLegendOverrides.legend,
815
+ ...(bubbleBand.legend ?? {}),
797
816
  },
798
817
  paper_bgcolor: bgColor,
799
818
  plot_bgcolor: bgColor,
800
- showlegend: preparedSeries.length > 1,
801
- margin: { l: 60, r: hasBubbleSecondaryAxis ? 60 : 20, t: 50, b: 50 },
819
+ showlegend: bubbleLegendOverrides.showlegend ?? preparedSeries.length > 1,
820
+ // The plot shrinks to leave the legend its own band (#1089).
821
+ margin: bubbleBand.margin,
802
822
  };
803
823
  setPlotData(traces);
804
824
  setLayout(bubbleLayout);
@@ -1076,20 +1096,43 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1076
1096
  });
1077
1097
  }
1078
1098
  const radarScale = indicators[0] ?? { min: 0, max: 1 };
1099
+ // The legend gets its own band so it never covers the circle (#1089).
1100
+ const radarLegendOverrides = applyLegendOverridesPlotly({ font: { color: textColor } }, spec.legend);
1101
+ const radarBand = reservePlotlyLegendBand({
1102
+ position: spec.legend?.position,
1103
+ visible: (radarLegendOverrides.showlegend ?? radarTraces.length > 1) !== false,
1104
+ labels: radarInputs.map((input) => input.name),
1105
+ legendFontSize: spec.legend?.textFormat?.fontSize ?? 12,
1106
+ width: Math.max(0, (width ?? 0) - 2),
1107
+ height: Math.max(0, (height ?? 0) - 2),
1108
+ hasTitle: Boolean(title),
1109
+ titleFontSize: spec.titleTextFormat?.fontSize ?? 16,
1110
+ hasBottomAxisTitle: false,
1111
+ hasLeftAxisTitle: false,
1112
+ leftAxisDepth: 50,
1113
+ rightAxisDepth: 50,
1114
+ axisFontSize: 12,
1115
+ });
1079
1116
  setPlotData(radarTraces);
1080
1117
  setLayout({
1081
1118
  title: { text: plotlyTitleText(title, spec.titleTextFormat) },
1082
1119
  font: { color: textColor },
1083
1120
  paper_bgcolor: bgColor,
1084
1121
  plot_bgcolor: bgColor,
1085
- showlegend: radarTraces.length > 1,
1122
+ showlegend: radarLegendOverrides.showlegend ?? radarTraces.length > 1,
1123
+ legend: { ...radarLegendOverrides.legend, ...(radarBand.legend ?? {}) },
1086
1124
  polar: {
1087
1125
  radialaxis: {
1088
1126
  visible: radarAxisOptions?.visible !== false,
1089
1127
  range: [radarScale.min, radarScale.max],
1090
1128
  },
1091
1129
  },
1092
- margin: { l: 50, r: 50, t: 50, b: 50 },
1130
+ margin: {
1131
+ l: Math.max(50, radarBand.margin.l),
1132
+ r: Math.max(50, radarBand.margin.r),
1133
+ t: Math.max(50, radarBand.margin.t),
1134
+ b: Math.max(50, radarBand.margin.b),
1135
+ },
1093
1136
  autosize: true,
1094
1137
  });
1095
1138
  return;
@@ -1705,62 +1748,34 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
1705
1748
  else if (effectiveChartType === "boxplot") {
1706
1749
  const rawValues = seriesValuesOf(seriesData);
1707
1750
  const quartileMethod = spec.quartileMethod;
1708
- const showOutliers = spec.showOutliers;
1751
+ const showOutliers = spec.showOutliers !== false;
1709
1752
  const showMeanMarker = spec.showMeanMarker === true;
1710
- // Plotly's box trace computes quartiles/fences itself from
1711
- // raw values, so a spec-pinned quartile method (or Excel's
1712
- // "no outliers → whiskers span min/max" semantics) requires
1713
- // switching to plotly's precomputed-statistics signature
1714
- // (q1/median/q3/lowerfence/upperfence arrays). When neither
1715
- // option is set the raw-values trace is kept unchanged.
1716
- const usePrecomputed = quartileMethod != null || showOutliers === false;
1717
- let trace;
1718
- if (usePrecomputed) {
1719
- const summary = computeBoxplotSummary(rawValues, {
1720
- quartileMethod,
1721
- });
1722
- if (!summary)
1723
- continue;
1724
- const hideOutliers = showOutliers === false;
1725
- trace = {
1726
- type: "box",
1727
- name: dataLabel,
1728
- // Precomputed boxes need an explicit category so each
1729
- // series lands on its own slot (raw mode derives it from
1730
- // the trace name).
1731
- x: [dataLabel || `trace ${currentSeriesIndex}`],
1732
- q1: [summary.q1],
1733
- median: [summary.median],
1734
- q3: [summary.q3],
1735
- // Excel: hiding outlier points extends the whiskers to
1736
- // the full min/max; with outliers shown they stop at the
1737
- // most extreme points inside the 1.5×IQR fences.
1738
- lowerfence: [hideOutliers ? summary.min : summary.lowerWhisker],
1739
- upperfence: [hideOutliers ? summary.max : summary.upperWhisker],
1740
- ...(hideOutliers
1741
- ? { boxpoints: false }
1742
- : {
1743
- // Sample points ride along so plotly can still draw
1744
- // the outlier dots against the precomputed fences.
1745
- y: [rawValues.filter((v) => v != null)],
1746
- boxpoints: "outliers",
1747
- }),
1748
- ...(showMeanMarker
1749
- ? { mean: [summary.mean], boxmean: true }
1750
- : {}),
1751
- };
1752
- }
1753
- else {
1754
- trace = {
1755
- type: "box",
1756
- y: rawValues.map((v) => (v ?? null)),
1757
- name: dataLabel,
1758
- // Raw mode is used when outliers are visible (today's
1759
- // default and `showOutliers: true` alike).
1760
- boxpoints: "outliers",
1761
- ...(showMeanMarker ? { boxmean: true } : {}),
1762
- };
1763
- }
1753
+ const showInnerPoints = spec.showInnerPoints === true;
1754
+ const showMeanLine = spec.showMeanLine === true;
1755
+ // A category domain groups the series by its values (one box per
1756
+ // category per series, `boxmode: "group"` in the layout); without
1757
+ // one each series is a box on its own slot (#1092). Plotly's box
1758
+ // trace computes the quartiles (Excel's inclusive / exclusive
1759
+ // methods are native), keeps the whiskers at the 1.5×IQR fences
1760
+ // and only hides the points when outliers are off.
1761
+ const boxDomainSource = spec.domains?.[0]?.sources?.[0];
1762
+ const boxCategories = boxDomainSource
1763
+ ? (getDomainValuesFromRange(boxDomainSource, spec.hiddenDimensionStrategy) ?? []).map((v) => (v === null || v === undefined ? "" : `${v}`.trim()))
1764
+ : [];
1765
+ const grouped = boxCategories.some((c) => c !== "");
1766
+ const values = rawValues.map((v) => (v ?? null));
1767
+ const trace = {
1768
+ type: "box",
1769
+ name: dataLabel,
1770
+ y: values,
1771
+ ...(grouped
1772
+ ? { x: values.map((_, i) => boxCategories[i] ?? "") }
1773
+ : {}),
1774
+ ...(quartileMethod ? { quartilemethod: quartileMethod } : {}),
1775
+ boxpoints: showInnerPoints ? "all" : showOutliers ? "outliers" : false,
1776
+ ...(showInnerPoints ? { jitter: 0, pointpos: 0 } : {}),
1777
+ ...(showMeanMarker || showMeanLine ? { boxmean: true } : {}),
1778
+ };
1764
1779
  applyTraceColor(trace, series.color ?? undefined, {
1765
1780
  noFill: series.noFill,
1766
1781
  dataPointColors: series.dataPointColors,
@@ -2171,6 +2186,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
2171
2186
  ? // Excel's histogram columns touch by default (#1087).
2172
2187
  { bargap: 0 }
2173
2188
  : {}),
2189
+ // Box & whisker: boxes grouped by category, `gapWidth` as for columns
2190
+ // (#1092).
2191
+ ...(spec.chartType === "boxplot"
2192
+ ? {
2193
+ boxmode: "group",
2194
+ boxgap: Math.max(0, Math.min(0.95, (typeof spec.gapWidth === "number" ? spec.gapWidth : 150) / 400)),
2195
+ }
2196
+ : {}),
2174
2197
  font: {
2175
2198
  family: "Untitled Sans, apple-system, sans-serif",
2176
2199
  size: 12,
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,KACvC,MAmEF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;;;;;;;;CAgBzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SA0BX,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAiBjE,CAAC"}
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../plotly/basic-chart/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D,eAAO,MAAM,qBAAqB,GAChC,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,KACvC,MAmEF,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC9B,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC;;;;;;;;;CAgBzC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,QAAQ,MAAM,GAAG,SAAS,GAAG,IAAI,KAChC,MAAM,GAAG,SA0BX,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC,WAAW,CAAC,YAmBjE,CAAC"}
@@ -103,6 +103,8 @@ export const showXAxis = (type) => {
103
103
  type === "histogram" ||
104
104
  // A waterfall's steps are categories (Opening, Sales, …) (#1070).
105
105
  type === "waterfall" ||
106
+ // A box & whisker's slots are categories (or the series names) (#1092).
107
+ type === "boxplot" ||
106
108
  // Stock is line-derived on a category x-axis. Radar is polar and
107
109
  // does not use the cartesian x-axis.
108
110
  type === "stock") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowsncolumns/charts",
3
- "version": "28.1.12",
3
+ "version": "28.1.13",
4
4
  "author": "Vinay M",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "main": "./dist/cjs/index.js",
@@ -70,7 +70,7 @@
70
70
  "@rowsncolumns/common-types": ">=5.3.2",
71
71
  "@rowsncolumns/grid": ">=13.2.35",
72
72
  "@rowsncolumns/icons": ">=7.2.33",
73
- "@rowsncolumns/spreadsheet": ">=10.4.10",
73
+ "@rowsncolumns/spreadsheet": ">=10.4.11",
74
74
  "@rowsncolumns/spreadsheet-state": ">=29.1.8",
75
75
  "@rowsncolumns/toolkit": ">=24.5.10",
76
76
  "@rowsncolumns/ui": ">=16.0.4",