@rowsncolumns/charts 27.0.34 → 28.1.0
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/atoms/pivot-chart.atom.d.ts +14 -0
- package/dist/cjs/atoms/pivot-chart.atom.d.ts.map +1 -0
- package/dist/cjs/atoms/pivot-chart.atom.js +9 -0
- package/dist/cjs/basic-chart/basic-chart.d.ts +1 -1
- package/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/basic-chart/basic-chart.js +863 -371
- package/dist/cjs/components/dialogs/pivot-chart-dialog.d.ts +39 -0
- package/dist/cjs/components/dialogs/pivot-chart-dialog.d.ts.map +1 -0
- package/dist/cjs/components/dialogs/pivot-chart-dialog.js +100 -0
- package/dist/cjs/components/editors/chart-editor.d.ts +13 -1
- package/dist/cjs/components/editors/chart-editor.d.ts.map +1 -1
- package/dist/cjs/components/editors/chart-editor.js +225 -59
- package/dist/cjs/config.d.ts.map +1 -1
- package/dist/cjs/config.js +149 -12
- package/dist/cjs/hooks/use-charts.d.ts +21 -6
- package/dist/cjs/hooks/use-charts.d.ts.map +1 -1
- package/dist/cjs/hooks/use-charts.js +13 -1
- package/dist/cjs/hooks/use-on-create-chart.d.ts.map +1 -1
- package/dist/cjs/hooks/use-on-create-chart.js +7 -1
- package/dist/cjs/hooks/use-on-move-chart-to-sheet.d.ts +12 -0
- package/dist/cjs/hooks/use-on-move-chart-to-sheet.d.ts.map +1 -0
- package/dist/cjs/hooks/use-on-move-chart-to-sheet.js +26 -0
- package/dist/cjs/hooks/use-on-move-chart.d.ts +1 -1
- package/dist/cjs/hooks/use-on-move-chart.d.ts.map +1 -1
- package/dist/cjs/hooks/use-on-move-chart.js +2 -2
- package/dist/cjs/hooks/use-on-resize-chart.d.ts +1 -1
- package/dist/cjs/hooks/use-on-resize-chart.d.ts.map +1 -1
- package/dist/cjs/hooks/use-on-resize-chart.js +2 -2
- package/dist/cjs/index.d.ts +11 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +34 -1
- package/dist/cjs/interface/create-chart.d.ts.map +1 -1
- package/dist/cjs/interface/create-chart.js +33 -42
- package/dist/cjs/interface/delete-chart.d.ts.map +1 -1
- package/dist/cjs/interface/delete-chart.js +4 -0
- package/dist/cjs/interface/move-chart-to-sheet.d.ts +13 -0
- package/dist/cjs/interface/move-chart-to-sheet.d.ts.map +1 -0
- package/dist/cjs/interface/move-chart-to-sheet.js +136 -0
- package/dist/cjs/interface/move-chart.d.ts +2 -2
- package/dist/cjs/interface/move-chart.d.ts.map +1 -1
- package/dist/cjs/interface/move-chart.js +7 -1
- package/dist/cjs/interface/resize-chart.d.ts +2 -2
- package/dist/cjs/interface/resize-chart.d.ts.map +1 -1
- package/dist/cjs/interface/resize-chart.js +7 -1
- package/dist/cjs/interface/update-chart.d.ts.map +1 -1
- package/dist/cjs/interface/update-chart.js +4 -0
- package/dist/cjs/plotly/basic-chart/basic-chart.d.ts +1 -1
- package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/plotly/basic-chart/basic-chart.js +970 -585
- package/dist/cjs/plotly/basic-chart/helpers.d.ts.map +1 -1
- package/dist/cjs/plotly/basic-chart/helpers.js +3 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/types.d.ts +7 -0
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/utils/axis-number-format.d.ts +69 -0
- package/dist/cjs/utils/axis-number-format.d.ts.map +1 -0
- package/dist/cjs/utils/axis-number-format.js +217 -0
- package/dist/cjs/utils/category-levels.d.ts +27 -0
- package/dist/cjs/utils/category-levels.d.ts.map +1 -0
- package/dist/cjs/utils/category-levels.js +80 -0
- package/dist/cjs/utils/chart-image.d.ts +8 -0
- package/dist/cjs/utils/chart-image.d.ts.map +1 -0
- package/dist/cjs/utils/chart-image.js +28 -0
- package/dist/cjs/utils/chart-orientation.d.ts +67 -0
- package/dist/cjs/utils/chart-orientation.d.ts.map +1 -0
- package/dist/cjs/utils/chart-orientation.js +265 -0
- package/dist/cjs/utils/chart-spec-utils.d.ts +3 -0
- package/dist/cjs/utils/chart-spec-utils.d.ts.map +1 -1
- package/dist/cjs/utils/chart-spec-utils.js +12 -4
- package/dist/cjs/utils/chart-styles.d.ts +75 -0
- package/dist/cjs/utils/chart-styles.d.ts.map +1 -0
- package/dist/cjs/utils/chart-styles.js +283 -0
- package/dist/cjs/utils/empty-cells.d.ts +30 -0
- package/dist/cjs/utils/empty-cells.d.ts.map +1 -0
- package/dist/cjs/utils/empty-cells.js +91 -0
- package/dist/cjs/utils/overrides.d.ts +6 -1
- package/dist/cjs/utils/overrides.d.ts.map +1 -1
- package/dist/cjs/utils/overrides.js +19 -7
- package/dist/cjs/utils/pie-explosion.d.ts +30 -0
- package/dist/cjs/utils/pie-explosion.d.ts.map +1 -0
- package/dist/cjs/utils/pie-explosion.js +55 -0
- package/dist/cjs/utils/plotly-click.d.ts +34 -0
- package/dist/cjs/utils/plotly-click.d.ts.map +1 -0
- package/dist/cjs/utils/plotly-click.js +49 -0
- package/dist/cjs/utils/plotly-surface.d.ts +14 -0
- package/dist/cjs/utils/plotly-surface.d.ts.map +1 -0
- package/dist/cjs/utils/plotly-surface.js +64 -0
- package/dist/cjs/utils/plotly-title.d.ts +12 -0
- package/dist/cjs/utils/plotly-title.d.ts.map +1 -0
- package/dist/cjs/utils/plotly-title.js +24 -0
- package/dist/cjs/utils/series-click.d.ts +39 -0
- package/dist/cjs/utils/series-click.d.ts.map +1 -0
- package/dist/cjs/utils/series-click.js +75 -0
- package/dist/cjs/utils/three-d.d.ts +17 -1
- package/dist/cjs/utils/three-d.d.ts.map +1 -1
- package/dist/cjs/utils/three-d.js +32 -4
- package/dist/esm/atoms/pivot-chart.atom.d.ts +14 -0
- package/dist/esm/atoms/pivot-chart.atom.d.ts.map +1 -0
- package/dist/esm/atoms/pivot-chart.atom.js +4 -0
- package/dist/esm/basic-chart/basic-chart.d.ts +1 -1
- package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/basic-chart/basic-chart.js +680 -178
- package/dist/esm/components/dialogs/pivot-chart-dialog.d.ts +39 -0
- package/dist/esm/components/dialogs/pivot-chart-dialog.d.ts.map +1 -0
- package/dist/esm/components/dialogs/pivot-chart-dialog.js +89 -0
- package/dist/esm/components/editors/chart-editor.d.ts +13 -1
- package/dist/esm/components/editors/chart-editor.d.ts.map +1 -1
- package/dist/esm/components/editors/chart-editor.js +164 -20
- package/dist/esm/config.d.ts.map +1 -1
- package/dist/esm/config.js +149 -12
- package/dist/esm/hooks/use-charts.d.ts +21 -6
- package/dist/esm/hooks/use-charts.d.ts.map +1 -1
- package/dist/esm/hooks/use-charts.js +13 -1
- package/dist/esm/hooks/use-on-create-chart.d.ts.map +1 -1
- package/dist/esm/hooks/use-on-create-chart.js +7 -1
- package/dist/esm/hooks/use-on-move-chart-to-sheet.d.ts +12 -0
- package/dist/esm/hooks/use-on-move-chart-to-sheet.d.ts.map +1 -0
- package/dist/esm/hooks/use-on-move-chart-to-sheet.js +19 -0
- package/dist/esm/hooks/use-on-move-chart.d.ts +1 -1
- package/dist/esm/hooks/use-on-move-chart.d.ts.map +1 -1
- package/dist/esm/hooks/use-on-move-chart.js +2 -2
- package/dist/esm/hooks/use-on-resize-chart.d.ts +1 -1
- package/dist/esm/hooks/use-on-resize-chart.d.ts.map +1 -1
- package/dist/esm/hooks/use-on-resize-chart.js +2 -2
- package/dist/esm/index.d.ts +11 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +14 -0
- package/dist/esm/interface/create-chart.d.ts.map +1 -1
- package/dist/esm/interface/create-chart.js +36 -58
- package/dist/esm/interface/delete-chart.d.ts.map +1 -1
- package/dist/esm/interface/delete-chart.js +4 -0
- package/dist/esm/interface/move-chart-to-sheet.d.ts +13 -0
- package/dist/esm/interface/move-chart-to-sheet.d.ts.map +1 -0
- package/dist/esm/interface/move-chart-to-sheet.js +126 -0
- package/dist/esm/interface/move-chart.d.ts +2 -2
- package/dist/esm/interface/move-chart.d.ts.map +1 -1
- package/dist/esm/interface/move-chart.js +7 -1
- package/dist/esm/interface/resize-chart.d.ts +2 -2
- package/dist/esm/interface/resize-chart.d.ts.map +1 -1
- package/dist/esm/interface/resize-chart.js +7 -1
- package/dist/esm/interface/update-chart.d.ts.map +1 -1
- package/dist/esm/interface/update-chart.js +4 -0
- package/dist/esm/plotly/basic-chart/basic-chart.d.ts +1 -1
- package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/plotly/basic-chart/basic-chart.js +858 -463
- package/dist/esm/plotly/basic-chart/helpers.d.ts.map +1 -1
- package/dist/esm/plotly/basic-chart/helpers.js +3 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/types.d.ts +7 -0
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/utils/axis-number-format.d.ts +69 -0
- package/dist/esm/utils/axis-number-format.d.ts.map +1 -0
- package/dist/esm/utils/axis-number-format.js +170 -0
- package/dist/esm/utils/category-levels.d.ts +27 -0
- package/dist/esm/utils/category-levels.d.ts.map +1 -0
- package/dist/esm/utils/category-levels.js +65 -0
- package/dist/esm/utils/chart-image.d.ts +8 -0
- package/dist/esm/utils/chart-image.d.ts.map +1 -0
- package/dist/esm/utils/chart-image.js +24 -0
- package/dist/esm/utils/chart-orientation.d.ts +67 -0
- package/dist/esm/utils/chart-orientation.d.ts.map +1 -0
- package/dist/esm/utils/chart-orientation.js +234 -0
- package/dist/esm/utils/chart-spec-utils.d.ts +3 -0
- package/dist/esm/utils/chart-spec-utils.d.ts.map +1 -1
- package/dist/esm/utils/chart-spec-utils.js +10 -2
- package/dist/esm/utils/chart-styles.d.ts +75 -0
- package/dist/esm/utils/chart-styles.d.ts.map +1 -0
- package/dist/esm/utils/chart-styles.js +357 -0
- package/dist/esm/utils/empty-cells.d.ts +30 -0
- package/dist/esm/utils/empty-cells.d.ts.map +1 -0
- package/dist/esm/utils/empty-cells.js +67 -0
- package/dist/esm/utils/overrides.d.ts +6 -1
- package/dist/esm/utils/overrides.d.ts.map +1 -1
- package/dist/esm/utils/overrides.js +14 -2
- package/dist/esm/utils/pie-explosion.d.ts +30 -0
- package/dist/esm/utils/pie-explosion.d.ts.map +1 -0
- package/dist/esm/utils/pie-explosion.js +43 -0
- package/dist/esm/utils/plotly-click.d.ts +34 -0
- package/dist/esm/utils/plotly-click.d.ts.map +1 -0
- package/dist/esm/utils/plotly-click.js +44 -0
- package/dist/esm/utils/plotly-surface.d.ts +14 -0
- package/dist/esm/utils/plotly-surface.d.ts.map +1 -0
- package/dist/esm/utils/plotly-surface.js +45 -0
- package/dist/esm/utils/plotly-title.d.ts +12 -0
- package/dist/esm/utils/plotly-title.d.ts.map +1 -0
- package/dist/esm/utils/plotly-title.js +20 -0
- package/dist/esm/utils/series-click.d.ts +39 -0
- package/dist/esm/utils/series-click.d.ts.map +1 -0
- package/dist/esm/utils/series-click.js +66 -0
- package/dist/esm/utils/three-d.d.ts +17 -1
- package/dist/esm/utils/three-d.d.ts.map +1 -1
- package/dist/esm/utils/three-d.js +29 -3
- package/package.json +16 -12
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { ChevronRightIcon, DotsVerticalIcon } from "@rowsncolumns/icons";
|
|
2
|
-
import { getStringifiedColor, useFocusSheet } from "@rowsncolumns/spreadsheet";
|
|
2
|
+
import { getStringifiedColor, useFocusSheet, useRegisterChartActions, useSetSelectedChartSeries, } from "@rowsncolumns/spreadsheet";
|
|
3
3
|
import { useSubscribeToMultipleRanges } from "@rowsncolumns/spreadsheet-state";
|
|
4
4
|
import { createDownloadableFile } from "@rowsncolumns/toolkit";
|
|
5
5
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, DropdownRightSlot, IconButton, useIsomorphicLayoutEffect, useOnVisibilityChange, } from "@rowsncolumns/ui";
|
|
6
6
|
import { debug, isAFormula, isNil } from "@rowsncolumns/utils";
|
|
7
|
-
import { BarChart, BoxplotChart, CandlestickChart, CustomChart, FunnelChart, LineChart, PieChart, RadarChart as EchartsRadarChart, ScatterChart, SunburstChart, TreemapChart as EchartsTreemapChart, } from "echarts/charts";
|
|
8
|
-
import { DatasetComponent, GridComponent, LegendComponent, RadarComponent, TitleComponent, TooltipComponent, } from "echarts/components";
|
|
7
|
+
import { BarChart, BoxplotChart, CandlestickChart, CustomChart, FunnelChart, HeatmapChart, LineChart, PieChart, RadarChart as EchartsRadarChart, ScatterChart, SunburstChart, TreemapChart as EchartsTreemapChart, } from "echarts/charts";
|
|
8
|
+
import { DatasetComponent, GridComponent, LegendComponent, RadarComponent, TitleComponent, TooltipComponent, VisualMapComponent, } from "echarts/components";
|
|
9
9
|
import * as echarts from "echarts/core";
|
|
10
10
|
import { CanvasRenderer } from "echarts/renderers";
|
|
11
11
|
import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
12
12
|
import { aggregateSeriesByDomain, getDomainValuesForAxis, sanitizeDomainValues, shouldAggregateSeries, } from "../utils/aggregation";
|
|
13
|
+
import { circularTooltipFormatter, formatAxisValue, resolveSeriesNumberFormats, resolveSourceNumberFormat, resolveValueAxisNumberFormat, tooltipValueFormatter, valueAxisLabelFormatter, } from "../utils/axis-number-format";
|
|
13
14
|
import { gapWidthToBarCategoryGap, overlapToBarGap, } from "../utils/bar-layout";
|
|
14
15
|
import { computeBoxplotSummary } from "../utils/boxplot";
|
|
15
16
|
import { createBubbleSizeScale, prepareBubbleSeriesData, } from "../utils/bubble";
|
|
16
17
|
import { coerceCalculatedLabel, getChartDataRanges, } from "../utils/chart-spec-utils";
|
|
18
|
+
import { canSwitchRowColumn, cellLabelGetterFromRangeReader, switchRowColumn, } from "../utils/chart-orientation";
|
|
19
|
+
import { dataUrlToBlob } from "../utils/chart-image";
|
|
20
|
+
import { getChartPalette } from "../utils/chart-styles";
|
|
21
|
+
import { applyEmptyCellsPolicy, connectsAcrossEmptyCells, isLineLikeChartType, } from "../utils/empty-cells";
|
|
17
22
|
import { getEchartsGlState, loadEchartsGl, } from "../utils/echarts-gl-loader";
|
|
18
23
|
import { computeErrorBars, resolveCustomErrorBarValues, } from "../utils/error-bars";
|
|
24
|
+
import { echartsGroupedCategoryAxes, resolveCategoryLevels, } from "../utils/category-levels";
|
|
19
25
|
import { gradientToEcharts } from "../utils/gradient";
|
|
20
26
|
import { buildHierarchyTree, extractHierarchyRows, getHierarchyHeaderTitle, } from "../utils/hierarchy";
|
|
21
27
|
import { binHistogram, computePareto, isParetoSpec, } from "../utils/histogram";
|
|
@@ -24,8 +30,10 @@ import { applyAxisOverrides, applyLegendOverrides, buildSeriesLabelOption, } fro
|
|
|
24
30
|
import { buildRadarIndicators, buildRadarSeriesData } from "../utils/radar";
|
|
25
31
|
import { buildRegionMapData, buildSequentialColorRamp, isRegionMapSpec, resolveGeoScope, warnOnceForUnmatchedRegions, } from "../utils/region-map";
|
|
26
32
|
import { loadRegionMapGeo } from "../utils/region-map-loader";
|
|
27
|
-
import { toCandlestick, toLineSeries, toVolumeSeries, } from "../utils/stock";
|
|
28
|
-
import {
|
|
33
|
+
import { STOCK_ROLES, toCandlestick, toLineSeries, toVolumeSeries, } from "../utils/stock";
|
|
34
|
+
import { explosionOffsetPx, pieRadiusPx, resolveExplosion, } from "../utils/pie-explosion";
|
|
35
|
+
import { chartSeriesId, chartSeriesSpecIndex, resolveClickedDataIndex, resolveClickedSeriesIndex, } from "../utils/series-click";
|
|
36
|
+
import { buildBar3DData, buildContourData, buildLine3DSeries, buildSurfaceData, getCategoryCount, getValueExtent, isContourSpec, resolve3DRenderKind, SURFACE_COLOR_RAMP, } from "../utils/three-d";
|
|
29
37
|
import { computeTrendline } from "../utils/trendline";
|
|
30
38
|
import { computeWaterfall } from "../utils/waterfall";
|
|
31
39
|
import { isWebGLAvailable } from "../utils/webgl";
|
|
@@ -164,13 +172,15 @@ const buildHierarchyLevels = (maxDepth) => {
|
|
|
164
172
|
}
|
|
165
173
|
return levels;
|
|
166
174
|
};
|
|
175
|
+
/** Series whose tooltip value is one number on a value axis. */
|
|
176
|
+
const VALUE_TOOLTIP_SERIES_TYPES = new Set(["bar", "line", "scatter", "funnel"]);
|
|
167
177
|
const defaultLegendConfig = {
|
|
168
178
|
padding: 10,
|
|
169
179
|
itemWidth: 10,
|
|
170
180
|
itemHeight: 4,
|
|
171
181
|
top: 20,
|
|
172
182
|
};
|
|
173
|
-
export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, onDeleteChart, onRequestEdit, onRequestCalculate, isSelected, }) => {
|
|
183
|
+
export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, getFormattingFromRange, onDeleteChart, onUpdateChart, onRequestEdit, onRequestCalculate, isSelected, fieldButtons, }) => {
|
|
174
184
|
const spec = chart.spec;
|
|
175
185
|
const explicitChartTitle = chart.spec.title ?? undefined;
|
|
176
186
|
const hierarchyTitleRef = useRef(explicitChartTitle);
|
|
@@ -178,6 +188,47 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
178
188
|
const el = useRef(null);
|
|
179
189
|
const chartRef = useRef(undefined);
|
|
180
190
|
const focusSheet = useFocusSheet();
|
|
191
|
+
// Excel selects a data series when it is clicked inside the chart and
|
|
192
|
+
// the grid highlights that series' source range; clicking empty chart
|
|
193
|
+
// area steps back to the whole chart. Published for the grid to draw.
|
|
194
|
+
const setSelectedChartSeries = useSetSelectedChartSeries();
|
|
195
|
+
const chartForClickRef = useRef(chart);
|
|
196
|
+
chartForClickRef.current = chart;
|
|
197
|
+
// An exploded pie draws every slice in echarts' `select` state (the only
|
|
198
|
+
// per-slice offset it has); echarts unselects a slice on click, so that
|
|
199
|
+
// click is folded back into a select to keep the slice out.
|
|
200
|
+
const explodedRef = useRef(false);
|
|
201
|
+
const bindSeriesSelection = (instance) => {
|
|
202
|
+
instance.on("click", (params) => {
|
|
203
|
+
const current = chartForClickRef.current;
|
|
204
|
+
const clickParams = params;
|
|
205
|
+
const seriesIndex = resolveClickedSeriesIndex(clickParams, current.spec.series?.length ?? 0);
|
|
206
|
+
if (seriesIndex === undefined)
|
|
207
|
+
return;
|
|
208
|
+
const dataIndex = resolveClickedDataIndex(clickParams);
|
|
209
|
+
setSelectedChartSeries({
|
|
210
|
+
chartId: current.chartId,
|
|
211
|
+
seriesIndex,
|
|
212
|
+
...(dataIndex === undefined ? {} : { dataIndex }),
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
instance.on("selectchanged", (event) => {
|
|
216
|
+
const { fromAction, isFromClick, fromActionPayload } = event;
|
|
217
|
+
if (!explodedRef.current || !isFromClick || fromAction !== "unselect") {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
instance.dispatchAction({
|
|
221
|
+
type: "select",
|
|
222
|
+
seriesIndex: fromActionPayload?.seriesIndex,
|
|
223
|
+
dataIndex: fromActionPayload?.dataIndexInside ?? fromActionPayload?.dataIndex,
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
instance.getZr().on("click", (event) => {
|
|
227
|
+
if (!event.target) {
|
|
228
|
+
setSelectedChartSeries(undefined);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
};
|
|
181
232
|
const shouldFocusSheetRef = useRef(false);
|
|
182
233
|
const previousChartType = useRef(undefined);
|
|
183
234
|
const previousSeriesCount = useRef(undefined);
|
|
@@ -191,10 +242,15 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
191
242
|
// visualMap) doesn't merge back into a cartesian one, so the instance
|
|
192
243
|
// is re-initialized when entering/leaving the region-map rendering.
|
|
193
244
|
const renderedAsRegionMap = useRef(false);
|
|
245
|
+
// Contour (Excel's flat Surface view) renders as a heatmap with a
|
|
246
|
+
// visualMap; that component doesn't merge back into a plain cartesian
|
|
247
|
+
// option, so the instance is re-initialized when entering/leaving it.
|
|
248
|
+
const renderedAsContour = useRef(false);
|
|
194
249
|
// Initialize line chart
|
|
195
250
|
const initializeChart = () => {
|
|
196
251
|
if (el.current && !chartRef.current) {
|
|
197
252
|
chartRef.current = echarts.init(el.current);
|
|
253
|
+
bindSeriesSelection(chartRef.current);
|
|
198
254
|
chartRef.current.setOption({
|
|
199
255
|
title: {
|
|
200
256
|
left: "top",
|
|
@@ -260,6 +316,16 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
260
316
|
}
|
|
261
317
|
const numericValues = seriesValues;
|
|
262
318
|
if (spec.chartType === "pie") {
|
|
319
|
+
if (options.pieLabels) {
|
|
320
|
+
const { domainLabels, offset } = options.pieLabels;
|
|
321
|
+
const labels = offset === undefined
|
|
322
|
+
? domainLabels
|
|
323
|
+
: domainLabels.slice(offset, offset + numericValues.length);
|
|
324
|
+
return labels.map((name, idx) => ({
|
|
325
|
+
value: numericValues[idx],
|
|
326
|
+
name,
|
|
327
|
+
}));
|
|
328
|
+
}
|
|
263
329
|
const domainSheetRange = spec.domains[0]?.sources[0];
|
|
264
330
|
if (!domainSheetRange) {
|
|
265
331
|
return [];
|
|
@@ -270,7 +336,9 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
270
336
|
name,
|
|
271
337
|
}));
|
|
272
338
|
}
|
|
273
|
-
|
|
339
|
+
// Excel's "Hidden and Empty Cells": gaps / zero / connect, and
|
|
340
|
+
// whether #N/A counts as empty.
|
|
341
|
+
return applyEmptyCellsPolicy(numericValues, rawSeriesValues, spec, options.lineLike);
|
|
274
342
|
}, [getSeriesValuesFromRange, getDomainValuesFromRange]);
|
|
275
343
|
// Resize a chart
|
|
276
344
|
const resizeChart = () => {
|
|
@@ -353,6 +421,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
353
421
|
}
|
|
354
422
|
if (el.current && !chartRef.current) {
|
|
355
423
|
chartRef.current = echarts.init(el.current);
|
|
424
|
+
bindSeriesSelection(chartRef.current);
|
|
356
425
|
}
|
|
357
426
|
if (!geo) {
|
|
358
427
|
// Map asset failed to load (or unknown scope) — the loader
|
|
@@ -633,15 +702,22 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
633
702
|
const seriesDisplayName = explicitChartTitle ?? headerTitle ?? rootName ?? "";
|
|
634
703
|
const hierarchyDepth = getHierarchyDepth(hierarchyData);
|
|
635
704
|
const levels = buildHierarchyLevels(hierarchyDepth);
|
|
705
|
+
// Sizes format like the cells they were read from (Excel's "Linked
|
|
706
|
+
// to source"), unless the data labels carry their own pattern.
|
|
707
|
+
const sizeNumberFormat = resolveSourceNumberFormat(spec.sizeData?.[0]
|
|
708
|
+
?.sources, getFormattingFromRange, spec.hiddenDimensionStrategy);
|
|
636
709
|
const tooltipFormatter = (params) => {
|
|
637
710
|
const value = Array.isArray(params.value)
|
|
638
711
|
? params.value[params.value.length - 1]
|
|
639
712
|
: params.value ?? "";
|
|
640
|
-
return `${params.name}: ${value}`;
|
|
713
|
+
return `${params.name}: ${formatAxisValue(sizeNumberFormat, value)}`;
|
|
641
714
|
};
|
|
642
715
|
// Spec-level overrides — treemap/sunburst default to no legend,
|
|
643
716
|
// but an authored legend (visibility/position) still wins.
|
|
644
|
-
const
|
|
717
|
+
const hierarchyDataLabels = spec.dataLabels;
|
|
718
|
+
const hierarchySeriesLabel = buildSeriesLabelOption(hierarchyDataLabels && !hierarchyDataLabels.numberFormatPattern
|
|
719
|
+
? { ...hierarchyDataLabels, numberFormatPattern: sizeNumberFormat }
|
|
720
|
+
: hierarchyDataLabels);
|
|
645
721
|
chartRef.current?.setOption({
|
|
646
722
|
xAxis: [
|
|
647
723
|
{
|
|
@@ -708,6 +784,129 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
708
784
|
debug("Unsupported chart type", spec.chartType);
|
|
709
785
|
return;
|
|
710
786
|
}
|
|
787
|
+
// --------------------------- Contour ---------------------------
|
|
788
|
+
// Excel's Contour (`surfaceChart`) is the 3-D Surface seen from above:
|
|
789
|
+
// a categories × series grid colored by value. Drawn as a cartesian
|
|
790
|
+
// heatmap (no WebGL), distinct from the SURFACE_3D branch below.
|
|
791
|
+
const isContour = isContourSpec(spec);
|
|
792
|
+
if (renderedAsContour.current && !isContour) {
|
|
793
|
+
chartRef.current?.dispose();
|
|
794
|
+
chartRef.current = undefined;
|
|
795
|
+
renderedAsContour.current = false;
|
|
796
|
+
previousChartType.current = undefined;
|
|
797
|
+
previousSeriesCount.current = undefined;
|
|
798
|
+
initializeChart();
|
|
799
|
+
applyThemeOptions();
|
|
800
|
+
}
|
|
801
|
+
if (isContour) {
|
|
802
|
+
const categories = flattenChartDataValues(spec.domains, getDomainValuesFromRange, spec.hiddenDimensionStrategy, false).map((v) => String(v ?? ""));
|
|
803
|
+
const contourSeriesInputs = [];
|
|
804
|
+
for (const seriesEntry of spec.series ?? []) {
|
|
805
|
+
const dataLabel = !isNil(seriesEntry.dataLabel) && isAFormula(seriesEntry.dataLabel)
|
|
806
|
+
? coerceCalculatedLabel(await onRequestCalculate(seriesEntry.dataLabel, position.sheetId ?? sheetId, position.overlayPosition.anchorCell.rowIndex, position.overlayPosition.anchorCell.columnIndex))
|
|
807
|
+
: seriesEntry.dataLabel ?? "";
|
|
808
|
+
const values = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map(sanitizeChartValue);
|
|
809
|
+
contourSeriesInputs.push({ name: dataLabel, values });
|
|
810
|
+
}
|
|
811
|
+
const seriesNames = contourSeriesInputs.map((entry, index) => entry.name || `Series ${index + 1}`);
|
|
812
|
+
const contourPoints = buildContourData(contourSeriesInputs);
|
|
813
|
+
const [zMin, zMax] = getValueExtent(contourPoints.filter((point) => typeof point[2] === "number"));
|
|
814
|
+
// Values format like the cells of the series (row) they came from.
|
|
815
|
+
const contourFormats = resolveSeriesNumberFormats(spec.series, getFormattingFromRange, spec.hiddenDimensionStrategy);
|
|
816
|
+
const contourLabel = buildSeriesLabelOption(spec.dataLabels);
|
|
817
|
+
if (!renderedAsContour.current && chartRef.current) {
|
|
818
|
+
chartRef.current.dispose();
|
|
819
|
+
chartRef.current = undefined;
|
|
820
|
+
}
|
|
821
|
+
if (el.current && !chartRef.current) {
|
|
822
|
+
chartRef.current = echarts.init(el.current);
|
|
823
|
+
bindSeriesSelection(chartRef.current);
|
|
824
|
+
}
|
|
825
|
+
chartRef.current?.setOption({
|
|
826
|
+
tooltip: {
|
|
827
|
+
textStyle: { fontSize: 12 },
|
|
828
|
+
trigger: "item",
|
|
829
|
+
formatter: (params) => {
|
|
830
|
+
const [x, y, z] = (params?.value ?? []);
|
|
831
|
+
const value = z === "-" || z === undefined
|
|
832
|
+
? ""
|
|
833
|
+
: formatAxisValue(contourFormats[y], z);
|
|
834
|
+
return `${seriesNames[y] ?? ""}<br/>${categories[x] ?? ""}: ${value}`;
|
|
835
|
+
},
|
|
836
|
+
},
|
|
837
|
+
legend: applyLegendOverrides({ ...defaultLegendConfig, show: false }, spec.legend),
|
|
838
|
+
grid: { left: "3%", right: "12%", bottom: "3%", containLabel: true },
|
|
839
|
+
xAxis: [
|
|
840
|
+
applyAxisOverrides({
|
|
841
|
+
type: "category",
|
|
842
|
+
data: categories,
|
|
843
|
+
splitArea: { show: true },
|
|
844
|
+
name: spec.horizontalAxisTitle ?? "",
|
|
845
|
+
nameLocation: "middle",
|
|
846
|
+
nameGap: 30,
|
|
847
|
+
}, spec.categoryAxisOptions),
|
|
848
|
+
],
|
|
849
|
+
yAxis: [
|
|
850
|
+
{
|
|
851
|
+
type: "category",
|
|
852
|
+
data: seriesNames,
|
|
853
|
+
splitArea: { show: true },
|
|
854
|
+
name: spec.verticalAxisTitle ?? "",
|
|
855
|
+
nameLocation: "middle",
|
|
856
|
+
nameGap: 50,
|
|
857
|
+
},
|
|
858
|
+
],
|
|
859
|
+
visualMap: [
|
|
860
|
+
{
|
|
861
|
+
type: "continuous",
|
|
862
|
+
min: zMin,
|
|
863
|
+
max: zMax,
|
|
864
|
+
calculable: false,
|
|
865
|
+
orient: "vertical",
|
|
866
|
+
right: 0,
|
|
867
|
+
top: "center",
|
|
868
|
+
inRange: { color: [...SURFACE_COLOR_RAMP] },
|
|
869
|
+
formatter: (value) => formatAxisValue(contourFormats[0], value),
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
series: [
|
|
873
|
+
{
|
|
874
|
+
type: "heatmap",
|
|
875
|
+
id: chartSeriesId(0, 0),
|
|
876
|
+
data: contourPoints,
|
|
877
|
+
...(contourLabel
|
|
878
|
+
? {
|
|
879
|
+
label: {
|
|
880
|
+
...contourLabel,
|
|
881
|
+
position: "inside",
|
|
882
|
+
formatter: (params) => {
|
|
883
|
+
const [, y, z] = (params?.value ?? []);
|
|
884
|
+
return z === "-" || z === undefined
|
|
885
|
+
? ""
|
|
886
|
+
: formatAxisValue(contourFormats[y], z);
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
}
|
|
890
|
+
: {}),
|
|
891
|
+
},
|
|
892
|
+
],
|
|
893
|
+
}, {
|
|
894
|
+
replaceMerge: [
|
|
895
|
+
"series",
|
|
896
|
+
"xAxis",
|
|
897
|
+
"yAxis",
|
|
898
|
+
"visualMap",
|
|
899
|
+
"legend",
|
|
900
|
+
"grid",
|
|
901
|
+
"tooltip",
|
|
902
|
+
],
|
|
903
|
+
});
|
|
904
|
+
renderedAsContour.current = true;
|
|
905
|
+
applyThemeOptions();
|
|
906
|
+
previousChartType.current = spec.chartType;
|
|
907
|
+
previousSeriesCount.current = 1;
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
711
910
|
// ----------------------------- 3-D -----------------------------
|
|
712
911
|
// True 3-D rendering for degraded Excel 3-D / surface payloads,
|
|
713
912
|
// keyed off `sourceChartType` (see the gating block above):
|
|
@@ -729,14 +928,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
729
928
|
threeDSeriesInputs.push({ name: dataLabel, values });
|
|
730
929
|
}
|
|
731
930
|
const seriesNames = threeDSeriesInputs.map((entry, index) => entry.name || `Series ${index + 1}`);
|
|
732
|
-
const threeDPalette = [
|
|
931
|
+
const threeDPalette = getChartPalette(spec, [
|
|
733
932
|
theme.themeColors.accent1,
|
|
734
933
|
theme.themeColors.accent2,
|
|
735
934
|
theme.themeColors.accent3,
|
|
736
935
|
theme.themeColors.accent4,
|
|
737
936
|
theme.themeColors.accent5,
|
|
738
937
|
theme.themeColors.accent6,
|
|
739
|
-
];
|
|
938
|
+
]);
|
|
740
939
|
const seriesColorAt = (index) => spec.series?.[index]?.color ??
|
|
741
940
|
threeDPalette[index % threeDPalette.length];
|
|
742
941
|
// Entering 3-D from a 2-D render: the cartesian option doesn't
|
|
@@ -748,22 +947,71 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
748
947
|
}
|
|
749
948
|
if (el.current && !chartRef.current) {
|
|
750
949
|
chartRef.current = echarts.init(el.current);
|
|
950
|
+
bindSeriesSelection(chartRef.current);
|
|
751
951
|
}
|
|
752
952
|
// Value axes label integer positions with the category / series
|
|
753
953
|
// name they map to; fractional ticks stay blank.
|
|
754
954
|
const categoryAxisLabel = (value) => Number.isInteger(value) ? categories[value] ?? "" : "";
|
|
755
955
|
const seriesAxisLabel = (value) => Number.isInteger(value) ? seriesNames[value] ?? "" : "";
|
|
956
|
+
// Excel's 3-D Stacked / 3-D 100% Stacked column and bar: the series
|
|
957
|
+
// share one depth row and stack on the value axis (echarts-gl bar3D
|
|
958
|
+
// stacks through echarts' data stacking) instead of fanning out one
|
|
959
|
+
// depth row per series like 3-D Column.
|
|
960
|
+
const stackedBar3D = threeDRenderKind === "bar3D" &&
|
|
961
|
+
(spec.stackedType === "STACKED" ||
|
|
962
|
+
spec.stackedType === "PERCENT_STACKED");
|
|
963
|
+
// Tooltip values are "Linked to source": each series' own cells.
|
|
964
|
+
const threeDFormats = resolveSeriesNumberFormats(spec.series, getFormattingFromRange, spec.hiddenDimensionStrategy);
|
|
756
965
|
const threeDTooltipFormatter = (params) => {
|
|
757
966
|
const value = (params?.value ?? []);
|
|
758
967
|
const categoryLabel = categories[Math.round(value[0] ?? 0)] ?? String(value[0] ?? "");
|
|
759
|
-
|
|
760
|
-
|
|
968
|
+
// Stacked bars and line3D are one echarts series per spec series;
|
|
969
|
+
// a single bar3D / surface series carries the spec index on y.
|
|
970
|
+
const seriesIndex = stackedBar3D || threeDRenderKind === "line3D"
|
|
971
|
+
? params?.seriesIndex
|
|
972
|
+
: Math.round(value[1] ?? 0);
|
|
973
|
+
const seriesLabel = stackedBar3D
|
|
974
|
+
? params?.seriesName ?? ""
|
|
975
|
+
: seriesNames[Math.round(value[1] ?? 0)] ?? params?.seriesName ?? "";
|
|
976
|
+
const valueLabel = spec.stackedType === "PERCENT_STACKED" && stackedBar3D
|
|
977
|
+
? `${Math.round((value[2] ?? 0) * 10) / 10}%`
|
|
978
|
+
: formatAxisValue(threeDFormats[seriesIndex], value[2]);
|
|
979
|
+
return `${seriesLabel ? `${seriesLabel}<br/>` : ""}${categoryLabel}: ${valueLabel}`;
|
|
761
980
|
};
|
|
762
981
|
let threeDSeries;
|
|
763
982
|
let visualMap;
|
|
764
983
|
let xAxis3D;
|
|
765
984
|
let yAxis3D;
|
|
766
|
-
if (
|
|
985
|
+
if (stackedBar3D) {
|
|
986
|
+
xAxis3D = { type: "category", data: categories };
|
|
987
|
+
yAxis3D = { type: "category", data: [""] };
|
|
988
|
+
const categoryCount = getCategoryCount(threeDSeriesInputs);
|
|
989
|
+
const categoryTotals = new Array(categoryCount).fill(0);
|
|
990
|
+
for (const entry of threeDSeriesInputs) {
|
|
991
|
+
entry.values.forEach((value, categoryIndex) => {
|
|
992
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
993
|
+
categoryTotals[categoryIndex] += value;
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
const isPercent = spec.stackedType === "PERCENT_STACKED";
|
|
998
|
+
threeDSeries = threeDSeriesInputs.map((entry, index) => ({
|
|
999
|
+
type: "bar3D",
|
|
1000
|
+
name: seriesNames[index],
|
|
1001
|
+
stack: "total",
|
|
1002
|
+
shading: "lambert",
|
|
1003
|
+
itemStyle: { color: seriesColorAt(index) },
|
|
1004
|
+
data: entry.values.flatMap((value, categoryIndex) => {
|
|
1005
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1006
|
+
return [];
|
|
1007
|
+
}
|
|
1008
|
+
const total = categoryTotals[categoryIndex];
|
|
1009
|
+
const z = isPercent ? (total === 0 ? 0 : (value / total) * 100) : value;
|
|
1010
|
+
return [{ value: [categoryIndex, 0, z] }];
|
|
1011
|
+
}),
|
|
1012
|
+
}));
|
|
1013
|
+
}
|
|
1014
|
+
else if (threeDRenderKind === "bar3D") {
|
|
767
1015
|
xAxis3D = { type: "category", data: categories };
|
|
768
1016
|
yAxis3D = { type: "category", data: seriesNames };
|
|
769
1017
|
threeDSeries = [
|
|
@@ -834,7 +1082,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
834
1082
|
textStyle: { fontSize: 12 },
|
|
835
1083
|
formatter: threeDTooltipFormatter,
|
|
836
1084
|
},
|
|
837
|
-
legend: threeDRenderKind === "line3D"
|
|
1085
|
+
legend: threeDRenderKind === "line3D" || stackedBar3D
|
|
838
1086
|
? applyLegendOverrides({
|
|
839
1087
|
...defaultLegendConfig,
|
|
840
1088
|
show: seriesNames.length > 1,
|
|
@@ -889,8 +1137,23 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
889
1137
|
const radarStyle = spec.radarStyle ?? "standard";
|
|
890
1138
|
const radarColors = radarSeriesInputs.map((_, i) => spec.series?.[i]?.color);
|
|
891
1139
|
// Spec-level overrides — radar is polar (no cartesian axes), so
|
|
892
|
-
// only legend + dataLabel overrides apply here.
|
|
893
|
-
|
|
1140
|
+
// only legend + dataLabel overrides apply here. Each data item is
|
|
1141
|
+
// one spec series, so labels and tooltip values format with that
|
|
1142
|
+
// series' own source cells.
|
|
1143
|
+
const radarFormats = resolveSeriesNumberFormats(spec.series, getFormattingFromRange, spec.hiddenDimensionStrategy);
|
|
1144
|
+
const radarDataLabels = spec.dataLabels;
|
|
1145
|
+
const radarSeriesLabelFor = (seriesIndex) => buildSeriesLabelOption(radarDataLabels && !radarDataLabels.numberFormatPattern
|
|
1146
|
+
? {
|
|
1147
|
+
...radarDataLabels,
|
|
1148
|
+
numberFormatPattern: radarFormats[seriesIndex],
|
|
1149
|
+
}
|
|
1150
|
+
: radarDataLabels);
|
|
1151
|
+
const radarTooltipFormatter = (params) => {
|
|
1152
|
+
const values = Array.isArray(params?.value) ? params.value : [];
|
|
1153
|
+
const pattern = radarFormats[params?.dataIndex ?? 0];
|
|
1154
|
+
const lines = indicators.map((indicator, i) => `${indicator.name}: ${formatAxisValue(pattern, values[i])}`);
|
|
1155
|
+
return [params?.name ?? "", ...lines].filter(Boolean).join("<br/>");
|
|
1156
|
+
};
|
|
894
1157
|
chartRef.current?.setOption({
|
|
895
1158
|
radar: {
|
|
896
1159
|
indicator: indicators.length
|
|
@@ -902,15 +1165,17 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
902
1165
|
show: radarData.length > 1,
|
|
903
1166
|
data: radarData.map((d) => d.name),
|
|
904
1167
|
}, spec.legend),
|
|
905
|
-
tooltip: { trigger: "item" },
|
|
1168
|
+
tooltip: { trigger: "item", formatter: radarTooltipFormatter },
|
|
906
1169
|
series: [
|
|
907
1170
|
{
|
|
908
1171
|
type: "radar",
|
|
909
1172
|
symbol: radarStyle === "standard" ? "none" : "circle",
|
|
910
|
-
...(radarSeriesLabel ? { label: radarSeriesLabel } : {}),
|
|
911
1173
|
data: radarData.map((d, i) => ({
|
|
912
1174
|
value: d.value,
|
|
913
1175
|
name: d.name,
|
|
1176
|
+
...(radarSeriesLabelFor(i)
|
|
1177
|
+
? { label: radarSeriesLabelFor(i) }
|
|
1178
|
+
: {}),
|
|
914
1179
|
...(radarStyle === "filled" ? { areaStyle: {} } : {}),
|
|
915
1180
|
...(radarColors[i]
|
|
916
1181
|
? {
|
|
@@ -952,7 +1217,15 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
952
1217
|
// volume pane exists), the value-axis overrides on the price
|
|
953
1218
|
// pane's y-axis only — the volume pane keeps its own hidden-label
|
|
954
1219
|
// value axis.
|
|
955
|
-
|
|
1220
|
+
// Each price line / the volume column maps back to one spec series
|
|
1221
|
+
// (`STOCK_ROLES` order), whose cells give its number format.
|
|
1222
|
+
const stockFormats = resolveSeriesNumberFormats(spec.series, getFormattingFromRange, spec.hiddenDimensionStrategy);
|
|
1223
|
+
const stockRoleIndex = (role) => STOCK_ROLES[variant].indexOf(role);
|
|
1224
|
+
const stockFormatFor = (role) => stockFormats[stockRoleIndex(role)];
|
|
1225
|
+
const stockDataLabels = spec.dataLabels;
|
|
1226
|
+
const stockSeriesLabelFor = (role) => buildSeriesLabelOption(stockDataLabels && !stockDataLabels.numberFormatPattern
|
|
1227
|
+
? { ...stockDataLabels, numberFormatPattern: stockFormatFor(role) }
|
|
1228
|
+
: stockDataLabels);
|
|
956
1229
|
// V-variants (VHLC / VOHLC) carry a volume series that renders as
|
|
957
1230
|
// a column sub-pane under the price plot: two grids stacked
|
|
958
1231
|
// vertically (price ~70% of the plot, volume below on its own
|
|
@@ -1017,6 +1290,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1017
1290
|
value: p.value,
|
|
1018
1291
|
itemStyle: { color: p.up ? upColor : downColor },
|
|
1019
1292
|
})),
|
|
1293
|
+
...(tooltipValueFormatter(stockFormatFor("volume"))
|
|
1294
|
+
? {
|
|
1295
|
+
tooltip: {
|
|
1296
|
+
valueFormatter: tooltipValueFormatter(stockFormatFor("volume")),
|
|
1297
|
+
},
|
|
1298
|
+
}
|
|
1299
|
+
: {}),
|
|
1020
1300
|
},
|
|
1021
1301
|
]
|
|
1022
1302
|
: [];
|
|
@@ -1059,6 +1339,13 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1059
1339
|
borderColor: upColor,
|
|
1060
1340
|
borderColor0: downColor,
|
|
1061
1341
|
},
|
|
1342
|
+
...(tooltipValueFormatter(stockFormatFor("close"))
|
|
1343
|
+
? {
|
|
1344
|
+
tooltip: {
|
|
1345
|
+
valueFormatter: tooltipValueFormatter(stockFormatFor("close")),
|
|
1346
|
+
},
|
|
1347
|
+
}
|
|
1348
|
+
: {}),
|
|
1062
1349
|
},
|
|
1063
1350
|
...volumeSeries,
|
|
1064
1351
|
],
|
|
@@ -1077,12 +1364,12 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1077
1364
|
return;
|
|
1078
1365
|
}
|
|
1079
1366
|
const lineSeries = toLineSeries(seriesValues, variant);
|
|
1080
|
-
const stockColors = [
|
|
1367
|
+
const stockColors = getChartPalette(spec, [
|
|
1081
1368
|
theme.themeColors.accent1,
|
|
1082
1369
|
theme.themeColors.accent2,
|
|
1083
1370
|
theme.themeColors.accent3,
|
|
1084
1371
|
theme.themeColors.accent4,
|
|
1085
|
-
];
|
|
1372
|
+
]);
|
|
1086
1373
|
chartRef.current?.setOption({
|
|
1087
1374
|
xAxis: stockXAxis,
|
|
1088
1375
|
yAxis: [
|
|
@@ -1112,7 +1399,16 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1112
1399
|
showSymbol: false,
|
|
1113
1400
|
itemStyle: { color: stockColors[i % stockColors.length] },
|
|
1114
1401
|
lineStyle: { color: stockColors[i % stockColors.length] },
|
|
1115
|
-
...(
|
|
1402
|
+
...(stockSeriesLabelFor(s.role)
|
|
1403
|
+
? { label: stockSeriesLabelFor(s.role) }
|
|
1404
|
+
: {}),
|
|
1405
|
+
...(tooltipValueFormatter(stockFormatFor(s.role))
|
|
1406
|
+
? {
|
|
1407
|
+
tooltip: {
|
|
1408
|
+
valueFormatter: tooltipValueFormatter(stockFormatFor(s.role)),
|
|
1409
|
+
},
|
|
1410
|
+
}
|
|
1411
|
+
: {}),
|
|
1116
1412
|
})),
|
|
1117
1413
|
...volumeSeries,
|
|
1118
1414
|
],
|
|
@@ -1145,32 +1441,51 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1145
1441
|
? sanitizeDomainValues(getDomainValuesFromRange(primaryDomainSource, spec.hiddenDimensionStrategy) ?? [])
|
|
1146
1442
|
: [];
|
|
1147
1443
|
const aggregateDomainValues = getDomainValuesForAxis(rawPrimaryDomainValues, shouldAggregate);
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1444
|
+
// A hierarchical category axis (PivotChart over nested row fields): the
|
|
1445
|
+
// innermost level is the category axis, the outer levels label-only axes
|
|
1446
|
+
// grouped beneath it — the domain cells are not read.
|
|
1447
|
+
const categoryLevels = isCircularChart
|
|
1448
|
+
? undefined
|
|
1449
|
+
: resolveCategoryLevels(spec.categoryLevels?.map((level) => flattenChartDataValues([level], getDomainValuesFromRange, spec.hiddenDimensionStrategy, false)));
|
|
1450
|
+
if (categoryLevels) {
|
|
1451
|
+
chartDomains.push(...echartsGroupedCategoryAxes(categoryLevels, {
|
|
1452
|
+
type: "category",
|
|
1453
|
+
show: showAxis,
|
|
1454
|
+
axisLine: { show: true },
|
|
1455
|
+
axisTick: { show: true },
|
|
1456
|
+
splitLine: { show: true },
|
|
1457
|
+
name: isHorizontalBarChart
|
|
1458
|
+
? spec.verticalAxisTitle ?? ""
|
|
1459
|
+
: spec.horizontalAxisTitle ?? "",
|
|
1460
|
+
nameLocation: "middle",
|
|
1461
|
+
nameGap: 30 + 24 * categoryLevels.outer.length,
|
|
1462
|
+
}));
|
|
1463
|
+
}
|
|
1464
|
+
// One category axis per domain (Excel): a domain drawn from several
|
|
1465
|
+
// ranges lists their categories in sequence on that single axis.
|
|
1466
|
+
(categoryLevels ? [] : spec.domains ?? []).forEach((domain, domainIndex) => {
|
|
1467
|
+
const axisData = domain.sources.flatMap((sheetRange, sourceIndex) => {
|
|
1151
1468
|
if (domainIndex === 0 && sourceIndex === 0) {
|
|
1152
|
-
|
|
1469
|
+
return aggregateDomainValues;
|
|
1153
1470
|
}
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
?
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
nameGap: 30,
|
|
1173
|
-
});
|
|
1471
|
+
const rawValues = sanitizeDomainValues(getDomainValuesFromRange(sheetRange, spec.hiddenDimensionStrategy) ?? []);
|
|
1472
|
+
return getDomainValuesForAxis(rawValues, shouldAggregate);
|
|
1473
|
+
});
|
|
1474
|
+
const axisTitle = domainIndex === 0 && !isCircularChart
|
|
1475
|
+
? isHorizontalBarChart
|
|
1476
|
+
? spec.verticalAxisTitle ?? ""
|
|
1477
|
+
: spec.horizontalAxisTitle ?? ""
|
|
1478
|
+
: undefined;
|
|
1479
|
+
chartDomains.push({
|
|
1480
|
+
type: "category",
|
|
1481
|
+
data: axisData,
|
|
1482
|
+
show: showAxis && !isCircularChart,
|
|
1483
|
+
axisLine: { show: !isCircularChart },
|
|
1484
|
+
axisTick: { show: !isCircularChart },
|
|
1485
|
+
splitLine: { show: !isCircularChart },
|
|
1486
|
+
name: axisTitle,
|
|
1487
|
+
nameLocation: "middle",
|
|
1488
|
+
nameGap: 30,
|
|
1174
1489
|
});
|
|
1175
1490
|
});
|
|
1176
1491
|
if (chartDomains.length === 0 && !isCircularChart) {
|
|
@@ -1347,7 +1662,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1347
1662
|
: (spec.series ?? []).reduce((count, s) => {
|
|
1348
1663
|
const seriesType = s.seriesChartType ?? spec.chartType;
|
|
1349
1664
|
return seriesType === "bar" || seriesType === "column"
|
|
1350
|
-
? count +
|
|
1665
|
+
? count + 1
|
|
1351
1666
|
: count;
|
|
1352
1667
|
}, 0);
|
|
1353
1668
|
const specBarGap = overlapToBarGap(spec.overlap);
|
|
@@ -1355,7 +1670,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1355
1670
|
seriesCount: sideBySideBarCount,
|
|
1356
1671
|
overlap: isStacked ? 0 : spec.overlap,
|
|
1357
1672
|
});
|
|
1358
|
-
for (const series of spec.series ?? []) {
|
|
1673
|
+
for (const [specSeriesIndex, series] of (spec.series ?? []).entries()) {
|
|
1359
1674
|
const dataLabel = !isNil(series.dataLabel) && isAFormula(series.dataLabel)
|
|
1360
1675
|
? coerceCalculatedLabel(await onRequestCalculate(series.dataLabel, position.sheetId ?? sheetId, position.overlayPosition.anchorCell.rowIndex, position.overlayPosition.anchorCell.columnIndex))
|
|
1361
1676
|
: series.dataLabel ?? "";
|
|
@@ -1363,133 +1678,203 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1363
1678
|
if (dataLabel) {
|
|
1364
1679
|
legend.data?.push(dataLabel);
|
|
1365
1680
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1681
|
+
// Support per-series chart type for combo charts
|
|
1682
|
+
const effectiveChartType = series.seriesChartType ?? spec.chartType;
|
|
1683
|
+
const lineLike = isLineLikeChartType(effectiveChartType);
|
|
1684
|
+
// Excel plots a series drawn from several ranges as ONE series over
|
|
1685
|
+
// their union — every source's points in sequence, in step with the
|
|
1686
|
+
// single category axis that lists every domain source's categories
|
|
1687
|
+
// (C-68) — not one echarts series (and legend entry) per range.
|
|
1688
|
+
// Pie slices over several ranges label from the domain UNION — every
|
|
1689
|
+
// domain range's labels in sequence, each series range taking its own
|
|
1690
|
+
// window — not the primary domain range alone (C-78).
|
|
1691
|
+
const domainSources = spec.domains?.[0]?.sources ?? [];
|
|
1692
|
+
const pieDomainLabels = spec.chartType === "pie" &&
|
|
1693
|
+
(domainSources.length > 1 || series.sources.length > 1)
|
|
1694
|
+
? domainSources.flatMap((sheetRange) => getDomainValuesFromRange(sheetRange, spec.hiddenDimensionStrategy) ?? [])
|
|
1695
|
+
: undefined;
|
|
1696
|
+
let piePointOffset = 0;
|
|
1697
|
+
const data = series.sources.flatMap((sheetRange) => {
|
|
1698
|
+
const part = getSeriesData(spec, sheetRange, {
|
|
1368
1699
|
aggregateEnabled: shouldAggregate,
|
|
1369
1700
|
aggregateType: series.aggregateType,
|
|
1370
1701
|
rawDomainValues: rawPrimaryDomainValues,
|
|
1371
1702
|
orderedDomainValues: aggregateDomainValues,
|
|
1703
|
+
lineLike,
|
|
1704
|
+
pieLabels: pieDomainLabels
|
|
1705
|
+
? {
|
|
1706
|
+
domainLabels: pieDomainLabels,
|
|
1707
|
+
offset: series.sources.length > 1 ? piePointOffset : undefined,
|
|
1708
|
+
}
|
|
1709
|
+
: undefined,
|
|
1372
1710
|
});
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1711
|
+
if (pieDomainLabels)
|
|
1712
|
+
piePointOffset += part.length;
|
|
1713
|
+
return part;
|
|
1714
|
+
});
|
|
1715
|
+
const seriesEchartsType = chartTypeToEchartsType(effectiveChartType);
|
|
1716
|
+
const seriesStyleAttrs = getEchartsSeriesStyle(effectiveChartType);
|
|
1717
|
+
const seriesEntry = {
|
|
1718
|
+
type: seriesEchartsType,
|
|
1719
|
+
// Ties the rendered series back to spec.series for click selection
|
|
1720
|
+
// One rendered series per spec series (source index 0).
|
|
1721
|
+
id: chartSeriesId(specSeriesIndex, 0),
|
|
1722
|
+
name: dataLabel,
|
|
1723
|
+
data,
|
|
1724
|
+
stack: isStacked ? "dataLabel" : undefined,
|
|
1725
|
+
smooth: spec.smooth ?? undefined,
|
|
1726
|
+
radius: spec.radius ?? (isCircularChart ? [0, "75%"] : undefined),
|
|
1727
|
+
// "Connect data points with line" bridges the empty cells.
|
|
1728
|
+
...(connectsAcrossEmptyCells(spec, lineLike)
|
|
1729
|
+
? { connectNulls: true }
|
|
1730
|
+
: {}),
|
|
1731
|
+
...seriesStyleAttrs,
|
|
1732
|
+
};
|
|
1733
|
+
// Plot this series against a secondary value axis. The 2nd
|
|
1734
|
+
// yAxis element is appended below (when any series opts in);
|
|
1735
|
+
// here we point the series at index 1.
|
|
1736
|
+
if (series.useSecondaryAxis) {
|
|
1737
|
+
seriesEntry.yAxisIndex = 1;
|
|
1738
|
+
}
|
|
1739
|
+
// Handle invertIfNegative for bar/column charts
|
|
1740
|
+
const isBarOrColumn = effectiveChartType === "bar" || effectiveChartType === "column";
|
|
1741
|
+
// Bar layout (overlap/gapWidth) — echarts reads barGap /
|
|
1742
|
+
// barCategoryGap from bar series (the last bar series wins for
|
|
1743
|
+
// the whole coordinate system), so apply them to every
|
|
1744
|
+
// bar/column series. Set before the gradient/noFill branches so
|
|
1745
|
+
// every push path carries them.
|
|
1746
|
+
if (isBarOrColumn) {
|
|
1747
|
+
if (specBarGap !== undefined) {
|
|
1748
|
+
seriesEntry.barGap = specBarGap;
|
|
1391
1749
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
// Bar layout (overlap/gapWidth) — echarts reads barGap /
|
|
1395
|
-
// barCategoryGap from bar series (the last bar series wins for
|
|
1396
|
-
// the whole coordinate system), so apply them to every
|
|
1397
|
-
// bar/column series. Set before the gradient/noFill branches so
|
|
1398
|
-
// every push path carries them.
|
|
1399
|
-
if (isBarOrColumn) {
|
|
1400
|
-
if (specBarGap !== undefined) {
|
|
1401
|
-
seriesEntry.barGap = specBarGap;
|
|
1402
|
-
}
|
|
1403
|
-
if (specBarCategoryGap !== undefined) {
|
|
1404
|
-
seriesEntry.barCategoryGap = specBarCategoryGap;
|
|
1405
|
-
}
|
|
1750
|
+
if (specBarCategoryGap !== undefined) {
|
|
1751
|
+
seriesEntry.barCategoryGap = specBarCategoryGap;
|
|
1406
1752
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
//
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
};
|
|
1423
|
-
}
|
|
1424
|
-
chartSeries.push(seriesEntry);
|
|
1425
|
-
pushSeriesOverlays(series, data, dataLabel);
|
|
1426
|
-
continue;
|
|
1753
|
+
}
|
|
1754
|
+
const seriesAny = series;
|
|
1755
|
+
if (effectiveChartType === "line") {
|
|
1756
|
+
// Excel's "Line" draws no markers; "Line with Markers"
|
|
1757
|
+
// (spec.showMarkers) draws one per point. A series' own marker
|
|
1758
|
+
// wins either way: an authored symbol shows, "none" hides.
|
|
1759
|
+
const markerSymbol = seriesAny.marker?.symbol;
|
|
1760
|
+
const wantsMarkers = markerSymbol === "none"
|
|
1761
|
+
? false
|
|
1762
|
+
: Boolean(spec.showMarkers) || !isNil(markerSymbol);
|
|
1763
|
+
seriesEntry.showSymbol = wantsMarkers;
|
|
1764
|
+
if (wantsMarkers) {
|
|
1765
|
+
seriesEntry.symbol =
|
|
1766
|
+
mapMarkerSymbolToEcharts(markerSymbol) ?? "circle";
|
|
1767
|
+
seriesEntry.symbolSize = seriesAny.marker?.size ?? 6;
|
|
1427
1768
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1769
|
+
}
|
|
1770
|
+
// Gradient fill — overrides solid color when present. echarts
|
|
1771
|
+
// paints it natively via a LinearGradient on itemStyle.color
|
|
1772
|
+
// (and areaStyle for area series / lineStyle for lines).
|
|
1773
|
+
const gradientArgs = gradientToEcharts(series.gradient);
|
|
1774
|
+
if (gradientArgs && !seriesAny.noFill) {
|
|
1775
|
+
const gradient = new echarts.graphic.LinearGradient(gradientArgs.x, gradientArgs.y, gradientArgs.x2, gradientArgs.y2, gradientArgs.colorStops);
|
|
1776
|
+
seriesEntry.itemStyle = {
|
|
1777
|
+
...(seriesEntry.itemStyle ?? {}),
|
|
1778
|
+
color: gradient,
|
|
1779
|
+
};
|
|
1780
|
+
if (effectiveChartType === "area") {
|
|
1781
|
+
seriesEntry.areaStyle = {
|
|
1782
|
+
...(seriesEntry.areaStyle ?? {}),
|
|
1783
|
+
color: gradient,
|
|
1437
1784
|
};
|
|
1438
1785
|
}
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1786
|
+
}
|
|
1787
|
+
else if (seriesAny.noFill) {
|
|
1788
|
+
// Transparent series (Gantt lag offset, etc.) — fill it
|
|
1789
|
+
// with `transparent` so layout is preserved but nothing
|
|
1790
|
+
// paints. Legend entries are also suppressed via `name: ""`
|
|
1791
|
+
// since the series carries no semantic information.
|
|
1792
|
+
seriesEntry.itemStyle = {
|
|
1793
|
+
...(seriesEntry.itemStyle ?? {}),
|
|
1794
|
+
color: "transparent",
|
|
1795
|
+
borderColor: "transparent",
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
else if (isBarOrColumn &&
|
|
1799
|
+
Array.isArray(seriesAny.dataPointColors) &&
|
|
1800
|
+
seriesAny.dataPointColors.length) {
|
|
1801
|
+
// Per-data-point color overrides — Gantt-style charts colour
|
|
1802
|
+
// each task differently via `<c:dPt>`. Echarts accepts an
|
|
1803
|
+
// `itemStyle.color` function that receives the data index;
|
|
1804
|
+
// we look up the override or fall back to the series color.
|
|
1805
|
+
const overrides = new Map();
|
|
1806
|
+
for (const dp of seriesAny.dataPointColors) {
|
|
1807
|
+
if (typeof dp.idx === "number" && typeof dp.color === "string") {
|
|
1808
|
+
overrides.set(dp.idx, dp.color);
|
|
1451
1809
|
}
|
|
1452
|
-
const fallback = series.color ?? theme.themeColors.accent1;
|
|
1453
|
-
seriesEntry.itemStyle = {
|
|
1454
|
-
...(seriesEntry.itemStyle ?? {}),
|
|
1455
|
-
color: (params) => overrides.get(params.dataIndex) ?? fallback,
|
|
1456
|
-
};
|
|
1457
1810
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1811
|
+
const fallback = series.color ?? theme.themeColors.accent1;
|
|
1812
|
+
seriesEntry.itemStyle = {
|
|
1813
|
+
...(seriesEntry.itemStyle ?? {}),
|
|
1814
|
+
color: (params) => overrides.get(params.dataIndex) ?? fallback,
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
else if (isCircularChart &&
|
|
1818
|
+
Array.isArray(seriesAny.dataPointColors) &&
|
|
1819
|
+
seriesAny.dataPointColors.length) {
|
|
1820
|
+
// Per-slice colours (`<c:dPt>` on a pie / doughnut series): an
|
|
1821
|
+
// authored slice colour wins, the other slices keep the palette
|
|
1822
|
+
// cycle the chart-level `color` option would give them.
|
|
1823
|
+
const overrides = new Map();
|
|
1824
|
+
for (const dp of seriesAny.dataPointColors) {
|
|
1825
|
+
if (typeof dp.idx === "number" && typeof dp.color === "string") {
|
|
1826
|
+
overrides.set(dp.idx, dp.color);
|
|
1827
|
+
}
|
|
1473
1828
|
}
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1829
|
+
const slicePalette = getChartPalette(spec, [
|
|
1830
|
+
theme.themeColors.accent1,
|
|
1831
|
+
theme.themeColors.accent2,
|
|
1832
|
+
theme.themeColors.accent3,
|
|
1833
|
+
theme.themeColors.accent4,
|
|
1834
|
+
theme.themeColors.accent5,
|
|
1835
|
+
theme.themeColors.accent6,
|
|
1836
|
+
]);
|
|
1837
|
+
seriesEntry.itemStyle = {
|
|
1838
|
+
...(seriesEntry.itemStyle ?? {}),
|
|
1839
|
+
color: (params) => overrides.get(params.dataIndex) ??
|
|
1840
|
+
series.color ??
|
|
1841
|
+
slicePalette[params.dataIndex % slicePalette.length],
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
else if (isBarOrColumn && series.invertIfNegative) {
|
|
1845
|
+
const positiveColor = series.color ?? theme.themeColors.accent1;
|
|
1846
|
+
const negativeColor = series.negativeColor ?? "#FF0000";
|
|
1847
|
+
// Use a color function that returns different colors based on value
|
|
1848
|
+
seriesEntry.itemStyle = {
|
|
1849
|
+
...(seriesEntry.itemStyle ?? {}),
|
|
1850
|
+
color: (params) => {
|
|
1851
|
+
const value = Array.isArray(params.data)
|
|
1852
|
+
? params.data[1]
|
|
1853
|
+
: params.data?.value ?? params.data;
|
|
1854
|
+
return typeof value === "number" && value < 0
|
|
1855
|
+
? negativeColor
|
|
1856
|
+
: positiveColor;
|
|
1857
|
+
},
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
else if (series.color) {
|
|
1861
|
+
seriesEntry.itemStyle = {
|
|
1862
|
+
...(seriesEntry.itemStyle ?? {}),
|
|
1863
|
+
color: series.color,
|
|
1864
|
+
};
|
|
1865
|
+
seriesEntry.lineStyle = {
|
|
1866
|
+
...(seriesEntry.lineStyle ?? {}),
|
|
1867
|
+
color: series.color,
|
|
1868
|
+
};
|
|
1869
|
+
if (effectiveChartType === "area") {
|
|
1870
|
+
seriesEntry.areaStyle = {
|
|
1871
|
+
...(seriesEntry.areaStyle ?? {}),
|
|
1481
1872
|
color: series.color,
|
|
1482
1873
|
};
|
|
1483
|
-
if (effectiveChartType === "area") {
|
|
1484
|
-
seriesEntry.areaStyle = {
|
|
1485
|
-
...(seriesEntry.areaStyle ?? {}),
|
|
1486
|
-
color: series.color,
|
|
1487
|
-
};
|
|
1488
|
-
}
|
|
1489
1874
|
}
|
|
1490
|
-
chartSeries.push(seriesEntry);
|
|
1491
|
-
pushSeriesOverlays(series, data, dataLabel);
|
|
1492
1875
|
}
|
|
1876
|
+
chartSeries.push(seriesEntry);
|
|
1877
|
+
pushSeriesOverlays(series, data, dataLabel);
|
|
1493
1878
|
}
|
|
1494
1879
|
if (isOfPie) {
|
|
1495
1880
|
// Pie-of-pie / bar-of-pie — Excel splits the minor slices into a
|
|
@@ -1540,14 +1925,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1540
1925
|
chartSeries.length = 0;
|
|
1541
1926
|
// Palette continues from the main plot into the secondary one so
|
|
1542
1927
|
// slices keep distinct colors across both plots (Excel behavior).
|
|
1543
|
-
const ofPiePalette = [
|
|
1928
|
+
const ofPiePalette = getChartPalette(spec, [
|
|
1544
1929
|
theme.themeColors.accent1,
|
|
1545
1930
|
theme.themeColors.accent2,
|
|
1546
1931
|
theme.themeColors.accent3,
|
|
1547
1932
|
theme.themeColors.accent4,
|
|
1548
1933
|
theme.themeColors.accent5,
|
|
1549
1934
|
theme.themeColors.accent6,
|
|
1550
|
-
];
|
|
1935
|
+
]);
|
|
1551
1936
|
const paletteAt = (index) => ofPiePalette[index % ofPiePalette.length];
|
|
1552
1937
|
chartSeries.push({
|
|
1553
1938
|
type: "pie",
|
|
@@ -1951,30 +2336,82 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
1951
2336
|
const hasSecondaryAxis = !isCircularChart &&
|
|
1952
2337
|
!isHorizontalBarChart &&
|
|
1953
2338
|
((spec.series ?? []).some((s) => s.useSecondaryAxis) || isPareto);
|
|
2339
|
+
// "Linked to source" number formats. A percent-stacked value axis
|
|
2340
|
+
// plots normalized 0-100 percentages, so it never links to the
|
|
2341
|
+
// source cells; its data labels still format `originalValue` below.
|
|
2342
|
+
const isPercentStackedBars = spec.stackedType === "PERCENT_STACKED" &&
|
|
2343
|
+
(spec.chartType === "bar" || spec.chartType === "column");
|
|
2344
|
+
const primaryAxisNumberFormat = isPercentStackedBars
|
|
2345
|
+
? undefined
|
|
2346
|
+
: resolveValueAxisNumberFormat({
|
|
2347
|
+
axisOptions: spec.valueAxisOptions,
|
|
2348
|
+
series: (spec.series ?? []).filter((s) => !s.useSecondaryAxis),
|
|
2349
|
+
getFormattingFromRange,
|
|
2350
|
+
hiddenDimensionStrategy: spec.hiddenDimensionStrategy,
|
|
2351
|
+
});
|
|
2352
|
+
const secondaryAxisNumberFormat = resolveValueAxisNumberFormat({
|
|
2353
|
+
axisOptions: spec.secondaryValueAxisOptions,
|
|
2354
|
+
series: (spec.series ?? []).filter((s) => s.useSecondaryAxis),
|
|
2355
|
+
getFormattingFromRange,
|
|
2356
|
+
hiddenDimensionStrategy: spec.hiddenDimensionStrategy,
|
|
2357
|
+
});
|
|
2358
|
+
const axisNumberFormatFor = (s) => s?.yAxisIndex === 1
|
|
2359
|
+
? secondaryAxisNumberFormat
|
|
2360
|
+
: primaryAxisNumberFormat;
|
|
2361
|
+
// Data labels and tooltips are linked to each series' OWN source
|
|
2362
|
+
// cells (Excel's `<c:dLbls><c:numFmt sourceLinked="1">`); the axis
|
|
2363
|
+
// format only steps in for a series whose cells are General.
|
|
2364
|
+
const seriesNumberFormats = resolveSeriesNumberFormats(spec.series, getFormattingFromRange, spec.hiddenDimensionStrategy);
|
|
2365
|
+
const seriesNumberFormatFor = (s) => {
|
|
2366
|
+
const specIndex = chartSeriesSpecIndex(s?.id);
|
|
2367
|
+
return ((specIndex === undefined ? undefined : seriesNumberFormats[specIndex]) ??
|
|
2368
|
+
axisNumberFormatFor(s));
|
|
2369
|
+
};
|
|
1954
2370
|
const tooltipConfig = {
|
|
1955
2371
|
textStyle: {
|
|
1956
2372
|
fontSize: 12,
|
|
1957
2373
|
},
|
|
1958
2374
|
trigger: isCircularChart ? "item" : "axis",
|
|
1959
|
-
formatter: isCircularChart
|
|
2375
|
+
formatter: isCircularChart
|
|
2376
|
+
? primaryAxisNumberFormat
|
|
2377
|
+
? circularTooltipFormatter(primaryAxisNumberFormat)
|
|
2378
|
+
: "{b}: {c} ({d}%)"
|
|
2379
|
+
: undefined,
|
|
1960
2380
|
};
|
|
1961
2381
|
// Phase 5.1: apply legend / axis / dataLabels / doughnut overrides.
|
|
1962
2382
|
const finalLegend = applyLegendOverrides(legend, spec.legend);
|
|
1963
|
-
|
|
2383
|
+
// Data labels fall back to the series' axis format when the spec has
|
|
2384
|
+
// no explicit `<c:dLbls><c:numFmt>` — Excel's "Linked to source".
|
|
2385
|
+
const dataLabelOptions = spec.dataLabels;
|
|
2386
|
+
const seriesLabelOptionFor = (s) => buildSeriesLabelOption(dataLabelOptions && !dataLabelOptions.numberFormatPattern
|
|
2387
|
+
? {
|
|
2388
|
+
...dataLabelOptions,
|
|
2389
|
+
numberFormatPattern: seriesNumberFormatFor(s),
|
|
2390
|
+
}
|
|
2391
|
+
: dataLabelOptions);
|
|
1964
2392
|
// For doughnut, surface the inner-radius hole on each pie series.
|
|
1965
2393
|
// Excel persists `holeSize` as a 0-90 percentage; we normalize to a
|
|
1966
2394
|
// 0-1 fraction (doughnutHoleSize). Default to 0.5 when unset.
|
|
1967
2395
|
const isDoughnut = spec.chartType === "doughnut";
|
|
1968
2396
|
const doughnutHoleSize = spec.doughnutHoleSize;
|
|
2397
|
+
// Exploded pie / doughnut: every slice sits in the `select` state,
|
|
2398
|
+
// offset outward by the spec's percentage of the pie radius.
|
|
2399
|
+
const explosion = isCircularChart && !isOfPie ? resolveExplosion(spec) : 0;
|
|
2400
|
+
explodedRef.current = explosion > 0;
|
|
2401
|
+
const explosionOffset = explosionOffsetPx(explosion, pieRadiusPx(isDoughnut ? [0, "75%"] : spec.radius, width ?? 0, height ?? 0));
|
|
1969
2402
|
// PERCENT_STACKED — echarts has no built-in `barnorm: 'percent'`,
|
|
1970
2403
|
// so we normalize per category here: each cell becomes
|
|
1971
2404
|
// (value / category_total) * 100, while the original value is
|
|
1972
2405
|
// tucked into a sibling field so the label formatter can still
|
|
1973
|
-
// surface the dollar amount. Touches only
|
|
1974
|
-
// the
|
|
2406
|
+
// surface the dollar amount. Touches only the stacked base series
|
|
2407
|
+
// (bar/column, and the line series of 100% stacked line / area
|
|
2408
|
+
// charts); overlays and other charts pass through unchanged.
|
|
1975
2409
|
if (spec.stackedType === "PERCENT_STACKED" &&
|
|
1976
|
-
(spec.chartType === "bar" ||
|
|
1977
|
-
|
|
2410
|
+
(spec.chartType === "bar" ||
|
|
2411
|
+
spec.chartType === "column" ||
|
|
2412
|
+
spec.chartType === "line" ||
|
|
2413
|
+
spec.chartType === "area")) {
|
|
2414
|
+
const barSeries = chartSeries.filter((s) => s?.stack !== undefined && (s?.type === "bar" || s?.type === "line"));
|
|
1978
2415
|
// Length of the value axis = max length across stacked series.
|
|
1979
2416
|
const len = barSeries.reduce((m, s) => Math.max(m, Array.isArray(s.data) ? s.data.length : 0), 0);
|
|
1980
2417
|
const categoryTotals = new Array(len).fill(0);
|
|
@@ -2003,9 +2440,37 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2003
2440
|
}
|
|
2004
2441
|
const seriesWithLabels = chartSeries.map((s) => {
|
|
2005
2442
|
const next = { ...s };
|
|
2443
|
+
const seriesLabelOption = seriesLabelOptionFor(s);
|
|
2006
2444
|
if (seriesLabelOption) {
|
|
2007
2445
|
next.label = { ...(s?.label ?? {}), ...seriesLabelOption };
|
|
2008
2446
|
}
|
|
2447
|
+
// Tooltip values of the value-axis series (a boxplot / candlestick
|
|
2448
|
+
// tooltip lists its own statistics and keeps echarts' default).
|
|
2449
|
+
const seriesTooltipValueFormatter = VALUE_TOOLTIP_SERIES_TYPES.has(s?.type)
|
|
2450
|
+
? valueAxisLabelFormatter(seriesNumberFormatFor(s))
|
|
2451
|
+
: undefined;
|
|
2452
|
+
if (seriesTooltipValueFormatter && s?.tooltip?.show !== false) {
|
|
2453
|
+
next.tooltip = {
|
|
2454
|
+
...(s?.tooltip ?? {}),
|
|
2455
|
+
valueFormatter: seriesTooltipValueFormatter,
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
if (next.type === "pie") {
|
|
2459
|
+
const pieNumberFormat = seriesNumberFormatFor(s);
|
|
2460
|
+
if (pieNumberFormat && s?.tooltip?.show !== false) {
|
|
2461
|
+
next.tooltip = {
|
|
2462
|
+
...(s?.tooltip ?? {}),
|
|
2463
|
+
formatter: circularTooltipFormatter(pieNumberFormat),
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
if (explosionOffset > 0 && Array.isArray(next.data)) {
|
|
2467
|
+
next.selectedMode = "multiple";
|
|
2468
|
+
next.selectedOffset = explosionOffset;
|
|
2469
|
+
next.data = next.data.map((item) => item !== null && typeof item === "object"
|
|
2470
|
+
? { ...item, selected: true }
|
|
2471
|
+
: { value: item, selected: true });
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2009
2474
|
if (isDoughnut && next.type === "pie") {
|
|
2010
2475
|
const inner = typeof doughnutHoleSize === "number"
|
|
2011
2476
|
? Math.round(Math.max(0, Math.min(0.9, doughnutHoleSize)) * 100)
|
|
@@ -2061,7 +2526,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2061
2526
|
applyAxisOverrides({
|
|
2062
2527
|
...valueAxisBase,
|
|
2063
2528
|
name: spec.horizontalAxisTitle ?? "",
|
|
2064
|
-
}, spec.valueAxisOptions),
|
|
2529
|
+
}, spec.valueAxisOptions, primaryAxisNumberFormat),
|
|
2065
2530
|
];
|
|
2066
2531
|
}
|
|
2067
2532
|
const base = chartDomains.length > 0
|
|
@@ -2136,7 +2601,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2136
2601
|
return base.map((axis) => applyAxisOverrides(axis, spec.categoryAxisOptions));
|
|
2137
2602
|
}
|
|
2138
2603
|
const axes = [
|
|
2139
|
-
applyAxisOverrides({ ...valueAxisBase, name: spec.verticalAxisTitle ?? "" }, spec.valueAxisOptions),
|
|
2604
|
+
applyAxisOverrides({ ...valueAxisBase, name: spec.verticalAxisTitle ?? "" }, spec.valueAxisOptions, primaryAxisNumberFormat),
|
|
2140
2605
|
];
|
|
2141
2606
|
if (hasSecondaryAxis) {
|
|
2142
2607
|
axes.push(applyAxisOverrides({
|
|
@@ -2151,7 +2616,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2151
2616
|
axisLabel: { formatter: "{value}%" },
|
|
2152
2617
|
}
|
|
2153
2618
|
: {}),
|
|
2154
|
-
}, spec.secondaryValueAxisOptions));
|
|
2619
|
+
}, spec.secondaryValueAxisOptions, secondaryAxisNumberFormat));
|
|
2155
2620
|
}
|
|
2156
2621
|
return axes;
|
|
2157
2622
|
})(),
|
|
@@ -2175,6 +2640,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2175
2640
|
CandlestickChart,
|
|
2176
2641
|
CustomChart,
|
|
2177
2642
|
FunnelChart,
|
|
2643
|
+
HeatmapChart,
|
|
2178
2644
|
LineChart,
|
|
2179
2645
|
PieChart,
|
|
2180
2646
|
EchartsRadarChart,
|
|
@@ -2183,6 +2649,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2183
2649
|
SunburstChart,
|
|
2184
2650
|
CanvasRenderer,
|
|
2185
2651
|
DatasetComponent,
|
|
2652
|
+
VisualMapComponent,
|
|
2186
2653
|
]);
|
|
2187
2654
|
initializeChart();
|
|
2188
2655
|
}, []);
|
|
@@ -2244,14 +2711,17 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2244
2711
|
color: textColor,
|
|
2245
2712
|
},
|
|
2246
2713
|
},
|
|
2247
|
-
color: [
|
|
2714
|
+
color: getChartPalette(chart.spec, [
|
|
2248
2715
|
theme.themeColors.accent1,
|
|
2249
2716
|
theme.themeColors.accent2,
|
|
2250
2717
|
theme.themeColors.accent3,
|
|
2251
2718
|
theme.themeColors.accent4,
|
|
2252
2719
|
theme.themeColors.accent5,
|
|
2253
2720
|
theme.themeColors.accent6,
|
|
2254
|
-
],
|
|
2721
|
+
]),
|
|
2722
|
+
// Excel's Format Chart Area fill; `transparent` (echarts' own
|
|
2723
|
+
// default) so clearing the fill undoes an earlier setOption.
|
|
2724
|
+
backgroundColor: chart.spec.backgroundColor ?? "transparent",
|
|
2255
2725
|
};
|
|
2256
2726
|
// Skip when rendering 3-D or a region map: merging a cartesian
|
|
2257
2727
|
// yAxis into a grid3D / map option would create a stray empty
|
|
@@ -2273,7 +2743,14 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2273
2743
|
// Update chart properties
|
|
2274
2744
|
useIsomorphicLayoutEffect(() => {
|
|
2275
2745
|
applyThemeOptions();
|
|
2276
|
-
}, [
|
|
2746
|
+
}, [
|
|
2747
|
+
theme,
|
|
2748
|
+
title,
|
|
2749
|
+
isDarkMode,
|
|
2750
|
+
chart.spec.chartType,
|
|
2751
|
+
chart.spec.palette,
|
|
2752
|
+
chart.spec.backgroundColor,
|
|
2753
|
+
]);
|
|
2277
2754
|
// Draw chart. `glUpgradeTick` re-runs the draw when the lazily loaded
|
|
2278
2755
|
// echarts-gl module resolves, upgrading a 3-D spec's immediate 2-D
|
|
2279
2756
|
// fallback to the true 3-D rendering.
|
|
@@ -2283,6 +2760,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2283
2760
|
chart.spec,
|
|
2284
2761
|
getDomainValuesFromRange,
|
|
2285
2762
|
getSeriesValuesFromRange,
|
|
2763
|
+
getFormattingFromRange,
|
|
2286
2764
|
glUpgradeTick,
|
|
2287
2765
|
]);
|
|
2288
2766
|
// Resize chart
|
|
@@ -2297,15 +2775,38 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2297
2775
|
useIsomorphicLayoutEffect(() => {
|
|
2298
2776
|
chartRef.current?.resize({ silent: true });
|
|
2299
2777
|
}, [width, height]);
|
|
2778
|
+
const chartPngDataUrl = () => chartRef.current?.getDataURL({
|
|
2779
|
+
type: "png",
|
|
2780
|
+
pixelRatio: 2,
|
|
2781
|
+
backgroundColor: chart.spec.backgroundColor ??
|
|
2782
|
+
getStringifiedColor({ theme: 0, tint: isDarkMode ? -1 : 1 }, theme),
|
|
2783
|
+
});
|
|
2300
2784
|
const onDownloadChart = () => {
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
type: "png",
|
|
2304
|
-
backgroundColor: getStringifiedColor({ theme: 0, tint: isDarkMode ? -1 : 1 }, theme),
|
|
2305
|
-
});
|
|
2785
|
+
const url = chartPngDataUrl();
|
|
2786
|
+
if (url) {
|
|
2306
2787
|
createDownloadableFile(chart.spec.title ?? "Chart", url);
|
|
2307
2788
|
}
|
|
2308
2789
|
};
|
|
2790
|
+
// Copy as Picture / print preview — the same raster as the download.
|
|
2791
|
+
const exportChartPng = async () => dataUrlToBlob(chartPngDataUrl());
|
|
2792
|
+
// Excel's "Switch Row/Column" — offered to the grid's right-click menu
|
|
2793
|
+
// when the host can persist the chart and the sources transpose.
|
|
2794
|
+
const switchRowColumnAction = onUpdateChart && !readonly && canSwitchRowColumn(spec)
|
|
2795
|
+
? () => {
|
|
2796
|
+
const next = switchRowColumn(spec, {
|
|
2797
|
+
getFormattedValue: cellLabelGetterFromRangeReader(getDomainValuesFromRange),
|
|
2798
|
+
});
|
|
2799
|
+
if (next)
|
|
2800
|
+
onUpdateChart({ ...chart, spec: next });
|
|
2801
|
+
}
|
|
2802
|
+
: undefined;
|
|
2803
|
+
// The grid's right-click menu offers these without knowing the renderer.
|
|
2804
|
+
useRegisterChartActions(chart.chartId, {
|
|
2805
|
+
requestEdit: onRequestEdit ? () => onRequestEdit(chart) : undefined,
|
|
2806
|
+
downloadPng: onDownloadChart,
|
|
2807
|
+
exportPng: exportChartPng,
|
|
2808
|
+
switchRowColumn: switchRowColumnAction,
|
|
2809
|
+
});
|
|
2309
2810
|
// Excel's "Rounded Corners" chart style (`<c:roundedCorners val="1"/>`
|
|
2310
2811
|
// at the chartSpace root). Default per OOXML §21.2.2.183 is square,
|
|
2311
2812
|
// so only opt in when the workbook explicitly set the flag — otherwise
|
|
@@ -2314,6 +2815,7 @@ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, r
|
|
|
2314
2815
|
? { borderRadius: 8, overflow: "hidden" }
|
|
2315
2816
|
: undefined;
|
|
2316
2817
|
return (React.createElement("div", { className: "flex flex-1", style: roundedCornersStyle },
|
|
2818
|
+
fieldButtons,
|
|
2317
2819
|
isSelected ? (React.createElement(DropdownMenu, null,
|
|
2318
2820
|
React.createElement(DropdownMenuTrigger, { asChild: true },
|
|
2319
2821
|
React.createElement(IconButton, { className: "absolute right-1 top-1 z-10" },
|