// author vtloc
|
// date 2014Jan09
|
// author GradualStudent
|
// date 2015jan06
|
// This module's purpose is to transform
|
var _ = require('lodash') // replacing underscore to get merge function
|
var xmlBuilder = require('xmlbuilder')
|
var _chartSpecs = require('./charts')
|
|
function OfficeChart(chartInfo) {
|
if (chartInfo instanceof OfficeChart) {
|
return chartInfo
|
}
|
|
return {
|
chartSpec: null, // Javascript object that represents the XML tree for the PowerPoint chart
|
|
toXML: function() {
|
return xmlBuilder
|
.create(this.chartSpec, {
|
version: '1.0',
|
encoding: 'UTF-8',
|
standalone: true
|
})
|
.end({ pretty: true, indent: ' ', newline: '\n' })
|
},
|
|
toJSON: function() {
|
return this.chartSpec
|
},
|
|
getClass: function() {
|
return 'OfficeChart'
|
},
|
|
// @brief Create XML representation of chart object
|
// @param chartInfo {object}
|
// {
|
// [
|
// title: 'eSurvey chart',
|
// data: [ // array of series
|
// {
|
// name: 'Income',
|
// labels: ['2005', '2006', '2007', '2008', '2009'],
|
// values: [23.5, 26.2, 30.1, 29.5, 24.6],
|
// color: 'ff0000'
|
// }
|
// ],
|
// overlap: "0",
|
// gapWidth: "150"
|
// }
|
|
initialize: function(chartInfo) {
|
if (chartInfo.getClass && chartInfo.getClass() === 'OfficeChart') {
|
return chartInfo
|
}
|
|
// overlap ["50"] is handled as an option within the chartbase
|
// gapWidth ["150"] is handled as an option within the chartbase
|
// valAxisCrossAtMaxCategory [true|false] is handled as an option within the chart base
|
// catAxisReverseOrder [true|false] is handled as an option within the chart base
|
|
this.chartSpec = OfficeChart.getChartBase(chartInfo) // get foundation XML for the chart type
|
|
// Below are methods for handling options with more complex XML to mix in
|
this.setData(chartInfo['data'])
|
this.setTitle(chartInfo.title || chartInfo.name)
|
this.setValAxisTitle(chartInfo.valAxisTitle)
|
this.setCatAxisTitle(chartInfo.catAxisTitle)
|
this.setValAxisNumFmt(chartInfo.valAxisNumFmt)
|
this.setValAxisScale(chartInfo.valAxisMinValue, chartInfo.valAxisMaxValue)
|
this.setTextSize(chartInfo.fontSize)
|
this.mergeChartXml(chartInfo.xml)
|
this.setValAxisMajorGridlines(chartInfo.valAxisMajorGridlines)
|
this.setValAxisMinorGridlines(chartInfo.valAxisMinorGridlines)
|
|
return this
|
},
|
|
setTextSize: function(textSize) {
|
if (textSize !== undefined) {
|
var textRef = this._text(textSize)
|
_.merge(this.chartSpec['c:chartSpace'], textRef)
|
}
|
},
|
|
setTitle: function(title) {
|
if (title !== undefined) {
|
var titleRef = this._title(chartInfo.title || chartInfo.name)
|
_.merge(this.chartSpec['c:chartSpace']['c:chart'], titleRef)
|
}
|
},
|
|
setValAxisTitle: function(title) {
|
if (title) {
|
var titleRef = this._title(title)
|
_.merge(
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx'],
|
titleRef
|
)
|
}
|
},
|
|
setCatAxisTitle: function(title) {
|
if (title) {
|
var titleRef = this._title(title)
|
_.merge(
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:catAx'],
|
titleRef
|
)
|
}
|
},
|
|
setValAxisNumFmt: function(format) {
|
if (format !== undefined) {
|
var numFmtRef = this._numFmt(format)
|
_.merge(
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx'],
|
numFmtRef
|
)
|
}
|
},
|
|
setValAxisScale: function(min, max) {
|
if (min !== undefined || max !== undefined) {
|
var scalingRef = this._scaling(min, max)
|
_.merge(
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx'],
|
scalingRef
|
)
|
}
|
},
|
|
mergeChartXml: function(xml) {
|
if (xml !== undefined) {
|
_.merge(this.chartSpec['c:chartSpace'], xml)
|
}
|
},
|
|
setValAxisMajorGridlines: function(boolean) {
|
if (boolean) {
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx'][
|
'c:majorGridlines'
|
] = {}
|
}
|
},
|
setValAxisMinorGridlines: function(boolean) {
|
if (boolean) {
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:valAx'][
|
'c:minorGridlines'
|
] = {}
|
}
|
},
|
|
setData: function(data) {
|
var seriesDataRef
|
|
if (data) {
|
this.data = data
|
|
// Mix in data series
|
if (
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea']['c:lineChart']
|
) {
|
seriesDataRef = this.chartSpec['c:chartSpace']['c:chart'][
|
'c:plotArea'
|
]['c:lineChart']['#text']
|
} else {
|
if (
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea'][
|
'c:areaChart'
|
]
|
) {
|
seriesDataRef = this.chartSpec['c:chartSpace']['c:chart'][
|
'c:plotArea'
|
]['c:areaChart']['#text']
|
} else {
|
if (
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea'][
|
'c:barChart'
|
]
|
) {
|
seriesDataRef = this.chartSpec['c:chartSpace']['c:chart'][
|
'c:plotArea'
|
]['c:barChart']['#text']
|
} else if (
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea'][
|
'c:doughnutChart'
|
]
|
) {
|
seriesDataRef = this.chartSpec['c:chartSpace']['c:chart'][
|
'c:plotArea'
|
]['c:doughnutChart']['#text']
|
} else if (
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea'][
|
'c:lineChart'
|
]
|
) {
|
seriesDataRef = this.chartSpec['c:chartSpace']['c:chart'][
|
'c:plotArea'
|
]['c:lineChart']['#text']
|
} else if (
|
this.chartSpec['c:chartSpace']['c:chart']['c:plotArea'][
|
'c:pieChart'
|
]
|
) {
|
seriesDataRef = this.chartSpec['c:chartSpace']['c:chart'][
|
'c:plotArea'
|
]['c:pieChart']['#text']
|
} else {
|
throw new Error(
|
"Can't add data to chart that is not initialized or not a recognized type"
|
)
|
} // Endif.
|
} // Endif.
|
} // Endif.
|
|
// the barChart/pieChart objects has other attributes too, so we push the series data on, not replace it
|
if (chartInfo['data']) {
|
var seriesData = this.getSeriesRef(chartInfo['data'])
|
for (var i = 0; i < seriesData.length; i++) {
|
seriesDataRef.push(seriesData[i])
|
}
|
}
|
}
|
return this
|
},
|
|
/**
|
* Transform an array of string into an office's compliance structure
|
*
|
* @param region {string} The reference cell of the string, for example: $A$1.
|
* @param stringArr {Array} An array of string, for example: ['foo', 'bar'].
|
*/
|
_strRef: function(region, stringArr) {
|
var obj = {
|
'c:strRef': {
|
'c:f': region,
|
'c:strCache': function() {
|
var result = {}
|
|
result['c:ptCount'] = { '@val': stringArr.length }
|
result['#text'] = []
|
for (var i = 0; i < stringArr.length; i++) {
|
result['#text'].push({
|
'c:pt': { '@idx': i, 'c:v': stringArr[i] }
|
})
|
} // End of for loop.
|
|
return result
|
}
|
}
|
}
|
|
return obj
|
},
|
|
/**
|
* Transform an array of numbers into an office's compliance structure
|
*
|
* @param region {string} The reference cell of the string, for example: $A$1.
|
* @param numArr {Array} An array of numArr, for example: [4, 7, 8].
|
* @param formatCode {string} A string describe the number's format. Example: General.
|
*/
|
_numRef: function(region, numArr, formatCode) {
|
var obj = {
|
'c:numRef': {
|
'c:f': region,
|
'c:numCache': {
|
'c:formatCode': formatCode,
|
'c:ptCount': { '@val': '' + numArr.length },
|
'#text': function() {
|
var result = []
|
for (var i = 0; i < numArr.length; i++) {
|
result.push({
|
'c:pt': { '@idx': i, 'c:v': numArr[i].toString() }
|
})
|
} // End of for loop.
|
|
return result
|
}
|
}
|
}
|
}
|
|
return obj
|
},
|
|
_numFmt: function(formatCode) {
|
return {
|
'c:numFmt': {
|
'@formatCode': formatCode || 'General',
|
'@sourceLinked': formatCode ? '0' : '1'
|
}
|
}
|
},
|
|
/**
|
* Transform an array of string into an office's compliance structure
|
*
|
* @param colorArr {Array} An array of colorArr, for example: ['ff0000', '00ff00', '0000ff'].
|
*/
|
_colorRef: function(colorArr) {
|
var arr = []
|
for (var i = 0; i < colorArr.length; i++) {
|
arr.push({
|
'c:dPt': {
|
'c:idx': { '@val': i },
|
'c:bubble3D': { '@val': 0 },
|
'c:spPr': {
|
'a:solidFill': {
|
'a:srgbClr': { '@val': colorArr[i].toString() }
|
}
|
}
|
}
|
})
|
} // End of for loop.
|
|
return arr
|
},
|
|
/**
|
* @brief Transform an array of string into an office's compliance structure
|
*
|
* @param row {number} Row index.
|
* @param col {number} Col index.
|
* @param isRowAbsolute {boolean} Will add $ into cell's address if this parameter is true.
|
* @param isColAbsolute {boolean} Will add $ into cell's address if this parameter is true.
|
*/
|
_rowColToSheetAddress: function(row, col, isRowAbsolute, isColAbsolute) {
|
var address = ''
|
|
if (isColAbsolute) {
|
address += '$'
|
} // Endif.
|
|
// these lines of code will transform the number 1-26 into A->Z
|
// used in excel's cell's coordination
|
while (col > 0) {
|
var num = col % 26
|
col = (col - num) / 26
|
address += String.fromCharCode(65 + num - 1)
|
} // End of while loop.
|
|
if (isRowAbsolute) {
|
address += '$'
|
} // Endif.
|
|
address += row
|
|
return address
|
},
|
|
// returns XML snippet for a chart dataseries
|
_ser: function(serie, i) {
|
var rc2a = this._rowColToSheetAddress // shortcut
|
|
var serieData = {
|
'c:ser': {
|
'c:idx': { '@val': i },
|
'c:order': { '@val': i },
|
'c:tx': this._strRef('Sheet1!' + rc2a(1, 2 + i, true, true), [
|
serie.name
|
]), // serie's value
|
'c:invertIfNegative': { '@val': '0' },
|
'c:cat': this._strRef(
|
'Sheet1!' +
|
rc2a(2, 1, true, true) +
|
':' +
|
rc2a(2 + serie.labels.length - 1, 1, true, true),
|
serie.labels
|
),
|
'c:val': this._numRef(
|
'Sheet1!' +
|
rc2a(2, 2 + i, true, true) +
|
':' +
|
rc2a(2 + serie.labels.length - 1, 2 + i, true, true),
|
serie.values,
|
'General'
|
)
|
}
|
}
|
|
if (serie.color) {
|
serieData['c:ser']['c:spPr'] = {
|
'a:solidFill': {
|
'a:srgbClr': { '@val': serie.color }
|
}
|
}
|
} else if (serie.schemeColor) {
|
serieData['c:ser']['c:spPr'] = {
|
'a:solidFill': {
|
'a:schemeClr': { '@val': serie.schemeColor }
|
}
|
}
|
} // Endif.
|
|
if (serie.xml) {
|
serieData['c:ser'] = _.merge(serieData['c:ser'], serie.xml)
|
} // Endif.
|
|
// for pie charts
|
if (serie.colors) {
|
serieData['c:ser']['#text'] = this._colorRef(serie.colors)
|
} // Endif.
|
|
return serieData
|
},
|
|
// returns XML snippet for a chart dataseries
|
getSeriesRef: function(data) {
|
return data.map(this._ser, this)
|
},
|
|
// returns XML snippet for axis number format e.g. "$0" for US currency, "0%" for percentages
|
xmlValAxisFormat: function(formatCode) {
|
return {
|
'c:chartSpace': {
|
'c:chart': {
|
'c:plotArea': {
|
'c:valAx': {
|
'c:majorGridlines': {},
|
'c:numFmt': {
|
'@formatCode': formatCode,
|
'@sourceLinked': '0'
|
}
|
}
|
}
|
}
|
}
|
}
|
},
|
|
// returns XML snippet for an axis scale
|
// currently just min/max are supported
|
/*
|
<c:scaling><c:orientation val="minMax"/>
|
<c:max val="24.0"/>
|
<c:min val="24.0"/>
|
</c:scaling>
|
*/
|
_scaling: function(minValue, maxValue) {
|
var ref = {
|
'c:scaling': {
|
'c:orientation': {
|
'@val': 'minMax'
|
}
|
}
|
}
|
|
if (maxValue !== undefined) {
|
ref['c:scaling']['c:max'] = {
|
'@val': '' + (maxValue || '')
|
}
|
} // Endif.
|
|
if (minValue !== undefined) {
|
ref['c:scaling']['c:min'] = {
|
'@val': '' + (minValue || '')
|
}
|
} // Endif.
|
|
return ref
|
},
|
|
_text: function(textSize) {
|
return {
|
'c:txPr': {
|
'a:bodyPr': {},
|
'a:listStyle': {},
|
'a:p': {
|
'a:pPr': {
|
'a:defRPr': {
|
'@sz': textSize
|
}
|
},
|
'a:endParaRPr': {
|
'@lang': 'en-US'
|
}
|
}
|
}
|
}
|
},
|
|
// @brief returns XML snippet for an axis title
|
_title: function(title) {
|
if (typeof title === 'object') return title // assume it's an XML representations
|
return {
|
'c:title': {
|
'c:tx': {
|
'c:rich': {
|
'a:bodyPr': {},
|
'a:lstStyle': {},
|
'a:p': {
|
'a:pPr': {
|
'a:defRPr': {}
|
},
|
'a:r': {
|
'a:rPr': {
|
'@lang': 'en-US',
|
'@dirty': '0',
|
'@smtClean': '0'
|
},
|
'a:t': title
|
},
|
'a:endParaRPr': {
|
'@lang': 'en-US',
|
'@dirty': '0'
|
}
|
}
|
}
|
},
|
'c:layout': {},
|
'c:overlay': {
|
'@val': '0'
|
}
|
}
|
}
|
}
|
}.initialize(chartInfo)
|
}
|
|
OfficeChart.getChartBase = function(chartInfo) {
|
var chartBase
|
|
if (typeof chartInfo === 'string') {
|
chartBase = _chartSpecs[chartInfo]()
|
} else if (typeof chartInfo.renderType === 'string') {
|
chartBase = _chartSpecs[chartInfo.renderType](chartInfo)
|
} else if (chartInfo.xml) {
|
chartBase = chartInfo.xml
|
} else {
|
throw new Error('invalid chart type')
|
}
|
// return deep copy so can create multiple charts from same base within one PowerPoint document
|
return JSON.parse(JSON.stringify(chartBase))
|
}
|
|
module.exports = OfficeChart
|
|
/*
|
// Column chart
|
new OfficeChart({
|
title: 'eSurvey chart',
|
renderType: 'column',
|
data: [ // array of series
|
{
|
name: 'Income',
|
labels: ['2005', '2006', '2007', '2008', '2009'],
|
values: [23.5, 26.2, 30.1, 29.5, 24.6],
|
colors: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff'] // optional
|
},
|
{
|
name: 'Expense',
|
labels: ['2005', '2006', '2007', '2008', '2009'],
|
values: [18.1, 22.8, 23.9, 25.1, 25],
|
colors: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff'] // optional
|
}
|
]
|
});
|
|
// Pie chart
|
new OfficeChart({
|
title: 'eSurvey chart',
|
renderType: 'pie',
|
data: [ // array of series
|
{
|
name: 'Income',
|
labels: ['2005', '2006', '2007', '2008', '2009'],
|
values: [23.5, 26.2, 30.1, 29.5, 24.6],
|
colors: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff'] // optional
|
}
|
]
|
});
|
|
// Clustered bar chat
|
new OfficeChart({
|
title: 'eSurvey chart',
|
renderType: 'group-bar',
|
data: [ // array of series
|
{
|
name: 'Income',
|
labels: ['2005', '2006', '2007', '2008', '2009'],
|
values: [23.5, 26.2, 30.1, 29.5, 24.6],
|
colors: ['ff0000', '00ff00', '0000ff', 'ffff00', '00ffff'] // optional
|
}
|
]
|
});
|
*/
|