@randstad-design/orbit-multitheme
Version:
multitheme Front-end code based on Randstad Human Forward components
400 lines (353 loc) • 11 kB
JavaScript
/**
* charts.js
* load google charts based on table element
*/
import ElementHelpers from '../helpers/element-helpers';
import { GoogleChartsApi } from './charts/google-charts-api';
/**
* Declare constants
*/
const attributeBase = 'data-rs-charts';
export class Charts {
constructor(element) {
this.element = element;
this.type = this.getType();
this.addEventHandlers();
this.initializeChart();
}
/**
* Declare types
*/
get types() {
return {
column: 'column',
donut: 'donut',
line: 'line'
};
}
/**
* Declare attributes
*/
get attributes() {
return {
placeholder: `${attributeBase}-placeholder`,
source: `${attributeBase}-source`,
legend: `${attributeBase}-legend`,
legendItem: `${attributeBase}-legend-item`,
horizAxisLabel: `${attributeBase}-horiz-axis-label`,
vertAxisLabel: `${attributeBase}-vert-axis-label`,
valueLabel: `${attributeBase}-value-label`
};
}
/**
* Add event handlers
*/
addEventHandlers() {
window.addEventListener('resize', () => {
this.createChart();
});
}
/**
* Get type - get type of chart from base attribute
*/
getType() {
let type = null;
const typeRaw = this.element.getAttribute(attributeBase);
if (
typeof typeRaw === 'string' &&
typeof this.types[typeRaw.toLowerCase()] !== 'undefined'
) {
type = this.types[typeRaw.toLowerCase()];
}
if (type === null) {
throw Error('Chart type is invalid');
}
return type;
}
/**
* Initialize chart - Load Google Maps API
*/
initializeChart() {
// get elements
this.chartElement = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.placeholder
);
// create map, controls and markers after the API is ready
const chartsApi = new GoogleChartsApi();
chartsApi
.load()
.then(() => {
this.createChart();
this.createLegend();
})
.catch((reason) => {
console.error(reason);
});
}
/**
* Create chart - create Google Visualization chart based on type
*/
createChart() {
// get source element
const sourceElement = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.source
);
const horizAxisLabel = this.element.getAttribute(this.attributes.horizAxisLabel);
const vertAxisLabel = this.element.getAttribute(this.attributes.vertAxisLabel);
const valueLabel = this.element.getAttribute(this.attributes.valueLabel);
// create Google Visualization chart based on type
switch (this.type) {
case this.types.column:
this.chart = new google.visualization.ColumnChart(this.chartElement);
this.options = this.getColumnOptions(horizAxisLabel, vertAxisLabel, (valueLabel === 'none' || !valueLabel), valueLabel==='percentage');
// draw chart
this.data = this.getData(sourceElement, (valueLabel === 'percentage'));
this.chart.draw(this.data, this.options);
break;
case this.types.donut:
this.options = this.getDonutOptions(valueLabel);
this.data = this.getData(sourceElement);
this.drawDonut(this.data, this.options, this.chartElement);
break;
case this.types.line:
this.chart = new google.visualization.LineChart(this.chartElement);
this.options = this.getLineOptions(horizAxisLabel, vertAxisLabel, (valueLabel === 'none'));
// draw chart
this.data = this.getData(sourceElement);
this.chart.draw(this.data, this.options);
break;
}
this.options.adjustLegendPosition = !! horizAxisLabel;
}
/**
* Get data - parse data from source table in to data table
* @param sourceTable
*/
getData(sourceTable, calculateAsPercentage) {
this.sourceData = this.parseData(sourceTable,calculateAsPercentage);
let data = google.visualization.arrayToDataTable(this.sourceData);
return data;
}
/**
* Parse data from table to array
* @param sourceTable
*/
parseData(sourceTable, calculateAsPercentage) {
let sourceData = [];
// parse header text from th nodes and convert to array
const thNodes = [...sourceTable.querySelectorAll('thead th')];
let headers = thNodes.map((thNode) => {
return thNode.innerText;
});
// push header with 'string' and 'style' for column and donut chart
if (this.type !== this.types.line) {
headers.push({ type: 'string', role: 'style' });
}
sourceData.push(headers);
// parse graph data from body
let trNodes = [...sourceTable.querySelectorAll('tbody tr')];
// calulateAsPercentage is a column mode adjusting values to a % of total values
let sumOfValues = 0.0;
if(calculateAsPercentage) { // go through data rows, grab 1th values and sum them
const get1thValueFromRow = (trNode) => {
return parseFloat([...trNode.querySelectorAll('td')][1].innerText);
}
sumOfValues = trNodes.reduce((sumSoFar,trNode) => sumSoFar + get1thValueFromRow(trNode), sumOfValues);
}
trNodes.forEach((trNode, index) => {
let row = [...trNode.querySelectorAll('td')].map((tdNode, index) => {
// first item is used for display, other are used as values
return index === 0 ? tdNode.innerText : parseFloat(tdNode.innerText);
});
if(calculateAsPercentage) { // calculate from 0.0-1.0 (percentage display formatting handled elsewhere)
row[1] = row[1] / (sumOfValues ? sumOfValues: 1.0); // 1.0 just on the odd chance sumOfValues was zero
}
// push 'color' row for column and donut chart
if (this.type !== this.types.line) {
row.push('color: ' + this.options.colors[index]);
}
sourceData.push(row);
});
return sourceData;
}
/**
* Get default options - options for all charts
*/
getDefaultOptions() {
return {
backgroundColor: 'transparent',
colors: [
'#f0f0f0',
'#e8e8e8',
'#e0e0e0',
'#dcdcdc',
'#d8d8d8',
'#d3d3d3',
'#d0d0d0',
'#c8c8c8',
'#c0c0c0',
'#bebebe',
'#b8b8b8',
'#b0b0b0',
'#a9a9a9',
'#a8a8a8',
'#a0a0a0'
],
fontSize: 18,
legend: { position: 'none' },
tooltip: {
textStyle: {
color: '#000000',
bold: false
},
isHtml: false,
trigger: 'selection'
},
animation: {
duration: 1000,
easing: 'in',
startup: true
}
};
}
/**
* Get options for column chart
*/
getColumnOptions(horizAxisLabel, vertAxisLabel, hideValueLabels, formatAsPercentage) {
let options = this.getDefaultOptions();
const chartWidth = this.chartElement.offsetWidth;
const chartHeight = this.chartElement.offsetHeight;
options.chartArea = {
width: chartWidth - 80,
left: 80,
top: 20,
height: chartHeight - 100
};
options.hAxis = hideValueLabels ? { textPosition: 'none' } : {};
options.vAxis = hideValueLabels ? { textPosition: 'none' } : {};
horizAxisLabel && (options.hAxis.title = horizAxisLabel);
vertAxisLabel && (options.vAxis.title = vertAxisLabel);
formatAsPercentage && (options.vAxis.format = 'percent');
return options;
}
/**
* Get options for donut chart
*/
getDonutOptions(valueLabel) {
let options = this.getDefaultOptions();
const chartWidth = this.chartElement.offsetWidth;
options.chartArea = {
width: chartWidth,
left: 0,
top: 0,
height: chartWidth
};
options.height = chartWidth + 70;
options.width = chartWidth;
options.pieHole = 0.5;
options.pieSliceBorderColor = 'white';
options.pieSliceText = valueLabel ? valueLabel : 'percentage';
return options;
}
/**
* Draw the donut chart for animation
*/
drawDonut(data, options, chartElement) {
var chart = new google.visualization.PieChart(chartElement);
chart.draw(data, options);
}
/**
* Get options for line chart
*/
getLineOptions(horizAxisLabel, vertAxisLabel, hideValueLabels) {
let options = this.getDefaultOptions();
const chartWidth = this.chartElement.offsetWidth;
const chartHeight = this.chartElement.offsetHeight;
options.chartArea = {
width: chartWidth - 50,
left: 50,
top: 20,
height: chartHeight - 130
};
options.curveType = 'function';
options.hAxis = hideValueLabels ? { textPosition: 'none' } : {};
options.vAxis = hideValueLabels ? { textPosition: 'none' } : {};
horizAxisLabel && (options.hAxis.title = horizAxisLabel);
vertAxisLabel && (options.vAxis.title = vertAxisLabel);
return options;
}
/**
* Create legend - create customized legend
*/
createLegend() {
// get legend element
const legend = ElementHelpers.getElementByAttributeWithinElement(
this.element,
this.attributes.legend
);
const legendParent = legend.parentNode;
// create legend based on chart type
switch (this.type) {
case this.types.donut:
case this.types.column:
this.createDonutOrColumnLegend(legend);
legendParent.style.marginTop = this.options.adjustLegendPosition ? '15px' : '10px';
break;
case this.types.line:
this.createLineLegend(legend);
this.options.adjustLegendPosition && (legendParent.style.marginTop = '-10px');
break;
}
}
/**
* Create legend for donut or column chart
* @param legend
*/
createDonutOrColumnLegend(legend) {
// delete irrelevant first object from source data array
this.sourceData.shift();
// bind each value to li item
this.sourceData.forEach((dataArray, index) => {
if (legend !== null) {
this.appendListItemsToList(index, legend, dataArray[0]);
}
});
}
/**
* Create legend for line chart
* @param legend
*/
createLineLegend(legend) {
// delete irrelevant first object from source data array
this.sourceData[0].shift();
// bind each value to li item
this.sourceData[0].forEach((string, index) => {
if (legend !== null) {
this.appendListItemsToList(index, legend, string);
}
});
}
/**
* Append list items to list - create li node and add to list
* @param index
* @param list
* @param listItemText
*/
appendListItemsToList(index, list, listItemText) {
// create a <li> node
const node = document.createElement('li');
node.setAttribute(this.attributes.legendItem, this.options.colors[index]);
// create a text node
const textnode = document.createTextNode(listItemText);
node.appendChild(textnode);
list.appendChild(node);
}
/**
* Get selector
*/
static getSelector() {
return `[${attributeBase}]`;
}
}