Use react-container-dimensions instead of react-measure

This commit is contained in:
felixlinker
2018-08-31 15:02:17 +02:00
parent d46ad89dc5
commit 8a386c4ece
4 changed files with 479 additions and 492 deletions

View File

@@ -88,7 +88,7 @@
"less": "^2.7.2", "less": "^2.7.2",
"less-loader": "^4.0.3", "less-loader": "^4.0.3",
"react-addons-perf": "^15.4.2", "react-addons-perf": "^15.4.2",
"react-measure": "^1.4.7", "react-container-dimensions": "^1.4.1",
"react-testutils-additions": "^15.2.0", "react-testutils-additions": "^15.2.0",
"react-transition-group": "^1.1.2", "react-transition-group": "^1.1.2",
"rimraf": "^2.6.1", "rimraf": "^2.6.1",

View File

@@ -1,283 +1,281 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Measure from 'react-measure'; import ContainerDimensions from 'react-container-dimensions';
import * as d3 from 'd3'; import * as d3 from 'd3';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 }; const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
/** /**
* Line Chart * Line Chart
*/ */
export default class LineChart extends TranslatedComponent { export default class LineChart extends TranslatedComponent {
static defaultProps = { static defaultProps = {
code: '', code: '',
xMin: 0, xMin: 0,
yMin: 0, yMin: 0,
points: 20, points: 20,
colors: ['#ff8c0d'], colors: ['#ff8c0d'],
aspect: 0.5 aspect: 0.5
}; };
static propTypes = { static propTypes = {
func: PropTypes.func.isRequired, func: PropTypes.func.isRequired,
xLabel: PropTypes.string.isRequired, xLabel: PropTypes.string.isRequired,
xMin: PropTypes.number, xMin: PropTypes.number,
xMax: PropTypes.number.isRequired, xMax: PropTypes.number.isRequired,
xUnit: PropTypes.string.isRequired, xUnit: PropTypes.string.isRequired,
xMark: PropTypes.number, xMark: PropTypes.number,
yLabel: PropTypes.string.isRequired, yLabel: PropTypes.string.isRequired,
yMin: PropTypes.number, yMin: PropTypes.number,
yMax: PropTypes.number.isRequired, yMax: PropTypes.number.isRequired,
yUnit: PropTypes.string, yUnit: PropTypes.string,
series: PropTypes.array, series: PropTypes.array,
colors: PropTypes.array, colors: PropTypes.array,
points: PropTypes.number, points: PropTypes.number,
aspect: PropTypes.number, aspect: PropTypes.number,
code: PropTypes.string, code: PropTypes.string,
}; };
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
* @param {Object} context React Component context * @param {Object} context React Component context
*/ */
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this._updateDimensions = this._updateDimensions.bind(this); this._updateDimensions = this._updateDimensions.bind(this);
this._updateSeries = this._updateSeries.bind(this); this._updateSeries = this._updateSeries.bind(this);
this._tooltip = this._tooltip.bind(this); this._tooltip = this._tooltip.bind(this);
this._showTip = this._showTip.bind(this); this._showTip = this._showTip.bind(this);
this._hideTip = this._hideTip.bind(this); this._hideTip = this._hideTip.bind(this);
this._moveTip = this._moveTip.bind(this); this._moveTip = this._moveTip.bind(this);
const series = props.series; const series = props.series;
let xScale = d3.scaleLinear(); let xScale = d3.scaleLinear();
let yScale = d3.scaleLinear(); let yScale = d3.scaleLinear();
let xAxisScale = d3.scaleLinear(); let xAxisScale = d3.scaleLinear();
this.xAxis = d3.axisBottom(xAxisScale).tickSizeOuter(0); this.xAxis = d3.axisBottom(xAxisScale).tickSizeOuter(0);
this.yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0); this.yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0);
this.state = { this.state = {
xScale, xScale,
xAxisScale, xAxisScale,
yScale, yScale,
tipHeight: 2 + (1.2 * (series ? series.length : 0.8)), tipHeight: 2 + (1.2 * (series ? series.length : 0.8)),
dimensions: { };
width: 100, }
height: 100
} /**
}; * Update tooltip content
} * @param {number} xPos x coordinate
* @param {number} width current container width
/** */
* Update tooltip content _tooltip(xPos, width) {
* @param {number} xPos x coordinate let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props;
*/ let { xScale, yScale } = this.state;
_tooltip(xPos) { let { formats, translate } = this.context.language;
let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; let x0 = xScale.invert(xPos),
let { xScale, yScale } = this.state; y0 = func(x0),
let { width } = this.state.dimensions; tips = this.tipContainer,
let { formats, translate } = this.context.language; yTotal = 0,
let x0 = xScale.invert(xPos), flip = (xPos / width > 0.50),
y0 = func(x0), tipWidth = 0,
tips = this.tipContainer, tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
yTotal = 0,
flip = (xPos / width > 0.50),
tipWidth = 0, xPos = xScale(x0); // Clamp xPos
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
tips.selectAll('text.text-tip.y').text(function(d, i) {
let yVal = series ? y0[series[i]] : y0;
xPos = xScale(x0); // Clamp xPos yTotal += yVal;
return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal);
tips.selectAll('text.text-tip.y').text(function(d, i) { }).append('tspan').attr('class', 'metric').text(yUnit ? ' ' + yUnit : '');
let yVal = series ? y0[series[i]] : y0;
yTotal += yVal; tips.selectAll('text').each(function() {
return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal); if (this.getBBox().width > tipWidth) {
}).append('tspan').attr('class', 'metric').text(yUnit ? ' ' + yUnit : ''); tipWidth = Math.ceil(this.getBBox().width);
}
tips.selectAll('text').each(function() { });
if (this.getBBox().width > tipWidth) {
tipWidth = Math.ceil(this.getBBox().width); let tipY = Math.floor(yScale(yTotal / (series ? series.length : 1)) - (tipHeightPx / 2));
}
}); tipWidth += 8;
tips.attr('transform', 'translate(' + xPos + ',' + tipY + ')');
let tipY = Math.floor(yScale(yTotal / (series ? series.length : 1)) - (tipHeightPx / 2)); tips.selectAll('text.text-tip').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start');
tips.selectAll('text.text-tip.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit);
tipWidth += 8; tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).attr('y', 0).style('text-anchor', flip ? 'end' : 'start');
tips.attr('transform', 'translate(' + xPos + ',' + tipY + ')'); this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0));
tips.selectAll('text.text-tip').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start'); }
tips.selectAll('text.text-tip.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit);
tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).attr('y', 0).style('text-anchor', flip ? 'end' : 'start'); /**
this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0)); * Update dimensions based on properties and scale
} * @param {Object} props React Component properties
* @param {number} scale size ratio / scale
/** * @param {number} width current width of the container
* Update dimensions based on properties and scale * @returns {Object} calculated dimensions
* @param {Object} props React Component properties */
* @param {number} scale size ratio / scale _updateDimensions(props, scale, width) {
* @returns {Object} calculated dimensions const { xMax, xMin, yMin, yMax } = props;
*/ const innerWidth = width - MARGIN.left - MARGIN.right;
_updateDimensions(props, scale) { const outerHeight = Math.round(width * props.aspect);
const { xMax, xMin, yMin, yMax } = props; const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
const { width, height } = this.state.dimensions;
const innerWidth = width - MARGIN.left - MARGIN.right; this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true);
const outerHeight = Math.round(width * props.aspect); this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true);
const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility
return { innerWidth, outerHeight, innerHeight };
this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); }
this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true);
this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility /**
return { innerWidth, outerHeight, innerHeight }; * Show tooltip
} * @param {SyntheticEvent} e Event
*/
/** _showTip(e) {
* Show tooltip e.preventDefault();
* @param {SyntheticEvent} e Event this.tipContainer.style('display', null);
*/ this.markersContainer.style('display', null);
_showTip(e) { this._moveTip(e);
e.preventDefault(); }
this.tipContainer.style('display', null);
this.markersContainer.style('display', null); /**
this._moveTip(e); * Move and update tooltip
} * @param {SyntheticEvent} e Event
* @param {number} width current container width
/** */
* Move and update tooltip _moveTip(e, width) {
* @param {SyntheticEvent} e Event let clientX = e.touches ? e.touches[0].clientX : e.clientX;
*/ this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left), width);
_moveTip(e) { }
let clientX = e.touches ? e.touches[0].clientX : e.clientX;
this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left)); /**
} * Hide tooltip
* @param {SyntheticEvent} e Event
/** */
* Hide tooltip _hideTip(e) {
* @param {SyntheticEvent} e Event e.preventDefault();
*/ this.tipContainer.style('display', 'none');
_hideTip(e) { this.markersContainer.style('display', 'none');
e.preventDefault(); }
this.tipContainer.style('display', 'none');
this.markersContainer.style('display', 'none'); /**
} * Update series generated from props
* @param {Object} props React Component properties
/** * @param {Object} state React Component state
* Update series generated from props */
* @param {Object} props React Component properties _updateSeries(props, state) {
* @param {Object} state React Component state let { func, xMin, xMax, series, points } = props;
*/ let delta = (xMax - xMin) / points;
_updateSeries(props, state) { let seriesData = new Array(points);
let { func, xMin, xMax, series, points } = props;
let delta = (xMax - xMin) / points; if (delta) {
let seriesData = new Array(points); seriesData = new Array(points);
for (let i = 0, x = xMin; i < points; i++) {
if (delta) { seriesData[i] = [x, func(x)];
seriesData = new Array(points); x += delta;
for (let i = 0, x = xMin; i < points; i++) { }
seriesData[i] = [x, func(x)]; seriesData[points - 1] = [xMax, func(xMax)];
x += delta; } else {
} let yVal = func(xMin);
seriesData[points - 1] = [xMax, func(xMax)]; seriesData = [[0, yVal], [1, yVal]];
} else { }
let yVal = func(xMin);
seriesData = [[0, yVal], [1, yVal]]; const markerElems = [];
} const detailElems = [<text key='lbl' className='text-tip x' y='1.25em'/>];
const seriesLines = [];
const markerElems = []; for (let i = 0, l = series ? series.length : 1; i < l; i++) {
const detailElems = [<text key='lbl' className='text-tip x' y='1.25em'/>]; const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]);
const seriesLines = []; seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor));
for (let i = 0, l = series ? series.length : 1; i < l; i++) { detailElems.push(<text key={i} className='text-tip y' strokeWidth={0} fill={props.colors[i]} y={1.25 * (i + 2) + 'em'}/>);
const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]); markerElems.push(<circle key={i} className='marker' r='4' />);
seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor)); }
detailElems.push(<text key={i} className='text-tip y' strokeWidth={0} fill={props.colors[i]} y={1.25 * (i + 2) + 'em'}/>);
markerElems.push(<circle key={i} className='marker' r='4' />); const tipHeight = 2 + (1.2 * (seriesLines ? seriesLines.length : 0.8));
}
this.setState({ markerElems, detailElems, seriesLines, seriesData, tipHeight });
const tipHeight = 2 + (1.2 * (seriesLines ? seriesLines.length : 0.8)); }
this.setState({ markerElems, detailElems, seriesLines, seriesData, tipHeight }); /**
} * Update dimensions and series data based on props and context.
*/
/** componentWillMount() {
* Update dimensions and series data based on props and context. this._updateSeries(this.props, this.state);
*/ }
componentWillMount() {
this._updateSeries(this.props, this.state); /**
} * Update state based on property and context changes
* @param {Object} nextProps Incoming/Next properties
/** * @param {Object} nextContext Incoming/Next conext
* Update state based on property and context changes */
* @param {Object} nextProps Incoming/Next properties componentWillReceiveProps(nextProps, nextContext) {
* @param {Object} nextContext Incoming/Next conext const props = this.props;
*/
componentWillReceiveProps(nextProps, nextContext) { if (props.code != nextProps.code) {
const props = this.props; this._updateSeries(nextProps, this.state);
}
if (props.code != nextProps.code) { }
this._updateSeries(nextProps, this.state);
} /**
} * Render the chart
* @return {React.Component} Chart SVG
/** */
* Render the chart render() {
* @return {React.Component} Chart SVG return (
*/ <ContainerDimensions>
render() { { ({ width, height }) => {
const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio); const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio, width, height);
const { width, height } = this.state.dimensions; const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props;
const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state;
const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; const lines = seriesLines.map((line, i) => <path key={i} className='line' fill='none' stroke={colors[i]} strokeWidth='1' d={line(seriesData)} />).reverse();
const line = this.line;
const lines = seriesLines.map((line, i) => <path key={i} className='line' fill='none' stroke={colors[i]} strokeWidth='1' d={line(seriesData)} />).reverse(); const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0;
const xmark = xMark ? <path key={'mark'} className='line' fill='none' strokeDasharray='5,5' stroke={'#ff8c0d'} strokeWidth='1' d={'M ' + markX + ' ' + innerHeight + ' L ' + markX + ' 0'} /> : '';
const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; return (
const xmark = xMark ? <path key={'mark'} className='line' fill='none' strokeDasharray='5,5' stroke={'#ff8c0d'} strokeWidth='1' d={'M ' + markX + ' ' + innerHeight + ' L ' + markX + ' 0'} /> : ''; <div width={width} height={height}>
<svg style={{ width: '100%', height: outerHeight }}>
return ( <g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
<Measure width='100%' whitelist={['width', 'top']} onMeasure={ (dimensions) => { this.setState({ dimensions }); }}> <g>{xmark}</g>
<div width={width} height={height}> <g>{lines}</g>
<svg style={{ width: '100%', height: outerHeight }}> <g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}>
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}> <text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}>
<g>{xmark}</g> <tspan>{xLabel}</tspan>
<g>{lines}</g> <tspan className='metric'> ({xUnit})</tspan>
<g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> </text>
<text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}> </g>
<tspan>{xLabel}</tspan> <g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}>
<tspan className='metric'> ({xUnit})</tspan> <text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}>
</text> <tspan>{yLabel}</tspan>
</g> { yUnit && <tspan className='metric'> ({yUnit})</tspan> }
<g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}> </text>
<text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}> </g>
<tspan>{yLabel}</tspan> <g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}>
{ yUnit && <tspan className='metric'> ({yUnit})</tspan> } <rect className='tooltip' height={tipHeight + 'em'}></rect>
</text> {detailElems}
</g> </g>
<g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}> <g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>
<rect className='tooltip' height={tipHeight + 'em'}></rect> {markerElems}
{detailElems} </g>
</g> <rect
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}> fillOpacity='0'
{markerElems} height={innerHeight}
</g> width={innerWidth + 1}
<rect onMouseEnter={this._showTip}
fillOpacity='0' onTouchStart={this._showTip}
height={innerHeight} onMouseLeave={this._hideTip}
width={innerWidth + 1} onTouchEnd={this._hideTip}
onMouseEnter={this._showTip} onMouseMove={e => this._moveTip(e, width)}
onTouchStart={this._showTip} onTouchMove={e => this._moveTip(e, width)}
onMouseLeave={this._hideTip} />
onTouchEnd={this._hideTip} </g>
onMouseMove={this._moveTip} </svg>
onTouchMove={this._moveTip} </div>
/> );
</g> }}
</svg> </ContainerDimensions>
</div> );
</Measure> }
); }
}
}

