import { select as d3Select } from 'd3-selection';
import { axisBottom as d3AxisBottom } from 'd3-axis';
import { format as d3Format } from 'd3-format';
import { scaleLinear as d3ScaleLinear } from 'd3-scale';
import { transition as d3Transition } from 'd3-transition';
import OECDChart from './OECDChart';
/**
* A pearl chart component.
*
* @example <caption>browser usage:</caption>
* const PearlChartExample = new OECDCharts.PearlChart({
* container: '#PearlChartExample',
* extent: [300, 600],
* title: 'Pearl Chart',
* renderInfoButton: true,
* showTicks: true,
* showLabels: false,
* colorLabels: true,
* callback: data => console.log(data),
* data: [
* {
* value: 410,
* color: '#900c3f',
* showLabel: true, // default is false
* labelPlacement: 'bottom' // default 'top'
* },
* {
* value: 520,
* color: '#189aa8',
* showLabel: false, // default is false
* labelPlacement: 'top'
* }
* ],
* labelFormat: val => `${Math.round(val)} $`
* });
* @example <caption>ES6 modules usage:</caption>
* import { PearlChart } from 'oecd-simple-charts';
* import 'oecd-simple-charts/build/oecd-simple-charts.css'
*
* const pearlChart = new PearlChart({ chartOptions });
* @constructor
* @param {object} options - The options object for the pearl 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.radius = 10] - The radius for the pearl in px
* @param {int} [options.ticks = 4] - The number of ticks displayed under the pearl chart, will only be used if tickValues is not set
* @param {array} options.tickValues - An array of numbers that are displayed as ticks
* @param {bool} [options.showTicks = true] - Hide or show ticks
* @param {function} options.callback - A function that is called on circle click
* @param {function} [options.labelFormat = val => Math.round(val * 10) / 10] - A function for formatting circle labels
* @param {function} [options.colorLabels = false] - Fill labels in circle color or black
* @param {array} options.data - The data as array. i.e.:
* ```
* [
* {
* value: 410,
* color: '#900c3f'
* },
* {
* value: 520,
* color: '#189aa8'
* }
* ]
* ```
*/
class PearlChart extends OECDChart {
constructor(options = {}) {
super();
this.defaultOptions = {
container: null,
extent: [0, 100],
data: [],
height: 55,
marginLeft: 35,
marginRight: 35,
fontSize: 14,
radius: 10,
labelOffset: 5,
ticks: 4,
tickValues: null,
colorLabels: false,
callback: null,
showTicks: true,
labelAccessor: data => d.value,
labelFormat: val => val
};
this.init(options);
}
render() {
const {
data,
height,
container,
extent,
marginLeft,
marginRight,
labelOffset,
} = this.options;
const d3Container = d3Select(container);
const outerWidth = d3Container.node().clientWidth;
const innerWidth = outerWidth - marginLeft - marginRight;
const innerHeight = height + labelOffset;
this.removeSelections(['.pearlchart__svg']);
this.scale = d3ScaleLinear()
.domain(extent)
.range([0, innerWidth]);
const svg = d3Container
.classed('OECDCharts__PearlChart', true)
.append('svg')
.classed('pearlchart__svg', true)
.attr('width', outerWidth)
.attr('height', innerHeight);
this.chartWrapper = svg.append('g')
.classed('pearlchart__chart', true)
.attr('transform', `translate(${marginLeft - labelOffset}, ${this.options.title || this.options.renderInfoButton ? -10 : 10})`);
this.getAxis({ chartWrapper: this.chartWrapper, extent, innerWidth, innerHeight, labelOffset });
this.nodesWrapper = this.chartWrapper
.append('g')
.classed('pearlchart__nodes', true);
this.update(data);
}
/**
* @memberof PearlChart
* @param {array} data - an array containing objects with the new data
* @example
* PearlChartExample.update([
* {
* value: 490,
* color: '#900c3f'
* },
* {
* value: 820,
* color: '#189aa8'
* }
* ]);
*/
update(_data) {
this.options.data = _data;
const data = PearlChart.parseData(_data);
const transitionFunc = d3Transition().duration(750);
const { labelOffset, radius, height, labelPlacement } = this.options;
const innerHeight = height + labelOffset;
this.getCircles(data, innerHeight, radius, transitionFunc, labelOffset, labelPlacement);
}
getAxis(options = this.defaultOptions) {
const { chartWrapper, extent, innerWidth, innerHeight, labelOffset } = options;
// render background line
const axis = chartWrapper.append('g')
.classed('pearlchart__x-axis', true);
axis.append('line')
.attr('x1', 0)
.attr('x2', innerWidth)
.attr('y1', innerHeight / 2)
.attr('y2', innerHeight / 2);
// render axis labels
const axisLabel = axis.selectAll('.pearlchart__axis-label')
.data(extent);
axisLabel.exit().remove();
axisLabel.enter()
.append('text')
.classed('pearlchart__axis-label', true)
.text(d => d)
.attr('font-size', this.options.fontSize)
.attr('x', (d, i) => this.scale.range()[i])
.attr('dx', (d, i) => (i === 0 ? `-${labelOffset}px` : `${labelOffset}px`))
.attr('y', innerHeight / 2)
.attr('dy', labelOffset)
.attr('text-anchor', (d, i) => (i === 0 ? 'end' : 'start'));
const xAxis = d3AxisBottom(this.scale)
.tickFormat(d3Format('.0f'))
.tickSize(15);
if (this.options.tickValues) {
xAxis.tickValues(this.options.tickValues);
} else {
xAxis.ticks(this.options.ticks);
}
axis.append('g')
.classed('pearlchart__axis-ticks', true)
.style('display', this.options.showTicks ? 'block' : 'none')
.attr('transform', `translate(0, ${(this.options.radius * 2) + 10})`)
.call(xAxis);
// remove first and last axis label to avoid duplicates with the extent labels
if (+axis.select('.tick:last-of-type').text() === this.options.extent[1]) {
axis.select('.tick:last-of-type').style('display', 'none');
}
if (+axis.select('.tick').text() === this.options.extent[0]) {
axis.select('.tick').style('display', 'none');
}
axis.selectAll('pearlchart__axis-ticks').style('font-size', (this.options.fontSize * 0.8));
}
getCircles(_data, innerHeight, radius, transition, labelOffset, labelPlacement) {
const circles = this.nodesWrapper.selectAll('.pearlchart__circle-wrapper')
.data(_data, d => d.value);
circles.exit().remove();
circles
.transition(transition)
.style('fill', d => d.color);
const circle = circles.enter()
.append('g')
.classed('pearlchart__circle-wrapper', true)
.classed('clickable', this.options.callback !== null)
.on('mouseenter', (d, i, nodes) => {
d3Select(nodes[i]).select('.pearlchart__circle-tooltip').style('display', 'block');
})
.on('mouseleave', (d, i, nodes) => {
d3Select(nodes[i]).select('.pearlchart__circle-tooltip').style('display', 'none');
})
.on('click', () => {
if (this.options.callback) {
this.options.callback(this.options.data);
}
});
circle.append('circle')
.classed('pearlchart__circle', true)
.attr('cx', d => this.scale(d.value))
.attr('cy', innerHeight / 2)
.attr('r', radius)
.style('fill', d => d.color);
const cirlcesWithLabels = circle
.append('text')
.classed('pearlchart__circle-label', d => d.showLabel)
.classed('pearlchart__circle-tooltip', d => !d.showLabel)
.attr('font-size', this.options.fontSize)
.attr('x', d => this.scale(d.value))
.attr('y', (innerHeight / 2))
.attr('dy', d => (d.labelPlacement === 'bottom' ? 1 : -1) * (radius * 2) + labelOffset)
.text(d => {
const value = this.options.labelAccessor(d);
return this.options.labelFormat(value);
})
.attr('text-anchor', 'middle')
.style('fill', d => (!this.options.colorLabels ? '#000' : d.color));
cirlcesWithLabels.filter(d => !d.showLabel).style('display', 'none');
}
static parseData(_data) {
return _data.map((d) => {
d.color = d.color || '#777777';
return d;
});
}
}
export default PearlChart;