@rowsncolumns/charts 28.1.7 → 28.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/basic-chart/basic-chart.js +525 -419
- package/dist/cjs/basic-chart/helpers.d.ts.map +1 -1
- package/dist/cjs/basic-chart/helpers.js +2 -0
- package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/plotly/basic-chart/basic-chart.js +351 -192
- package/dist/cjs/plotly/basic-chart/helpers.d.ts.map +1 -1
- package/dist/cjs/plotly/basic-chart/helpers.js +2 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/utils/bubble.d.ts +34 -1
- package/dist/cjs/utils/bubble.d.ts.map +1 -1
- package/dist/cjs/utils/bubble.js +39 -13
- package/dist/cjs/utils/doughnut.d.ts +19 -0
- package/dist/cjs/utils/doughnut.d.ts.map +1 -0
- package/dist/cjs/utils/doughnut.js +28 -0
- package/dist/cjs/utils/echarts-overlays.d.ts +46 -0
- package/dist/cjs/utils/echarts-overlays.d.ts.map +1 -0
- package/dist/cjs/utils/echarts-overlays.js +134 -0
- package/dist/cjs/utils/hierarchy.d.ts +23 -1
- package/dist/cjs/utils/hierarchy.d.ts.map +1 -1
- package/dist/cjs/utils/hierarchy.js +179 -55
- package/dist/cjs/utils/overrides.d.ts +9 -0
- package/dist/cjs/utils/overrides.d.ts.map +1 -1
- package/dist/cjs/utils/overrides.js +18 -3
- package/dist/cjs/utils/radar.d.ts +31 -5
- package/dist/cjs/utils/radar.d.ts.map +1 -1
- package/dist/cjs/utils/radar.js +67 -27
- package/dist/cjs/utils/trendline.d.ts +7 -1
- package/dist/cjs/utils/trendline.d.ts.map +1 -1
- package/dist/cjs/utils/trendline.js +82 -18
- package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/basic-chart/basic-chart.js +376 -248
- package/dist/esm/basic-chart/helpers.d.ts.map +1 -1
- package/dist/esm/basic-chart/helpers.js +2 -0
- package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/plotly/basic-chart/basic-chart.js +279 -69
- package/dist/esm/plotly/basic-chart/helpers.d.ts.map +1 -1
- package/dist/esm/plotly/basic-chart/helpers.js +2 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/utils/bubble.d.ts +34 -1
- package/dist/esm/utils/bubble.d.ts.map +1 -1
- package/dist/esm/utils/bubble.js +35 -11
- package/dist/esm/utils/doughnut.d.ts +19 -0
- package/dist/esm/utils/doughnut.d.ts.map +1 -0
- package/dist/esm/utils/doughnut.js +21 -0
- package/dist/esm/utils/echarts-overlays.d.ts +46 -0
- package/dist/esm/utils/echarts-overlays.d.ts.map +1 -0
- package/dist/esm/utils/echarts-overlays.js +135 -0
- package/dist/esm/utils/hierarchy.d.ts +23 -1
- package/dist/esm/utils/hierarchy.d.ts.map +1 -1
- package/dist/esm/utils/hierarchy.js +168 -50
- package/dist/esm/utils/overrides.d.ts +9 -0
- package/dist/esm/utils/overrides.d.ts.map +1 -1
- package/dist/esm/utils/overrides.js +16 -2
- package/dist/esm/utils/radar.d.ts +31 -5
- package/dist/esm/utils/radar.d.ts.map +1 -1
- package/dist/esm/utils/radar.js +44 -16
- package/dist/esm/utils/trendline.d.ts +7 -1
- package/dist/esm/utils/trendline.d.ts.map +1 -1
- package/dist/esm/utils/trendline.js +67 -14
- package/package.json +4 -4
|
@@ -18,16 +18,17 @@ import { coerceCalculatedLabel, getChartDataRanges, } from "../utils/chart-spec-
|
|
|
18
18
|
import { canSwitchRowColumn, cellLabelGetterFromRangeReader, switchRowColumn, } from "../utils/chart-orientation";
|
|
19
19
|
import { dataUrlToBlob } from "../utils/chart-image";
|
|
20
20
|
import { getChartPalette } from "../utils/chart-styles";
|
|
21
|
+
import { buildEchartsOverlaySeries } from "../utils/echarts-overlays";
|
|
21
22
|
import { applyEmptyCellsPolicy, connectsAcrossEmptyCells, isolatedPointIndexes, isLineLikeChartType, } from "../utils/empty-cells";
|
|
22
23
|
import { getEchartsGlState, loadEchartsGl, } from "../utils/echarts-gl-loader";
|
|
23
|
-
import { computeErrorBars, resolveCustomErrorBarValues, } from "../utils/error-bars";
|
|
24
24
|
import { echartsGroupedCategoryAxes, resolveCategoryLevels, } from "../utils/category-levels";
|
|
25
25
|
import { gradientToEcharts } from "../utils/gradient";
|
|
26
26
|
import { buildHierarchyTree, extractHierarchyRows, getHierarchyHeaderTitle, } from "../utils/hierarchy";
|
|
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 {
|
|
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";
|
|
@@ -35,7 +36,6 @@ import { STOCK_ROLES, toCandlestick, toLineSeries, toVolumeSeries, } from "../ut
|
|
|
35
36
|
import { explosionOffsetPx, pieRadiusPx, resolveExplosion, } from "../utils/pie-explosion";
|
|
36
37
|
import { chartSeriesId, chartSeriesSpecIndex, resolveClickedDataIndex, resolveClickedSeriesIndex, } from "../utils/series-click";
|
|
37
38
|
import { buildBar3DData, buildContourData, buildLine3DSeries, buildSurfaceData, getCategoryCount, getValueExtent, isContourSpec, resolve3DRenderKind, SURFACE_COLOR_RAMP, } from "../utils/three-d";
|
|
38
|
-
import { computeTrendline } from "../utils/trendline";
|
|
39
39
|
import { computeWaterfall } from "../utils/waterfall";
|
|
40
40
|
import { isWebGLAvailable } from "../utils/webgl";
|
|
41
41
|
import { chartTypeToEchartsType, circularLegendEntries, getEchartsSeriesStyle, isCircularChartType, mapMarkerSymbolToEcharts, showXAxis, } from "./helpers";
|
|
@@ -102,29 +102,41 @@ const HIERARCHY_CHART_TYPES = new Set([
|
|
|
102
102
|
const isBasicChartSpec = (spec) => BASIC_CHART_TYPES.has(spec.chartType);
|
|
103
103
|
const isBubbleChartSpec = (spec) => BUBBLE_CHART_TYPES.has(spec.chartType);
|
|
104
104
|
const isHierarchyChartSpec = (spec) => HIERARCHY_CHART_TYPES.has(spec.chartType);
|
|
105
|
+
// Every level keeps the colour its node was given (one solid colour per
|
|
106
|
+
// top-level category, like Excel — no per-level saturation ramp); thin
|
|
107
|
+
// white gaps separate the cells.
|
|
105
108
|
const HIERARCHY_LEVEL_STYLES = [
|
|
106
109
|
{
|
|
107
110
|
itemStyle: {
|
|
108
|
-
gapWidth:
|
|
109
|
-
|
|
111
|
+
gapWidth: 2,
|
|
112
|
+
borderColor: "#ffffff",
|
|
113
|
+
borderWidth: 0,
|
|
110
114
|
},
|
|
111
115
|
upperLabel: { show: false },
|
|
112
116
|
},
|
|
113
117
|
{
|
|
114
|
-
colorSaturation: [0.35, 0.65],
|
|
115
|
-
itemStyle: {
|
|
116
|
-
gapWidth: 1,
|
|
117
|
-
borderColorSaturation: 0.6,
|
|
118
|
-
},
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
colorSaturation: [0.45, 0.75],
|
|
122
118
|
itemStyle: {
|
|
123
119
|
gapWidth: 1,
|
|
124
|
-
|
|
120
|
+
borderColor: "#ffffff",
|
|
121
|
+
borderWidth: 1,
|
|
125
122
|
},
|
|
126
123
|
},
|
|
127
124
|
];
|
|
125
|
+
/**
|
|
126
|
+
* Colour a hierarchy like Excel: each top-level node takes the next
|
|
127
|
+
* palette colour and every descendant inherits it (#1072).
|
|
128
|
+
*/
|
|
129
|
+
const colorHierarchyNodes = (nodes, palette) => nodes.map((node, index) => {
|
|
130
|
+
const color = chartPaletteColorAt(palette, index);
|
|
131
|
+
const paint = (n) => ({
|
|
132
|
+
...n,
|
|
133
|
+
itemStyle: { color },
|
|
134
|
+
...(n.children ? { children: n.children.map(paint) } : {}),
|
|
135
|
+
});
|
|
136
|
+
return paint(node);
|
|
137
|
+
});
|
|
138
|
+
/** Sunburst rings: Excel leaves the centre empty, so even the first level is a ring (#1073). */
|
|
139
|
+
const SUNBURST_RADIUS = ["15%", "80%"];
|
|
128
140
|
const getHierarchyDepth = (nodes, depth = 1) => {
|
|
129
141
|
let maxDepth = depth;
|
|
130
142
|
nodes.forEach((node) => {
|
|
@@ -175,6 +187,16 @@ const buildHierarchyLevels = (maxDepth) => {
|
|
|
175
187
|
};
|
|
176
188
|
/** Series whose tooltip value is one number on a value axis. */
|
|
177
189
|
const VALUE_TOOLTIP_SERIES_TYPES = new Set(["bar", "line", "scatter", "funnel"]);
|
|
190
|
+
/** The transparent spacer bars a waterfall stacks its visible bars on. */
|
|
191
|
+
const WATERFALL_PLACEHOLDER_ID = "rnc-waterfall-placeholder";
|
|
192
|
+
/** The invisible funnel that carries the stage names beside the bars. */
|
|
193
|
+
const FUNNEL_NAMES_ID = "rnc-funnel-names";
|
|
194
|
+
/**
|
|
195
|
+
* Layout-only series: they carry no data of their own, so they get no
|
|
196
|
+
* data labels and no tooltip entry (#1070 labelled every waterfall bar
|
|
197
|
+
* with its spacer's height too).
|
|
198
|
+
*/
|
|
199
|
+
const DECORATION_SERIES_IDS = new Set([WATERFALL_PLACEHOLDER_ID, FUNNEL_NAMES_ID]);
|
|
178
200
|
const defaultLegendConfig = {
|
|
179
201
|
padding: 10,
|
|
180
202
|
itemWidth: 10,
|
|
@@ -555,7 +577,38 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
555
577
|
sizeValues: sharedSizeValues,
|
|
556
578
|
series: seriesInputs,
|
|
557
579
|
});
|
|
558
|
-
|
|
580
|
+
// Bubble diameters scale from zero (area by default, width when
|
|
581
|
+
// `sizeRepresents` says so) and the largest bubble is a fraction of
|
|
582
|
+
// the plot area — the grid keeps ~85% of the width and ~75% of the
|
|
583
|
+
// height for the plot (#1068).
|
|
584
|
+
const sizeScale = createBubbleSizeScale(sizeValues, {
|
|
585
|
+
sizeRepresents: spec.sizeRepresents,
|
|
586
|
+
bubbleScale: spec.bubbleScale,
|
|
587
|
+
plotMinSide: Math.min((width ?? 0) * 0.85, (height ?? 0) * 0.75),
|
|
588
|
+
});
|
|
589
|
+
const bubblePalette = getChartPalette(spec, [
|
|
590
|
+
theme.themeColors.accent1,
|
|
591
|
+
theme.themeColors.accent2,
|
|
592
|
+
theme.themeColors.accent3,
|
|
593
|
+
theme.themeColors.accent4,
|
|
594
|
+
theme.themeColors.accent5,
|
|
595
|
+
theme.themeColors.accent6,
|
|
596
|
+
]);
|
|
597
|
+
// Per-point colours (`<c:dPt>`) key on the point's source row, which
|
|
598
|
+
// `BubbleSeriesPoint.index` keeps (points without a Y are skipped).
|
|
599
|
+
const bubbleItemColor = (seriesEntry, index) => {
|
|
600
|
+
const base = seriesEntry?.color ?? chartPaletteColorAt(bubblePalette, index);
|
|
601
|
+
const overrides = new Map();
|
|
602
|
+
for (const dp of seriesEntry?.dataPointColors ?? []) {
|
|
603
|
+
if (typeof dp?.idx === "number" && typeof dp?.color === "string") {
|
|
604
|
+
overrides.set(dp.idx, dp.color);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return overrides.size
|
|
608
|
+
? (params) => overrides.get(params?.data?.index) ?? base
|
|
609
|
+
: base;
|
|
610
|
+
};
|
|
611
|
+
const hasBubbleSecondaryAxis = (spec.series ?? []).some((seriesEntry) => seriesEntry?.useSecondaryAxis);
|
|
559
612
|
const xAxisConfig = categories.length > 0
|
|
560
613
|
? {
|
|
561
614
|
type: "category",
|
|
@@ -581,8 +634,105 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
581
634
|
};
|
|
582
635
|
// Spec-level overrides — axis min/max/visibility/textFormat, legend
|
|
583
636
|
// position/visibility, dataLabel toggles. Same helpers as the
|
|
584
|
-
// non-bubble layout below.
|
|
585
|
-
|
|
637
|
+
// non-bubble layout below. A series' own `dataLabels` replace the
|
|
638
|
+
// chart-level ones for it (#1059).
|
|
639
|
+
const bubbleSeriesLabelFor = (index) => buildSeriesLabelOption(resolveSeriesDataLabels(spec.dataLabels, spec.series?.[index]), undefined, locale);
|
|
640
|
+
// Scatter vs bubble — scatter uses small fixed markers (optionally
|
|
641
|
+
// connected with a line per scatterStyle); bubble applies a size
|
|
642
|
+
// scale on the 3rd dimension. Pre-fix scatter routed through the
|
|
643
|
+
// bubble code, producing Y-scaled giant circles instead of Excel's
|
|
644
|
+
// small dots.
|
|
645
|
+
const bubbleSeries = preparedSeries
|
|
646
|
+
.map((entry, index) => {
|
|
647
|
+
const isScatter = spec.chartType === "scatter";
|
|
648
|
+
const scatterStyle = spec.scatterStyle ?? (isScatter ? "lineMarker" : undefined);
|
|
649
|
+
const seriesEntry = spec.series?.[index];
|
|
650
|
+
const seriesColor = seriesEntry?.color;
|
|
651
|
+
const perMarker = seriesEntry?.marker;
|
|
652
|
+
const itemColor = bubbleItemColor(seriesEntry, index);
|
|
653
|
+
const onSecondary = seriesEntry?.useSecondaryAxis ? { yAxisIndex: 1 } : {};
|
|
654
|
+
if (isScatter) {
|
|
655
|
+
const symbol = perMarker?.symbol;
|
|
656
|
+
const noMarkers = symbol === "none";
|
|
657
|
+
const wantsLine = scatterStyle === "line" ||
|
|
658
|
+
scatterStyle === "lineMarker" ||
|
|
659
|
+
scatterStyle === "smooth" ||
|
|
660
|
+
scatterStyle === "smoothMarker";
|
|
661
|
+
const wantsMarkers = !noMarkers &&
|
|
662
|
+
(scatterStyle === "marker" ||
|
|
663
|
+
scatterStyle === "lineMarker" ||
|
|
664
|
+
scatterStyle === "smoothMarker" ||
|
|
665
|
+
!scatterStyle);
|
|
666
|
+
const smooth = scatterStyle === "smooth" || scatterStyle === "smoothMarker";
|
|
667
|
+
// Labels hang off the point symbols: a marker-less line keeps
|
|
668
|
+
// an invisible 0-size symbol per point when they show (#1049).
|
|
669
|
+
const labelsWithoutMarkers = !wantsMarkers &&
|
|
670
|
+
Boolean(resolveSeriesDataLabels(spec.dataLabels, seriesEntry)
|
|
671
|
+
?.show);
|
|
672
|
+
// The series colour paints the line; the markers take the
|
|
673
|
+
// per-point overrides when the series has any.
|
|
674
|
+
const markerStyle = seriesColor || typeof itemColor === "function"
|
|
675
|
+
? { itemStyle: { color: itemColor } }
|
|
676
|
+
: {};
|
|
677
|
+
return wantsLine
|
|
678
|
+
? {
|
|
679
|
+
type: "line",
|
|
680
|
+
name: entry.name,
|
|
681
|
+
data: entry.points,
|
|
682
|
+
showSymbol: wantsMarkers || labelsWithoutMarkers,
|
|
683
|
+
smooth,
|
|
684
|
+
symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
|
|
685
|
+
symbolSize: labelsWithoutMarkers ? 0 : perMarker?.size ?? 6,
|
|
686
|
+
...markerStyle,
|
|
687
|
+
...(seriesColor ? { lineStyle: { color: seriesColor } } : {}),
|
|
688
|
+
...onSecondary,
|
|
689
|
+
}
|
|
690
|
+
: {
|
|
691
|
+
type: "scatter",
|
|
692
|
+
name: entry.name,
|
|
693
|
+
data: entry.points,
|
|
694
|
+
symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
|
|
695
|
+
symbolSize: perMarker?.size ?? 6,
|
|
696
|
+
...markerStyle,
|
|
697
|
+
...onSecondary,
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
type: chartType,
|
|
702
|
+
name: entry.name,
|
|
703
|
+
data: entry.points,
|
|
704
|
+
symbolSize: (data) => sizeScale(data?.[2] ?? 0),
|
|
705
|
+
label: {
|
|
706
|
+
show: true,
|
|
707
|
+
formatter: (params) => params.name ?? "",
|
|
708
|
+
},
|
|
709
|
+
...(seriesColor || typeof itemColor === "function"
|
|
710
|
+
? { itemStyle: { color: itemColor } }
|
|
711
|
+
: {}),
|
|
712
|
+
...onSecondary,
|
|
713
|
+
};
|
|
714
|
+
})
|
|
715
|
+
.map((s, index) => {
|
|
716
|
+
const bubbleSeriesLabel = bubbleSeriesLabelFor(index);
|
|
717
|
+
return bubbleSeriesLabel
|
|
718
|
+
? { ...s, label: { ...(s.label ?? {}), ...bubbleSeriesLabel } }
|
|
719
|
+
: s;
|
|
720
|
+
});
|
|
721
|
+
// Trendlines and error bars on the real (x, y) points: the fit is a
|
|
722
|
+
// regression of Y on X, not on the point index (#1067).
|
|
723
|
+
const bubbleOverlays = preparedSeries.flatMap((entry, index) => {
|
|
724
|
+
const seriesEntry = spec.series?.[index];
|
|
725
|
+
if (!seriesEntry)
|
|
726
|
+
return [];
|
|
727
|
+
return buildEchartsOverlaySeries({
|
|
728
|
+
series: seriesEntry,
|
|
729
|
+
values: entry.points.map((point) => point.value[1]),
|
|
730
|
+
xValues: entry.points.map((point) => point.value[0]),
|
|
731
|
+
dataLabel: entry.name,
|
|
732
|
+
seriesColor: seriesEntry.color ?? chartPaletteColorAt(bubblePalette, index),
|
|
733
|
+
resolveRange: (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy),
|
|
734
|
+
});
|
|
735
|
+
});
|
|
586
736
|
chartRef.current?.setOption({
|
|
587
737
|
xAxis: [
|
|
588
738
|
applyAxisOverrides({ ...xAxisConfig }, spec.categoryAxisOptions),
|
|
@@ -594,6 +744,19 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
594
744
|
nameLocation: "middle",
|
|
595
745
|
nameGap: 45,
|
|
596
746
|
}, spec.valueAxisOptions, undefined, locale),
|
|
747
|
+
// Secondary value axis on the right for the series that opt in
|
|
748
|
+
// (`useSecondaryAxis`), as the category layout has (#1067).
|
|
749
|
+
...(hasBubbleSecondaryAxis
|
|
750
|
+
? [
|
|
751
|
+
applyAxisOverrides({
|
|
752
|
+
type: "value",
|
|
753
|
+
position: "right",
|
|
754
|
+
nameLocation: "middle",
|
|
755
|
+
nameGap: 45,
|
|
756
|
+
splitLine: { show: false },
|
|
757
|
+
}, spec.secondaryValueAxisOptions, undefined, locale),
|
|
758
|
+
]
|
|
759
|
+
: []),
|
|
597
760
|
],
|
|
598
761
|
legend: applyLegendOverrides({
|
|
599
762
|
...legend,
|
|
@@ -610,77 +773,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
610
773
|
trigger: "item",
|
|
611
774
|
formatter: tooltipFormatter,
|
|
612
775
|
},
|
|
613
|
-
|
|
614
|
-
// (optionally connected with a line per scatterStyle); bubble
|
|
615
|
-
// applies a size scale on the 3rd dimension. Pre-fix scatter
|
|
616
|
-
// routed through the bubble code, producing Y-scaled giant
|
|
617
|
-
// circles instead of Excel's small dots.
|
|
618
|
-
series: preparedSeries.map((entry, index) => {
|
|
619
|
-
const isScatter = spec.chartType === "scatter";
|
|
620
|
-
const scatterStyle = spec.scatterStyle ??
|
|
621
|
-
(isScatter ? "lineMarker" : undefined);
|
|
622
|
-
const seriesEntry = spec.series?.[index];
|
|
623
|
-
const seriesColor = seriesEntry?.color;
|
|
624
|
-
const perMarker = seriesEntry?.marker;
|
|
625
|
-
if (isScatter) {
|
|
626
|
-
const symbol = perMarker?.symbol;
|
|
627
|
-
const noMarkers = symbol === "none";
|
|
628
|
-
const wantsLine = scatterStyle === "line" ||
|
|
629
|
-
scatterStyle === "lineMarker" ||
|
|
630
|
-
scatterStyle === "smooth" ||
|
|
631
|
-
scatterStyle === "smoothMarker";
|
|
632
|
-
const wantsMarkers = !noMarkers &&
|
|
633
|
-
(scatterStyle === "marker" ||
|
|
634
|
-
scatterStyle === "lineMarker" ||
|
|
635
|
-
scatterStyle === "smoothMarker" ||
|
|
636
|
-
!scatterStyle);
|
|
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);
|
|
641
|
-
return wantsLine
|
|
642
|
-
? {
|
|
643
|
-
type: "line",
|
|
644
|
-
name: entry.name,
|
|
645
|
-
data: entry.points,
|
|
646
|
-
showSymbol: wantsMarkers || labelsWithoutMarkers,
|
|
647
|
-
smooth,
|
|
648
|
-
symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
|
|
649
|
-
symbolSize: labelsWithoutMarkers
|
|
650
|
-
? 0
|
|
651
|
-
: perMarker?.size ?? 6,
|
|
652
|
-
...(seriesColor
|
|
653
|
-
? {
|
|
654
|
-
itemStyle: { color: seriesColor },
|
|
655
|
-
lineStyle: { color: seriesColor },
|
|
656
|
-
}
|
|
657
|
-
: {}),
|
|
658
|
-
}
|
|
659
|
-
: {
|
|
660
|
-
type: "scatter",
|
|
661
|
-
name: entry.name,
|
|
662
|
-
data: entry.points,
|
|
663
|
-
symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
|
|
664
|
-
symbolSize: perMarker?.size ?? 6,
|
|
665
|
-
...(seriesColor
|
|
666
|
-
? { itemStyle: { color: seriesColor } }
|
|
667
|
-
: {}),
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
return {
|
|
671
|
-
type: chartType,
|
|
672
|
-
name: entry.name,
|
|
673
|
-
data: entry.points,
|
|
674
|
-
symbolSize: (data) => sizeScale(data?.[2] ?? 0),
|
|
675
|
-
label: {
|
|
676
|
-
show: true,
|
|
677
|
-
formatter: (params) => params.name ?? "",
|
|
678
|
-
},
|
|
679
|
-
...(seriesColor ? { itemStyle: { color: seriesColor } } : {}),
|
|
680
|
-
};
|
|
681
|
-
}).map((s) => bubbleSeriesLabel
|
|
682
|
-
? { ...s, label: { ...(s.label ?? {}), ...bubbleSeriesLabel } }
|
|
683
|
-
: s),
|
|
776
|
+
series: [...bubbleSeries, ...bubbleOverlays],
|
|
684
777
|
}, {
|
|
685
778
|
replaceMerge: [
|
|
686
779
|
"series",
|
|
@@ -692,7 +785,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
692
785
|
],
|
|
693
786
|
});
|
|
694
787
|
previousChartType.current = spec.chartType;
|
|
695
|
-
previousSeriesCount.current =
|
|
788
|
+
previousSeriesCount.current = bubbleSeries.length + bubbleOverlays.length;
|
|
696
789
|
return;
|
|
697
790
|
}
|
|
698
791
|
if (isHierarchy) {
|
|
@@ -707,8 +800,17 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
707
800
|
hiddenDimensionStrategy: spec.hiddenDimensionStrategy,
|
|
708
801
|
});
|
|
709
802
|
hierarchyTitleRef.current = explicitChartTitle ?? headerTitle;
|
|
710
|
-
const
|
|
803
|
+
const hierarchyPalette = getChartPalette(spec, [
|
|
804
|
+
theme.themeColors.accent1,
|
|
805
|
+
theme.themeColors.accent2,
|
|
806
|
+
theme.themeColors.accent3,
|
|
807
|
+
theme.themeColors.accent4,
|
|
808
|
+
theme.themeColors.accent5,
|
|
809
|
+
theme.themeColors.accent6,
|
|
810
|
+
]);
|
|
811
|
+
const hierarchyData = colorHierarchyNodes(buildHierarchyTree(hierarchyRows), hierarchyPalette);
|
|
711
812
|
const rootName = hierarchyData.length === 1 ? hierarchyData[0]?.name ?? "" : "";
|
|
813
|
+
const drillDown = Boolean(spec.drillDown);
|
|
712
814
|
const seriesDisplayName = explicitChartTitle ?? headerTitle ?? rootName ?? "";
|
|
713
815
|
const hierarchyDepth = getHierarchyDepth(hierarchyData);
|
|
714
816
|
const levels = buildHierarchyLevels(hierarchyDepth);
|
|
@@ -761,6 +863,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
761
863
|
label: {
|
|
762
864
|
show: true,
|
|
763
865
|
formatter: "{b}",
|
|
866
|
+
...(spec.chartType === "treemap"
|
|
867
|
+
? { position: "insideBottomLeft" }
|
|
868
|
+
: {}),
|
|
764
869
|
...(hierarchySeriesLabel ?? {}),
|
|
765
870
|
},
|
|
766
871
|
upperLabel: spec.chartType === "treemap"
|
|
@@ -770,7 +875,16 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
770
875
|
}
|
|
771
876
|
: undefined,
|
|
772
877
|
levels,
|
|
773
|
-
|
|
878
|
+
// Excel has no drill-down on either chart: zooming into a
|
|
879
|
+
// branch on click (and the treemap breadcrumb) is opt-in.
|
|
880
|
+
nodeClick: drillDown
|
|
881
|
+
? spec.chartType === "treemap"
|
|
882
|
+
? "zoomToNode"
|
|
883
|
+
: "rootToNode"
|
|
884
|
+
: false,
|
|
885
|
+
...(spec.chartType === "treemap"
|
|
886
|
+
? { breadcrumb: { show: drillDown } }
|
|
887
|
+
: { radius: [...SUNBURST_RADIUS] }),
|
|
774
888
|
},
|
|
775
889
|
],
|
|
776
890
|
}, {
|
|
@@ -1142,7 +1256,10 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1142
1256
|
const values = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
|
|
1143
1257
|
radarSeriesInputs.push({ name: label, values });
|
|
1144
1258
|
}
|
|
1145
|
-
|
|
1259
|
+
// One value scale for every axis (Excel), from the spec's value-axis
|
|
1260
|
+
// min / max when set (#1066).
|
|
1261
|
+
const radarAxisOptions = spec.valueAxisOptions;
|
|
1262
|
+
const indicators = buildRadarIndicators(categories, radarSeriesInputs, radarAxisOptions);
|
|
1146
1263
|
const radarData = buildRadarSeriesData(radarSeriesInputs, indicators.length);
|
|
1147
1264
|
const radarStyle = spec.radarStyle ?? "standard";
|
|
1148
1265
|
const radarColors = radarSeriesInputs.map((_, i) => spec.series?.[i]?.color);
|
|
@@ -1166,9 +1283,23 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1166
1283
|
};
|
|
1167
1284
|
chartRef.current?.setOption({
|
|
1168
1285
|
radar: {
|
|
1169
|
-
|
|
1286
|
+
// The common scale's tick labels along ONE axis, as Excel draws
|
|
1287
|
+
// them: echarts labels every indicator axis, so each indicator
|
|
1288
|
+
// carries its own `axisLabel.show` (an indicator's own options win
|
|
1289
|
+
// over the radar-level ones in echarts' merge) and only the first
|
|
1290
|
+
// shows (#1066 follow-up); hidden with the spec's value axis.
|
|
1291
|
+
indicator: (indicators.length
|
|
1170
1292
|
? indicators
|
|
1171
|
-
: [{ name: "", max: 1 }],
|
|
1293
|
+
: [{ name: "", min: 0, max: 1 }]).map((indicator, index) => ({
|
|
1294
|
+
...indicator,
|
|
1295
|
+
axisLabel: {
|
|
1296
|
+
show: index === 0 && radarAxisOptions?.visible !== false,
|
|
1297
|
+
},
|
|
1298
|
+
})),
|
|
1299
|
+
axisLabel: {
|
|
1300
|
+
show: radarAxisOptions?.visible !== false,
|
|
1301
|
+
formatter: (value) => formatAxisValue(radarFormats[0], value, locale),
|
|
1302
|
+
},
|
|
1172
1303
|
},
|
|
1173
1304
|
legend: applyLegendOverrides({
|
|
1174
1305
|
...legend,
|
|
@@ -1532,135 +1663,15 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1532
1663
|
: [];
|
|
1533
1664
|
if (numericValues.length === 0)
|
|
1534
1665
|
return;
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
forecastBackward: tl.forecastBackward,
|
|
1545
|
-
});
|
|
1546
|
-
if (!result || result.points.length < 2)
|
|
1547
|
-
continue;
|
|
1548
|
-
const tlName = tl.name ??
|
|
1549
|
-
`${dataLabel ? dataLabel + " " : ""}${tl.type ?? "linear"} trend`;
|
|
1550
|
-
chartSeries.push({
|
|
1551
|
-
type: "line",
|
|
1552
|
-
name: tlName,
|
|
1553
|
-
// Index x lets the trendline overlay the category axis even
|
|
1554
|
-
// when forecasting extends beyond the data range.
|
|
1555
|
-
data: result.points.map((p) => [p.x, p.y]),
|
|
1556
|
-
showSymbol: false,
|
|
1557
|
-
smooth: tl.type === "poly",
|
|
1558
|
-
lineStyle: {
|
|
1559
|
-
color: tl.color ?? seriesColor,
|
|
1560
|
-
type: "dashed",
|
|
1561
|
-
width: 2,
|
|
1562
|
-
},
|
|
1563
|
-
itemStyle: { color: tl.color ?? seriesColor },
|
|
1564
|
-
tooltip: { show: false },
|
|
1565
|
-
z: 5,
|
|
1566
|
-
...useSecondary,
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
// Error bars — drawn as a custom series of vertical/horizontal
|
|
1570
|
-
// whiskers. valueType "custom" reads per-point magnitudes from the
|
|
1571
|
-
// spec's plus/minus ranges, resolved through the same extractor
|
|
1572
|
-
// the series values use.
|
|
1573
|
-
const magnitudes = computeErrorBars(numericValues, resolveCustomErrorBarValues(series.errorBars, (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy)));
|
|
1574
|
-
if (magnitudes) {
|
|
1575
|
-
const direction = series.errorBars?.direction ?? "y";
|
|
1576
|
-
const noEndCap = series.errorBars?.noEndCap ?? false;
|
|
1577
|
-
const errorData = numericValues.map((v, i) => [
|
|
1578
|
-
i,
|
|
1579
|
-
typeof v === "number" ? v : 0,
|
|
1580
|
-
magnitudes.plus[i] ?? 0,
|
|
1581
|
-
magnitudes.minus[i] ?? 0,
|
|
1582
|
-
]);
|
|
1583
|
-
chartSeries.push({
|
|
1584
|
-
type: "custom",
|
|
1585
|
-
name: `${dataLabel ? dataLabel + " " : ""}error`,
|
|
1586
|
-
data: errorData,
|
|
1587
|
-
z: 6,
|
|
1588
|
-
tooltip: { show: false },
|
|
1589
|
-
...useSecondary,
|
|
1590
|
-
renderItem: (_params, api) => {
|
|
1591
|
-
const value = api.value(1);
|
|
1592
|
-
const plus = api.value(2);
|
|
1593
|
-
const minus = api.value(3);
|
|
1594
|
-
const capHalf = 5;
|
|
1595
|
-
if (direction === "x") {
|
|
1596
|
-
const center = api.coord([value, api.value(0)]);
|
|
1597
|
-
const hi = api.coord([value + plus, api.value(0)]);
|
|
1598
|
-
const lo = api.coord([value - minus, api.value(0)]);
|
|
1599
|
-
const children = [
|
|
1600
|
-
{
|
|
1601
|
-
type: "line",
|
|
1602
|
-
shape: { x1: lo[0], y1: center[1], x2: hi[0], y2: center[1] },
|
|
1603
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1604
|
-
},
|
|
1605
|
-
];
|
|
1606
|
-
if (!noEndCap) {
|
|
1607
|
-
children.push({
|
|
1608
|
-
type: "line",
|
|
1609
|
-
shape: {
|
|
1610
|
-
x1: hi[0],
|
|
1611
|
-
y1: center[1] - capHalf,
|
|
1612
|
-
x2: hi[0],
|
|
1613
|
-
y2: center[1] + capHalf,
|
|
1614
|
-
},
|
|
1615
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1616
|
-
}, {
|
|
1617
|
-
type: "line",
|
|
1618
|
-
shape: {
|
|
1619
|
-
x1: lo[0],
|
|
1620
|
-
y1: center[1] - capHalf,
|
|
1621
|
-
x2: lo[0],
|
|
1622
|
-
y2: center[1] + capHalf,
|
|
1623
|
-
},
|
|
1624
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1625
|
-
});
|
|
1626
|
-
}
|
|
1627
|
-
return { type: "group", children };
|
|
1628
|
-
}
|
|
1629
|
-
const center = api.coord([api.value(0), value]);
|
|
1630
|
-
const hi = api.coord([api.value(0), value + plus]);
|
|
1631
|
-
const lo = api.coord([api.value(0), value - minus]);
|
|
1632
|
-
const children = [
|
|
1633
|
-
{
|
|
1634
|
-
type: "line",
|
|
1635
|
-
shape: { x1: center[0], y1: lo[1], x2: center[0], y2: hi[1] },
|
|
1636
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1637
|
-
},
|
|
1638
|
-
];
|
|
1639
|
-
if (!noEndCap) {
|
|
1640
|
-
children.push({
|
|
1641
|
-
type: "line",
|
|
1642
|
-
shape: {
|
|
1643
|
-
x1: center[0] - capHalf,
|
|
1644
|
-
y1: hi[1],
|
|
1645
|
-
x2: center[0] + capHalf,
|
|
1646
|
-
y2: hi[1],
|
|
1647
|
-
},
|
|
1648
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1649
|
-
}, {
|
|
1650
|
-
type: "line",
|
|
1651
|
-
shape: {
|
|
1652
|
-
x1: center[0] - capHalf,
|
|
1653
|
-
y1: lo[1],
|
|
1654
|
-
x2: center[0] + capHalf,
|
|
1655
|
-
y2: lo[1],
|
|
1656
|
-
},
|
|
1657
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1658
|
-
});
|
|
1659
|
-
}
|
|
1660
|
-
return { type: "group", children };
|
|
1661
|
-
},
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1666
|
+
chartSeries.push(...buildEchartsOverlaySeries({
|
|
1667
|
+
series,
|
|
1668
|
+
values: numericValues,
|
|
1669
|
+
// A horizontal bar keeps its categories on y (#1061).
|
|
1670
|
+
horizontal: isHorizontalBarChart,
|
|
1671
|
+
dataLabel,
|
|
1672
|
+
seriesColor: series.color ?? theme.themeColors.accent1,
|
|
1673
|
+
resolveRange: (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy),
|
|
1674
|
+
}));
|
|
1664
1675
|
};
|
|
1665
1676
|
// Excel `<c:overlap>` / `<c:gapWidth>` → echarts barGap /
|
|
1666
1677
|
// barCategoryGap (see utils/bar-layout.ts for the exact mapping).
|
|
@@ -1788,7 +1799,8 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1788
1799
|
// marker-less line with labels (Excel's default "Line" with values
|
|
1789
1800
|
// shown) keeps an invisible 0-size symbol per point for the label
|
|
1790
1801
|
// to hang off (#1049).
|
|
1791
|
-
const labelsWithoutMarkers = !wantsMarkers &&
|
|
1802
|
+
const labelsWithoutMarkers = !wantsMarkers &&
|
|
1803
|
+
Boolean(resolveSeriesDataLabels(spec.dataLabels, seriesAny)?.show);
|
|
1792
1804
|
seriesEntry.showSymbol = wantsMarkers || labelsWithoutMarkers;
|
|
1793
1805
|
if (wantsMarkers) {
|
|
1794
1806
|
seriesEntry.symbol =
|
|
@@ -2132,17 +2144,30 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2132
2144
|
nameGap: 30,
|
|
2133
2145
|
});
|
|
2134
2146
|
chartSeries.length = 0;
|
|
2147
|
+
// The spacer bars are layout only: no labels, no tooltip entry.
|
|
2135
2148
|
chartSeries.push({
|
|
2136
2149
|
type: "bar",
|
|
2150
|
+
id: WATERFALL_PLACEHOLDER_ID,
|
|
2137
2151
|
stack: "waterfall",
|
|
2138
2152
|
itemStyle: { color: "transparent" },
|
|
2139
2153
|
emphasis: { itemStyle: { color: "transparent" } },
|
|
2140
2154
|
data: steps.map((s) => s.placeholder),
|
|
2141
2155
|
silent: true,
|
|
2156
|
+
label: { show: false },
|
|
2157
|
+
tooltip: { show: false },
|
|
2142
2158
|
});
|
|
2143
2159
|
const accent = seriesEntry?.color ?? theme.themeColors.accent1;
|
|
2144
2160
|
const decreaseColor = seriesEntry?.negativeColor ?? theme.themeColors.accent2 ?? accent;
|
|
2145
2161
|
const totalColor = theme.themeColors.accent3 ?? accent;
|
|
2162
|
+
// Per-bar colours (`<c:dPt>`) win over the increase / decrease /
|
|
2163
|
+
// total colours — an importer gives totals the workbook's own
|
|
2164
|
+
// colour and keeps bars recoloured in Excel.
|
|
2165
|
+
const pointColors = new Map();
|
|
2166
|
+
for (const dp of seriesEntry?.dataPointColors ?? []) {
|
|
2167
|
+
if (typeof dp?.idx === "number" && typeof dp?.color === "string") {
|
|
2168
|
+
pointColors.set(dp.idx, dp.color);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2146
2171
|
// Excel-style connector lines: a horizontal segment at the running
|
|
2147
2172
|
// total after bar i, spanning across to bar i+1. Within the bars
|
|
2148
2173
|
// the segment overlaps their top/bottom edges, so only the gap
|
|
@@ -2153,16 +2178,22 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2153
2178
|
]);
|
|
2154
2179
|
chartSeries.push({
|
|
2155
2180
|
type: "bar",
|
|
2181
|
+
id: chartSeriesId(0, 0),
|
|
2156
2182
|
stack: "waterfall",
|
|
2157
2183
|
name: seriesEntry?.dataLabel ?? "",
|
|
2184
|
+
// A bar's height is the step's magnitude; the signed source value
|
|
2185
|
+
// rides along as `originalValue` so labels and tooltips read
|
|
2186
|
+
// "-90" for a decrease, like Excel.
|
|
2158
2187
|
data: steps.map((s, i) => ({
|
|
2159
2188
|
value: s.value,
|
|
2189
|
+
originalValue: rawValues[i],
|
|
2160
2190
|
itemStyle: {
|
|
2161
|
-
color:
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2191
|
+
color: pointColors.get(i) ??
|
|
2192
|
+
(s.isTotal
|
|
2193
|
+
? totalColor
|
|
2194
|
+
: rawValues[i] < 0
|
|
2195
|
+
? decreaseColor
|
|
2196
|
+
: accent),
|
|
2166
2197
|
},
|
|
2167
2198
|
})),
|
|
2168
2199
|
markLine: {
|
|
@@ -2192,14 +2223,64 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2192
2223
|
: [];
|
|
2193
2224
|
chartDomains.length = 0;
|
|
2194
2225
|
chartSeries.length = 0;
|
|
2226
|
+
// Excel draws every stage in the series colour (only `<c:dPt>`
|
|
2227
|
+
// recolours a stage), keeps the rows' order top to bottom, and
|
|
2228
|
+
// prints the values inside the bars (#1069).
|
|
2229
|
+
const funnelPalette = getChartPalette(spec, [
|
|
2230
|
+
theme.themeColors.accent1,
|
|
2231
|
+
theme.themeColors.accent2,
|
|
2232
|
+
theme.themeColors.accent3,
|
|
2233
|
+
theme.themeColors.accent4,
|
|
2234
|
+
theme.themeColors.accent5,
|
|
2235
|
+
theme.themeColors.accent6,
|
|
2236
|
+
]);
|
|
2237
|
+
const funnelColor = seriesEntry?.color ?? chartPaletteColorAt(funnelPalette, 0);
|
|
2238
|
+
const stageColors = new Map();
|
|
2239
|
+
for (const dp of seriesEntry?.dataPointColors ?? []) {
|
|
2240
|
+
if (typeof dp?.idx === "number" && typeof dp?.color === "string") {
|
|
2241
|
+
stageColors.set(dp.idx, dp.color);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
const funnelData = rawValues.map((value, i) => ({
|
|
2245
|
+
name: labels[i] ?? String(i),
|
|
2246
|
+
value,
|
|
2247
|
+
}));
|
|
2248
|
+
const funnelLayout = {
|
|
2249
|
+
sort: "none",
|
|
2250
|
+
left: "22%",
|
|
2251
|
+
right: "10%",
|
|
2252
|
+
top: 60,
|
|
2253
|
+
bottom: 20,
|
|
2254
|
+
};
|
|
2195
2255
|
chartSeries.push({
|
|
2196
2256
|
type: "funnel",
|
|
2257
|
+
id: chartSeriesId(0, 0),
|
|
2197
2258
|
name: seriesEntry?.dataLabel ?? "",
|
|
2198
|
-
data:
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2259
|
+
data: funnelData,
|
|
2260
|
+
...funnelLayout,
|
|
2261
|
+
itemStyle: {
|
|
2262
|
+
color: (params) => stageColors.get(params.dataIndex) ?? funnelColor,
|
|
2263
|
+
},
|
|
2264
|
+
label: {
|
|
2265
|
+
show: true,
|
|
2266
|
+
position: "inside",
|
|
2267
|
+
formatter: (params) => formatAxisValue(undefined, params.value, locale),
|
|
2268
|
+
},
|
|
2269
|
+
});
|
|
2270
|
+
// The stage names sit to the left of the bars, where Excel's
|
|
2271
|
+
// category axis puts them: an invisible twin funnel carries them.
|
|
2272
|
+
chartSeries.push({
|
|
2273
|
+
type: "funnel",
|
|
2274
|
+
id: FUNNEL_NAMES_ID,
|
|
2275
|
+
data: funnelData,
|
|
2276
|
+
...funnelLayout,
|
|
2277
|
+
silent: true,
|
|
2278
|
+
itemStyle: { color: "transparent", borderWidth: 0 },
|
|
2279
|
+
emphasis: { disabled: true },
|
|
2280
|
+
tooltip: { show: false },
|
|
2281
|
+
label: { show: true, position: "left", formatter: "{b}" },
|
|
2282
|
+
labelLine: { show: false },
|
|
2283
|
+
z: 1,
|
|
2203
2284
|
});
|
|
2204
2285
|
legend.data = [];
|
|
2205
2286
|
}
|
|
@@ -2456,19 +2537,37 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2456
2537
|
}
|
|
2457
2538
|
: legendBand.grid;
|
|
2458
2539
|
// 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
|
-
|
|
2461
|
-
const
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2540
|
+
// no explicit `<c:dLbls><c:numFmt>` — Excel's "Linked to source". A
|
|
2541
|
+
// series' own `dataLabels` replace the chart-level ones for it (#1059).
|
|
2542
|
+
const chartDataLabels = spec.dataLabels;
|
|
2543
|
+
const seriesLabelOptionFor = (s) => {
|
|
2544
|
+
const specIndex = chartSeriesSpecIndex(s?.id);
|
|
2545
|
+
const dataLabelOptions = resolveSeriesDataLabels(chartDataLabels, specIndex === undefined ? undefined : spec.series?.[specIndex]);
|
|
2546
|
+
return buildSeriesLabelOption(dataLabelOptions && !dataLabelOptions.numberFormatPattern
|
|
2547
|
+
? {
|
|
2548
|
+
...dataLabelOptions,
|
|
2549
|
+
numberFormatPattern: seriesNumberFormatFor(s),
|
|
2550
|
+
}
|
|
2551
|
+
: dataLabelOptions, s?.type, locale);
|
|
2552
|
+
};
|
|
2467
2553
|
// For doughnut, surface the inner-radius hole on each pie series.
|
|
2468
2554
|
// Excel persists `holeSize` as a 0-90 percentage; we normalize to a
|
|
2469
2555
|
// 0-1 fraction (doughnutHoleSize). Default to 0.5 when unset.
|
|
2470
2556
|
const isDoughnut = spec.chartType === "doughnut";
|
|
2471
2557
|
const doughnutHoleSize = spec.doughnutHoleSize;
|
|
2558
|
+
// Excel nests a doughnut's series as concentric rings, series 0
|
|
2559
|
+
// innermost (#1065): the ring each pie series draws is its rank among
|
|
2560
|
+
// the spec-tagged pie series.
|
|
2561
|
+
const doughnutSeriesOrder = isDoughnut
|
|
2562
|
+
? chartSeries
|
|
2563
|
+
.filter((s) => s?.type === "pie")
|
|
2564
|
+
.map((s) => chartSeriesSpecIndex(s?.id))
|
|
2565
|
+
.filter((i) => i !== undefined)
|
|
2566
|
+
.sort((a, b) => a - b)
|
|
2567
|
+
: [];
|
|
2568
|
+
const doughnutRingSet = isDoughnut
|
|
2569
|
+
? doughnutRings(clampDoughnutHole(doughnutHoleSize), doughnutSeriesOrder.length)
|
|
2570
|
+
: [];
|
|
2472
2571
|
// Exploded pie / doughnut: every slice sits in the `select` state,
|
|
2473
2572
|
// offset outward by the spec's percentage of the pie radius.
|
|
2474
2573
|
const explosion = isCircularChart && !isOfPie ? resolveExplosion(spec) : 0;
|
|
@@ -2515,19 +2614,43 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2515
2614
|
}
|
|
2516
2615
|
const seriesWithLabels = chartSeries.map((s) => {
|
|
2517
2616
|
const next = { ...s };
|
|
2617
|
+
if (DECORATION_SERIES_IDS.has(s?.id)) {
|
|
2618
|
+
return next;
|
|
2619
|
+
}
|
|
2518
2620
|
const seriesLabelOption = seriesLabelOptionFor(s);
|
|
2519
2621
|
if (seriesLabelOption) {
|
|
2520
2622
|
next.label = { ...(s?.label ?? {}), ...seriesLabelOption };
|
|
2521
2623
|
}
|
|
2624
|
+
else if (s?.type === "pie") {
|
|
2625
|
+
// echarts labels every pie slice by default (category name on a
|
|
2626
|
+
// leader line); Excel draws nothing until data labels are on (#1062).
|
|
2627
|
+
next.label = { ...(s?.label ?? {}), show: false };
|
|
2628
|
+
next.labelLine = { ...(s?.labelLine ?? {}), show: false };
|
|
2629
|
+
}
|
|
2522
2630
|
// Tooltip values of the value-axis series (a boxplot / candlestick
|
|
2523
2631
|
// tooltip lists its own statistics and keeps echarts' default).
|
|
2524
2632
|
const seriesTooltipValueFormatter = VALUE_TOOLTIP_SERIES_TYPES.has(s?.type)
|
|
2525
2633
|
? valueAxisLabelFormatter(seriesNumberFormatFor(s), locale)
|
|
2526
2634
|
: undefined;
|
|
2527
|
-
|
|
2635
|
+
// A data item carrying `originalValue` (a waterfall step's signed
|
|
2636
|
+
// source value, a percent-stacked bar's raw amount) reads as that
|
|
2637
|
+
// value in the tooltip, like its data label.
|
|
2638
|
+
const originalValueAt = (dataIndex) => {
|
|
2639
|
+
const item = Array.isArray(s?.data) ? s.data[dataIndex] : undefined;
|
|
2640
|
+
return item !== null && typeof item === "object"
|
|
2641
|
+
? item.originalValue
|
|
2642
|
+
: undefined;
|
|
2643
|
+
};
|
|
2644
|
+
const carriesOriginalValues = VALUE_TOOLTIP_SERIES_TYPES.has(s?.type) &&
|
|
2645
|
+
Array.isArray(s?.data) &&
|
|
2646
|
+
s.data.some((item) => item !== null && typeof item === "object" && "originalValue" in item);
|
|
2647
|
+
if ((seriesTooltipValueFormatter || carriesOriginalValues) &&
|
|
2648
|
+
s?.tooltip?.show !== false) {
|
|
2649
|
+
const formatValue = seriesTooltipValueFormatter ??
|
|
2650
|
+
((value) => formatAxisValue(undefined, value, locale));
|
|
2528
2651
|
next.tooltip = {
|
|
2529
2652
|
...(s?.tooltip ?? {}),
|
|
2530
|
-
valueFormatter:
|
|
2653
|
+
valueFormatter: (value, dataIndex) => formatValue(originalValueAt(dataIndex) ?? value),
|
|
2531
2654
|
};
|
|
2532
2655
|
}
|
|
2533
2656
|
if (next.type === "pie") {
|
|
@@ -2547,10 +2670,15 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2547
2670
|
}
|
|
2548
2671
|
}
|
|
2549
2672
|
if (isDoughnut && next.type === "pie") {
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2673
|
+
// The hole is a fraction of the OUTER radius (Excel's `<c:holeSize>`),
|
|
2674
|
+
// and the doughnut's outer edge sits at 75% of the chart — so every
|
|
2675
|
+
// ring edge is its fraction of 75%, not of the whole chart (#1064:
|
|
2676
|
+
// Excel's default 0.75 used to meet the outer edge, an empty ring).
|
|
2677
|
+
const rank = doughnutSeriesOrder.indexOf(chartSeriesSpecIndex(s?.id));
|
|
2678
|
+
const ring = doughnutRingSet[rank] ??
|
|
2679
|
+
doughnutRings(clampDoughnutHole(doughnutHoleSize), 1)[0];
|
|
2680
|
+
const pct = (fraction) => `${Math.round(fraction * 75 * 100) / 100}%`;
|
|
2681
|
+
next.radius = [pct(ring.inner), pct(ring.outer)];
|
|
2554
2682
|
}
|
|
2555
2683
|
return next;
|
|
2556
2684
|
});
|