View File

@@ -1,97 +1,92 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Measure from 'react-measure'; import ContainerDimensions from 'react-container-dimensions';
import * as d3 from 'd3'; import * as d3 from 'd3';
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
const LABEL_COLOUR = '#000000'; const LABEL_COLOUR = '#000000';
/** /**
* A pie chart * A pie chart
*/ */
export default class PieChart extends Component { export default class PieChart extends Component {
static propTypes = { static propTypes = {
data : PropTypes.array.isRequired data : PropTypes.array.isRequired
}; };
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
* @param {Object} context React Component context * @param {Object} context React Component context
*/ */
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this.pie = d3.pie().value((d) => d.value); this.pie = d3.pie().value((d) => d.value);
this.colors = CORIOLIS_COLOURS; this.colors = CORIOLIS_COLOURS;
this.arc = d3.arc(); this.arc = d3.arc();
this.arc.innerRadius(0); this.arc.innerRadius(0);
}
this.state = {
dimensions: {
width: 100, /**
height: 100 * Generate a slice of the pie chart
} * @param {Object} d the data for this slice
}; * @param {number} i the index of this slice
} * @param {number} width the current width of the parent container
* @returns {Object} the SVG for the slice
*/
/** sliceGenerator(d, i, width) {
* Generate a slice of the pie chart if (!d || d.value == 0) {
* @param {Object} d the data for this slice // Ignore 0 values
* @param {number} i the index of this slice return null;
* @returns {Object} the SVG for the slice }
*/
sliceGenerator(d, i) { const { data } = this.props;
if (!d || d.value == 0) {
// Ignore 0 values // Push the labels further out from the centre of the slice
return null; let [labelX, labelY] = this.arc.centroid(d);
} const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`;
const { width, height } = this.state.dimensions; // Put the keys in a line with equal spacing
const { data } = this.props; const nonZeroItems = data.filter(d => d.value != 0).length;
const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1;
// Push the labels further out from the centre of the slice const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5);
let [labelX, labelY] = this.arc.centroid(d); const keyTranslate = `translate(${keyX}, ${width * 0.45})`;
const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`;
return (
// Put the keys in a line with equal spacing <g key={`group-${i}`}>
const nonZeroItems = data.filter(d => d.value != 0).length; <path key={`arc-${i}`} d={this.arc(d)} style={{ fill: this.colors[i] }} />
const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1; <text key={`label-${i}`} transform={labelTranslate} style={{ strokeWidth: '0px', fill: LABEL_COLOUR }} textAnchor='middle'>{d.value}</text>
const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5); <text key={`key-${i}`} transform={keyTranslate} style={{ strokeWidth:'0px', fill: this.colors[i] }} textAnchor='middle'>{d.data.label}</text>
const keyTranslate = `translate(${keyX}, ${width * 0.45})`; </g>
);
return ( }
<g key={`group-${i}`}>
<path key={`arc-${i}`} d={this.arc(d)} style={{ fill: this.colors[i] }} /> /**
<text key={`label-${i}`} transform={labelTranslate} style={{ strokeWidth: '0px', fill: LABEL_COLOUR }} textAnchor='middle'>{d.value}</text> * Render the component
<text key={`key-${i}`} transform={keyTranslate} style={{ strokeWidth:'0px', fill: this.colors[i] }} textAnchor='middle'>{d.data.label}</text> * @returns {object} Markup
</g> */
); render() {
} return (
<ContainerDimensions>
/** { ({ width }) => {
* Render the component const pie = this.pie(this.props.data),
* @returns {object} Markup translate = `translate(${width / 2}, ${width * 0.4})`;
*/
render() { this.arc.outerRadius(width * 0.4);
const { width, height } = this.state.dimensions; return (
const pie = this.pie(this.props.data), <div width={width} height={width}>
translate = `translate(${width / 2}, ${width * 0.4})`; <svg style={{ stroke: 'None' }} width={width} height={width * 0.9}>
<g transform={translate}>
this.arc.outerRadius(width * 0.4); {pie.map((d, i) => this.sliceGenerator(d, i, width))}
</g>
return ( </svg>
<Measure width='100%' whitelist={['width', 'top']} onMeasure={ (dimensions) => { this.setState({ dimensions }); }}> </div>
<div width={width} height={width}> );
<svg style={{ stroke: 'None' }} width={width} height={width * 0.9}> }}
<g transform={translate}> </ContainerDimensions>
{pie.map((d, i) => this.sliceGenerator(d, i))} );
</g> }
</svg> }
</div>
</Measure>
);
}
}

