Skip to content

Commit 47cb7a1

Browse files
committed
[IMP] charts: import/export title colors
This PR aims to add the ability to import and export title and axis title color from/to xlsx file Related Task: Task: 4952020
1 parent 5dbedb1 commit 47cb7a1

6 files changed

Lines changed: 373 additions & 293 deletions

File tree

src/xlsx/conversion/figure_conversion.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ function convertChartData(chartData: ExcelChartDefinition): ChartDefinition<stri
123123
isDoughnut: chartData.isDoughnut,
124124
pieHolePercentage: chartData.pieHolePercentage,
125125
showValues: chartData.showValues,
126+
axesDesign: chartData.axesDesign,
126127
};
127128
try {
128129
const ChartTypeBuilder = chartTypeRegistry.get(chartData.type);

src/xlsx/extraction/chart_extractor.ts

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { isColorValid, toHex } from "../../helpers/color";
22
import {
3+
AxesDesign,
34
ExcelChartDataset,
45
ExcelChartDefinition,
56
ExcelChartTrendConfiguration,
67
ExcelTrendlineType,
8+
TitleDesign,
79
} from "../../types/chart";
810
import { XLSX_CHART_TYPES, XLSXChartType } from "../../types/xlsx";
911
import { CHART_TYPE_CONVERSION_MAP, DRAWING_LEGEND_POSITION_CONVERSION_MAP } from "../conversion";
@@ -30,6 +32,10 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
3032
return textElement.textContent || "";
3133
}
3234
).join("");
35+
const chartTitleStyle = this.extractDefRPrStyle(
36+
rootChartElement,
37+
"c:chart > c:title a:p a:pPr a:defRPr"
38+
);
3339
const barChartGrouping = this.extractChildAttr(rootChartElement, "c:grouping", "val", {
3440
default: "clustered",
3541
}).asString();
@@ -45,8 +51,9 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
4551
(el) => el.attributes.getNamedItem("val")?.value === "1"
4652
);
4753
return {
48-
title: { text: chartTitle },
54+
title: { text: chartTitle, ...chartTitleStyle },
4955
type: CHART_TYPE_CONVERSION_MAP[chartType]!,
56+
axesDesign: this.extractAxesDesign(rootChartElement),
5057
dataSets: this.extractChartDatasets(
5158
this.querySelectorAll(rootChartElement, `c:${chartType}`)!,
5259
chartType
@@ -95,6 +102,10 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
95102
return textElement.textContent || "";
96103
}
97104
).join("");
105+
const chartTitleStyle = this.extractDefRPrStyle(
106+
chartElement,
107+
"c:chart > c:title a:p a:pPr a:defRPr"
108+
);
98109
const barChartGrouping = this.extractChildAttr(chartElement, "c:grouping", "val", {
99110
default: "clustered",
100111
}).asString();
@@ -103,8 +114,9 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
103114
(el) => el.attributes.getNamedItem("val")?.value === "1"
104115
);
105116
return {
106-
title: { text: chartTitle },
117+
title: { text: chartTitle, ...chartTitleStyle },
107118
type: "combo",
119+
axesDesign: this.extractAxesDesign(chartElement),
108120
dataSets: [
109121
...this.extractChartDatasets(
110122
this.querySelectorAll(chartElement, `c:barChart`),
@@ -136,6 +148,105 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
136148
};
137149
}
138150

151+
private extractDefRPrStyle(
152+
element: Element,
153+
defRPrQuery: string
154+
): Pick<TitleDesign, "bold" | "italic" | "fontSize" | "color"> {
155+
const defRPr = this.querySelector(element, defRPrQuery);
156+
if (!defRPr) {
157+
return {};
158+
}
159+
const bAttr = defRPr.getAttribute("b");
160+
const bold = bAttr === "1" || bAttr === "true" ? true : undefined;
161+
const iAttr = defRPr.getAttribute("i");
162+
const italic = iAttr === "1" || iAttr === "true" ? true : undefined;
163+
const szAttr = defRPr.getAttribute("sz");
164+
const fontSize = szAttr ? Math.round(parseInt(szAttr) / 100) : undefined;
165+
const color = this.extractDrawingFillColor(defRPr);
166+
return { bold, italic, fontSize, color };
167+
}
168+
169+
private extractDrawingFillColor(element: Element): string | undefined {
170+
const srgbClr = this.querySelector(element, "a:solidFill a:srgbClr");
171+
if (srgbClr) {
172+
const val = srgbClr.getAttribute("val");
173+
return val && isColorValid(val) ? toHex(val) : undefined;
174+
}
175+
const schemeClr = this.querySelector(element, "a:solidFill a:schemeClr");
176+
if (schemeClr) {
177+
const schemeName = schemeClr.getAttribute("val");
178+
if (schemeName) {
179+
return this.resolveSchemeColor(schemeName);
180+
}
181+
}
182+
return undefined;
183+
}
184+
185+
/**
186+
* Resolve a DrawingML scheme color name (e.g. "accent1", "dk1") to its hex
187+
* RGB value by looking it up in the theme's `a:clrScheme` element.
188+
* Returns `undefined` if the theme is unavailable or the color cannot be found.
189+
*/
190+
private resolveSchemeColor(schemeName: string): string | undefined {
191+
const themeFile = this.xlsxFileStructure.theme;
192+
if (!themeFile) {
193+
return undefined;
194+
}
195+
const schemeEl = this.querySelector(themeFile.file.xml, `a:clrScheme a:${schemeName}`);
196+
if (!schemeEl) {
197+
return undefined;
198+
}
199+
const srgbClr = this.querySelector(schemeEl, "a:srgbClr");
200+
if (srgbClr) {
201+
const val = srgbClr.getAttribute("val");
202+
return val && isColorValid(val) ? toHex(val) : undefined;
203+
}
204+
205+
const sysClr = this.querySelector(schemeEl, "a:sysClr");
206+
if (sysClr) {
207+
const lastClr = sysClr.getAttribute("lastClr");
208+
return lastClr && isColorValid(lastClr) ? toHex(lastClr) : undefined;
209+
}
210+
return undefined;
211+
}
212+
213+
private extractAxisTitleDesign(axElement: Element | null): TitleDesign | undefined {
214+
if (axElement === null) {
215+
return undefined;
216+
}
217+
const titleText = this.mapOnElements(
218+
{ parent: axElement, query: "c:title a:t" },
219+
(el) => el.textContent || ""
220+
).join("");
221+
if (!titleText) {
222+
return undefined;
223+
}
224+
const style = this.extractDefRPrStyle(axElement, "c:title a:p a:pPr a:defRPr");
225+
return { text: titleText, ...style };
226+
}
227+
228+
private extractAxesDesign(chartElement: Element): AxesDesign | undefined {
229+
const catAx = this.querySelector(chartElement, "c:catAx");
230+
const valAx = this.querySelector(chartElement, "c:valAx");
231+
const axPos = catAx ? this.extractChildAttr(catAx, "c:axPos", "val")?.asString() : undefined;
232+
const isHorizontalChart = axPos === "l" || axPos === "r";
233+
const xAx = isHorizontalChart ? valAx : catAx;
234+
const yAx = isHorizontalChart ? catAx : valAx;
235+
const xTitle = this.extractAxisTitleDesign(xAx);
236+
const yTitle = this.extractAxisTitleDesign(yAx);
237+
if (!xTitle && !yTitle) {
238+
return undefined;
239+
}
240+
const axesDesign: AxesDesign = {};
241+
if (xTitle) {
242+
axesDesign.x = { title: xTitle };
243+
}
244+
if (yTitle) {
245+
axesDesign.y = { title: yTitle };
246+
}
247+
return axesDesign;
248+
}
249+
139250
private extractChartDatasets(
140251
chartElements: NodeListOf<Element>,
141252
chartType: XLSXChartType

src/xlsx/functions/charts.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export function createChart(
6464
// <manualLayout/> to manually position the chart in the figure container
6565
let title = escapeXml``;
6666
if (chart.data.title?.text) {
67-
const titleColor = toXlsxHexColor(chartMutedFontColor(chart.data.backgroundColor));
67+
const titleColor = chart.data.title.color
68+
? toXlsxHexColor(chart.data.title.color)
69+
: toXlsxHexColor(chartMutedFontColor(chart.data.backgroundColor));
6870
const fontSize = chart.data.title.fontSize ?? CHART_TITLE_FONT_SIZE;
6971
title = escapeXml/*xml*/ `
7072
<c:title>
@@ -179,13 +181,14 @@ function insertText(
179181
<a:lstStyle />
180182
<a:p>
181183
<a:pPr lvl="0">
182-
<a:defRPr b="${style?.bold ? 1 : 0}" i="${style?.italic ? 1 : 0}">
184+
<a:defRPr b="${style?.bold ? 1 : 0}" i="${style?.italic ? 1 : 0}" sz="${
185+
fontsize * 100
186+
}">
183187
${solidFill(fontColor)}
184188
<a:latin typeface="+mn-lt"/>
185189
</a:defRPr>
186190
</a:pPr>
187191
<a:r> <!-- Runs -->
188-
<a:rPr b="${style?.bold ? 1 : 0}" i="${style?.italic ? 1 : 0}" sz="${fontsize * 100}"/>
189192
<a:t>${text}</a:t>
190193
</a:r>
191194
</a:p>

0 commit comments

Comments
 (0)