var React = require('react'), d3 = require('d3'); import {merge} from 'lodash'; var bisector = d3.bisector(function(d) { return d.date; }).left; var oneDay = 86400000; function _translate(x, y) { return 'translate(' + x + ',' + y + ')'; } export default React.createClass({ displayName: 'Chart', getInitialState() { return {ready: false}; }, getDefaultProps() { return { defaultOptions: { legend: {}, axis: { x: { label: 'X axis' }, y: { label: 'Y axis' } }, margin: { top: 20, right: 145, bottom: 30, left: 50 } } }; }, el: null, svg: null, componentDidMount() { // should be better way to do this this.el = this.getDOMNode(); this.svg = {}; this._calculateInitialState(this.props); }, componentWillReceiveProps(nextProps) { this._calculateInitialState(nextProps); }, componentDidUpdate() { if (this.state.ready === false) { return; } if (typeof this.svg.svg === 'undefined') { this._create(); } this._update(); return; }, componentWillUnmount() { this.el = null; this.svg = null; }, render() { return (
); }, _getHeight(top, bottom) { var height; if (this.el.offsetHeight) { height = this.el.offsetHeight; } else if (this.el.style.pixelHeight) { height = this.el.style.pixelHeight; } height = window.parseFloat(height) - top - bottom; return height; }, _getWidth(left, right) { var width; if (this.el.offsetWidth) { width = this.el.offsetWidth; } else if (this.el.style.pixelWidth) { width = this.el.style.pixelWidth; } width = window.parseFloat(width) - left - right; return width; }, _create() { var svg = d3.select(this.el) .append('svg') .attr('class', 'd3-line-chart') .attr('width', this.state.fullWidth) .attr('height', this.state.fullHeight) .append('g') .attr('class', 'd3-line-chart__inner') .attr('transform', _translate(this.state.margin.left, this.state.margin.top)); this.svg.svg = svg; this.svg.axis = svg.append('g') .attr('class', 'd3-axis'); this.svg.axis.append('g') .attr('class', 'd3-axis__x'); this.svg.axis.append('g') .attr('class', 'd3-axis__y'); this.svg.legend = svg.append('g') .attr('class', 'd3-legend'); this.svg.graph = svg.append('g') .attr('class', 'd3-graph'); this.svg.focus = this.svg.graph.append('g') .attr('class', 'd3-graph__focus'); }, _update() { this._drawAxisX(); this._drawAxisY(); this._drawLegend(); this._drawLine(); this._drawFocus(); }, _calculateInitialState(props) { var options = merge({}, props.options, props.defaultOptions); var width = this._getWidth(options.margin.left, options.margin.right), height = this._getHeight(options.margin.top, options.margin.bottom), fullWidth = ( width + options.margin.left + options.margin.right ), fullHeight = ( height + options.margin.top + options.margin.bottom ); var color = d3.scale.category10(); color.domain(d3.keys(options.legend) .filter(function(key) { return key !== 'date'; })); var data = color.domain().map(function(name) { return { name: name, values: props.data.map(function(d) { return {date: d.date, value: +d[name]}; }) }; }); var xExtent = d3.extent(props.data, function(d) { return d.date; }); var currentDate = (new Date()).getTime(); var xMin = xExtent[0] || new Date(currentDate - oneDay), xMax = xExtent[1] || new Date(currentDate); var yMin = d3.min(data, function(c) { return d3.min(c.values, function(v) { return v.value; }); }) || 0; if (yMin < 1) { yMin = 0; } var yMax = d3.max(data, function(c) { return d3.max(c.values, function(v) { return v.value; }); }) || 4; yMax += 1; if (yMax < 5) { yMax = 5; } var x = this._getScaleX('time') .range([0, width]) .domain([xMin, xMax]), y = this._getScaleX('linear') .range([height, 0]) .domain([yMin, yMax]), legendScale = d3.scale.ordinal() .domain([]) .range([0, 20, 40, 60, 80, 100, 120, 140]), scales = {x: x, y: y, yMax: yMax, legend: legendScale}; var line = this._getLine(scales); this.setState({ ready: true, legend: options.legend, axis: options.axis, margin: options.margin, width: width, height: height, fullWidth: fullWidth, fullHeight: fullHeight, color: color, scales: scales, lineStencil: line, data: data }); }, _getScaleX(type) { if (type === 'linear') { return d3.scale.linear(); } else if (type === 'time') { return d3.time.scale(); } return null; }, _getLine(scales) { return d3.svg.line() .x(function(d) { return scales.x(d.date); }) .y(function(d) { return scales.y(d.value); }); }, _drawAxisX() { var xAxis = d3.svg.axis() .scale(this.state.scales.x) .orient('bottom'); this.svg.axis.select('.d3-axis__x') .attr('transform', _translate(0, this.state.height)) .call(xAxis); }, _drawAxisY() { var yAxis = d3.svg.axis() .scale(this.state.scales.y) .orient('left') .ticks(this.state.scales.yMax > 10 ? 10 : this.state.scales.yMax) .tickFormat(d3.format('d')) .tickSubdivide(0); var axis = this.svg.axis.select('.d3-axis__y') .call(yAxis); if (typeof this.state.yAxisLabel === 'string') { this._drawAxisLabel(axis, this.state.yAxisLabel, true); } }, _drawAxisLabel(axis, text, rotate) { var label = axis.append('text'); if (typeof rotate === 'boolean' && rotate === true) { label.attr('transform', 'rotate(-90)') .attr('y', 0 - this.state.margin.left) .attr('x', 0 - (this.state.height / 2)) .attr('dy', '1em') .style('text-anchor', 'middle') .text(text); } else { label.attr('transform', _translate(this.state.width / 2, this.state.height + this.state.margin.bottom)) .style('text-anchor', 'middle') .text(text); } }, _drawLegend() { var self = this, legend, enter; legend = this.svg.legend .selectAll('.d3-legend__item') .data(d3.keys(this.state.legend)); enter = legend .enter() .append('g') .style('opacity', 0.9) .attr('class', 'd3-legend__item') .attr('data-graph-line-id', function(d) { return 'd3-legend__item--' + d; }) .on('mouseover', function(d) { self._handleLegendMouseover(this, d); }) .on('mouseout', function(d) { self._handleLegendMouseout(this, d); }) .on('click', function(d) { self._handleLegendClick(this, d); }); enter.append('circle') .attr('cx', this.state.width + 30) .attr('cy', function(d, i) { return self.state.scales.legend(i) - 3.1; }) .attr('r', 5.5) .style('fill', function(d) { return self.state.color(d); }); enter.append('text') .attr('x', this.state.width + 40) .attr('y', function(d, i) { return self.state.scales.legend(i); }) .text(function(d) { return self.state.legend[d]; }); }, _handleLegendMouseover(that) { var opacity = parseFloat(that.style.opacity); if (opacity >= 0.9) { d3.select(that) .style('opacity', 1); } else { d3.select(that) .style('opacity', 0.5); } }, _handleLegendMouseout(that) { var opacity = parseFloat(that.style.opacity); if (opacity >= 0.9) { d3.select(that) .style('opacity', 0.9); } else { d3.select(that) .style('opacity', 0.2); } }, _handleLegendClick(that) { var line = document.getElementById(that.getAttribute('data-graph-line-id')); if (line === null) { return; } if (line.style.opacity !== '0') { d3.select(line).transition() .duration(500) .style('opacity', 0); d3.select(that).transition() .duration(500) .style('opacity', 0.2); } else { d3.select(line).transition() .duration(500) .style('opacity', 1); d3.select(that).transition() .duration(500) .style('opacity', 0.9); } }, _drawLine() { var self = this, line; this.svg.graph.selectAll('.d3-graph__line') .remove(); line = this.svg.graph.selectAll('.d3-graph__line') .data(this.state.data) .enter() .append('g') .attr('class', 'd3-graph__line') .attr('id', function(d) { return 'd3-legend__item--' + d.name; }); line.append('path') .attr('class', 'line') .attr('d', function(d) { return self.state.lineStencil(d.values); }) .style('stroke', function(d) { return self.state.color(d.name); }); }, _drawFocus() { var self = this; var circle = this.svg.focus.selectAll('.d3-graph__focus-circle') .data(this.state.data) .enter() .append('g') .attr('class', 'd3-graph__focus-circle') .attr('id', function(d) { return 'd3-graph__focus-circle--' + d.name; }) .style('display', 'none'); circle.append('circle') .attr('r', 4.5); circle.append('text') .attr('x', 9) .attr('dy', '.35em'); this.svg.focus.selectAll('.d3-graph__focus-line') .data([1]) .enter() .append('line') .attr('class', 'd3-graph__focus-line') .attr('x1', 0) .attr('y1', 0) .attr('x2', 0) .attr('y2', this.state.height) .style('display', 'none') .style('stroke', 'gray') .style('stroke-width', 0.5) .style('stroke-dasharray', '5,10'); this.svg.graph.selectAll('.d3-graph__overlay') .data([1]) .enter() .append('rect') .attr('class', 'd3-graph__overlay') .attr('width', this.state.width) .attr('height', this.state.height) .on('mouseover', function() { self.svg.focus.selectAll('.d3-graph__focus-line') .style('display', null); }) .on('mouseout', function() { self.svg.focus.selectAll('.d3-graph__focus-line') .style('display', 'none'); self.svg.focus.selectAll('.d3-graph__focus-circle') .style('display', 'none'); }) .on('mousemove', function() { self._handleFocusMousemove(this); }); }, _handleFocusMousemove(overlay) { var self = this, mouse = d3.mouse(overlay)[0], x0 = this.state.scales.x.invert(mouse); var bisectData = function(data) { var i = bisector(data.values, x0, 1), d0 = data.values[i - 1], d1 = data.values[i], ret = {name: data.name, data: {value: []}}; if (typeof d0 === 'object' || typeof d1 === 'object') { var d = x0 - d0.date > d1.date - x0 ? d1 : d0; ret.data = d; } return ret; }; this.svg.focus.selectAll('.d3-graph__focus-line') .attr('transform', _translate(this.state.scales.x(x0), 0)); this.state.data.map(function(e) { return bisectData(e); }).map(function(d) { var circle = document.getElementById('d3-graph__focus-circle--' + d.name), line = document.getElementById('d3-legend__item--' + d.name); if (d.data.value > 0 && line.style.opacity !== '0') { d3.select(circle) .style('display', null) .attr('transform', _translate(self.state.scales.x(d.data.date), self.state.scales.y(d.data.value))) .select('text').text(d3.format(',.2f')(d.data.value)); } else { d3.select(circle) .style('display', 'none'); } }); } });