diff --git a/demos/modules/demo_chart.mjs b/demos/modules/demo_chart.mjs index ec30763db..5059c0052 100644 --- a/demos/modules/demo_chart.mjs +++ b/demos/modules/demo_chart.mjs @@ -63,6 +63,8 @@ export function genSlides_Chart(pptx) { genSlide16(pptx); genSlide17(pptx); genSlide18(pptx); + genSlide19(pptx); + genSlide20(pptx); } function initTestData() { @@ -1642,9 +1644,6 @@ function genSlide16(pptx) { // SLIDE 17: Multi-Type Charts function genSlide17(pptx) { - // powerpoint 2016 add secondary category axis labels - // https://peltiertech.com/chart-with-a-dual-category-axis/ - let slide = pptx.addSlide({ sectionTitle: "Charts" }); slide.addNotes("API Docs: https://gitbrent.github.io/PptxGenJS/docs/api-charts.html"); slide.addTable([[{ text: "Chart Examples: Multi-Type Charts", options: BASE_TEXT_OPTS_L }, BASE_TEXT_OPTS_R]], BASE_TABLE_OPTS); @@ -2200,3 +2199,184 @@ function genSlide18(pptx) { slide.addChart(pptx.charts.BAR, arrDataRegions, optsChartBar3); slide.addChart(pptx.charts.BAR, arrDataHighVals, optsChartBar4); } + +// SLIDE 19: Chart Examples: Multi Level Category Axes +function genSlide19(pptx) { + let slide = pptx.addSlide({ sectionTitle: "Charts" }); + slide.addNotes("API Docs: https://gitbrent.github.io/PptxGenJS/docs/api-charts.html"); + slide.addTable([[{ text: "Chart Examples: Multi Level Category Axes", options: BASE_TEXT_OPTS_L }, BASE_TEXT_OPTS_R]], BASE_TABLE_OPTS); + + const arrDataRegions = [ + { + name: 'Mechanical', + labels: [ + [ + 'Gear', 'Bearing', + 'Motor', 'Switch', + 'Plug', 'Cord', + 'Fuse', 'Bulb', + 'Pump', 'Leak', + 'Seals' + ], + [ + 'Mechanical', '', + '', 'Electrical', + '', '', + '', '', + 'Hydraulic', '', + '' + ] + ], + values: [ + 11, 8, 3, 0, 0, + 0, 0, 0, 0, 0, + 0 + ] + }, + { + name: 'Electrical', + labels: [ + [ + 'Gear', 'Bearing', + 'Motor', 'Switch', + 'Plug', 'Cord', + 'Fuse', 'Bulb', + 'Pump', 'Leak', + 'Seals' + ], + [ + 'Mechanical', '', + '', 'Electrical', + '', '', + '', '', + 'Hydraulic', '', + '' + ] + ], + values: [ + 0, 0, 0, 19, 12, + 11, 3, 2, 0, 0, + 0 + ] + }, + { + name: 'Hydraulic', + labels: [ + [ + 'Gear', 'Bearing', + 'Motor', 'Switch', + 'Plug', 'Cord', + 'Fuse', 'Bulb', + 'Pump', 'Leak', + 'Seals' + ], + [ + 'Mechanical', '', + '', 'Electrical', + '', '', + '', '', + 'Hydraulic', '', + '' + ] + ], + values: [ + 0, 0, 0, 0, 0, + 0, 0, 0, 4, 3, + 1 + ] + } + ]; + + const opts1 = { + catAxisMultiLevelLabels: true, + x: 0.6, + y: 0.6, + w: 6.0, + h: 3.0, + }; + + const opts2 = { + barDir: 'col', + + catAxisMultiLevelLabels: true, + x: 7.0, + y: 0.6, + w: 6.0, + h: 3.0, + }; + + const opts3 = { + barDir: 'col', + + catAxisMultiLevelLabels: true, + x: 0.6, + y: 4.0, + w: 6.0, + h: 3.0, + }; + + const opts4 = { + catAxisMultiLevelLabels: true, + x: 7, + y: 4.0, + w: 6.0, + h: 3.0, + }; + + slide.addChart(pptx.charts.AREA, arrDataRegions, opts1); + slide.addChart(pptx.charts.BAR, arrDataRegions, opts2); + slide.addChart(pptx.charts.BAR3D, arrDataRegions, opts3); + slide.addChart(pptx.charts.LINE, arrDataRegions, opts4); +} + +// SLIDE 20: Chart Examples: Three Levels Category Axes +function genSlide20(pptx) { + let slide = pptx.addSlide({ sectionTitle: "Charts" }); + slide.addNotes("API Docs: https://gitbrent.github.io/PptxGenJS/docs/api-charts.html"); + slide.addTable([[{ text: "Chart Examples: Three Levels Category Axes", options: BASE_TEXT_OPTS_L }, BASE_TEXT_OPTS_R]], BASE_TABLE_OPTS); + + const arrDataRegions = [ + { + name: 'Fruits', + labels: [ + [ + '1/3', '1/25', '6/5', + '6/21', '7/27', '2/20', + '3/17', '4/24', '6/23', + '8/5', '4/16', '1/29', + '2/23', '4/4', '7/15' + ], + [ + 'Apple', '', '', + '', '', 'Orange', + '', '', '', + 'Orange', '', 'Peach', + 'Pear', '', '', + '' + ], + [ + '2014', '', '', '', + '', '', '', '', + '', '2015', '', '', + '', '', '', '' + ] + ], + values: [ + 734, 465, 656, 176, + 434, 165, 613, 359, + 279, 660, 307, 270, + 539, 142, 554 + ] + } + ]; + + const opts1 = { + catAxisMultiLevelLabels: true, + x: 0.2, + y: 0.2, + w: 12.9, + h: 7.1 + }; + + slide.addChart(pptx.charts.BAR, arrDataRegions, opts1); +} diff --git a/src/core-interfaces.ts b/src/core-interfaces.ts index 215d470c6..f18a69200 100644 --- a/src/core-interfaces.ts +++ b/src/core-interfaces.ts @@ -1107,7 +1107,7 @@ export interface OptsDataLabelPosition { export type ChartAxisTickMark = 'none' | 'inside' | 'outside' | 'cross' export interface OptsChartData { index?: number - labels?: string[] + labels?: string[] | string[][] name?: string sizes?: number[] values?: number[] @@ -1116,6 +1116,10 @@ export interface OptsChartData { */ //color?: string // TODO: WIP: (Pull #727) } +// Used internally, probably shouldn't be used by end users +export interface IOptsChartData extends OptsChartData { + labels?: string[][] +} export interface OptsChartGridLine { /** * Gridline color (hex) @@ -1231,6 +1235,7 @@ export interface IChartPropsAxisCat { catAxisMinorTimeUnit?: string catAxisMinorUnit?: string catAxisMinVal?: number + catAxisMultiLevelLabels?: boolean catAxisOrientation?: 'minMax' catAxisTitle?: string catAxisTitleColor?: string @@ -1490,7 +1495,7 @@ export interface IChartOptsLib extends IChartOpts { export interface ISlideRelChart extends OptsChartData { type: CHART_NAME | IChartMulti[] opts: IChartOptsLib - data: OptsChartData[] + data: IOptsChartData[] // internal below rId: number Target: string diff --git a/src/gen-charts.ts b/src/gen-charts.ts index c1663d245..c41aa65fe 100644 --- a/src/gen-charts.ts +++ b/src/gen-charts.ts @@ -19,7 +19,7 @@ import { LETTERS, ONEPT, } from './core-enums' -import { IChartOptsLib, ISlideRelChart, ShadowProps, OptsChartData, IChartPropsTitle, OptsChartGridLine } from './core-interfaces' +import { IChartOptsLib, ISlideRelChart, ShadowProps, IChartPropsTitle, OptsChartGridLine, IOptsChartData } from './core-interfaces' import { createColorElement, genXmlColorSelection, convertRotationDegrees, encodeXmlEntities, getMix, getUuid, valToPts } from './gen-utils' import JSZip from 'jszip' @@ -146,13 +146,18 @@ export function createExcelWorksheet(chartObject: ISlideRelChart, zip: JSZip): P strSharedStrings += '' } else { + // series names + all labels of one series + number of label groups (data.labels.length) of one series (i.e. how many times the blank string is used) + const count = data.length + data[0].labels.length * data[0].labels[0].length + data[0].labels.length + // series names + labels of one series + blank string (same for all label groups) + const uniqueCount = data.length + data[0].labels.length * data[0].labels[0].length + 1 + strSharedStrings += '' - // B: Add 'blank' for A1 + // B: Add 'blank' for A1, B1, ..., of every label group inside data[n].labels strSharedStrings += '' } @@ -173,8 +178,10 @@ export function createExcelWorksheet(chartObject: ISlideRelChart, zip: JSZip): P // D: Add `labels`/Categories if (chartObject.opts._type !== CHART_TYPE.BUBBLE && chartObject.opts._type !== CHART_TYPE.BUBBLE3D && chartObject.opts._type !== CHART_TYPE.SCATTER) { - data[0].labels.forEach(label => { - strSharedStrings += '' + encodeXmlEntities(label) + '' + data[0].labels.forEach(labelsGroup => { + labelsGroup.forEach(label => { + strSharedStrings += '' + encodeXmlEntities(label) + '' + }) }) } @@ -204,13 +211,15 @@ export function createExcelWorksheet(chartObject: ISlideRelChart, zip: JSZip): P } else { strTableXml += '' - strTableXml += '' - strTableXml += '' + strTableXml += '' + data[0].labels.forEach((_labelsGroup, idx) => { + strTableXml += '' + }) data.forEach((obj, idx) => { - strTableXml += '' + strTableXml += '' }) } strTableXml += '' @@ -229,7 +238,7 @@ export function createExcelWorksheet(chartObject: ISlideRelChart, zip: JSZip): P } else if (chartObject.opts._type === CHART_TYPE.SCATTER) { strSheetXml += '' } else { - strSheetXml += '' + strSheetXml += '' } strSheetXml += '' @@ -352,26 +361,33 @@ export function createExcelWorksheet(chartObject: ISlideRelChart, zip: JSZip): P -|-------|-----|-----|-----| */ - // A: Create header row first (NOTE: Start at index=1 as headers cols start with 'B') - strSheetXml += '' - strSheetXml += '0' + // A: Create header row first + strSheetXml += '' + data[0].labels.forEach((_labelsGroup, idx) => { + strSheetXml += '' + strSheetXml += '0' + strSheetXml += '' + }) for (let idx = 1; idx <= data.length; idx++) { // FIXME: Max cols is 52 - strSheetXml += '' // NOTE: use `t="s"` for label cols! + strSheetXml += '' // NOTE: use `t="s"` for label cols! strSheetXml += '' + idx + '' strSheetXml += '' } strSheetXml += '' // B: Add data row(s) for each category - data[0].labels.forEach((_cat, idx) => { - // Leading col is reserved for the label, so hard-code it, then loop over col values - strSheetXml += '' - strSheetXml += '' - strSheetXml += '' + (data.length + idx + 1) + '' - strSheetXml += '' + data[0].labels[0].forEach((_cat, idx) => { + strSheetXml += '' + // Leading cols are reserved for the label groups + for (let idx2 = data[0].labels.length - 1; idx2 >= 0; idx2--) { + strSheetXml += '' + strSheetXml += '' + (data.length + idx + (idx2 * (data[0].labels[0].length)) + 1) + '' + strSheetXml += '' + } + for (let idy = 0; idy < data.length; idy++) { - strSheetXml += '' + strSheetXml += '' strSheetXml += '' + (data[idy].values[idx] || '') + '' strSheetXml += '' } @@ -645,7 +661,7 @@ export function makeXmlCharts(rel: ISlideRelChart): string { * @example '' * @return {string} XML */ -function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChartOptsLib, valAxisId: string, catAxisId: string, isMultiTypeChart: boolean): string { +function makeChartType(chartType: CHART_NAME, data: IOptsChartData[], opts: IChartOptsLib, valAxisId: string, catAxisId: string, isMultiTypeChart: boolean): string { // NOTE: "Chart Range" (as shown in "select Chart Area dialog") is calculated. // ....: Ensure each X/Y Axis/Col has same row height (esp. applicable to XY Scatter where X can often be larger than Y's) let strXml: string = '' @@ -674,20 +690,40 @@ function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChar strXml += '' // 2: "Series" block for every data row - /* EX: + /* EX1: data: [ { name: 'Region 1', - labels: ['April', 'May', 'June', 'July'], + labels: [['April', 'May', 'June', 'July']], values: [17, 26, 53, 96] }, { name: 'Region 2', - labels: ['April', 'May', 'June', 'July'], + labels: [['April', 'May', 'June', 'July']], values: [55, 43, 70, 58] } ] */ + /* EX2: + data: [ + { + name: 'Region 1', + labels: [ + ['April', 'May', 'June', 'April', 'May', 'June'], + ['2020', '', '', '2021', '', ''] + ], + values: [17, 26, 53, 96, 40, 33] + }, + { + name: 'Region 2', + labels: [ + ['April', 'May', 'June', 'April', 'May', 'June'], + ['2020', '', '', '2021', '', ''] + ], + values: [55, 43, 70, 58, 78, 63] + } + ] + */ let colorIndex = -1 // Maintain the color index by region data.forEach(obj => { colorIndex++ @@ -697,7 +733,7 @@ function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChar strXml += ' ' strXml += ' ' strXml += ' ' - strXml += ' Sheet1!$' + getExcelColName(idx + 1) + '$1' + strXml += ' Sheet1!$' + getExcelColName(idx + obj.labels.length) + '$1' strXml += ' ' + encodeXmlEntities(obj.name) + '' strXml += ' ' strXml += ' ' @@ -842,25 +878,29 @@ function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChar if (opts.catLabelFormatCode) { // Use 'numRef' as catLabelFormatCode implies that we are expecting numbers here strXml += ' ' - strXml += ' Sheet1!$A$2:$A$' + (obj.labels.length + 1) + '' + strXml += ' Sheet1!$A$2:$A$' + (obj.labels[0].length + 1) + '' strXml += ' ' strXml += ' ' + (opts.catLabelFormatCode || 'General') + '' - strXml += ' ' - obj.labels.forEach((label, idx) => { + strXml += ' ' + obj.labels[0].forEach((label, idx) => { strXml += '' + encodeXmlEntities(label) + '' }) strXml += ' ' strXml += ' ' } else { - strXml += ' ' - strXml += ' Sheet1!$A$2:$A$' + (obj.labels.length + 1) + '' - strXml += ' ' - strXml += ' ' - obj.labels.forEach((label, idx) => { - strXml += '' + encodeXmlEntities(label) + '' + strXml += ' ' + strXml += ' Sheet1!$A$2:$' + getExcelColName(obj.labels.length - 1) + '$' + (obj.labels[0].length + 1) + '' + strXml += ' ' + strXml += ' ' + obj.labels.forEach(labelsGroup => { + strXml += ' ' + labelsGroup.forEach((label, idx) => { + strXml += '' + encodeXmlEntities(label) + '' + }) + strXml += ' ' }) - strXml += ' ' - strXml += ' ' + strXml += ' ' + strXml += ' ' } strXml += '' } @@ -869,10 +909,10 @@ function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChar { strXml += '' strXml += ' ' - strXml += ' Sheet1!$' + getExcelColName(idx + 1) + '$2:$' + getExcelColName(idx + 1) + '$' + (obj.labels.length + 1) + '' + strXml += ' Sheet1!$' + getExcelColName(idx + obj.labels.length) + '$2:$' + getExcelColName(idx + obj.labels.length) + '$' + (obj.labels[0].length + 1) + '' strXml += ' ' strXml += ' ' + (opts.valLabelFormatCode || opts.dataTableFormatCode || 'General') + '' - strXml += ' ' + strXml += ' ' obj.values.forEach((value, idx) => { strXml += '' + (value || value === 0 ? value : '') + '' }) @@ -1024,9 +1064,9 @@ function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChar // Option: scatter data point labels if (opts.showLabel) { let chartUuid = getUuid('-xxxx-xxxx-xxxx-xxxxxxxxxxxx') - if (obj.labels && (opts.dataLabelFormatScatter === 'custom' || opts.dataLabelFormatScatter === 'customXY')) { + if (obj.labels[0] && (opts.dataLabelFormatScatter === 'custom' || opts.dataLabelFormatScatter === 'customXY')) { strXml += '' - obj.labels.forEach((label, idx) => { + obj.labels[0].forEach((label, idx) => { if (opts.dataLabelFormatScatter === 'custom' || opts.dataLabelFormatScatter === 'customXY') { strXml += ' ' strXml += ' ' @@ -1456,7 +1496,7 @@ function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChar //strXml += '' // 2: "Data Point" block for every data row - obj.labels.forEach((_label, idx) => { + obj.labels[0].forEach((_label, idx) => { strXml += '' strXml += ` ` strXml += ' ' @@ -1476,7 +1516,7 @@ function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChar // 3: "Data Label" block for every data Label strXml += '' - obj.labels.forEach((_label, idx) => { + obj.labels[0].forEach((_label, idx) => { strXml += '' strXml += ` ` strXml += ` ` @@ -1525,10 +1565,10 @@ function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChar // 2: "Categories" strXml += '' strXml += ' ' - strXml += ' Sheet1!$A$2:$A$' + (obj.labels.length + 1) + '' + strXml += ' Sheet1!$A$2:$A$' + (obj.labels[0].length + 1) + '' strXml += ' ' - strXml += ' ' - obj.labels.forEach((label, idx) => { + strXml += ' ' + obj.labels[0].forEach((label, idx) => { strXml += '' + encodeXmlEntities(label) + '' }) strXml += ' ' @@ -1538,9 +1578,9 @@ function makeChartType(chartType: CHART_NAME, data: OptsChartData[], opts: IChar // 3: Create vals strXml += ' ' strXml += ' ' - strXml += ' Sheet1!$B$2:$B$' + (obj.labels.length + 1) + '' + strXml += ' Sheet1!$B$2:$B$' + (obj.labels[0].length + 1) + '' strXml += ' ' - strXml += ' ' + strXml += ' ' obj.values.forEach((value, idx) => { strXml += '' + (value || value === 0 ? value : '') + '' }) @@ -1646,7 +1686,7 @@ function makeCatAxis(opts: IChartOptsLib, axisId: string, valAxisId: string): st strXml += ` ` strXml += ' ' strXml += ' ' - strXml += ' ' + strXml += ' ' if (opts.catAxisLabelFrequency) strXml += ' ' // Issue#149: PPT will auto-adjust these as needed after calcing the date bounds, so we only include them when specified by user diff --git a/src/gen-objects.ts b/src/gen-objects.ts index 3fbaf2545..b86acfa0c 100644 --- a/src/gen-objects.ts +++ b/src/gen-objects.ts @@ -167,6 +167,11 @@ export function addChartDefinition(target: PresSlide, type: CHART_NAME | IChartM } tmpData.forEach((item, i) => { item.index = i + + // Converts the 'labels' array from string[] to string[][] (or the respective primitive type), if needed + if (item.labels !== undefined && !Array.isArray(item.labels[0])) { + item.labels = [item.labels as string[]] + } }) options = tmpOpt && typeof tmpOpt === 'object' ? tmpOpt : {} @@ -335,6 +340,12 @@ export function addChartDefinition(target: PresSlide, type: CHART_NAME | IChartM options.lineSize = typeof options.lineSize === 'number' ? options.lineSize : 2 options.valAxisMajorUnit = typeof options.valAxisMajorUnit === 'number' ? options.valAxisMajorUnit : null + if (options._type === CHART_TYPE.AREA || options._type === CHART_TYPE.BAR || options._type === CHART_TYPE.BAR3D || options._type === CHART_TYPE.LINE) { + options.catAxisMultiLevelLabels = !!options.catAxisMultiLevelLabels + } else { + delete options.catAxisMultiLevelLabels + } + // STEP 4: Set props resultObject._type = 'chart' resultObject.options = options diff --git a/types/index.d.ts b/types/index.d.ts index 893746e6c..b2ec04def 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1903,7 +1903,7 @@ declare namespace PptxGenJS { export type ChartAxisTickMark = 'none' | 'inside' | 'outside' | 'cross' export interface OptsChartData { index?: number - labels?: string[] + labels?: string[] | string[][] name?: string sizes?: number[] values?: number[]