Skip to content

Commit 2d5c4c7

Browse files
committed
[IMP] chart: support import/export of dataset trendlines
This commit introduces the ability to import and export trendlines associated with datasets in chart configurations. Task: 4319957
1 parent 7b1c39b commit 2d5c4c7

15 files changed

+335
-30
lines changed

src/helpers/figures/charts/bar_chart.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
ChartCreationContext,
1818
CustomizedDataSet,
1919
DataSet,
20-
DatasetDesign,
2120
ExcelChartDataset,
2221
ExcelChartDefinition,
2322
} from "../../../types/chart/chart";
@@ -62,7 +61,7 @@ export class BarChart extends AbstractChart {
6261
readonly aggregated?: boolean;
6362
readonly type = "bar";
6463
readonly dataSetsHaveTitle: boolean;
65-
readonly dataSetDesign?: DatasetDesign[];
64+
readonly customDatasets?: CustomizedDataSet[];
6665
readonly axesDesign?: AxesDesign;
6766
readonly horizontal?: boolean;
6867
readonly showValues?: boolean;
@@ -81,7 +80,7 @@ export class BarChart extends AbstractChart {
8180
this.stacked = definition.stacked;
8281
this.aggregated = definition.aggregated;
8382
this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
84-
this.dataSetDesign = definition.dataSets;
83+
this.customDatasets = definition.dataSets;
8584
this.axesDesign = definition.axesDesign;
8685
this.horizontal = definition.horizontal;
8786
this.showValues = definition.showValues;
@@ -121,7 +120,7 @@ export class BarChart extends AbstractChart {
121120
const range: CustomizedDataSet[] = [];
122121
for (const [i, dataSet] of this.dataSets.entries()) {
123122
range.push({
124-
...this.dataSetDesign?.[i],
123+
...this.customDatasets?.[i],
125124
dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
126125
});
127126
}
@@ -162,7 +161,7 @@ export class BarChart extends AbstractChart {
162161
const ranges: CustomizedDataSet[] = [];
163162
for (const [i, dataSet] of dataSets.entries()) {
164163
ranges.push({
165-
...this.dataSetDesign?.[i],
164+
...this.customDatasets?.[i],
166165
dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
167166
});
168167
}
@@ -188,7 +187,7 @@ export class BarChart extends AbstractChart {
188187
// Excel does not support aggregating labels
189188
if (this.aggregated) return undefined;
190189
const dataSets: ExcelChartDataset[] = this.dataSets
191-
.map((ds: DataSet) => toExcelDataset(this.getters, ds))
190+
.map((ds: DataSet, i: number) => toExcelDataset(this.getters, ds, this.customDatasets?.[i]))
192191
.filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
193192
const labelRange = toExcelLabelRange(
194193
this.getters,

src/helpers/figures/charts/chart_common.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@ import {
2424
DataSet,
2525
DatasetValues,
2626
ExcelChartDataset,
27+
ExcelChartTrendConfiguration,
2728
GenericDefinition,
2829
} from "../../../types/chart/chart";
2930
import { CellErrorType } from "../../../types/errors";
31+
import { CHART_TRENDLINE_TYPE_CONVERSION_MAP_REVERSE } from "../../../xlsx/conversion";
3032
import { ColorGenerator, relativeLuminance } from "../../color";
3133
import { formatValue } from "../../format/format";
3234
import { isDefined, largeMax } from "../../misc";
@@ -246,7 +248,11 @@ function createDataSet(
246248
/**
247249
* Transform a dataSet to a ExcelDataSet
248250
*/
249-
export function toExcelDataset(getters: CoreGetters, ds: DataSet): ExcelChartDataset {
251+
export function toExcelDataset(
252+
getters: CoreGetters,
253+
ds: DataSet,
254+
customDs?: CustomizedDataSet
255+
): ExcelChartDataset {
250256
const labelZone = ds.labelCell?.zone;
251257
let dataZone = ds.dataRange.zone;
252258
if (labelZone) {
@@ -272,6 +278,24 @@ export function toExcelDataset(getters: CoreGetters, ds: DataSet): ExcelChartDat
272278
};
273279
}
274280

281+
let trend: ExcelChartTrendConfiguration | undefined = undefined;
282+
if (customDs?.trend?.type) {
283+
trend = {
284+
type: CHART_TRENDLINE_TYPE_CONVERSION_MAP_REVERSE[customDs.trend.type],
285+
order: customDs?.trend?.order,
286+
color: customDs?.trend?.color,
287+
window: customDs?.trend?.window,
288+
};
289+
}
290+
if (trend) {
291+
return {
292+
label,
293+
range: getters.getRangeString(dataRange, "forceSheetReference", { useFixedReference: true }),
294+
backgroundColor: ds.backgroundColor,
295+
rightYAxis: ds.rightYAxis,
296+
trend,
297+
};
298+
}
275299
return {
276300
label,
277301
range: getters.getRangeString(dataRange, "forceSheetReference", { useFixedReference: true }),

src/helpers/figures/charts/combo_chart.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export class ComboChart extends AbstractChart {
156156
return undefined;
157157
}
158158
const dataSets: ExcelChartDataset[] = this.dataSets
159-
.map((ds: DataSet) => toExcelDataset(this.getters, ds))
159+
.map((ds: DataSet, i: number) => toExcelDataset(this.getters, ds, this.dataSetDesign?.[i]))
160160
.filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
161161
const labelRange = toExcelLabelRange(
162162
this.getters,

src/helpers/figures/charts/line_chart.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
ChartJSRuntime,
1818
CustomizedDataSet,
1919
DataSet,
20-
DatasetDesign,
2120
ExcelChartDataset,
2221
ExcelChartDefinition,
2322
} from "../../../types/chart/chart";
@@ -65,7 +64,7 @@ export class LineChart extends AbstractChart {
6564
readonly type = "line";
6665
readonly dataSetsHaveTitle: boolean;
6766
readonly cumulative: boolean;
68-
readonly dataSetDesign?: DatasetDesign[];
67+
readonly customDatasets?: CustomizedDataSet[];
6968
readonly axesDesign?: AxesDesign;
7069
readonly fillArea?: boolean;
7170
readonly showValues?: boolean;
@@ -86,7 +85,7 @@ export class LineChart extends AbstractChart {
8685
this.aggregated = definition.aggregated;
8786
this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
8887
this.cumulative = definition.cumulative;
89-
this.dataSetDesign = definition.dataSets;
88+
this.customDatasets = definition.dataSets;
9089
this.axesDesign = definition.axesDesign;
9190
this.fillArea = definition.fillArea;
9291
this.showValues = definition.showValues;
@@ -137,7 +136,7 @@ export class LineChart extends AbstractChart {
137136
const ranges: CustomizedDataSet[] = [];
138137
for (const [i, dataSet] of dataSets.entries()) {
139138
ranges.push({
140-
...this.dataSetDesign?.[i],
139+
...this.customDatasets?.[i],
141140
dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
142141
});
143142
}
@@ -165,7 +164,7 @@ export class LineChart extends AbstractChart {
165164
const range: CustomizedDataSet[] = [];
166165
for (const [i, dataSet] of this.dataSets.entries()) {
167166
range.push({
168-
...this.dataSetDesign?.[i],
167+
...this.customDatasets?.[i],
169168
dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
170169
});
171170
}
@@ -196,7 +195,7 @@ export class LineChart extends AbstractChart {
196195
// Excel does not support aggregating labels
197196
if (this.aggregated) return undefined;
198197
const dataSets: ExcelChartDataset[] = this.dataSets
199-
.map((ds: DataSet) => toExcelDataset(this.getters, ds))
198+
.map((ds: DataSet, i: number) => toExcelDataset(this.getters, ds, this.customDatasets?.[i]))
200199
.filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
201200
const labelRange = toExcelLabelRange(
202201
this.getters,

src/helpers/figures/charts/scatter_chart.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export class ScatterChart extends AbstractChart {
184184
return undefined;
185185
}
186186
const dataSets: ExcelChartDataset[] = this.dataSets
187-
.map((ds: DataSet) => toExcelDataset(this.getters, ds))
187+
.map((ds: DataSet, i: number) => toExcelDataset(this.getters, ds))
188188
.filter((ds) => ds.range !== "");
189189
const labelRange = toExcelLabelRange(
190190
this.getters,

src/types/chart/chart.ts

+10
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,18 @@ export interface ExcelChartDataset {
118118
readonly range: string;
119119
readonly backgroundColor?: Color;
120120
readonly rightYAxis?: boolean;
121+
readonly trend?: ExcelChartTrendConfiguration;
121122
}
122123

124+
export interface ExcelChartTrendConfiguration {
125+
readonly type?: ExcelTrendlineType;
126+
readonly order?: number;
127+
readonly color?: Color;
128+
readonly window?: number;
129+
}
130+
131+
export type ExcelTrendlineType = "poly" | "exp" | "log" | "movingAvg" | "linear";
132+
123133
export type ExcelChartType = "line" | "bar" | "pie" | "combo" | "scatter" | "radar";
124134

125135
export interface ExcelChartDefinition {

src/xlsx/conversion/conversion_maps.ts

+14
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,20 @@ export const CHART_TYPE_CONVERSION_MAP: Record<XLSXChartType, ExcelChartType | u
208208
comboChart: "combo",
209209
};
210210

211+
export const CHART_TRENDLINE_TYPE_CONVERSION_MAP = {
212+
exp: "exponential",
213+
log: "logarithmic",
214+
poly: "polynomial",
215+
movingAvg: "trailingMovingAverage",
216+
} as const;
217+
218+
export const CHART_TRENDLINE_TYPE_CONVERSION_MAP_REVERSE = {
219+
exponential: "exp",
220+
logarithmic: "log",
221+
polynomial: "poly",
222+
trailingMovingAverage: "movingAvg",
223+
} as const;
224+
211225
/** Conversion map for the SUBTOTAL(index, formula) function in xlsx, index <=> actual function*/
212226
export const SUBTOTAL_FUNCTION_CONVERSION_MAP: Record<number, string> = {
213227
"1": "AVERAGE",

src/xlsx/conversion/figure_conversion.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@ import {
55
toUnboundedZone,
66
zoneToXc,
77
} from "../../helpers";
8-
import { ChartDefinition, ExcelChartDefinition, FigureData } from "../../types";
8+
import {
9+
ChartDefinition,
10+
ExcelChartDefinition,
11+
ExcelChartTrendConfiguration,
12+
ExcelTrendlineType,
13+
FigureData,
14+
TrendConfiguration,
15+
} from "../../types";
916
import { ExcelImage } from "../../types/image";
1017
import { XLSXFigure, XLSXWorksheet } from "../../types/xlsx";
1118
import { convertEMUToDotValue, getColPosition, getRowPosition } from "../helpers/content_helpers";
1219
import { XLSXFigureAnchor } from "./../../types/xlsx";
1320
import { convertColor } from "./color_conversion";
21+
import { CHART_TRENDLINE_TYPE_CONVERSION_MAP } from "./conversion_maps";
1422

1523
export function convertFigures(sheetData: XLSXWorksheet): FigureData<any>[] {
1624
let id = 1;
@@ -84,6 +92,7 @@ function convertChartData(chartData: ExcelChartDefinition): ChartDefinition | un
8492
dataRange: convertExcelRangeToSheetXC(data.range, dataSetsHaveTitle),
8593
label,
8694
backgroundColor: data.backgroundColor,
95+
trend: convertExcelTrenline(data.trend),
8796
};
8897
});
8998
// For doughnut charts, in chartJS first dataset = outer dataset, in excel first dataset = inner dataset
@@ -121,6 +130,30 @@ function convertExcelRangeToSheetXC(range: string, dataSetsHaveTitle: boolean):
121130
return getFullReference(sheetName, dataXC);
122131
}
123132

133+
function convertExcelTrenline(
134+
trend: ExcelChartTrendConfiguration | undefined
135+
): TrendConfiguration | undefined {
136+
if (!trend) {
137+
return undefined;
138+
}
139+
if (trend.type === "linear") {
140+
return {
141+
type: "polynomial",
142+
order: 1,
143+
color: trend.color,
144+
window: trend.window,
145+
display: true,
146+
};
147+
}
148+
return {
149+
type: CHART_TRENDLINE_TYPE_CONVERSION_MAP[trend.type as ExcelTrendlineType],
150+
order: trend.order,
151+
color: trend.color,
152+
window: trend.window,
153+
display: true,
154+
};
155+
}
156+
124157
function getPositionFromAnchor(
125158
anchor: XLSXFigureAnchor,
126159
sheetData: XLSXWorksheet

src/xlsx/extraction/chart_extractor.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { toHex } from "../../helpers";
2-
import { ExcelChartDataset, ExcelChartDefinition } from "../../types";
2+
import {
3+
ExcelChartDataset,
4+
ExcelChartDefinition,
5+
ExcelChartTrendConfiguration,
6+
ExcelTrendlineType,
7+
} from "../../types";
38
import { XLSXChartType, XLSX_CHART_TYPES } from "../../types/xlsx";
49
import { CHART_TYPE_CONVERSION_MAP, DRAWING_LEGEND_POSITION_CONVERSION_MAP } from "../conversion";
510
import { removeTagEscapedNamespaces } from "../helpers/xml_helpers";
@@ -139,13 +144,33 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
139144
required: true,
140145
})!,
141146
backgroundColor: color ? `${toHex(color.asString())}` : undefined,
147+
trend: this.extractChartTrendline(chartDataElement),
142148
};
143149
}
144150
);
145151
})
146152
.flat();
147153
}
148154

155+
private extractChartTrendline(
156+
chartDataElement: Element
157+
): ExcelChartTrendConfiguration | undefined {
158+
const trendlineElement = this.querySelector(chartDataElement, "c:trendline");
159+
if (!trendlineElement) {
160+
return undefined;
161+
}
162+
const trendlineType = this.extractChildAttr(trendlineElement, "c:trendlineType", "val");
163+
const trendlineColor = this.extractChildAttr(trendlineElement, "a:solidFill a:srgbClr", "val");
164+
const trendlineOrder = this.extractChildAttr(trendlineElement, "c:order", "val");
165+
const trendlineWindow = this.extractChildAttr(trendlineElement, "c:period", "val");
166+
return {
167+
type: trendlineType ? (trendlineType.asString() as ExcelTrendlineType) : undefined,
168+
order: trendlineOrder ? trendlineOrder.asNum() : undefined,
169+
window: trendlineWindow ? trendlineWindow.asNum() : undefined,
170+
color: trendlineColor ? `${toHex(trendlineColor.asString())}` : undefined,
171+
};
172+
}
173+
149174
private extractScatterChartDatasets(chartElement: Element): ExcelChartDataset[] {
150175
return this.mapOnElements(
151176
{ parent: chartElement, query: "c:ser" },

0 commit comments

Comments
 (0)