Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/xlsx/conversion/figure_conversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ function convertChartData(chartData: ExcelChartDefinition): ChartDefinition<stri
isDoughnut: chartData.isDoughnut,
pieHolePercentage: chartData.pieHolePercentage,
showValues: chartData.showValues,
axesDesign: chartData.axesDesign,
};
try {
const ChartTypeBuilder = chartTypeRegistry.get(chartData.type);
Expand Down
122 changes: 120 additions & 2 deletions src/xlsx/extraction/chart_extractor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { isColorValid, toHex } from "../../helpers/color";
import {
AxesDesign,
ExcelChartDataset,
ExcelChartDefinition,
ExcelChartTrendConfiguration,
ExcelTrendlineType,
TitleDesign,
} from "../../types/chart/chart";
import { XLSX_CHART_TYPES, XLSXChartType } from "../../types/xlsx";
import {
Expand Down Expand Up @@ -33,6 +35,10 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
return textElement.textContent || "";
}
).join("");
const chartTitleStyle = this.extractTitleStyle(
rootChartElement,
"c:chart > c:title a:p a:pPr a:defRPr"
);
const barChartGrouping = this.extractChildAttr(rootChartElement, "c:grouping", "val", {
default: "clustered",
}).asString();
Expand All @@ -48,8 +54,9 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
(el) => el.attributes.getNamedItem("val")?.value === "1"
);
return {
title: { text: chartTitle },
title: { text: chartTitle, ...chartTitleStyle },
type: CHART_TYPE_CONVERSION_MAP[chartType]!,
axesDesign: this.extractAxesDesign(rootChartElement),
dataSets: this.extractChartDatasets(
this.querySelectorAll(rootChartElement, `c:${chartType}`)!,
chartType
Expand Down Expand Up @@ -98,6 +105,10 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
return textElement.textContent || "";
}
).join("");
const chartTitleStyle = this.extractTitleStyle(
chartElement,
"c:chart > c:title a:p a:pPr a:defRPr"
);
const barChartGrouping = this.extractChildAttr(chartElement, "c:grouping", "val", {
default: "clustered",
}).asString();
Expand All @@ -106,8 +117,9 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
(el) => el.attributes.getNamedItem("val")?.value === "1"
);
return {
title: { text: chartTitle },
title: { text: chartTitle, ...chartTitleStyle },
type: "combo",
axesDesign: this.extractAxesDesign(chartElement),
dataSets: [
...this.extractChartDatasets(
this.querySelectorAll(chartElement, `c:barChart`),
Expand Down Expand Up @@ -139,6 +151,112 @@ export class XlsxChartExtractor extends XlsxBaseExtractor {
};
}

/**
* Extracts the style properties (bold, italic, font size and color) for a chart or axis title from a `a:defRPr` element in the XML.
* defRPr stands for "default run properties" and is the element that contains the default style properties for a text run in DrawingML.
* For more information on the `a:defRPr` element and the style properties it can contain, see §21.1.2.3.2 defRPr (Default Text Run Properties).
* The `titleQuery` parameter is the query to select the `a:defRPr` element that contains the style properties.
* Returns an object with the extracted style properties. If a property is not defined in the XML, it will be `undefined` in the returned object.
*/
private extractTitleStyle(
element: Element,
titleQuery: string
): Pick<TitleDesign, "bold" | "italic" | "fontSize" | "color"> {
const defRPr = this.querySelector(element, titleQuery);
if (!defRPr) {
return {};
}
const bAttr = defRPr.getAttribute("b");
const bold = bAttr === "1" || bAttr === "true" ? true : undefined;
const iAttr = defRPr.getAttribute("i");
const italic = iAttr === "1" || iAttr === "true" ? true : undefined;
const szAttr = defRPr.getAttribute("sz");
const fontSize = szAttr ? Math.round(parseInt(szAttr) / 100) : undefined;
const color = this.extractDrawingFillColor(defRPr);
return { bold, italic, fontSize, color };
}

private extractDrawingFillColor(element: Element): string | undefined {
const srgbClr = this.querySelector(element, "a:solidFill a:srgbClr");
if (srgbClr) {
const val = srgbClr.getAttribute("val");
return val && isColorValid(val) ? toHex(val) : undefined;
}
const schemeClr = this.querySelector(element, "a:solidFill a:schemeClr");
if (schemeClr) {
const schemeName = schemeClr.getAttribute("val");
if (schemeName) {
return this.resolveSchemeColor(schemeName);
}
}
return undefined;
}

/**
* Resolve a DrawingML scheme color name (e.g. "accent1", "dk1") to its hex
* RGB value by looking it up in the theme's `a:clrScheme` element.
* Returns `undefined` if the theme is unavailable or the color cannot be found.
*/
private resolveSchemeColor(schemeName: string): string | undefined {
const themeFile = this.xlsxFileStructure.theme;
if (!themeFile) {
return undefined;
}
const schemeEl = this.querySelector(themeFile.file.xml, `a:clrScheme a:${schemeName}`);
if (!schemeEl) {
return undefined;
}
const srgbClr = this.querySelector(schemeEl, "a:srgbClr");
if (srgbClr) {
const val = srgbClr.getAttribute("val");
return val && isColorValid(val) ? toHex(val) : undefined;
}

const sysClr = this.querySelector(schemeEl, "a:sysClr");
if (sysClr) {
const lastClr = sysClr.getAttribute("lastClr");
return lastClr && isColorValid(lastClr) ? toHex(lastClr) : undefined;
}
return undefined;
}

private extractAxisTitleDesign(axElement: Element | null): TitleDesign | undefined {
if (axElement === null) {
return undefined;
}
const titleText = this.mapOnElements(
{ parent: axElement, query: "c:title a:t" },
(el) => el.textContent || ""
).join("");
if (!titleText) {
return undefined;
}
const style = this.extractTitleStyle(axElement, "c:title a:p a:pPr a:defRPr");
return { text: titleText, ...style };
}

private extractAxesDesign(chartElement: Element): AxesDesign | undefined {
const catAx = this.querySelector(chartElement, "c:catAx");
const valAx = this.querySelector(chartElement, "c:valAx");
const axPos = catAx ? this.extractChildAttr(catAx, "c:axPos", "val")?.asString() : undefined;
const isHorizontalChart = axPos === "l" || axPos === "r";
const xAx = isHorizontalChart ? valAx : catAx;
const yAx = isHorizontalChart ? catAx : valAx;
const xTitle = this.extractAxisTitleDesign(xAx);
const yTitle = this.extractAxisTitleDesign(yAx);
if (!xTitle && !yTitle) {
return undefined;
}
const axesDesign: AxesDesign = {};
if (xTitle) {
axesDesign.x = { title: xTitle };
}
if (yTitle) {
axesDesign.y = { title: yTitle };
}
return axesDesign;
}

private extractChartDatasets(
chartElements: NodeListOf<Element>,
chartType: XLSXChartType
Expand Down
9 changes: 6 additions & 3 deletions src/xlsx/functions/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export function createChart(
// <manualLayout/> to manually position the chart in the figure container
let title = escapeXml``;
if (chart.data.title?.text) {
const titleColor = toXlsxHexColor(chartMutedFontColor(chart.data.backgroundColor));
const titleColor = chart.data.title.color
Comment thread
anhe-odoo marked this conversation as resolved.
? toXlsxHexColor(chart.data.title.color)
: toXlsxHexColor(chartMutedFontColor(chart.data.backgroundColor));
const fontSize = chart.data.title.fontSize ?? CHART_TITLE_FONT_SIZE;
title = escapeXml/*xml*/ `
<c:title>
Expand Down Expand Up @@ -179,13 +181,14 @@ function insertText(
<a:lstStyle />
<a:p>
<a:pPr lvl="0">
<a:defRPr b="${style?.bold ? 1 : 0}" i="${style?.italic ? 1 : 0}">
<a:defRPr b="${style?.bold ? 1 : 0}" i="${style?.italic ? 1 : 0}" sz="${
fontsize * 100
}">
${solidFill(fontColor)}
<a:latin typeface="+mn-lt"/>
</a:defRPr>
</a:pPr>
<a:r> <!-- Runs -->
<a:rPr b="${style?.bold ? 1 : 0}" i="${style?.italic ? 1 : 0}" sz="${fontsize * 100}"/>
<a:t>${text}</a:t>
</a:r>
</a:p>
Expand Down
Loading