@rowsncolumns/charts 28.1.8 → 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 +483 -417
- 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 +315 -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 +9 -2
- 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 +320 -239
- 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 +235 -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 +9 -2
- 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,41 @@ 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.
|
|
106
108
|
const HIERARCHY_LEVEL_STYLES = [
|
|
107
109
|
{
|
|
108
110
|
itemStyle: {
|
|
109
|
-
gapWidth:
|
|
110
|
-
|
|
111
|
+
gapWidth: 2,
|
|
112
|
+
borderColor: "#ffffff",
|
|
113
|
+
borderWidth: 0,
|
|
111
114
|
},
|
|
112
115
|
upperLabel: { show: false },
|
|
113
116
|
},
|
|
114
117
|
{
|
|
115
|
-
colorSaturation: [0.35, 0.65],
|
|
116
118
|
itemStyle: {
|
|
117
119
|
gapWidth: 1,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
colorSaturation: [0.45, 0.75],
|
|
123
|
-
itemStyle: {
|
|
124
|
-
gapWidth: 1,
|
|
125
|
-
borderColorSaturation: 0.6,
|
|
120
|
+
borderColor: "#ffffff",
|
|
121
|
+
borderWidth: 1,
|
|
126
122
|
},
|
|
127
123
|
},
|
|
128
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%"];
|
|
129
140
|
const getHierarchyDepth = (nodes, depth = 1) => {
|
|
130
141
|
let maxDepth = depth;
|
|
131
142
|
nodes.forEach((node) => {
|
|
@@ -176,6 +187,16 @@ const buildHierarchyLevels = (maxDepth) => {
|
|
|
176
187
|
};
|
|
177
188
|
/** Series whose tooltip value is one number on a value axis. */
|
|
178
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]);
|
|
179
200
|
const defaultLegendConfig = {
|
|
180
201
|
padding: 10,
|
|
181
202
|
itemWidth: 10,
|
|
@@ -556,7 +577,38 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
556
577
|
sizeValues: sharedSizeValues,
|
|
557
578
|
series: seriesInputs,
|
|
558
579
|
});
|
|
559
|
-
|
|
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);
|
|
560
612
|
const xAxisConfig = categories.length > 0
|
|
561
613
|
? {
|
|
562
614
|
type: "category",
|
|
@@ -585,6 +637,102 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
585
637
|
// non-bubble layout below. A series' own `dataLabels` replace the
|
|
586
638
|
// chart-level ones for it (#1059).
|
|
587
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
|
+
});
|
|
588
736
|
chartRef.current?.setOption({
|
|
589
737
|
xAxis: [
|
|
590
738
|
applyAxisOverrides({ ...xAxisConfig }, spec.categoryAxisOptions),
|
|
@@ -596,6 +744,19 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
596
744
|
nameLocation: "middle",
|
|
597
745
|
nameGap: 45,
|
|
598
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
|
+
: []),
|
|
599
760
|
],
|
|
600
761
|
legend: applyLegendOverrides({
|
|
601
762
|
...legend,
|
|
@@ -612,82 +773,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
612
773
|
trigger: "item",
|
|
613
774
|
formatter: tooltipFormatter,
|
|
614
775
|
},
|
|
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
|
-
}),
|
|
776
|
+
series: [...bubbleSeries, ...bubbleOverlays],
|
|
691
777
|
}, {
|
|
692
778
|
replaceMerge: [
|
|
693
779
|
"series",
|
|
@@ -699,7 +785,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
699
785
|
],
|
|
700
786
|
});
|
|
701
787
|
previousChartType.current = spec.chartType;
|
|
702
|
-
previousSeriesCount.current =
|
|
788
|
+
previousSeriesCount.current = bubbleSeries.length + bubbleOverlays.length;
|
|
703
789
|
return;
|
|
704
790
|
}
|
|
705
791
|
if (isHierarchy) {
|
|
@@ -714,8 +800,17 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
714
800
|
hiddenDimensionStrategy: spec.hiddenDimensionStrategy,
|
|
715
801
|
});
|
|
716
802
|
hierarchyTitleRef.current = explicitChartTitle ?? headerTitle;
|
|
717
|
-
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);
|
|
718
812
|
const rootName = hierarchyData.length === 1 ? hierarchyData[0]?.name ?? "" : "";
|
|
813
|
+
const drillDown = Boolean(spec.drillDown);
|
|
719
814
|
const seriesDisplayName = explicitChartTitle ?? headerTitle ?? rootName ?? "";
|
|
720
815
|
const hierarchyDepth = getHierarchyDepth(hierarchyData);
|
|
721
816
|
const levels = buildHierarchyLevels(hierarchyDepth);
|
|
@@ -768,6 +863,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
768
863
|
label: {
|
|
769
864
|
show: true,
|
|
770
865
|
formatter: "{b}",
|
|
866
|
+
...(spec.chartType === "treemap"
|
|
867
|
+
? { position: "insideBottomLeft" }
|
|
868
|
+
: {}),
|
|
771
869
|
...(hierarchySeriesLabel ?? {}),
|
|
772
870
|
},
|
|
773
871
|
upperLabel: spec.chartType === "treemap"
|
|
@@ -777,7 +875,16 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
777
875
|
}
|
|
778
876
|
: undefined,
|
|
779
877
|
levels,
|
|
780
|
-
|
|
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] }),
|
|
781
888
|
},
|
|
782
889
|
],
|
|
783
890
|
}, {
|
|
@@ -1176,12 +1283,19 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1176
1283
|
};
|
|
1177
1284
|
chartRef.current?.setOption({
|
|
1178
1285
|
radar: {
|
|
1179
|
-
|
|
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
|
|
1180
1292
|
? indicators
|
|
1181
|
-
: [{ name: "", min: 0, max: 1 }],
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1293
|
+
: [{ name: "", min: 0, max: 1 }]).map((indicator, index) => ({
|
|
1294
|
+
...indicator,
|
|
1295
|
+
axisLabel: {
|
|
1296
|
+
show: index === 0 && radarAxisOptions?.visible !== false,
|
|
1297
|
+
},
|
|
1298
|
+
})),
|
|
1185
1299
|
axisLabel: {
|
|
1186
1300
|
show: radarAxisOptions?.visible !== false,
|
|
1187
1301
|
formatter: (value) => formatAxisValue(radarFormats[0], value, locale),
|
|
@@ -1549,135 +1663,15 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1549
1663
|
: [];
|
|
1550
1664
|
if (numericValues.length === 0)
|
|
1551
1665
|
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
|
-
}
|
|
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
|
+
}));
|
|
1681
1675
|
};
|
|
1682
1676
|
// Excel `<c:overlap>` / `<c:gapWidth>` → echarts barGap /
|
|
1683
1677
|
// barCategoryGap (see utils/bar-layout.ts for the exact mapping).
|
|
@@ -2150,17 +2144,30 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2150
2144
|
nameGap: 30,
|
|
2151
2145
|
});
|
|
2152
2146
|
chartSeries.length = 0;
|
|
2147
|
+
// The spacer bars are layout only: no labels, no tooltip entry.
|
|
2153
2148
|
chartSeries.push({
|
|
2154
2149
|
type: "bar",
|
|
2150
|
+
id: WATERFALL_PLACEHOLDER_ID,
|
|
2155
2151
|
stack: "waterfall",
|
|
2156
2152
|
itemStyle: { color: "transparent" },
|
|
2157
2153
|
emphasis: { itemStyle: { color: "transparent" } },
|
|
2158
2154
|
data: steps.map((s) => s.placeholder),
|
|
2159
2155
|
silent: true,
|
|
2156
|
+
label: { show: false },
|
|
2157
|
+
tooltip: { show: false },
|
|
2160
2158
|
});
|
|
2161
2159
|
const accent = seriesEntry?.color ?? theme.themeColors.accent1;
|
|
2162
2160
|
const decreaseColor = seriesEntry?.negativeColor ?? theme.themeColors.accent2 ?? accent;
|
|
2163
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
|
+
}
|
|
2164
2171
|
// Excel-style connector lines: a horizontal segment at the running
|
|
2165
2172
|
// total after bar i, spanning across to bar i+1. Within the bars
|
|
2166
2173
|
// the segment overlaps their top/bottom edges, so only the gap
|
|
@@ -2171,16 +2178,22 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2171
2178
|
]);
|
|
2172
2179
|
chartSeries.push({
|
|
2173
2180
|
type: "bar",
|
|
2181
|
+
id: chartSeriesId(0, 0),
|
|
2174
2182
|
stack: "waterfall",
|
|
2175
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.
|
|
2176
2187
|
data: steps.map((s, i) => ({
|
|
2177
2188
|
value: s.value,
|
|
2189
|
+
originalValue: rawValues[i],
|
|
2178
2190
|
itemStyle: {
|
|
2179
|
-
color:
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2191
|
+
color: pointColors.get(i) ??
|
|
2192
|
+
(s.isTotal
|
|
2193
|
+
? totalColor
|
|
2194
|
+
: rawValues[i] < 0
|
|
2195
|
+
? decreaseColor
|
|
2196
|
+
: accent),
|
|
2184
2197
|
},
|
|
2185
2198
|
})),
|
|
2186
2199
|
markLine: {
|
|
@@ -2210,14 +2223,64 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2210
2223
|
: [];
|
|
2211
2224
|
chartDomains.length = 0;
|
|
2212
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
|
+
};
|
|
2213
2255
|
chartSeries.push({
|
|
2214
2256
|
type: "funnel",
|
|
2257
|
+
id: chartSeriesId(0, 0),
|
|
2215
2258
|
name: seriesEntry?.dataLabel ?? "",
|
|
2216
|
-
data:
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
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,
|
|
2221
2284
|
});
|
|
2222
2285
|
legend.data = [];
|
|
2223
2286
|
}
|
|
@@ -2551,6 +2614,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2551
2614
|
}
|
|
2552
2615
|
const seriesWithLabels = chartSeries.map((s) => {
|
|
2553
2616
|
const next = { ...s };
|
|
2617
|
+
if (DECORATION_SERIES_IDS.has(s?.id)) {
|
|
2618
|
+
return next;
|
|
2619
|
+
}
|
|
2554
2620
|
const seriesLabelOption = seriesLabelOptionFor(s);
|
|
2555
2621
|
if (seriesLabelOption) {
|
|
2556
2622
|
next.label = { ...(s?.label ?? {}), ...seriesLabelOption };
|
|
@@ -2566,10 +2632,25 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2566
2632
|
const seriesTooltipValueFormatter = VALUE_TOOLTIP_SERIES_TYPES.has(s?.type)
|
|
2567
2633
|
? valueAxisLabelFormatter(seriesNumberFormatFor(s), locale)
|
|
2568
2634
|
: undefined;
|
|
2569
|
-
|
|
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));
|
|
2570
2651
|
next.tooltip = {
|
|
2571
2652
|
...(s?.tooltip ?? {}),
|
|
2572
|
-
valueFormatter:
|
|
2653
|
+
valueFormatter: (value, dataIndex) => formatValue(originalValueAt(dataIndex) ?? value),
|
|
2573
2654
|
};
|
|
2574
2655
|
}
|
|
2575
2656
|
if (next.type === "pie") {
|