import { select as d3Select } from 'd3-selection';
import { scaleLinear as d3ScaleLinear } from 'd3-scale';
import { sum as d3Sum } from 'd3-array';
import OECDChart from './OECDChart';
/**
* A stacked chart component
*
* @example <caption>browser usage:</caption>
* const StackedChartExample = new OECDCharts.StackedChart({
* container: '#StackedChartExample',
* title: 'Stacked Bar Chart',
* renderInfoButton: true,
* data: [
* {
* values: [1,2,3,4,5],
* barLabels: ['0%', '100%'],
* colors: ['#fddd5d', '#900c3f'],
* stackLabels: ['I', 'II', 'III', 'IV', 'V']
* },
* {
* values: [2,4,6,8,20],
* barLabels: ['0%', '100%'],
* colors: ['#fddd5d', '#189aa8']
* }
* ]
* });
* @example <caption>ES6 modules usage:</caption>
* import { StackedChart } from 'oecd-simple-charts';
* import 'oecd-simple-charts/build/oecd-simple-charts.css'
*
* const stackedChart = new StackedChart({ chartOptions });
* @constructor
* @param {object} options - The options object for the stacked chart
* @param {string} options.container - The DOM element to use as container
* @param {string} options.title - The title to display
* @param {bool} [options.renderInfoButton = false] - The info-Icon for the tooltip, renders after the title
* @param {int} [options.fontSize = 14] - The font-size for the labels in px
* @param {int} [options.marginTop = 15] - The space between the bars in px
* @param {int} [options.barHeight = 30] -The height of a bar in px
* @param {array} options.data - The data as array
* @param {array} options.data.values - The values to display as stacked bar chart
* @param {array} options.data.barLabels - The labels to display left and right to the chart
* @param {array} options.data.colors - Colors for the min and max value of the stacked bar chart
* @param {array} options.data.stackLabels - Labels for the stacked elements
*/
class StackedChart extends OECDChart {
constructor(options = {}) {
super();
this.defaultOptions = {
container: null,
extent: [0, 100],
data: [],
fontSize: 14,
marginTop: 15,
barHeight: 30,
marginLeft: 30,
marginRight: 45,
labelOffset: 5
};
this.init(options);
}
render() {
const {
data,
container,
extent,
marginTop,
barHeight,
fontSize,
marginLeft,
marginRight
} = this.options;
const d3Container = d3Select(container);
const dimensions = d3Container.node().getBoundingClientRect();
const outerWidth = dimensions.width;
const innerWidth = outerWidth - marginLeft - marginRight;
const innerHeight = ((barHeight + marginTop) * data.length);
this.removeSelections(['.stacked-chart__svg']);
this.x = d3ScaleLinear().range([0, innerWidth]).domain(extent);
this.colorScale = d3ScaleLinear();
const svg = d3Container
.classed('OECDCharts__StackedChart', true)
.append('svg')
.classed('stacked-chart__svg', true)
.attr('width', outerWidth)
.attr('height', innerHeight);
this.chartWrapper = svg.append('g')
.attr('class', 'stacked-chart')
.attr('transform', `translate(${marginLeft}, 0)`);
this.update(data);
}
/**
* @memberof StackedChart
* @param {array} data - an array containing objects with the new data
* @example
* StackedChartExample.update([
* {
* values: [1,10,3,4,5],
* barLabels: ['0%', '100%'],
* colors: ['#fddd5d', '#900c3f'],
* stackLabels: ['1', '2', '3', '4', '5']
* },
* {
* values: [2,4,10,15,20],
* barLabels: ['0%', '100%'],
* colors: ['#fddd5d', '#189aa8']
* }
* ]);
*/
update(_data) {
this.options.data = _data;
const data = StackedChart.parseData(_data);
StackedChart.validateData(_data);
const chart = this.getChart(data);
this.getBars(chart);
this.getStackedLabels(chart);
this.getBarLabels(chart);
}
getChart(_data) {
const chart = this.chartWrapper.selectAll('.stacked-chart__bar')
.data(_data, d => JSON.stringify(d));
chart.exit().remove();
return chart.enter()
.append('g')
.classed('stacked-chart__bar', true)
.attr('transform', (d, i) => `translate(0, ${i * (this.options.barHeight + this.options.marginTop)})`);
}
getBars(chart) {
chart.selectAll('.stacked-chart__rect')
.data(data => data.values.map((value, i) => ({
value,
offset: data.offset[i],
colors: data.colors
})))
.enter()
.append('rect')
.classed('stacked-chart__rect', true)
.attr('width', d => this.x(d.value))
.attr('x', d => this.x(d.offset))
.attr('height', this.options.barHeight)
.attr('fill', (d, i, nodes) => this.colorScale.domain([0, nodes.length]).range(d.colors)(i));
}
getStackedLabels(chart) {
chart.selectAll('.stacked-chart__stackedlabel')
.data(data => data.stackLabels.map((stackLabel, i) => ({
stackLabel,
offset: data.offset[i],
value: data.values[i]
})))
.enter()
.append('text')
.classed('stacked-chart__stackedlabel', true)
.attr('y', this.options.barHeight / 2)
.attr('x', d => this.x(d.offset + (d.value / 2)))
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('fill', '#fff')
.text(d => d.stackLabel)
.attr('font-size', this.options.fontSize);
}
getBarLabels(chart) {
const { labelOffset, barHeight, fontSize } = this.options;
chart.selectAll('.stacked-chart__barlabel')
.data(d => d.barLabels)
.enter()
.append('text')
.classed('stacked-chart__barlabel', true)
.attr('y', barHeight / 2)
.attr('x', (d, i, nodes) => (i === 0 ? 0 : nodes[i].parentNode.getBoundingClientRect().width))
.attr('dy', '.35em')
.attr('dx', (d, i) => (i === 0 ? `-${labelOffset}px` : `${labelOffset}px`))
.attr('text-anchor', (d, i) => (i === 0 ? 'end' : 'start'))
.text(d => d)
.attr('font-size', fontSize);
}
static parseData(_data) {
return _data.map((d) => {
const factor = 100 / d.values.reduce((a, b) => a + b);
d.values = d.values.map(value => value * factor);
d.offset = d.values.map((value, i) => d3Sum(d.values.slice(0, i)));
d.stackLabels = d.stackLabels || [];
return d;
});
}
static validateData(_data) {
const invalidData = _data.filter(
d => (d.values.length !== d.stackLabels.length && d.stackLabels.length)
);
if (invalidData.length) {
throw Error('invalid data: amount of stackLabels is not matching amount of values.');
}
}
}
export default StackedChart;