import React from 'react'; import PropTypes from 'prop-types'; import * as d3 from 'd3'; import TranslatedComponent from './TranslatedComponent'; const MARGIN = { top: 15, right: 20, bottom: 40, left: 150 }; const BAR_HEIGHT = 30; /** * Get ship and build name * @param {Object} build Ship build * @return {string} name and build name */ function bName(build) { return build.buildName + '\n' + build.name; } /** * Replace a SVG text element's content with * tspans that wrap on newline * @param {string} d Data point */ function insertLinebreaks(d) { let el = d3.select(this); let lines = d.split('\n'); el.text('').attr('y', -6); for (let i = 0; i < lines.length; i++) { let tspan = el.append('tspan').text(lines[i].length > 18 ? lines[i].substring(0, 15) + '...' : lines[i]); if (i > 0) { tspan.attr('x', -9).attr('dy', '1em'); } else { tspan.attr('class', 'primary'); } } } /** * Bar Chart */ export default class BarChart extends TranslatedComponent { static defaultProps = { colors: ['#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c'], labels: null, unit: '' }; static propTypes = { colors: PropTypes.array, data: PropTypes.array.isRequired, desc: PropTypes.bool, format: PropTypes.string.isRequired, labels: PropTypes.array, predicate: PropTypes.string, properties: PropTypes.array, title: PropTypes.string.isRequired, unit: PropTypes.string.isRequired, width: PropTypes.number.isRequired }; /** * Constructor * @param {Object} props React Component properties * @param {Object} context React Component context */ constructor(props, context) { super(props); this._updateDimensions = this._updateDimensions.bind(this); this._hideTip = this._hideTip.bind(this); let scale = d3.scaleLinear(); let y0 = d3.scaleBand(); let y1 = d3.scaleBand(); this.xAxis = d3.axisBottom(scale).ticks(5).tickSizeOuter(0).tickFormat(context.language.formats.s2); this.yAxis = d3.axisLeft(y0).tickSizeOuter(0); this.state = { scale, y0, y1, color: d3.scaleOrdinal().range(props.colors) }; } /** * Generate and Show tooltip * @param {Object} build Ship build * @param {string} property Property to display * @param {number} propertyIndex Property Label index */ _showTip(build, property, propertyIndex) { let { unit, format, labels } = this.props; let { scale, y0, y1 } = this.state; let { translate, formats } = this.context.language; let fontSize = parseFloat(window.getComputedStyle(document.getElementById('coriolis')).getPropertyValue('font-size') || 16); let val = build[property]; let lblStr = labels ? translate(labels[propertyIndex]) + ': ' : ''; let valStr = formats[format](val) + ' ' + unit; let valMidPoint = scale(val) / 2; let y = y0(bName(build)) + y1(property) - fontSize - 5; let width = ((lblStr.length + valStr.length) / 1.8) * fontSize; let midPoint = width / 2; let tooltip = {lblStr} {valStr} ; this.setState({ tooltip }); } /** * Hide tooltip */ _hideTip() { this.setState({ tooltip: null }); } /** * Update dimensions based on properties and scale * @param {Object} props React Component properties * @param {number} scale size ratio / scale */ _updateDimensions(props, scale) { let { width, data, properties } = props; let innerWidth = width - MARGIN.left - MARGIN.right; let barHeight = Math.round(BAR_HEIGHT * scale); let dataSize = data.length; let innerHeight = barHeight * dataSize; let outerHeight = innerHeight + MARGIN.top + MARGIN.bottom; let max = data.reduce((max, build) => (properties.reduce(((m, p) => (m > build[p] ? m : build[p])), max)), 0); this.state.scale.range([0, innerWidth]).domain([0, max]); this.state.y0.domain(data.map(bName)).range([0, innerHeight], 0.3).padding(0.4); this.state.y1.domain(properties).range([0, this.state.y0.bandwidth()]).padding(0.1); this.setState({ barHeight, dataSize, innerWidth, outerHeight, innerHeight }); } /** * Update dimensions based on props and context. */ componentWillMount() { this._updateDimensions(this.props, this.context.sizeRatio); } /** * Update state based on property and context changes * @param {Object} nextProps Incoming/Next properties * @param {Object} nextContext Incoming/Next conext */ componentWillReceiveProps(nextProps, nextContext) { let { data, width, predicate, desc } = nextProps; let props = this.props; if (width != props.width || this.context.sizeRatio != nextContext.sizeRatio || data != props.data) { this._updateDimensions(nextProps, nextContext.sizeRatio); } if (this.context.language != nextContext.language) { this.xAxis.tickFormat(nextContext.language.formats.s2); } if (predicate != props.predicate || desc != props.desc) { this.state.y0.domain(data.map(bName)); } } /** * Render the chart * @return {React.Component} Chart SVG */ render() { if (!this.props.width) { return null; } let { title, unit, width, data, properties } = this.props; let { innerWidth, outerHeight, innerHeight, y0, y1, scale, color, tooltip } = this.state; let bars = data.map((build, i) => { properties.map((p, propIndex) => )} ); return {bars} {tooltip} d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> {title} { unit ? ({unit}) : null } { let e = d3.select(elem); e.call(this.yAxis); e.selectAll('text').each(insertLinebreaks); }} /> ; } }