/** * PptxGenJS: Chart Generation */ import { AXIS_ID_CATEGORY_PRIMARY, AXIS_ID_CATEGORY_SECONDARY, AXIS_ID_SERIES_PRIMARY, AXIS_ID_VALUE_PRIMARY, AXIS_ID_VALUE_SECONDARY, BARCHART_COLORS, CHART_NAME, CHART_TYPE, DEF_CHART_GRIDLINE, DEF_FONT_COLOR, DEF_FONT_SIZE, DEF_FONT_TITLE_SIZE, DEF_SHAPE_SHADOW, LETTERS, ONEPT, } from './core-enums' import { IChartOptsLib, ISlideRelChart, ShadowProps, IChartPropsTitle, OptsChartGridLine, IOptsChartData, ChartLineCap } from './core-interfaces' import { createColorElement, genXmlColorSelection, convertRotationDegrees, encodeXmlEntities, getUuid, valToPts } from './gen-utils' import JSZip from 'jszip' /** * Based on passed data, creates Excel Worksheet that is used as a data source for a chart. * @param {ISlideRelChart} chartObject - chart object * @param {JSZip} zip - file that the resulting XLSX should be added to * @return {Promise} promise of generating the XLSX file */ export async function createExcelWorksheet (chartObject: ISlideRelChart, zip: JSZip): Promise { const data = chartObject.data return await new Promise((resolve, reject) => { const zipExcel = new JSZip() const intBubbleCols = (data.length - 1) * 2 + 1 // 1 for "X-Values", then 2 for every Y-Axis const IS_MULTI_CAT_AXES = data[0]?.labels?.length > 1 // A: Add folders zipExcel.folder('_rels') zipExcel.folder('docProps') zipExcel.folder('xl/_rels') zipExcel.folder('xl/tables') zipExcel.folder('xl/theme') zipExcel.folder('xl/worksheets') zipExcel.folder('xl/worksheets/_rels') // B: Add core contents { zipExcel.file( '[Content_Types].xml', '' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '\n' ) zipExcel.file( '_rels/.rels', '' + '' + '' + '' + '\n' ) zipExcel.file( 'docProps/app.xml', '' + 'Microsoft Macintosh Excel' + '0' + 'false' + 'Worksheets1' + 'Sheet1' + 'falsefalsefalse16.0300' + '\n' ) zipExcel.file( 'docProps/core.xml', '' + 'PptxGenJS' + 'PptxGenJS' + '' + new Date().toISOString() + '' + '' + new Date().toISOString() + '' + '' ) zipExcel.file( 'xl/_rels/workbook.xml.rels', '' + '' + '' + '' + '' + '' + '' ) zipExcel.file( 'xl/styles.xml', '' + '' + '\n' ) zipExcel.file( 'xl/theme/theme1.xml', '' ) zipExcel.file( 'xl/workbook.xml', '' + '' + '' + '' + '' + '' + '' + '\n' ) zipExcel.file( 'xl/worksheets/_rels/sheet1.xml.rels', '' + '' + '' + '\n' ) } // sharedStrings.xml { // A: Start XML let strSharedStrings = '' if (chartObject.opts._type === CHART_TYPE.BUBBLE || chartObject.opts._type === CHART_TYPE.BUBBLE3D) { strSharedStrings += `` } else if (chartObject.opts._type === CHART_TYPE.SCATTER) { strSharedStrings += `` } else if (IS_MULTI_CAT_AXES) { let totCount = data.length data[0].labels.forEach(arrLabel => (totCount += arrLabel.filter(label => label && label !== '').length)) strSharedStrings += `` 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 totCount = 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 unqCount = data.length + data[0].labels.length * data[0].labels[0].length + 1 // start `sst` strSharedStrings += `` // B: Add 'blank' for A1, B1, ..., of every label group inside data[n].labels strSharedStrings += '' } // C: Add `name`/Series if (chartObject.opts._type === CHART_TYPE.BUBBLE || chartObject.opts._type === CHART_TYPE.BUBBLE3D) { data.forEach((objData, idx) => { if (idx === 0) strSharedStrings += 'X-Axis' else { strSharedStrings += `${encodeXmlEntities(objData.name || `Y-Axis${idx}`)}` strSharedStrings += `${encodeXmlEntities(`Size${idx}`)}` } }) } else { data.forEach(objData => { strSharedStrings += `${encodeXmlEntities((objData.name || ' ').replace('X-Axis', 'X-Values'))}` }) } // D: Add `labels`/Categories if (chartObject.opts._type !== CHART_TYPE.BUBBLE && chartObject.opts._type !== CHART_TYPE.BUBBLE3D && chartObject.opts._type !== CHART_TYPE.SCATTER) { // Use forEach backwards & check for '' to support multi-cat axes data[0].labels .slice() .reverse() .forEach(labelsGroup => { labelsGroup .filter(label => label && label !== '') .forEach(label => { strSharedStrings += `${encodeXmlEntities(label)}` }) }) } // DONE: strSharedStrings += '\n' zipExcel.file('xl/sharedStrings.xml', strSharedStrings) } // tables/table1.xml { let strTableXml = '' if (chartObject.opts._type === CHART_TYPE.BUBBLE || chartObject.opts._type === CHART_TYPE.BUBBLE3D) { strTableXml += `` strTableXml += `` let idxColLtr = 1 data.forEach((obj, idx) => { if (idx === 0) { strTableXml += `` } else { strTableXml += `` idxColLtr++ strTableXml += `` } }) } else if (chartObject.opts._type === CHART_TYPE.SCATTER) { strTableXml += `
` strTableXml += `` data.forEach((_obj, idx) => { strTableXml += `` }) } else { strTableXml += `
` strTableXml += `` data[0].labels.forEach((_labelsGroup, idx) => { strTableXml += `` }) data.forEach((obj, idx) => { strTableXml += `` }) } strTableXml += '' strTableXml += '' strTableXml += '
' zipExcel.file('xl/tables/table1.xml', strTableXml) } // worksheets/sheet1.xml { let strSheetXml = '' strSheetXml += '' if (chartObject.opts._type === CHART_TYPE.BUBBLE || chartObject.opts._type === CHART_TYPE.BUBBLE3D) { strSheetXml += `` } else if (chartObject.opts._type === CHART_TYPE.SCATTER) { strSheetXml += `` } else { strSheetXml += `` } strSheetXml += '' strSheetXml += '' if (chartObject.opts._type === CHART_TYPE.BUBBLE || chartObject.opts._type === CHART_TYPE.BUBBLE3D) { // UNUSED: strSheetXml += `` /* EX: INPUT: `data` [ { name:'X-Axis' , values:[10,11,12,13,14,15,16,17,18,19,20] }, { name:'Y-Axis 1', values:[ 1, 6, 7, 8, 9], sizes:[ 4, 5, 6, 7, 8] }, { name:'Y-Axis 2', values:[33,32,42,53,63], sizes:[11,12,13,14,15] } ]; */ /* EX: OUTPUT: bubbleChart Worksheet: -|----A-----|------B-----|------C-----|------D-----|------E-----| 1| X-Values | Y-Values 1 | Y-Sizes 1 | Y-Values 2 | Y-Sizes 2 | 2| 11 | 22 | 4 | 33 | 8 | -|----------|------------|------------|------------|------------| */ strSheetXml += '' // A: Create header row first (NOTE: Start at index=1 as headers cols start with 'B') strSheetXml += `` strSheetXml += '0' for (let idx = 1; idx < intBubbleCols; idx++) { strSheetXml += `${idx}` // NOTE: add `t="s"` for label cols! } strSheetXml += '' // B: Add row for each X-Axis value (Y-Axis* value is optional) data[0].values.forEach((val, idx) => { // Leading col is reserved for the 'X-Axis' value, so hard-code it, then loop over col values strSheetXml += `` strSheetXml += `${val}` // Add Y-Axis 1->N (idy=0 = Xaxis) let idxColLtr = 2 for (let idy = 1; idy < data.length; idy++) { // y-value strSheetXml += `${data[idy].values[idx] || ''}` idxColLtr++ // y-size strSheetXml += `${data[idy].sizes[idx] || ''}` idxColLtr++ } strSheetXml += '' }) } else if (chartObject.opts._type === CHART_TYPE.SCATTER) { /* UNUSED: strSheetXml += '' strSheetXml += '' //data.forEach((obj,idx)=>{ strSheetXml += '' }); strSheetXml += '' */ /* EX: INPUT: `data` [ { name:'X-AxisA', values:[ 1, 2, 3, 4, 5] }, { name:'Y-AxisB', values:[ 2,22,42,52,62] }, { name:'Y-AxisC', values:[ 3,33,43,53,63] } ]; */ /* EX: OUTPUT: sheet1.xml: -|----A----|----B----|----C----| 1| X-AxisA | Y-AxisB | Y-AxisC | 2| 1 | 2 | 3 | -|---------|---------|---------| */ strSheetXml += '' // A: Create header row first (every `name` row provided) strSheetXml += `` for (let idx = 0; idx < data.length; idx++) { strSheetXml += `${idx}` // NOTE: add `t="s"` for label cols! } strSheetXml += '' // B: Add row for each X-Axis value (Y-Axis* value is optional) data[0].values.forEach((val, idx) => { // Leading col is reserved for the 'X-Axis' value, so hard-code it, then loop over col values strSheetXml += `` strSheetXml += `${val}` // Add Y-Axis 1->N for (let idy = 1; idy < data.length; idy++) { strSheetXml += `${data[idy].values[idx] || data[idy].values[idx] === 0 ? data[idy].values[idx] : '' }` } strSheetXml += '' }) } else { // strSheetXml += '' strSheetXml += '' /* EX: INPUT: `data` [ { name:'Red', labels:['Jan..May-17'], values:[11,13,14,15,16] }, { name:'Amb', labels:['Jan..May-17'], values:[22, 6, 7, 8, 9] }, { name:'Grn', labels:['Jan..May-17'], values:[33,32,42,53,63] } ]; */ /* EX: OUTPUT: lineChart Worksheet: -|---A---|--B--|--C--|--D--| 1| | Red | Amb | Grn | 2|Jan-17 | 11| 22| 33| 3|Feb-17 | 55| 43| 70| 4|Mar-17 | 56| 143| 99| 5|Apr-17 | 65| 3| 120| 6|May-17 | 75| 93| 170| -|-------|-----|-----|-----| */ if (!IS_MULTI_CAT_AXES) { // A: Create header row first strSheetXml += `` data[0].labels.forEach((_labelsGroup, idx) => { strSheetXml += `0` }) for (let idx = 0; idx < data.length; idx++) { strSheetXml += `${idx + 1}` // NOTE: use `t="s"` for label cols! } strSheetXml += '' // B: Add data row(s) for each category 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 + 1}` strSheetXml += '' } for (let idy = 0; idy < data.length; idy++) { strSheetXml += `${data[idy].values[idx] || ''}` } strSheetXml += '' }) } else { // A: create header row strSheetXml += `` for (let idx = 0; idx < data[0].labels.length; idx++) { strSheetXml += `0` } for (let idx = data[0].labels.length - 1; idx < data.length + data[0].labels.length - 1; idx++) { strSheetXml += `${idx}` // NOTE: use `t="s"` for label cols! } strSheetXml += '' // FIXME: 20220524 (v3.11.0) /** * @example INPUT * const LABELS = [ * ["Gear", "Berg", "Motr", "Swch", "Plug", "Cord", "Pump", "Leak", "Seal"], * ["Mech", "", "", "Elec", "", "", "Hydr", "", ""], * ]; * const arrDataRegions = [ * { name: "West", labels: LABELS, values: [11, 8, 3, 0, 11, 3, 0, 0, 0] }, * { name: "Ctrl", labels: LABELS, values: [0, 11, 6, 19, 12, 5, 0, 0, 0] }, * { name: "East", labels: LABELS, values: [0, 3, 2, 0, 0, 0, 4, 3, 1] }, * ]; */ /** * @example OUTPUT EXCEL SHEET * |/|---A--|---B--|---C--|---D--|---E--| * |1| | | West | Ctrl | East | * |2| Mech | Gear | ## | ## | ## | * |3| | Brng | ## | ## | ## | * |4| | Motr | ## | ## | ## | * |5| Elec | Swch | ## | ## | ## | * |6| | Plug | ## | ## | ## | * |7| | Cord | ## | ## | ## | * |8| Hydr | Pump | ## | ## | ## | * |9| | Leak | ## | ## | ## | *|10| | Seal | ## | ## | ## | */ /** * @example OUTPUT EXCEL SHEET XML * * 0 * 0 * 1 * 2 * 3 * * * 4 * 7 * ### * * * * 8 * ### * */ /** * @example SHARED-STRINGS * 1=West, 2=Ctrl, 3=East, 4=Mech, 5=Elec, 6=Mydr, 7=Gear, 8=Brng, [...], 15=Seal */ // B: Add data row(s) for each category /** * const LABELS = [ * ["Gear", "Berg", "Motr", "Swch", "Plug", "Cord", "Pump", "Leak", "Seal"], * ["Mech", "", "", "Elec", "", "", "Hydr", "", ""], * ["2010", "", "", "", "", "", "", "", ""], * ]; */ const TOT_SER = data.length const TOT_CAT = data[0].labels[0].length const TOT_LVL = data[0].labels.length // Iterate across labels/cats as these are the 's for (let idx = 0; idx < TOT_CAT; idx++) { // A: start row strSheetXml += `` // WIP: FIXME: // B: add a col for each label/cat let totLabels = TOT_SER const revLabelGroups = data[0].labels.slice().reverse() revLabelGroups.forEach((labelsGroup, idy) => { /** * const LABELS_REVERSED = [ * ["Mech", "", "", "Elec", "", "", "Hydr", "", ""], * ["Gear", "Berg", "Motr", "Swch", "Plug", "Cord", "Pump", "Leak", "Seal"], * ]; */ const colLabel = labelsGroup[idx] if (colLabel) { const totGrpLbls = idy === 0 ? 1 : revLabelGroups[idy - 1].filter(label => label && label !== '').length // get unique label so we can add to get proper shared-string # totLabels += totGrpLbls strSheetXml += `${totLabels}` } }) // WIP: FIXME: // C: add a col for each data value for (let idy = 0; idy < TOT_SER; idy++) { strSheetXml += `${data[idy].values[idx] || 0}` } // D: Done strSheetXml += '' } // console.log(strSheetXml) // WIP: CHECK: // console.log(`---CHECK ABOVE---------------------`) } } strSheetXml += '' /* FIXME: support multi-level if (IS_MULTI_CAT_AXES) { strSheetXml += '' strSheetXml += ' ' strSheetXml += ' ' strSheetXml += ' ' strSheetXml += '' } */ strSheetXml += '' // Link the `table1.xml` file to define an actual Table in Excel // NOTE: This only works with scatter charts - all others give a "cannot find linked file" error // ....: Since we dont need the table anyway (chart data can be edited/range selected, etc.), just dont use this // ....: Leaving this so nobody foolishly attempts to add this in the future // strSheetXml += '' strSheetXml += '\n' zipExcel.file('xl/worksheets/sheet1.xml', strSheetXml) } // C: Add XLSX to PPTX export zipExcel .generateAsync({ type: 'base64' }) .then(content => { // 1: Create the embedded Excel worksheet with labels and data zip.file(`ppt/embeddings/Microsoft_Excel_Worksheet${chartObject.globalId}.xlsx`, content, { base64: true }) // 2: Create the chart.xml and rel files zip.file( 'ppt/charts/_rels/' + chartObject.fileName + '.rels', '' + '' + `` + '' ) zip.file(`ppt/charts/${chartObject.fileName}`, makeXmlCharts(chartObject)) // 3: Done resolve('') }) .catch(strErr => { reject(strErr) }) }) } /** * Main entry point method for create charts * @see: http://www.datypic.com/sc/ooxml/s-dml-chart.xsd.html * @param {ISlideRelChart} rel - chart object * @return {string} XML */ export function makeXmlCharts (rel: ISlideRelChart): string { let strXml = '' let usesSecondaryValAxis = false // STEP 1: Create chart { // CHARTSPACE: BEGIN vvv strXml += '' strXml += '' // ppt defaults to 1904 dates, excel to 1900 strXml += `` strXml += '' // OPTION: Title if (rel.opts.showTitle) { strXml += genXmlTitle( { title: rel.opts.title || 'Chart Title', color: rel.opts.titleColor, fontFace: rel.opts.titleFontFace, fontSize: rel.opts.titleFontSize || DEF_FONT_TITLE_SIZE, titleAlign: rel.opts.titleAlign, titleBold: rel.opts.titleBold, titlePos: rel.opts.titlePos, titleRotate: rel.opts.titleRotate, }, rel.opts.x as number, rel.opts.y as number ) strXml += '' } else { // NOTE: Add autoTitleDeleted tag in else to prevent default creation of chart title even when showTitle is set to false strXml += '' } /** Add 3D view tag * @see: https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_perspective_topic_ID0E6BUQB.html */ if (rel.opts._type === CHART_TYPE.BAR3D) { strXml += `` } strXml += '' // IMPORTANT: Dont specify layout to enable auto-fit: PPT does a great job maximizing space with all 4 TRBL locations if (rel.opts.layout) { strXml += '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += '' } else { strXml += '' } } // A: Create Chart XML ----------------------------------------------------------- if (Array.isArray(rel.opts._type)) { rel.opts._type.forEach(type => { // TODO: FIXME: theres `options` on chart rels?? const options = { ...rel.opts, ...type.options } // let options: IChartOptsLib = { type: type.type, } const valAxisId = options.secondaryValAxis ? AXIS_ID_VALUE_SECONDARY : AXIS_ID_VALUE_PRIMARY const catAxisId = options.secondaryCatAxis ? AXIS_ID_CATEGORY_SECONDARY : AXIS_ID_CATEGORY_PRIMARY usesSecondaryValAxis = usesSecondaryValAxis || options.secondaryValAxis strXml += makeChartType(type.type, type.data, options, valAxisId, catAxisId, true) }) } else { strXml += makeChartType(rel.opts._type, rel.data, rel.opts, AXIS_ID_VALUE_PRIMARY, AXIS_ID_CATEGORY_PRIMARY, false) } // B: Axes ----------------------------------------------------------- if (rel.opts._type !== CHART_TYPE.PIE && rel.opts._type !== CHART_TYPE.DOUGHNUT) { // Param check if (rel.opts.valAxes && rel.opts.valAxes.length > 1 && !usesSecondaryValAxis) { throw new Error('Secondary axis must be used by one of the multiple charts') } if (rel.opts.catAxes) { if (!rel.opts.valAxes || rel.opts.valAxes.length !== rel.opts.catAxes.length) { throw new Error('There must be the same number of value and category axes.') } strXml += makeCatAxis({ ...rel.opts, ...rel.opts.catAxes[0] }, AXIS_ID_CATEGORY_PRIMARY, AXIS_ID_VALUE_PRIMARY) } else { strXml += makeCatAxis(rel.opts, AXIS_ID_CATEGORY_PRIMARY, AXIS_ID_VALUE_PRIMARY) } if (rel.opts.valAxes) { strXml += makeValAxis({ ...rel.opts, ...rel.opts.valAxes[0] }, AXIS_ID_VALUE_PRIMARY) if (rel.opts.valAxes[1]) { strXml += makeValAxis({ ...rel.opts, ...rel.opts.valAxes[1] }, AXIS_ID_VALUE_SECONDARY) } } else { strXml += makeValAxis(rel.opts, AXIS_ID_VALUE_PRIMARY) // Add series axis for 3D bar if (rel.opts._type === CHART_TYPE.BAR3D) { strXml += makeSerAxis(rel.opts, AXIS_ID_SERIES_PRIMARY, AXIS_ID_VALUE_PRIMARY) } } // Combo Charts: Add secondary axes after all vals if (rel.opts?.catAxes && rel.opts?.catAxes[1]) { strXml += makeCatAxis({ ...rel.opts, ...rel.opts.catAxes[1] }, AXIS_ID_CATEGORY_SECONDARY, AXIS_ID_VALUE_SECONDARY) } } // C: Chart Properties and plotArea Options: Border, Data Table, Fill, Legend { // NOTE: DataTable goes between '' and '' if (rel.opts.showDataTable) { strXml += '' strXml += ` ` strXml += ` ` strXml += ` ` strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += '' } strXml += ' ' // OPTION: Fill strXml += rel.opts.plotArea.fill?.color ? genXmlColorSelection(rel.opts.plotArea.fill) : '' // OPTION: Border strXml += rel.opts.plotArea.border ? `${genXmlColorSelection(rel.opts.plotArea.border.color)}` : '' // Close shapeProp/plotArea before Legend strXml += ' ' strXml += ' ' strXml += '' // OPTION: Legend // IMPORTANT: Dont specify layout to enable auto-fit: PPT does a great job maximizing space with all 4 TRBL locations if (rel.opts.showLegend) { strXml += '' strXml += '' // strXml += '' strXml += '' if (rel.opts.legendFontFace || rel.opts.legendFontSize || rel.opts.legendColor) { strXml += '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += rel.opts.legendFontSize ? `` : '' if (rel.opts.legendColor) strXml += genXmlColorSelection(rel.opts.legendColor) if (rel.opts.legendFontFace) strXml += '' if (rel.opts.legendFontFace) strXml += '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += '' } strXml += '' } } strXml += ' ' strXml += ' ' if (rel.opts._type === CHART_TYPE.SCATTER) strXml += '' strXml += '' // D: CHARTSPACE SHAPE PROPS strXml += '' strXml += rel.opts.chartArea.fill?.color ? genXmlColorSelection(rel.opts.chartArea.fill) : '' strXml += rel.opts.chartArea.border ? `${genXmlColorSelection(rel.opts.chartArea.border.color)}` : '' strXml += ' ' strXml += '' // E: DATA (Add relID) strXml += '' // LAST: chartSpace end strXml += '' return strXml } /** * Create XML string for any given chart type * @param {CHART_NAME} chartType chart type name * @param {IOptsChartData[]} data chart data * @param {IChartOptsLib} opts chart options * @param {string} valAxisId chart val axis id * @param {string} catAxisId chart cat axis id * @param {boolean} isMultiTypeChart is this a mutli-type chart? * @example 'bubble' returns * @example '' * @return {string} XML chart */ 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 colorIndex = -1 // Maintain the color index by region let idxColLtr = 1 let optsChartData: IOptsChartData = null let strXml = '' switch (chartType) { case CHART_TYPE.AREA: case CHART_TYPE.BAR: case CHART_TYPE.BAR3D: case CHART_TYPE.LINE: case CHART_TYPE.RADAR: // 1: Start Chart strXml += `` if (chartType === CHART_TYPE.AREA && opts.barGrouping === 'stacked') { strXml += '' } if (chartType === CHART_TYPE.BAR || chartType === CHART_TYPE.BAR3D) { strXml += '' strXml += '' } if (chartType === CHART_TYPE.RADAR) { strXml += '' } strXml += '' // 2: "Series" block for every data row /* EX1: data: [ { name: 'Region 1', labels: [['April', 'May', 'June', 'July']], values: [17, 26, 53, 96] }, { name: 'Region 2', 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] } ] */ data.forEach(obj => { colorIndex++ strXml += '' strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' Sheet1!$' + getExcelColName(obj._dataIndex + obj.labels.length + 1) + '$1' strXml += ' ' + encodeXmlEntities(obj.name) + '' strXml += ' ' strXml += ' ' // Fill and Border // TODO: CURRENT: Pull#727 // TODO: let seriesColor = obj.color ? obj.color : opts.chartColors ? opts.chartColors[colorIndex % opts.chartColors.length] : null const seriesColor = opts.chartColors ? opts.chartColors[colorIndex % opts.chartColors.length] : null strXml += ' ' if (seriesColor === 'transparent') { strXml += '' } else if (opts.chartColorsOpacity) { strXml += '' + createColorElement(seriesColor, ``) + '' } else { strXml += '' + createColorElement(seriesColor) + '' } if (chartType === CHART_TYPE.LINE || chartType === CHART_TYPE.RADAR) { if (opts.lineSize === 0) { strXml += '' } else { strXml += `${createColorElement(seriesColor)}` strXml += '' } } else if (opts.dataBorder) { strXml += `${createColorElement(opts.dataBorder.color)}` } strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW) strXml += ' ' strXml += ' ' // Data Labels per series // NOTE: [20190117] Adding these to RADAR chart causes unrecoverable corruption! if (chartType !== CHART_TYPE.RADAR) { strXml += '' strXml += `` if (opts.dataLabelBkgrdColors) strXml += `${createColorElement(seriesColor)}` strXml += '' strXml += `` strXml += `${createColorElement(opts.dataLabelColor || DEF_FONT_COLOR)}` strXml += `` strXml += '' if (opts.dataLabelPosition) strXml += `` strXml += '' strXml += `` strXml += `` strXml += `` strXml += '' } // 'c:marker' tag: `lineDataSymbol` if (chartType === CHART_TYPE.LINE || chartType === CHART_TYPE.RADAR) { strXml += '' strXml += ' ' if (opts.lineDataSymbolSize) strXml += `` // Defaults to "auto" otherwise (but this is usually too small, so there is a default) strXml += ' ' strXml += ` ${createColorElement(opts.chartColors[obj._dataIndex + 1 > opts.chartColors.length ? Math.floor(Math.random() * opts.chartColors.length) : obj._dataIndex])}` strXml += ` ${createColorElement(opts.lineDataSymbolLineColor || seriesColor)}` strXml += ' ' strXml += ' ' strXml += '' } // Allow users with a single data set to pass their own array of colors (check for this using != ours) // Color chart bars various colors when >1 color // NOTE: `` created with various colors will change PPT legend by design so each dataPt/color is an legend item! if ( (chartType === CHART_TYPE.BAR || chartType === CHART_TYPE.BAR3D) && data.length === 1 && ((opts.chartColors && opts.chartColors !== BARCHART_COLORS && opts.chartColors.length > 1) || (opts.invertedColors?.length)) ) { // Series Data Point colors obj.values.forEach((value, index) => { const arrColors = value < 0 ? opts.invertedColors || opts.chartColors || BARCHART_COLORS : opts.chartColors || [] strXml += ' ' strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' if (opts.lineSize === 0) { strXml += '' } else if (chartType === CHART_TYPE.BAR) { strXml += '' strXml += ' ' strXml += '' } else { strXml += '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += '' } strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW) strXml += ' ' strXml += ' ' }) } // 2: "Categories" { strXml += '' if (opts.catLabelFormatCode) { // Use 'numRef' as catLabelFormatCode implies that we are expecting numbers here strXml += ' ' strXml += ` Sheet1!$A$2:$A$${obj.labels[0].length + 1}` strXml += ' ' strXml += ' ' + (opts.catLabelFormatCode || 'General') + '' strXml += ` ` obj.labels[0].forEach((label, idx) => (strXml += `${encodeXmlEntities(label)}`)) strXml += ' ' strXml += ' ' } else { strXml += ' ' strXml += ` Sheet1!$A$2:$${getExcelColName(obj.labels.length)}$${obj.labels[0].length + 1}` strXml += ' ' strXml += ` ` obj.labels.forEach(labelsGroup => { strXml += '' labelsGroup.forEach((label, idx) => (strXml += `${encodeXmlEntities(label)}`)) strXml += '' }) strXml += ' ' strXml += ' ' } strXml += '' } // 3: "Values" { strXml += '' strXml += ' ' strXml += `Sheet1!$${getExcelColName(obj._dataIndex + obj.labels.length + 1)}$2:$${getExcelColName(obj._dataIndex + obj.labels.length + 1)}$${obj.labels[0].length + 1}` strXml += ' ' strXml += ' ' + (opts.valLabelFormatCode || opts.dataTableFormatCode || 'General') + '' strXml += ` ` obj.values.forEach((value, idx) => (strXml += `${value || value === 0 ? value : ''}`)) strXml += ' ' strXml += ' ' strXml += '' } // Option: `smooth` if (chartType === CHART_TYPE.LINE) strXml += '' // 4: Close "SERIES" strXml += '' }) // 3: "Data Labels" { strXml += ' ' strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' + createColorElement(opts.dataLabelColor || DEF_FONT_COLOR) + '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' if (opts.dataLabelPosition) strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' } // 4: Add more chart options (gapWidth, line Marker, etc.) if (chartType === CHART_TYPE.BAR) { strXml += ` ` strXml += ` ` } else if (chartType === CHART_TYPE.BAR3D) { strXml += ` ` strXml += ` ` strXml += ' ' } else if (chartType === CHART_TYPE.LINE) { strXml += ' ' } // 5: Add axisId (NOTE: order matters! (category comes first)) strXml += `` // 6: Close Chart tag strXml += `` // end switch break case CHART_TYPE.SCATTER: /* `data` = [ { name:'X-Axis', values:[1,2,3,4,5,6,7,8,9,10,11,12] }, { name:'Y-Value 1', values:[13, 20, 21, 25] }, { name:'Y-Value 2', values:[ 1, 2, 5, 9] } ]; */ // 1: Start Chart strXml += '' strXml += '' strXml += '' // 2: Series: (One for each Y-Axis) colorIndex = -1 data.filter((_obj, idx) => idx > 0).forEach((obj, idx) => { colorIndex++ strXml += '' strXml += ` ` strXml += ` ` strXml += ' ' strXml += ' ' strXml += ` Sheet1!$${getExcelColName(idx + 2)}$1` strXml += ' ' + encodeXmlEntities(obj.name) + '' strXml += ' ' strXml += ' ' // 'c:spPr': Fill, Border, Line, LineStyle (dash, etc.), Shadow strXml += ' ' { const tmpSerColor = opts.chartColors[colorIndex % opts.chartColors.length] if (tmpSerColor === 'transparent') { strXml += '' } else if (opts.chartColorsOpacity) { strXml += '' + createColorElement(tmpSerColor, '') + '' } else { strXml += '' + createColorElement(tmpSerColor) + '' } if (opts.lineSize === 0) { strXml += '' } else { strXml += `${createColorElement(tmpSerColor)}` strXml += `` } // Shadow strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW) } strXml += ' ' // 'c:marker' tag: `lineDataSymbol` { strXml += '' strXml += ' ' if (opts.lineDataSymbolSize) { // Defaults to "auto" otherwise (but this is usually too small, so there is a default) strXml += `` } strXml += '' strXml += `${createColorElement(opts.chartColors[idx + 1 > opts.chartColors.length ? Math.floor(Math.random() * opts.chartColors.length) : idx])}` strXml += `${createColorElement(opts.lineDataSymbolLineColor || opts.chartColors[colorIndex % opts.chartColors.length])}` strXml += '' strXml += '' strXml += '' } // Option: scatter data point labels if (opts.showLabel) { const chartUuid = getUuid('-xxxx-xxxx-xxxx-xxxxxxxxxxxx') if (obj.labels[0] && (opts.dataLabelFormatScatter === 'custom' || opts.dataLabelFormatScatter === 'customXY')) { strXml += '' obj.labels[0].forEach((label, idx) => { if (opts.dataLabelFormatScatter === 'custom' || opts.dataLabelFormatScatter === 'customXY') { strXml += ' ' strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' + encodeXmlEntities(label) + '' strXml += ' ' // Apply XY values at end of custom label // Do not apply the values if the label was empty or just spaces // This allows for selective labelling where required if (opts.dataLabelFormatScatter === 'customXY' && !/^ *$/.test(label)) { strXml += ' ' strXml += ' ' strXml += ' (' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' [' + encodeXmlEntities(obj.name) + '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' , ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' [' + encodeXmlEntities(obj.name) + ']' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' )' strXml += ' ' strXml += ' ' } strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' if (opts.dataLabelPosition) strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' strXml += ' ' strXml += '' } }) strXml += '' } if (opts.dataLabelFormatScatter === 'XY') { strXml += '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' if (opts.dataLabelPosition) strXml += ' ' strXml += ' ' strXml += ` ` strXml += ` ` strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += '' } } // Color bar chart bars various colors // Allow users with a single data set to pass their own array of colors (check for this using != ours) if (data.length === 1 && opts.chartColors !== BARCHART_COLORS) { // Series Data Point colors obj.values.forEach((value, index) => { const arrColors = value < 0 ? opts.invertedColors || opts.chartColors || BARCHART_COLORS : opts.chartColors || [] strXml += ' ' strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' if (opts.lineSize === 0) { strXml += '' } else { strXml += '' strXml += ' ' strXml += '' } strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW) strXml += ' ' strXml += ' ' }) } // 3: "Values": Scatter Chart has 2: `xVal` and `yVal` { // X-Axis is always the same strXml += '' strXml += ' ' strXml += ` Sheet1!$A$2:$A$${data[0].values.length + 1}` strXml += ' ' strXml += ' General' strXml += ` ` data[0].values.forEach((value, idx) => { strXml += `${value || value === 0 ? value : ''}` }) strXml += ' ' strXml += ' ' strXml += '' // Y-Axis vals are this object's `values` strXml += '' strXml += ' ' strXml += ` Sheet1!$${getExcelColName(idx + 2)}$2:$${getExcelColName(idx + 2)}$${data[0].values.length + 1}` strXml += ' ' strXml += ' General' // NOTE: Use pt count and iterate over data[0] (X-Axis) as user can have more values than data (eg: timeline where only first few months are populated) strXml += ` ` data[0].values.forEach((_value, idx) => { strXml += `${obj.values[idx] || obj.values[idx] === 0 ? obj.values[idx] : ''}` }) strXml += ' ' strXml += ' ' strXml += '' } // Option: `smooth` strXml += '' // 4: Close "SERIES" strXml += '' }) // 3: Data Labels { strXml += ' ' strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' + createColorElement(opts.dataLabelColor || DEF_FONT_COLOR) + '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' if (opts.dataLabelPosition) strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' } // 4: Add axis Id (NOTE: order matters! - category comes first) strXml += `` // 5: Close Chart tag strXml += '' // end switch break case CHART_TYPE.BUBBLE: case CHART_TYPE.BUBBLE3D: /* `data` = [ { name:'X-Axis', values:[1,2,3,4,5,6,7,8,9,10,11,12] }, { name:'Y-Values 1', values:[13, 20, 21, 25], sizes:[10, 5, 20, 15] }, { name:'Y-Values 2', values:[ 1, 2, 5, 9], sizes:[ 5, 3, 9, 3] } ]; */ // 1: Start Chart strXml += '' strXml += '' // 2: Series: (One for each Y-Axis) colorIndex = -1 data.filter((_obj, idx) => idx > 0).forEach((obj, idx) => { colorIndex++ strXml += '' strXml += ` ` strXml += ` ` // A: `` strXml += ' ' strXml += ' ' strXml += ' Sheet1!$' + getExcelColName(idxColLtr + 1) + '$1' strXml += ' ' + encodeXmlEntities(obj.name) + '' strXml += ' ' strXml += ' ' // B: '': Fill, Border, Line, LineStyle (dash, etc.), Shadow { strXml += '' const tmpSerColor = opts.chartColors[colorIndex % opts.chartColors.length] if (tmpSerColor === 'transparent') { strXml += '' } else if (opts.chartColorsOpacity) { strXml += `${createColorElement(tmpSerColor, '')}` } else { strXml += '' + createColorElement(tmpSerColor) + '' } if (opts.lineSize === 0) { strXml += '' } else if (opts.dataBorder) { strXml += `${createColorElement(opts.dataBorder.color)}` } else { strXml += `${createColorElement(tmpSerColor)}` strXml += `` } // Shadow strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW) strXml += '' } // C: '' "Data Labels" // Let it be defaulted for now // D: ''/'' "Values": Scatter Chart has 2: `xVal` and `yVal` { // X-Axis is always the same strXml += '' strXml += ' ' strXml += ` Sheet1!$A$2:$A$${data[0].values.length + 1}` strXml += ' ' strXml += ' General' strXml += ` ` data[0].values.forEach((value, idx) => { strXml += `${value || value === 0 ? value : ''}` }) strXml += ' ' strXml += ' ' strXml += '' // Y-Axis vals are this object's `values` strXml += '' strXml += ' ' strXml += `Sheet1!$${getExcelColName(idxColLtr + 1)}$2:$${getExcelColName(idxColLtr + 1)}$${data[0].values.length + 1}` idxColLtr++ strXml += ' ' strXml += ' General' // NOTE: Use pt count and iterate over data[0] (X-Axis) as user can have more values than data (eg: timeline where only first few months are populated) strXml += ` ` data[0].values.forEach((_value, idx) => { strXml += `${obj.values[idx] || obj.values[idx] === 0 ? obj.values[idx] : ''}` }) strXml += ' ' strXml += ' ' strXml += '' } // E: '' strXml += ' ' strXml += ' ' strXml += `Sheet1!$${getExcelColName(idxColLtr + 1)}$2:$${getExcelColName(idxColLtr + 1)}$${obj.sizes.length + 1}` idxColLtr++ strXml += ' ' strXml += ' General' strXml += ` ` obj.sizes.forEach((value, idx) => { strXml += `${value || ''}` }) strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' // F: Close "SERIES" strXml += '' }) // 3: Data Labels { strXml += '' strXml += `` strXml += '' strXml += `` strXml += `${createColorElement(opts.dataLabelColor || DEF_FONT_COLOR)}` strXml += `` strXml += '' if (opts.dataLabelPosition) strXml += `` strXml += '' strXml += `` strXml += `` strXml += '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += '' strXml += '' } // 4: Bubble options // strXml += ' '; // strXml += ' '; // Commented out to let it default to PPT until we create options // 5: AxisId (NOTE: order matters! (category comes first)) strXml += `` // 6: Close Chart tag strXml += '' // end switch break case CHART_TYPE.DOUGHNUT: case CHART_TYPE.PIE: // Use the same let name so code blocks from barChart are interchangeable optsChartData = data[0] /* EX: data: [ { name: 'Project Status', labels: ['Red', 'Amber', 'Green', 'Unknown'], values: [10, 20, 38, 2] } ] */ // 1: Start Chart strXml += '' strXml += ' ' strXml += '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' Sheet1!$B$1' strXml += ' ' strXml += ' ' strXml += ' ' + encodeXmlEntities(optsChartData.name) + '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' if (opts.dataNoEffects) { strXml += '' } else { strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW) } strXml += ' ' // strXml += '' // 2: "Data Point" block for every data row optsChartData.labels[0].forEach((_label, idx) => { strXml += '' strXml += ` ` strXml += ' ' strXml += ' ' strXml += `${createColorElement( opts.chartColors[idx + 1 > opts.chartColors.length ? Math.floor(Math.random() * opts.chartColors.length) : idx] )}` if (opts.dataBorder) { strXml += `${createColorElement( opts.dataBorder.color )}` } strXml += createShadowElement(opts.shadow, DEF_SHAPE_SHADOW) strXml += ' ' strXml += '' }) // 3: "Data Label" block for every data Label strXml += '' optsChartData.labels[0].forEach((_label, idx) => { strXml += '' strXml += ` ` strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' + createColorElement(opts.dataLabelColor || DEF_FONT_COLOR) + '' strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' if (chartType === CHART_TYPE.PIE && opts.dataLabelPosition) strXml += `` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' }) strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += chartType === CHART_TYPE.PIE ? '' : '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += '' // 2: "Categories" strXml += '' strXml += ' ' strXml += ` Sheet1!$A$2:$A$${optsChartData.labels[0].length + 1}` strXml += ' ' strXml += ` ` optsChartData.labels[0].forEach((label, idx) => { strXml += `${encodeXmlEntities(label)}` }) strXml += ' ' strXml += ' ' strXml += '' // 3: Create vals strXml += ' ' strXml += ' ' strXml += ` Sheet1!$B$2:$B$${optsChartData.labels[0].length + 1}` strXml += ' ' strXml += ` ` optsChartData.values.forEach((value, idx) => { strXml += `${value || value === 0 ? value : ''}` }) strXml += ' ' strXml += ' ' strXml += ' ' // 4: Close "SERIES" strXml += ' ' strXml += ` ` if (chartType === CHART_TYPE.DOUGHNUT) strXml += `` strXml += '' // Done with Doughnut/Pie break default: strXml += '' break } return strXml } /** * Create Category axis * @param {IChartOptsLib} opts - chart options * @param {string} axisId - value * @param {string} valAxisId - value * @return {string} XML */ function makeCatAxis (opts: IChartOptsLib, axisId: string, valAxisId: string): string { let strXml = '' // Build cat axis tag // NOTE: Scatter and Bubble chart need two Val axises as they display numbers on x axis if (opts._type === CHART_TYPE.SCATTER || opts._type === CHART_TYPE.BUBBLE || opts._type === CHART_TYPE.BUBBLE3D) { strXml += '' } else { strXml += '' } strXml += ' ' strXml += ' ' strXml += '' if (opts.catAxisMaxVal || opts.catAxisMaxVal === 0) strXml += `` if (opts.catAxisMinVal || opts.catAxisMinVal === 0) strXml += `` strXml += '' strXml += ' ' strXml += ' ' strXml += opts.catGridLine.style !== 'none' ? createGridLineElement(opts.catGridLine) : '' // '' comes between '' and '' if (opts.showCatAxisTitle) { strXml += genXmlTitle({ color: opts.catAxisTitleColor, fontFace: opts.catAxisTitleFontFace, fontSize: opts.catAxisTitleFontSize, titleRotate: opts.catAxisTitleRotate, title: opts.catAxisTitle || 'Axis Title', }) } // NOTE: Adding Val Axis Formatting if scatter or bubble charts if (opts._type === CHART_TYPE.SCATTER || opts._type === CHART_TYPE.BUBBLE || opts._type === CHART_TYPE.BUBBLE3D) { strXml += ' ' } else { strXml += ' ' } if (opts._type === CHART_TYPE.SCATTER) { strXml += ' ' strXml += ' ' strXml += ' ' } else { strXml += ' ' strXml += ' ' strXml += ' ' } strXml += ' ' strXml += ` ` strXml += !opts.catAxisLineShow ? '' : '' + createColorElement(opts.catAxisLineColor || DEF_CHART_GRIDLINE.color) + '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' if (opts.catAxisLabelRotate) { strXml += `` } else { // NOTE: don't specify "`rot=0" - that way the object will be auto behavior strXml += '' } strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' + createColorElement(opts.catAxisLabelColor || DEF_FONT_COLOR) + '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' 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 // Allow major and minor units to be set for double value axis charts if (opts.catLabelFormatCode || opts._type === CHART_TYPE.SCATTER || opts._type === CHART_TYPE.BUBBLE || opts._type === CHART_TYPE.BUBBLE3D) { if (opts.catLabelFormatCode) { ['catAxisBaseTimeUnit', 'catAxisMajorTimeUnit', 'catAxisMinorTimeUnit'].forEach(opt => { // Validate input as poorly chosen/garbage options will cause chart corruption and it wont render at all! if (opts[opt] && (typeof opts[opt] !== 'string' || !['days', 'months', 'years'].includes(opts[opt].toLowerCase()))) { console.warn(`"${opt}" must be one of: 'days','months','years' !`) opts[opt] = null } }) if (opts.catAxisBaseTimeUnit) strXml += '' if (opts.catAxisMajorTimeUnit) strXml += '' if (opts.catAxisMinorTimeUnit) strXml += '' } if (opts.catAxisMajorUnit) strXml += `` if (opts.catAxisMinorUnit) strXml += `` } // Close cat axis tag // NOTE: Added closing tag of val or cat axis based on chart type if (opts._type === CHART_TYPE.SCATTER || opts._type === CHART_TYPE.BUBBLE || opts._type === CHART_TYPE.BUBBLE3D) { strXml += '' } else { strXml += '' } return strXml } /** * Create Value Axis (Used by `bar3D`) * @param {IChartOptsLib} opts - chart options * @param {string} valAxisId - value * @return {string} XML */ function makeValAxis (opts: IChartOptsLib, valAxisId: string): string { let axisPos = valAxisId === AXIS_ID_VALUE_PRIMARY ? (opts.barDir === 'col' ? 'l' : 'b') : opts.barDir !== 'col' ? 'r' : 't' if (valAxisId === AXIS_ID_VALUE_SECONDARY) axisPos = 'r' // default behavior for PPT is showing 2nd val axis on right (primary axis on left) const crossAxId = valAxisId === AXIS_ID_VALUE_PRIMARY ? AXIS_ID_CATEGORY_PRIMARY : AXIS_ID_CATEGORY_SECONDARY let strXml = '' strXml += '' strXml += ' ' strXml += ' ' if (opts.valAxisLogScaleBase) strXml += `` strXml += '' if (opts.valAxisMaxVal || opts.valAxisMaxVal === 0) strXml += `` if (opts.valAxisMinVal || opts.valAxisMinVal === 0) strXml += `` strXml += ' ' strXml += ` ` strXml += ' ' if (opts.valGridLine.style !== 'none') strXml += createGridLineElement(opts.valGridLine) // '' comes between '' and '' if (opts.showValAxisTitle) { strXml += genXmlTitle({ color: opts.valAxisTitleColor, fontFace: opts.valAxisTitleFontFace, fontSize: opts.valAxisTitleFontSize, titleRotate: opts.valAxisTitleRotate, title: opts.valAxisTitle || 'Axis Title', }) } strXml += `` if (opts._type === CHART_TYPE.SCATTER) { strXml += ' ' strXml += ' ' strXml += ' ' } else { strXml += ' ' strXml += ' ' strXml += ' ' } strXml += ' ' strXml += ` ` strXml += !opts.valAxisLineShow ? '' : '' + createColorElement(opts.valAxisLineColor || DEF_CHART_GRIDLINE.color) + '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` // don't specify rot 0 so we get the auto behavior strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' + createColorElement(opts.valAxisLabelColor || DEF_FONT_COLOR) + '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' if (typeof opts.catAxisCrossesAt === 'number') { strXml += ` ` } else if (typeof opts.catAxisCrossesAt === 'string') { strXml += ' ' } else { const isRight = axisPos === 'r' || axisPos === 't' const crosses = isRight ? 'max' : 'autoZero' strXml += ' ' } strXml += ' ' if (opts.valAxisMajorUnit) strXml += ` ` if (opts.valAxisDisplayUnit) { strXml += `${opts.valAxisDisplayUnitLabel ? '' : ''}` } strXml += '' return strXml } /** * Create Series Axis (Used by `bar3D`) * @param {IChartOptsLib} opts - chart options * @param {string} axisId - axis ID * @param {string} valAxisId - value * @return {string} XML */ function makeSerAxis (opts: IChartOptsLib, axisId: string, valAxisId: string): string { let strXml = '' // Build ser axis tag strXml += '' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += opts.serGridLine.style !== 'none' ? createGridLineElement(opts.serGridLine) : '' // '' comes between '' and '' if (opts.showSerAxisTitle) { strXml += genXmlTitle({ color: opts.serAxisTitleColor, fontFace: opts.serAxisTitleFontFace, fontSize: opts.serAxisTitleFontSize, titleRotate: opts.serAxisTitleRotate, title: opts.serAxisTitle || 'Axis Title', }) } strXml += ` ` strXml += ' ' strXml += ' ' strXml += ` ` strXml += ' ' strXml += ' ' strXml += !opts.serAxisLineShow ? '' : `${createColorElement(opts.serAxisLineColor || DEF_CHART_GRIDLINE.color)}` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' // don't specify rot 0 so we get the auto behavior strXml += ' ' strXml += ' ' strXml += ' ' strXml += ` ` strXml += ` ${createColorElement(opts.serAxisLabelColor || DEF_FONT_COLOR)}` strXml += ` ` strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' strXml += ' ' if (opts.serAxisLabelFrequency) strXml += ' ' // Issue#149: PPT will auto-adjust these as needed after calcing the date bounds, so we only include them when specified by user if (opts.serLabelFormatCode) { ['serAxisBaseTimeUnit', 'serAxisMajorTimeUnit', 'serAxisMinorTimeUnit'].forEach(opt => { // Validate input as poorly chosen/garbage options will cause chart corruption and it wont render at all! if (opts[opt] && (typeof opts[opt] !== 'string' || !['days', 'months', 'years'].includes(opt.toLowerCase()))) { console.warn(`"${opt}" must be one of: 'days','months','years' !`) opts[opt] = null } }) if (opts.serAxisBaseTimeUnit) strXml += ` ` if (opts.serAxisMajorTimeUnit) strXml += ` ` if (opts.serAxisMinorTimeUnit) strXml += ` ` if (opts.serAxisMajorUnit) strXml += ` ` if (opts.serAxisMinorUnit) strXml += ` ` } // Close ser axis tag strXml += '' return strXml } /** * Create char title elements * @param {IChartPropsTitle} opts - options * @return {string} XML `` */ function genXmlTitle (opts: IChartPropsTitle, chartX?: number, chartY?: number): string { const align = opts.titleAlign === 'left' || opts.titleAlign === 'right' ? `` : '' const rotate = opts.titleRotate ? `` : '' // don't specify rotation to get default (ex. vertical for cat axis) const sizeAttr = opts.fontSize ? `sz="${Math.round(opts.fontSize * 100)}"` : '' // only set the font size if specified. Powerpoint will handle the default size const titleBold = opts.titleBold ? 1 : 0 let layout = '' if (opts.titlePos && typeof opts.titlePos.x === 'number' && typeof opts.titlePos.y === 'number') { // NOTE: manualLayout x/y vals are *relative to entire slide* const totalX = opts.titlePos.x + chartX const totalY = opts.titlePos.y + chartY let valX = totalX === 0 ? 0 : (totalX * (totalX / 5)) / 10 if (valX >= 1) valX = valX / 10 if (valX >= 0.1) valX = valX / 10 let valY = totalY === 0 ? 0 : (totalY * (totalY / 5)) / 10 if (valY >= 1) valY = valY / 10 if (valY >= 0.1) valY = valY / 10 layout = `` } return ` ${rotate} ${align} ${createColorElement(opts.color || DEF_FONT_COLOR)} ${createColorElement(opts.color || DEF_FONT_COLOR)} ${encodeXmlEntities(opts.title) || ''} ${layout} ` } /** * Calc and return excel column name for a given column length * @param colIndex column index * @return column name * @example 1 returns 'A' * @example 27 returns 'AA' */ function getExcelColName (colIndex: number): string { let colStr = '' const colIdx = colIndex - 1 // Subtract 1 so `LETTERS[columnIndex]` returns "A" etc if (colIdx <= 25) { // A-Z colStr = LETTERS[colIdx] } else { // AA-ZZ (ZZ = index 702) colStr = `${LETTERS[Math.floor(colIdx / LETTERS.length - 1)]}${LETTERS[colIdx % LETTERS.length]}` } return colStr } /** * Creates `a:innerShdw` or `a:outerShdw` depending on pass options `opts`. * @param {Object} opts optional shadow properties * @param {Object} defaults defaults for unspecified properties in `opts` * @see http://officeopenxml.com/drwSp-effects.php * @example { type: 'outer', blur: 3, offset: (23000 / 12700), angle: 90, color: '000000', opacity: 0.35, rotateWithShape: true }; * @return {string} XML */ function createShadowElement (options: ShadowProps, defaults: object): string { if (!options) { return '' } else if (typeof options !== 'object') { console.warn('`shadow` options must be an object. Ex: `{shadow: {type:\'none\'}}`') return '' } let strXml = '' const opts = { ...defaults, ...options } const type = opts.type || 'outer' const blur = valToPts(opts.blur) const offset = valToPts(opts.offset) const angle = Math.round(opts.angle * 60000) const color = opts.color const opacity = Math.round(opts.opacity * 100000) const rotShape = opts.rotateWithShape ? 1 : 0 strXml += `` strXml += `` strXml += `` strXml += `` strXml += '' return strXml } /** * Create Grid Line Element * @param {OptsChartGridLine} glOpts {size, color, style} * @return {string} XML */ function createGridLineElement (glOpts: OptsChartGridLine): string { let strXml = '' strXml += ' ' strXml += ` ` strXml += ' ' // should accept scheme colors as implemented in [Pull #135] strXml += ' ' strXml += ' ' strXml += ' ' strXml += '' return strXml } function createLineCap (lineCap: ChartLineCap): string { if (!lineCap || lineCap === 'flat') { return 'flat' } else if (lineCap === 'square') { return 'sq' } else if (lineCap === 'round') { return 'rnd' } else { const neverLineCap: never = lineCap throw new Error(`Invalid chart line cap: ${neverLineCap}`) } }