@rowsncolumns/charts 28.1.8 → 28.1.11
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 +538 -430
- 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 +333 -184
- 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/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.map +1 -1
- package/dist/cjs/utils/overrides.js +15 -3
- 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 +381 -251
- 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 +253 -58
- 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/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.map +1 -1
- package/dist/esm/utils/overrides.js +15 -3
- 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 +3 -3
|
@@ -18,9 +18,9 @@ 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";
|
|
@@ -36,7 +36,6 @@ import { STOCK_ROLES, toCandlestick, toLineSeries, toVolumeSeries, } from "../ut
|
|
|
36
36
|
import { explosionOffsetPx, pieRadiusPx, resolveExplosion, } from "../utils/pie-explosion";
|
|
37
37
|
import { chartSeriesId, chartSeriesSpecIndex, resolveClickedDataIndex, resolveClickedSeriesIndex, } from "../utils/series-click";
|
|
38
38
|
import { buildBar3DData, buildContourData, buildLine3DSeries, buildSurfaceData, getCategoryCount, getValueExtent, isContourSpec, resolve3DRenderKind, SURFACE_COLOR_RAMP, } from "../utils/three-d";
|
|
39
|
-
import { computeTrendline } from "../utils/trendline";
|
|
40
39
|
import { computeWaterfall } from "../utils/waterfall";
|
|
41
40
|
import { isWebGLAvailable } from "../utils/webgl";
|
|
42
41
|
import { chartTypeToEchartsType, circularLegendEntries, getEchartsSeriesStyle, isCircularChartType, mapMarkerSymbolToEcharts, showXAxis, } from "./helpers";
|
|
@@ -103,29 +102,54 @@ const HIERARCHY_CHART_TYPES = new Set([
|
|
|
103
102
|
const isBasicChartSpec = (spec) => BASIC_CHART_TYPES.has(spec.chartType);
|
|
104
103
|
const isBubbleChartSpec = (spec) => BUBBLE_CHART_TYPES.has(spec.chartType);
|
|
105
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. Index 0 is echarts' virtual root, 1 the
|
|
108
|
+
// top-level categories, 2+ the levels beneath them.
|
|
106
109
|
const HIERARCHY_LEVEL_STYLES = [
|
|
107
110
|
{
|
|
108
111
|
itemStyle: {
|
|
109
|
-
gapWidth:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
upperLabel: { show: false },
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
colorSaturation: [0.35, 0.65],
|
|
116
|
-
itemStyle: {
|
|
117
|
-
gapWidth: 1,
|
|
118
|
-
borderColorSaturation: 0.6,
|
|
112
|
+
gapWidth: 2,
|
|
113
|
+
borderColor: "#ffffff",
|
|
114
|
+
borderWidth: 0,
|
|
119
115
|
},
|
|
120
116
|
},
|
|
121
117
|
{
|
|
122
|
-
colorSaturation: [0.45, 0.75],
|
|
123
118
|
itemStyle: {
|
|
124
119
|
gapWidth: 1,
|
|
125
|
-
|
|
120
|
+
borderColor: "#ffffff",
|
|
121
|
+
borderWidth: 1,
|
|
126
122
|
},
|
|
127
123
|
},
|
|
128
124
|
];
|
|
125
|
+
/** Height of the strip a treemap category name is drawn in (Excel's "Banner"). */
|
|
126
|
+
const TREEMAP_BANNER_HEIGHT = 20;
|
|
127
|
+
/**
|
|
128
|
+
* Excel's treemap `parentLabelLayout`: the top-level category names are
|
|
129
|
+
* drawn in a banner strip (or not at all); the levels between the categories
|
|
130
|
+
* and the leaves are never labelled, so small groups keep their height for
|
|
131
|
+
* the leaf cells (rowsncolumns/spreadsheet#1072). Excel's default
|
|
132
|
+
* `overlapping` writes the name over the group's top-left corner, which an
|
|
133
|
+
* echarts treemap cannot draw (an upper label always reserves its strip and
|
|
134
|
+
* is dropped when the strip is too small for the text), so it draws as a
|
|
135
|
+
* banner.
|
|
136
|
+
*/
|
|
137
|
+
const treemapBannerShown = (layout) => layout !== "none";
|
|
138
|
+
/**
|
|
139
|
+
* Colour a hierarchy like Excel: each top-level node takes the next
|
|
140
|
+
* palette colour and every descendant inherits it (#1072).
|
|
141
|
+
*/
|
|
142
|
+
const colorHierarchyNodes = (nodes, palette) => nodes.map((node, index) => {
|
|
143
|
+
const color = chartPaletteColorAt(palette, index);
|
|
144
|
+
const paint = (n) => ({
|
|
145
|
+
...n,
|
|
146
|
+
itemStyle: { color },
|
|
147
|
+
...(n.children ? { children: n.children.map(paint) } : {}),
|
|
148
|
+
});
|
|
149
|
+
return paint(node);
|
|
150
|
+
});
|
|
151
|
+
/** Sunburst rings: Excel leaves the centre empty, so even the first level is a ring (#1073). */
|
|
152
|
+
const SUNBURST_RADIUS = ["15%", "80%"];
|
|
129
153
|
const getHierarchyDepth = (nodes, depth = 1) => {
|
|
130
154
|
let maxDepth = depth;
|
|
131
155
|
nodes.forEach((node) => {
|
|
@@ -159,23 +183,46 @@ const flattenChartDataValues = (chartData, extractor, hiddenStrategy, skipFirstR
|
|
|
159
183
|
});
|
|
160
184
|
return values;
|
|
161
185
|
};
|
|
162
|
-
const buildHierarchyLevels = (maxDepth) => {
|
|
186
|
+
const buildHierarchyLevels = (maxDepth, options) => {
|
|
163
187
|
if (maxDepth <= 0)
|
|
164
188
|
return undefined;
|
|
165
189
|
const levels = [];
|
|
166
|
-
|
|
190
|
+
// One entry per depth, the virtual root included, so every level's upper
|
|
191
|
+
// label is explicit: the categories' banner on level 1 only.
|
|
192
|
+
for (let i = 0; i <= maxDepth; i++) {
|
|
167
193
|
const baseStyle = HIERARCHY_LEVEL_STYLES[Math.min(i, HIERARCHY_LEVEL_STYLES.length - 1)];
|
|
168
194
|
levels.push({
|
|
169
195
|
...baseStyle,
|
|
170
196
|
itemStyle: {
|
|
171
197
|
...baseStyle.itemStyle,
|
|
172
198
|
},
|
|
199
|
+
...(options.treemap
|
|
200
|
+
? {
|
|
201
|
+
upperLabel: i === 1 && options.banner
|
|
202
|
+
? {
|
|
203
|
+
show: true,
|
|
204
|
+
height: TREEMAP_BANNER_HEIGHT,
|
|
205
|
+
padding: [0, 0, 0, 4],
|
|
206
|
+
}
|
|
207
|
+
: { show: false },
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
173
210
|
});
|
|
174
211
|
}
|
|
175
212
|
return levels;
|
|
176
213
|
};
|
|
177
214
|
/** Series whose tooltip value is one number on a value axis. */
|
|
178
215
|
const VALUE_TOOLTIP_SERIES_TYPES = new Set(["bar", "line", "scatter", "funnel"]);
|
|
216
|
+
/** The transparent spacer bars a waterfall stacks its visible bars on. */
|
|
217
|
+
const WATERFALL_PLACEHOLDER_ID = "rnc-waterfall-placeholder";
|
|
218
|
+
/** The invisible funnel that carries the stage names beside the bars. */
|
|
219
|
+
const FUNNEL_NAMES_ID = "rnc-funnel-names";
|
|
220
|
+
/**
|
|
221
|
+
* Layout-only series: they carry no data of their own, so they get no
|
|
222
|
+
* data labels and no tooltip entry (#1070 labelled every waterfall bar
|
|
223
|
+
* with its spacer's height too).
|
|
224
|
+
*/
|
|
225
|
+
const DECORATION_SERIES_IDS = new Set([WATERFALL_PLACEHOLDER_ID, FUNNEL_NAMES_ID]);
|
|
179
226
|
const defaultLegendConfig = {
|
|
180
227
|
padding: 10,
|
|
181
228
|
itemWidth: 10,
|
|
@@ -556,7 +603,38 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
556
603
|
sizeValues: sharedSizeValues,
|
|
557
604
|
series: seriesInputs,
|
|
558
605
|
});
|
|
559
|
-
|
|
606
|
+
// Bubble diameters scale from zero (area by default, width when
|
|
607
|
+
// `sizeRepresents` says so) and the largest bubble is a fraction of
|
|
608
|
+
// the plot area — the grid keeps ~85% of the width and ~75% of the
|
|
609
|
+
// height for the plot (#1068).
|
|
610
|
+
const sizeScale = createBubbleSizeScale(sizeValues, {
|
|
611
|
+
sizeRepresents: spec.sizeRepresents,
|
|
612
|
+
bubbleScale: spec.bubbleScale,
|
|
613
|
+
plotMinSide: Math.min((width ?? 0) * 0.85, (height ?? 0) * 0.75),
|
|
614
|
+
});
|
|
615
|
+
const bubblePalette = getChartPalette(spec, [
|
|
616
|
+
theme.themeColors.accent1,
|
|
617
|
+
theme.themeColors.accent2,
|
|
618
|
+
theme.themeColors.accent3,
|
|
619
|
+
theme.themeColors.accent4,
|
|
620
|
+
theme.themeColors.accent5,
|
|
621
|
+
theme.themeColors.accent6,
|
|
622
|
+
]);
|
|
623
|
+
// Per-point colours (`<c:dPt>`) key on the point's source row, which
|
|
624
|
+
// `BubbleSeriesPoint.index` keeps (points without a Y are skipped).
|
|
625
|
+
const bubbleItemColor = (seriesEntry, index) => {
|
|
626
|
+
const base = seriesEntry?.color ?? chartPaletteColorAt(bubblePalette, index);
|
|
627
|
+
const overrides = new Map();
|
|
628
|
+
for (const dp of seriesEntry?.dataPointColors ?? []) {
|
|
629
|
+
if (typeof dp?.idx === "number" && typeof dp?.color === "string") {
|
|
630
|
+
overrides.set(dp.idx, dp.color);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return overrides.size
|
|
634
|
+
? (params) => overrides.get(params?.data?.index) ?? base
|
|
635
|
+
: base;
|
|
636
|
+
};
|
|
637
|
+
const hasBubbleSecondaryAxis = (spec.series ?? []).some((seriesEntry) => seriesEntry?.useSecondaryAxis);
|
|
560
638
|
const xAxisConfig = categories.length > 0
|
|
561
639
|
? {
|
|
562
640
|
type: "category",
|
|
@@ -585,6 +663,102 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
585
663
|
// non-bubble layout below. A series' own `dataLabels` replace the
|
|
586
664
|
// chart-level ones for it (#1059).
|
|
587
665
|
const bubbleSeriesLabelFor = (index) => buildSeriesLabelOption(resolveSeriesDataLabels(spec.dataLabels, spec.series?.[index]), undefined, locale);
|
|
666
|
+
// Scatter vs bubble — scatter uses small fixed markers (optionally
|
|
667
|
+
// connected with a line per scatterStyle); bubble applies a size
|
|
668
|
+
// scale on the 3rd dimension. Pre-fix scatter routed through the
|
|
669
|
+
// bubble code, producing Y-scaled giant circles instead of Excel's
|
|
670
|
+
// small dots.
|
|
671
|
+
const bubbleSeries = preparedSeries
|
|
672
|
+
.map((entry, index) => {
|
|
673
|
+
const isScatter = spec.chartType === "scatter";
|
|
674
|
+
const scatterStyle = spec.scatterStyle ?? (isScatter ? "lineMarker" : undefined);
|
|
675
|
+
const seriesEntry = spec.series?.[index];
|
|
676
|
+
const seriesColor = seriesEntry?.color;
|
|
677
|
+
const perMarker = seriesEntry?.marker;
|
|
678
|
+
const itemColor = bubbleItemColor(seriesEntry, index);
|
|
679
|
+
const onSecondary = seriesEntry?.useSecondaryAxis ? { yAxisIndex: 1 } : {};
|
|
680
|
+
if (isScatter) {
|
|
681
|
+
const symbol = perMarker?.symbol;
|
|
682
|
+
const noMarkers = symbol === "none";
|
|
683
|
+
const wantsLine = scatterStyle === "line" ||
|
|
684
|
+
scatterStyle === "lineMarker" ||
|
|
685
|
+
scatterStyle === "smooth" ||
|
|
686
|
+
scatterStyle === "smoothMarker";
|
|
687
|
+
const wantsMarkers = !noMarkers &&
|
|
688
|
+
(scatterStyle === "marker" ||
|
|
689
|
+
scatterStyle === "lineMarker" ||
|
|
690
|
+
scatterStyle === "smoothMarker" ||
|
|
691
|
+
!scatterStyle);
|
|
692
|
+
const smooth = scatterStyle === "smooth" || scatterStyle === "smoothMarker";
|
|
693
|
+
// Labels hang off the point symbols: a marker-less line keeps
|
|
694
|
+
// an invisible 0-size symbol per point when they show (#1049).
|
|
695
|
+
const labelsWithoutMarkers = !wantsMarkers &&
|
|
696
|
+
Boolean(resolveSeriesDataLabels(spec.dataLabels, seriesEntry)
|
|
697
|
+
?.show);
|
|
698
|
+
// The series colour paints the line; the markers take the
|
|
699
|
+
// per-point overrides when the series has any.
|
|
700
|
+
const markerStyle = seriesColor || typeof itemColor === "function"
|
|
701
|
+
? { itemStyle: { color: itemColor } }
|
|
702
|
+
: {};
|
|
703
|
+
return wantsLine
|
|
704
|
+
? {
|
|
705
|
+
type: "line",
|
|
706
|
+
name: entry.name,
|
|
707
|
+
data: entry.points,
|
|
708
|
+
showSymbol: wantsMarkers || labelsWithoutMarkers,
|
|
709
|
+
smooth,
|
|
710
|
+
symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
|
|
711
|
+
symbolSize: labelsWithoutMarkers ? 0 : perMarker?.size ?? 6,
|
|
712
|
+
...markerStyle,
|
|
713
|
+
...(seriesColor ? { lineStyle: { color: seriesColor } } : {}),
|
|
714
|
+
...onSecondary,
|
|
715
|
+
}
|
|
716
|
+
: {
|
|
717
|
+
type: "scatter",
|
|
718
|
+
name: entry.name,
|
|
719
|
+
data: entry.points,
|
|
720
|
+
symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
|
|
721
|
+
symbolSize: perMarker?.size ?? 6,
|
|
722
|
+
...markerStyle,
|
|
723
|
+
...onSecondary,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
type: chartType,
|
|
728
|
+
name: entry.name,
|
|
729
|
+
data: entry.points,
|
|
730
|
+
symbolSize: (data) => sizeScale(data?.[2] ?? 0),
|
|
731
|
+
label: {
|
|
732
|
+
show: true,
|
|
733
|
+
formatter: (params) => params.name ?? "",
|
|
734
|
+
},
|
|
735
|
+
...(seriesColor || typeof itemColor === "function"
|
|
736
|
+
? { itemStyle: { color: itemColor } }
|
|
737
|
+
: {}),
|
|
738
|
+
...onSecondary,
|
|
739
|
+
};
|
|
740
|
+
})
|
|
741
|
+
.map((s, index) => {
|
|
742
|
+
const bubbleSeriesLabel = bubbleSeriesLabelFor(index);
|
|
743
|
+
return bubbleSeriesLabel
|
|
744
|
+
? { ...s, label: { ...(s.label ?? {}), ...bubbleSeriesLabel } }
|
|
745
|
+
: s;
|
|
746
|
+
});
|
|
747
|
+
// Trendlines and error bars on the real (x, y) points: the fit is a
|
|
748
|
+
// regression of Y on X, not on the point index (#1067).
|
|
749
|
+
const bubbleOverlays = preparedSeries.flatMap((entry, index) => {
|
|
750
|
+
const seriesEntry = spec.series?.[index];
|
|
751
|
+
if (!seriesEntry)
|
|
752
|
+
return [];
|
|
753
|
+
return buildEchartsOverlaySeries({
|
|
754
|
+
series: seriesEntry,
|
|
755
|
+
values: entry.points.map((point) => point.value[1]),
|
|
756
|
+
xValues: entry.points.map((point) => point.value[0]),
|
|
757
|
+
dataLabel: entry.name,
|
|
758
|
+
seriesColor: seriesEntry.color ?? chartPaletteColorAt(bubblePalette, index),
|
|
759
|
+
resolveRange: (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy),
|
|
760
|
+
});
|
|
761
|
+
});
|
|
588
762
|
chartRef.current?.setOption({
|
|
589
763
|
xAxis: [
|
|
590
764
|
applyAxisOverrides({ ...xAxisConfig }, spec.categoryAxisOptions),
|
|
@@ -596,6 +770,19 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
596
770
|
nameLocation: "middle",
|
|
597
771
|
nameGap: 45,
|
|
598
772
|
}, spec.valueAxisOptions, undefined, locale),
|
|
773
|
+
// Secondary value axis on the right for the series that opt in
|
|
774
|
+
// (`useSecondaryAxis`), as the category layout has (#1067).
|
|
775
|
+
...(hasBubbleSecondaryAxis
|
|
776
|
+
? [
|
|
777
|
+
applyAxisOverrides({
|
|
778
|
+
type: "value",
|
|
779
|
+
position: "right",
|
|
780
|
+
nameLocation: "middle",
|
|
781
|
+
nameGap: 45,
|
|
782
|
+
splitLine: { show: false },
|
|
783
|
+
}, spec.secondaryValueAxisOptions, undefined, locale),
|
|
784
|
+
]
|
|
785
|
+
: []),
|
|
599
786
|
],
|
|
600
787
|
legend: applyLegendOverrides({
|
|
601
788
|
...legend,
|
|
@@ -612,82 +799,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
612
799
|
trigger: "item",
|
|
613
800
|
formatter: tooltipFormatter,
|
|
614
801
|
},
|
|
615
|
-
|
|
616
|
-
// (optionally connected with a line per scatterStyle); bubble
|
|
617
|
-
// applies a size scale on the 3rd dimension. Pre-fix scatter
|
|
618
|
-
// routed through the bubble code, producing Y-scaled giant
|
|
619
|
-
// circles instead of Excel's small dots.
|
|
620
|
-
series: preparedSeries.map((entry, index) => {
|
|
621
|
-
const isScatter = spec.chartType === "scatter";
|
|
622
|
-
const scatterStyle = spec.scatterStyle ??
|
|
623
|
-
(isScatter ? "lineMarker" : undefined);
|
|
624
|
-
const seriesEntry = spec.series?.[index];
|
|
625
|
-
const seriesColor = seriesEntry?.color;
|
|
626
|
-
const perMarker = seriesEntry?.marker;
|
|
627
|
-
if (isScatter) {
|
|
628
|
-
const symbol = perMarker?.symbol;
|
|
629
|
-
const noMarkers = symbol === "none";
|
|
630
|
-
const wantsLine = scatterStyle === "line" ||
|
|
631
|
-
scatterStyle === "lineMarker" ||
|
|
632
|
-
scatterStyle === "smooth" ||
|
|
633
|
-
scatterStyle === "smoothMarker";
|
|
634
|
-
const wantsMarkers = !noMarkers &&
|
|
635
|
-
(scatterStyle === "marker" ||
|
|
636
|
-
scatterStyle === "lineMarker" ||
|
|
637
|
-
scatterStyle === "smoothMarker" ||
|
|
638
|
-
!scatterStyle);
|
|
639
|
-
const smooth = scatterStyle === "smooth" || scatterStyle === "smoothMarker";
|
|
640
|
-
// Labels hang off the point symbols: a marker-less line keeps
|
|
641
|
-
// an invisible 0-size symbol per point when they show (#1049).
|
|
642
|
-
const labelsWithoutMarkers = !wantsMarkers &&
|
|
643
|
-
Boolean(resolveSeriesDataLabels(spec.dataLabels, seriesEntry)
|
|
644
|
-
?.show);
|
|
645
|
-
return wantsLine
|
|
646
|
-
? {
|
|
647
|
-
type: "line",
|
|
648
|
-
name: entry.name,
|
|
649
|
-
data: entry.points,
|
|
650
|
-
showSymbol: wantsMarkers || labelsWithoutMarkers,
|
|
651
|
-
smooth,
|
|
652
|
-
symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
|
|
653
|
-
symbolSize: labelsWithoutMarkers
|
|
654
|
-
? 0
|
|
655
|
-
: perMarker?.size ?? 6,
|
|
656
|
-
...(seriesColor
|
|
657
|
-
? {
|
|
658
|
-
itemStyle: { color: seriesColor },
|
|
659
|
-
lineStyle: { color: seriesColor },
|
|
660
|
-
}
|
|
661
|
-
: {}),
|
|
662
|
-
}
|
|
663
|
-
: {
|
|
664
|
-
type: "scatter",
|
|
665
|
-
name: entry.name,
|
|
666
|
-
data: entry.points,
|
|
667
|
-
symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
|
|
668
|
-
symbolSize: perMarker?.size ?? 6,
|
|
669
|
-
...(seriesColor
|
|
670
|
-
? { itemStyle: { color: seriesColor } }
|
|
671
|
-
: {}),
|
|
672
|
-
};
|
|
673
|
-
}
|
|
674
|
-
return {
|
|
675
|
-
type: chartType,
|
|
676
|
-
name: entry.name,
|
|
677
|
-
data: entry.points,
|
|
678
|
-
symbolSize: (data) => sizeScale(data?.[2] ?? 0),
|
|
679
|
-
label: {
|
|
680
|
-
show: true,
|
|
681
|
-
formatter: (params) => params.name ?? "",
|
|
682
|
-
},
|
|
683
|
-
...(seriesColor ? { itemStyle: { color: seriesColor } } : {}),
|
|
684
|
-
};
|
|
685
|
-
}).map((s, index) => {
|
|
686
|
-
const bubbleSeriesLabel = bubbleSeriesLabelFor(index);
|
|
687
|
-
return bubbleSeriesLabel
|
|
688
|
-
? { ...s, label: { ...(s.label ?? {}), ...bubbleSeriesLabel } }
|
|
689
|
-
: s;
|
|
690
|
-
}),
|
|
802
|
+
series: [...bubbleSeries, ...bubbleOverlays],
|
|
691
803
|
}, {
|
|
692
804
|
replaceMerge: [
|
|
693
805
|
"series",
|
|
@@ -699,7 +811,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
699
811
|
],
|
|
700
812
|
});
|
|
701
813
|
previousChartType.current = spec.chartType;
|
|
702
|
-
previousSeriesCount.current =
|
|
814
|
+
previousSeriesCount.current = bubbleSeries.length + bubbleOverlays.length;
|
|
703
815
|
return;
|
|
704
816
|
}
|
|
705
817
|
if (isHierarchy) {
|
|
@@ -714,11 +826,43 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
714
826
|
hiddenDimensionStrategy: spec.hiddenDimensionStrategy,
|
|
715
827
|
});
|
|
716
828
|
hierarchyTitleRef.current = explicitChartTitle ?? headerTitle;
|
|
717
|
-
const
|
|
829
|
+
const hierarchyPalette = getChartPalette(spec, [
|
|
830
|
+
theme.themeColors.accent1,
|
|
831
|
+
theme.themeColors.accent2,
|
|
832
|
+
theme.themeColors.accent3,
|
|
833
|
+
theme.themeColors.accent4,
|
|
834
|
+
theme.themeColors.accent5,
|
|
835
|
+
theme.themeColors.accent6,
|
|
836
|
+
]);
|
|
837
|
+
const hierarchyData = colorHierarchyNodes(buildHierarchyTree(hierarchyRows), hierarchyPalette);
|
|
718
838
|
const rootName = hierarchyData.length === 1 ? hierarchyData[0]?.name ?? "" : "";
|
|
839
|
+
const drillDown = Boolean(spec.drillDown);
|
|
719
840
|
const seriesDisplayName = explicitChartTitle ?? headerTitle ?? rootName ?? "";
|
|
720
841
|
const hierarchyDepth = getHierarchyDepth(hierarchyData);
|
|
721
|
-
const
|
|
842
|
+
const isTreemap = spec.chartType === "treemap";
|
|
843
|
+
const banner = isTreemap && treemapBannerShown(spec.parentLabelLayout);
|
|
844
|
+
const levels = buildHierarchyLevels(hierarchyDepth, {
|
|
845
|
+
treemap: isTreemap,
|
|
846
|
+
banner,
|
|
847
|
+
});
|
|
848
|
+
// Excel lists the top-level categories with their colours in the
|
|
849
|
+
// legend. An echarts legend cannot address treemap nodes, so each
|
|
850
|
+
// category gets a data-less placeholder series carrying its name and
|
|
851
|
+
// colour; `selectedMode: false` keeps the swatches from toggling
|
|
852
|
+
// anything.
|
|
853
|
+
const hierarchyLegend = applyLegendOverrides({ ...defaultLegendConfig, show: false }, spec.legend);
|
|
854
|
+
const legendShown = hierarchyLegend.show !== false;
|
|
855
|
+
const categoryLegendSeries = legendShown
|
|
856
|
+
? hierarchyData.map((node) => ({
|
|
857
|
+
type: "bar",
|
|
858
|
+
id: `rnc-hierarchy-legend-${node.id ?? node.name}`,
|
|
859
|
+
name: node.name,
|
|
860
|
+
data: [],
|
|
861
|
+
silent: true,
|
|
862
|
+
itemStyle: { color: node.itemStyle?.color },
|
|
863
|
+
tooltip: { show: false },
|
|
864
|
+
}))
|
|
865
|
+
: [];
|
|
722
866
|
// Sizes format like the cells they were read from (Excel's "Linked
|
|
723
867
|
// to source"), unless the data labels carry their own pattern.
|
|
724
868
|
const sizeNumberFormat = resolveSourceNumberFormat(spec.sizeData?.[0]
|
|
@@ -748,7 +892,11 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
748
892
|
show: false,
|
|
749
893
|
},
|
|
750
894
|
],
|
|
751
|
-
legend:
|
|
895
|
+
legend: {
|
|
896
|
+
...hierarchyLegend,
|
|
897
|
+
data: hierarchyData.map((node) => node.name),
|
|
898
|
+
selectedMode: false,
|
|
899
|
+
},
|
|
752
900
|
grid: {
|
|
753
901
|
left: 0,
|
|
754
902
|
right: 0,
|
|
@@ -768,17 +916,25 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
768
916
|
label: {
|
|
769
917
|
show: true,
|
|
770
918
|
formatter: "{b}",
|
|
919
|
+
...(isTreemap ? { position: "insideBottomLeft" } : {}),
|
|
771
920
|
...(hierarchySeriesLabel ?? {}),
|
|
772
921
|
},
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
padding: [0, 0, 0, 4],
|
|
777
|
-
}
|
|
778
|
-
: undefined,
|
|
922
|
+
// Only `levels[1]` (the categories) may carry a banner; see
|
|
923
|
+
// buildHierarchyLevels.
|
|
924
|
+
upperLabel: isTreemap ? { show: false } : undefined,
|
|
779
925
|
levels,
|
|
780
|
-
|
|
926
|
+
// Excel has no drill-down on either chart: zooming into a
|
|
927
|
+
// branch on click (and the treemap breadcrumb) is opt-in.
|
|
928
|
+
nodeClick: drillDown
|
|
929
|
+
? spec.chartType === "treemap"
|
|
930
|
+
? "zoomToNode"
|
|
931
|
+
: "rootToNode"
|
|
932
|
+
: false,
|
|
933
|
+
...(isTreemap
|
|
934
|
+
? { breadcrumb: { show: drillDown } }
|
|
935
|
+
: { radius: [...SUNBURST_RADIUS] }),
|
|
781
936
|
},
|
|
937
|
+
...categoryLegendSeries,
|
|
782
938
|
],
|
|
783
939
|
}, {
|
|
784
940
|
replaceMerge: [
|
|
@@ -791,7 +947,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
791
947
|
],
|
|
792
948
|
});
|
|
793
949
|
previousChartType.current = spec.chartType;
|
|
794
|
-
previousSeriesCount.current = 1;
|
|
950
|
+
previousSeriesCount.current = 1 + categoryLegendSeries.length;
|
|
795
951
|
return;
|
|
796
952
|
}
|
|
797
953
|
else {
|
|
@@ -1176,12 +1332,19 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1176
1332
|
};
|
|
1177
1333
|
chartRef.current?.setOption({
|
|
1178
1334
|
radar: {
|
|
1179
|
-
|
|
1335
|
+
// The common scale's tick labels along ONE axis, as Excel draws
|
|
1336
|
+
// them: echarts labels every indicator axis, so each indicator
|
|
1337
|
+
// carries its own `axisLabel.show` (an indicator's own options win
|
|
1338
|
+
// over the radar-level ones in echarts' merge) and only the first
|
|
1339
|
+
// shows (#1066 follow-up); hidden with the spec's value axis.
|
|
1340
|
+
indicator: (indicators.length
|
|
1180
1341
|
? indicators
|
|
1181
|
-
: [{ name: "", min: 0, max: 1 }],
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1342
|
+
: [{ name: "", min: 0, max: 1 }]).map((indicator, index) => ({
|
|
1343
|
+
...indicator,
|
|
1344
|
+
axisLabel: {
|
|
1345
|
+
show: index === 0 && radarAxisOptions?.visible !== false,
|
|
1346
|
+
},
|
|
1347
|
+
})),
|
|
1185
1348
|
axisLabel: {
|
|
1186
1349
|
show: radarAxisOptions?.visible !== false,
|
|
1187
1350
|
formatter: (value) => formatAxisValue(radarFormats[0], value, locale),
|
|
@@ -1549,135 +1712,15 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1549
1712
|
: [];
|
|
1550
1713
|
if (numericValues.length === 0)
|
|
1551
1714
|
return;
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
forecastBackward: tl.forecastBackward,
|
|
1562
|
-
});
|
|
1563
|
-
if (!result || result.points.length < 2)
|
|
1564
|
-
continue;
|
|
1565
|
-
const tlName = tl.name ??
|
|
1566
|
-
`${dataLabel ? dataLabel + " " : ""}${tl.type ?? "linear"} trend`;
|
|
1567
|
-
chartSeries.push({
|
|
1568
|
-
type: "line",
|
|
1569
|
-
name: tlName,
|
|
1570
|
-
// Index x lets the trendline overlay the category axis even
|
|
1571
|
-
// when forecasting extends beyond the data range.
|
|
1572
|
-
data: result.points.map((p) => [p.x, p.y]),
|
|
1573
|
-
showSymbol: false,
|
|
1574
|
-
smooth: tl.type === "poly",
|
|
1575
|
-
lineStyle: {
|
|
1576
|
-
color: tl.color ?? seriesColor,
|
|
1577
|
-
type: "dashed",
|
|
1578
|
-
width: 2,
|
|
1579
|
-
},
|
|
1580
|
-
itemStyle: { color: tl.color ?? seriesColor },
|
|
1581
|
-
tooltip: { show: false },
|
|
1582
|
-
z: 5,
|
|
1583
|
-
...useSecondary,
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
// Error bars — drawn as a custom series of vertical/horizontal
|
|
1587
|
-
// whiskers. valueType "custom" reads per-point magnitudes from the
|
|
1588
|
-
// spec's plus/minus ranges, resolved through the same extractor
|
|
1589
|
-
// the series values use.
|
|
1590
|
-
const magnitudes = computeErrorBars(numericValues, resolveCustomErrorBarValues(series.errorBars, (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy)));
|
|
1591
|
-
if (magnitudes) {
|
|
1592
|
-
const direction = series.errorBars?.direction ?? "y";
|
|
1593
|
-
const noEndCap = series.errorBars?.noEndCap ?? false;
|
|
1594
|
-
const errorData = numericValues.map((v, i) => [
|
|
1595
|
-
i,
|
|
1596
|
-
typeof v === "number" ? v : 0,
|
|
1597
|
-
magnitudes.plus[i] ?? 0,
|
|
1598
|
-
magnitudes.minus[i] ?? 0,
|
|
1599
|
-
]);
|
|
1600
|
-
chartSeries.push({
|
|
1601
|
-
type: "custom",
|
|
1602
|
-
name: `${dataLabel ? dataLabel + " " : ""}error`,
|
|
1603
|
-
data: errorData,
|
|
1604
|
-
z: 6,
|
|
1605
|
-
tooltip: { show: false },
|
|
1606
|
-
...useSecondary,
|
|
1607
|
-
renderItem: (_params, api) => {
|
|
1608
|
-
const value = api.value(1);
|
|
1609
|
-
const plus = api.value(2);
|
|
1610
|
-
const minus = api.value(3);
|
|
1611
|
-
const capHalf = 5;
|
|
1612
|
-
if (direction === "x") {
|
|
1613
|
-
const center = api.coord([value, api.value(0)]);
|
|
1614
|
-
const hi = api.coord([value + plus, api.value(0)]);
|
|
1615
|
-
const lo = api.coord([value - minus, api.value(0)]);
|
|
1616
|
-
const children = [
|
|
1617
|
-
{
|
|
1618
|
-
type: "line",
|
|
1619
|
-
shape: { x1: lo[0], y1: center[1], x2: hi[0], y2: center[1] },
|
|
1620
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1621
|
-
},
|
|
1622
|
-
];
|
|
1623
|
-
if (!noEndCap) {
|
|
1624
|
-
children.push({
|
|
1625
|
-
type: "line",
|
|
1626
|
-
shape: {
|
|
1627
|
-
x1: hi[0],
|
|
1628
|
-
y1: center[1] - capHalf,
|
|
1629
|
-
x2: hi[0],
|
|
1630
|
-
y2: center[1] + capHalf,
|
|
1631
|
-
},
|
|
1632
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1633
|
-
}, {
|
|
1634
|
-
type: "line",
|
|
1635
|
-
shape: {
|
|
1636
|
-
x1: lo[0],
|
|
1637
|
-
y1: center[1] - capHalf,
|
|
1638
|
-
x2: lo[0],
|
|
1639
|
-
y2: center[1] + capHalf,
|
|
1640
|
-
},
|
|
1641
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
return { type: "group", children };
|
|
1645
|
-
}
|
|
1646
|
-
const center = api.coord([api.value(0), value]);
|
|
1647
|
-
const hi = api.coord([api.value(0), value + plus]);
|
|
1648
|
-
const lo = api.coord([api.value(0), value - minus]);
|
|
1649
|
-
const children = [
|
|
1650
|
-
{
|
|
1651
|
-
type: "line",
|
|
1652
|
-
shape: { x1: center[0], y1: lo[1], x2: center[0], y2: hi[1] },
|
|
1653
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1654
|
-
},
|
|
1655
|
-
];
|
|
1656
|
-
if (!noEndCap) {
|
|
1657
|
-
children.push({
|
|
1658
|
-
type: "line",
|
|
1659
|
-
shape: {
|
|
1660
|
-
x1: center[0] - capHalf,
|
|
1661
|
-
y1: hi[1],
|
|
1662
|
-
x2: center[0] + capHalf,
|
|
1663
|
-
y2: hi[1],
|
|
1664
|
-
},
|
|
1665
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1666
|
-
}, {
|
|
1667
|
-
type: "line",
|
|
1668
|
-
shape: {
|
|
1669
|
-
x1: center[0] - capHalf,
|
|
1670
|
-
y1: lo[1],
|
|
1671
|
-
x2: center[0] + capHalf,
|
|
1672
|
-
y2: lo[1],
|
|
1673
|
-
},
|
|
1674
|
-
style: { stroke: seriesColor, lineWidth: 1 },
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1677
|
-
return { type: "group", children };
|
|
1678
|
-
},
|
|
1679
|
-
});
|
|
1680
|
-
}
|
|
1715
|
+
chartSeries.push(...buildEchartsOverlaySeries({
|
|
1716
|
+
series,
|
|
1717
|
+
values: numericValues,
|
|
1718
|
+
// A horizontal bar keeps its categories on y (#1061).
|
|
1719
|
+
horizontal: isHorizontalBarChart,
|
|
1720
|
+
dataLabel,
|
|
1721
|
+
seriesColor: series.color ?? theme.themeColors.accent1,
|
|
1722
|
+
resolveRange: (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy),
|
|
1723
|
+
}));
|
|
1681
1724
|
};
|
|
1682
1725
|
// Excel `<c:overlap>` / `<c:gapWidth>` → echarts barGap /
|
|
1683
1726
|
// barCategoryGap (see utils/bar-layout.ts for the exact mapping).
|
|
@@ -2150,17 +2193,30 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2150
2193
|
nameGap: 30,
|
|
2151
2194
|
});
|
|
2152
2195
|
chartSeries.length = 0;
|
|
2196
|
+
// The spacer bars are layout only: no labels, no tooltip entry.
|
|
2153
2197
|
chartSeries.push({
|
|
2154
2198
|
type: "bar",
|
|
2199
|
+
id: WATERFALL_PLACEHOLDER_ID,
|
|
2155
2200
|
stack: "waterfall",
|
|
2156
2201
|
itemStyle: { color: "transparent" },
|
|
2157
2202
|
emphasis: { itemStyle: { color: "transparent" } },
|
|
2158
2203
|
data: steps.map((s) => s.placeholder),
|
|
2159
2204
|
silent: true,
|
|
2205
|
+
label: { show: false },
|
|
2206
|
+
tooltip: { show: false },
|
|
2160
2207
|
});
|
|
2161
2208
|
const accent = seriesEntry?.color ?? theme.themeColors.accent1;
|
|
2162
2209
|
const decreaseColor = seriesEntry?.negativeColor ?? theme.themeColors.accent2 ?? accent;
|
|
2163
2210
|
const totalColor = theme.themeColors.accent3 ?? accent;
|
|
2211
|
+
// Per-bar colours (`<c:dPt>`) win over the increase / decrease /
|
|
2212
|
+
// total colours — an importer gives totals the workbook's own
|
|
2213
|
+
// colour and keeps bars recoloured in Excel.
|
|
2214
|
+
const pointColors = new Map();
|
|
2215
|
+
for (const dp of seriesEntry?.dataPointColors ?? []) {
|
|
2216
|
+
if (typeof dp?.idx === "number" && typeof dp?.color === "string") {
|
|
2217
|
+
pointColors.set(dp.idx, dp.color);
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2164
2220
|
// Excel-style connector lines: a horizontal segment at the running
|
|
2165
2221
|
// total after bar i, spanning across to bar i+1. Within the bars
|
|
2166
2222
|
// the segment overlaps their top/bottom edges, so only the gap
|
|
@@ -2171,16 +2227,22 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2171
2227
|
]);
|
|
2172
2228
|
chartSeries.push({
|
|
2173
2229
|
type: "bar",
|
|
2230
|
+
id: chartSeriesId(0, 0),
|
|
2174
2231
|
stack: "waterfall",
|
|
2175
2232
|
name: seriesEntry?.dataLabel ?? "",
|
|
2233
|
+
// A bar's height is the step's magnitude; the signed source value
|
|
2234
|
+
// rides along as `originalValue` so labels and tooltips read
|
|
2235
|
+
// "-90" for a decrease, like Excel.
|
|
2176
2236
|
data: steps.map((s, i) => ({
|
|
2177
2237
|
value: s.value,
|
|
2238
|
+
originalValue: rawValues[i],
|
|
2178
2239
|
itemStyle: {
|
|
2179
|
-
color:
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2240
|
+
color: pointColors.get(i) ??
|
|
2241
|
+
(s.isTotal
|
|
2242
|
+
? totalColor
|
|
2243
|
+
: rawValues[i] < 0
|
|
2244
|
+
? decreaseColor
|
|
2245
|
+
: accent),
|
|
2184
2246
|
},
|
|
2185
2247
|
})),
|
|
2186
2248
|
markLine: {
|
|
@@ -2210,14 +2272,64 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2210
2272
|
: [];
|
|
2211
2273
|
chartDomains.length = 0;
|
|
2212
2274
|
chartSeries.length = 0;
|
|
2275
|
+
// Excel draws every stage in the series colour (only `<c:dPt>`
|
|
2276
|
+
// recolours a stage), keeps the rows' order top to bottom, and
|
|
2277
|
+
// prints the values inside the bars (#1069).
|
|
2278
|
+
const funnelPalette = getChartPalette(spec, [
|
|
2279
|
+
theme.themeColors.accent1,
|
|
2280
|
+
theme.themeColors.accent2,
|
|
2281
|
+
theme.themeColors.accent3,
|
|
2282
|
+
theme.themeColors.accent4,
|
|
2283
|
+
theme.themeColors.accent5,
|
|
2284
|
+
theme.themeColors.accent6,
|
|
2285
|
+
]);
|
|
2286
|
+
const funnelColor = seriesEntry?.color ?? chartPaletteColorAt(funnelPalette, 0);
|
|
2287
|
+
const stageColors = new Map();
|
|
2288
|
+
for (const dp of seriesEntry?.dataPointColors ?? []) {
|
|
2289
|
+
if (typeof dp?.idx === "number" && typeof dp?.color === "string") {
|
|
2290
|
+
stageColors.set(dp.idx, dp.color);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
const funnelData = rawValues.map((value, i) => ({
|
|
2294
|
+
name: labels[i] ?? String(i),
|
|
2295
|
+
value,
|
|
2296
|
+
}));
|
|
2297
|
+
const funnelLayout = {
|
|
2298
|
+
sort: "none",
|
|
2299
|
+
left: "22%",
|
|
2300
|
+
right: "10%",
|
|
2301
|
+
top: 60,
|
|
2302
|
+
bottom: 20,
|
|
2303
|
+
};
|
|
2213
2304
|
chartSeries.push({
|
|
2214
2305
|
type: "funnel",
|
|
2306
|
+
id: chartSeriesId(0, 0),
|
|
2215
2307
|
name: seriesEntry?.dataLabel ?? "",
|
|
2216
|
-
data:
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2308
|
+
data: funnelData,
|
|
2309
|
+
...funnelLayout,
|
|
2310
|
+
itemStyle: {
|
|
2311
|
+
color: (params) => stageColors.get(params.dataIndex) ?? funnelColor,
|
|
2312
|
+
},
|
|
2313
|
+
label: {
|
|
2314
|
+
show: true,
|
|
2315
|
+
position: "inside",
|
|
2316
|
+
formatter: (params) => formatAxisValue(undefined, params.value, locale),
|
|
2317
|
+
},
|
|
2318
|
+
});
|
|
2319
|
+
// The stage names sit to the left of the bars, where Excel's
|
|
2320
|
+
// category axis puts them: an invisible twin funnel carries them.
|
|
2321
|
+
chartSeries.push({
|
|
2322
|
+
type: "funnel",
|
|
2323
|
+
id: FUNNEL_NAMES_ID,
|
|
2324
|
+
data: funnelData,
|
|
2325
|
+
...funnelLayout,
|
|
2326
|
+
silent: true,
|
|
2327
|
+
itemStyle: { color: "transparent", borderWidth: 0 },
|
|
2328
|
+
emphasis: { disabled: true },
|
|
2329
|
+
tooltip: { show: false },
|
|
2330
|
+
label: { show: true, position: "left", formatter: "{b}" },
|
|
2331
|
+
labelLine: { show: false },
|
|
2332
|
+
z: 1,
|
|
2221
2333
|
});
|
|
2222
2334
|
legend.data = [];
|
|
2223
2335
|
}
|
|
@@ -2551,6 +2663,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2551
2663
|
}
|
|
2552
2664
|
const seriesWithLabels = chartSeries.map((s) => {
|
|
2553
2665
|
const next = { ...s };
|
|
2666
|
+
if (DECORATION_SERIES_IDS.has(s?.id)) {
|
|
2667
|
+
return next;
|
|
2668
|
+
}
|
|
2554
2669
|
const seriesLabelOption = seriesLabelOptionFor(s);
|
|
2555
2670
|
if (seriesLabelOption) {
|
|
2556
2671
|
next.label = { ...(s?.label ?? {}), ...seriesLabelOption };
|
|
@@ -2566,10 +2681,25 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2566
2681
|
const seriesTooltipValueFormatter = VALUE_TOOLTIP_SERIES_TYPES.has(s?.type)
|
|
2567
2682
|
? valueAxisLabelFormatter(seriesNumberFormatFor(s), locale)
|
|
2568
2683
|
: undefined;
|
|
2569
|
-
|
|
2684
|
+
// A data item carrying `originalValue` (a waterfall step's signed
|
|
2685
|
+
// source value, a percent-stacked bar's raw amount) reads as that
|
|
2686
|
+
// value in the tooltip, like its data label.
|
|
2687
|
+
const originalValueAt = (dataIndex) => {
|
|
2688
|
+
const item = Array.isArray(s?.data) ? s.data[dataIndex] : undefined;
|
|
2689
|
+
return item !== null && typeof item === "object"
|
|
2690
|
+
? item.originalValue
|
|
2691
|
+
: undefined;
|
|
2692
|
+
};
|
|
2693
|
+
const carriesOriginalValues = VALUE_TOOLTIP_SERIES_TYPES.has(s?.type) &&
|
|
2694
|
+
Array.isArray(s?.data) &&
|
|
2695
|
+
s.data.some((item) => item !== null && typeof item === "object" && "originalValue" in item);
|
|
2696
|
+
if ((seriesTooltipValueFormatter || carriesOriginalValues) &&
|
|
2697
|
+
s?.tooltip?.show !== false) {
|
|
2698
|
+
const formatValue = seriesTooltipValueFormatter ??
|
|
2699
|
+
((value) => formatAxisValue(undefined, value, locale));
|
|
2570
2700
|
next.tooltip = {
|
|
2571
2701
|
...(s?.tooltip ?? {}),
|
|
2572
|
-
valueFormatter:
|
|
2702
|
+
valueFormatter: (value, dataIndex) => formatValue(originalValueAt(dataIndex) ?? value),
|
|
2573
2703
|
};
|
|
2574
2704
|
}
|
|
2575
2705
|
if (next.type === "pie") {
|