View File

@@ -1,111 +1,105 @@
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import Measure from 'react-measure'; import ContainerDimensions from 'react-container-dimensions';
import { BarChart, Bar, XAxis, YAxis } from 'recharts'; import { BarChart, Bar, XAxis, YAxis } from 'recharts';
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
const LABEL_COLOUR = '#000000'; const LABEL_COLOUR = '#000000';
const AXIS_COLOUR = '#C06400'; const AXIS_COLOUR = '#C06400';
const ASPECT = 1; const ASPECT = 1;
const merge = function(one, two) { const merge = function(one, two) {
return Object.assign({}, one, two); return Object.assign({}, one, two);
}; };
/** /**
* A vertical bar chart * A vertical bar chart
*/ */
export default class VerticalBarChart extends TranslatedComponent { export default class VerticalBarChart extends TranslatedComponent {
static propTypes = { static propTypes = {
data : PropTypes.array.isRequired, data : PropTypes.array.isRequired,
yMax : PropTypes.number yMax : PropTypes.number
}; };
/** /**
* Constructor * Constructor
* @param {Object} props React Component properties * @param {Object} props React Component properties
* @param {Object} context React Component context * @param {Object} context React Component context
*/ */
constructor(props, context) { constructor(props, context) {
super(props); super(props);
this._termtip = this._termtip.bind(this); this._termtip = this._termtip.bind(this);
}
this.state = {
dimensions: { /**
width: 300, * Render the bar chart
height: 300 * @returns {Object} the markup
} */
}; render() {
} const { tooltip, termtip } = this.context;
/** // Calculate maximum for Y
* Render the bar chart let dataMax = Math.max(...this.props.data.map(d => d.value));
* @returns {Object} the markup if (dataMax == -Infinity) dataMax = 0;
*/ let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0;
render() { const localMax = Math.max(dataMax, yMax);
const { width, height } = this.state.dimensions;
const { tooltip, termtip } = this.context; return (
<ContainerDimensions>
// Calculate maximum for Y { ({ width }) => (
let dataMax = Math.max(...this.props.data.map(d => d.value)); <div width='100%'>
if (dataMax == -Infinity) dataMax = 0; <BarChart width={width} height={width * ASPECT} data={this.props.data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0; <XAxis interval={0} fontSize='0.8em' stroke={AXIS_COLOUR} dataKey='label' />
const localMax = Math.max(dataMax, yMax); <YAxis interval={'preserveStart'} tickCount={11} fontSize='0.8em' stroke={AXIS_COLOUR} type='number' domain={[0, localMax]}/>
<Bar dataKey='value' label={<ValueLabel />} fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}/>
return ( </BarChart>
<Measure whitelist={['width', 'top']} onMeasure={ (dimensions) => this.setState({ dimensions }) }> </div>
<div width='100%'> )}
<BarChart width={width} height={width * ASPECT} data={this.props.data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}> </ContainerDimensions>
<XAxis interval={0} fontSize='0.8em' stroke={AXIS_COLOUR} dataKey='label' /> );
<YAxis interval={'preserveStart'} tickCount={11} fontSize='0.8em' stroke={AXIS_COLOUR} type='number' domain={[0, localMax]}/> }
<Bar dataKey='value' label={<ValueLabel />} fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}/>
</BarChart> /**
</div> * Generate a term tip
</Measure> * @param {Object} d the data
); * @param {number} i the index
} * @param {Object} e the event
* @returns {Object} termtip markup
/** */
* Generate a term tip _termtip(d, i, e) {
* @param {Object} d the data if (this.props.data[i].tooltip) {
* @param {number} i the index return this.context.termtip(this.props.data[i].tooltip, e);
* @param {Object} e the event } else {
* @returns {Object} termtip markup return null;
*/ }
_termtip(d, i, e) { }
if (this.props.data[i].tooltip) { }
return this.context.termtip(this.props.data[i].tooltip, e);
} else { /**
return null; * A label that displays the value within the bar of the chart
} */
} class ValueLabel extends React.Component {
} static propTypes = {
x: PropTypes.number,
/** y: PropTypes.number,
* A label that displays the value within the bar of the chart payload: PropTypes.object,
*/ value: PropTypes.number
class ValueLabel extends React.Component { };
static propTypes = {
x: PropTypes.number, /**
y: PropTypes.number, * Render offence
payload: PropTypes.object, * @return {React.Component} contents
value: PropTypes.number */
}; render() {
const { x, y, payload, value } = this.props;
/**
* Render offence const em = value < 1000 ? '1em' : value < 1000 ? '0.8em' : '0.7em';
* @return {React.Component} contents
*/ return (
render() { <text x={x} y={y} fill="#000000" textAnchor="middle" dy={20} style={{ fontSize: em }}>{value}</text>
const { x, y, payload, value } = this.props; );
}
const em = value < 1000 ? '1em' : value < 1000 ? '0.8em' : '0.7em'; };
return (
<text x={x} y={y} fill="#000000" textAnchor="middle" dy={20} style={{ fontSize: em }}>{value}</text>
);
}
};