'use strict'; import React from 'react'; import d3 from 'd3'; import _ from 'lodash'; // import Popover from '../../utils/popover'; var LineChart = React.createClass({ displayName: 'LineChart', propTypes: { className: React.PropTypes.string, data: React.PropTypes.array, axisLineVal: React.PropTypes.number, axisLineMax: React.PropTypes.number, axisLineMin: React.PropTypes.number, dataUnitSuffix: React.PropTypes.string }, chart: null, onWindowResize: function () { this.chart.checkSize(); }, componentDidMount: function () { // Debounce event. this.onWindowResize = _.debounce(this.onWindowResize, 200); window.addEventListener('resize', this.onWindowResize); this.chart = Chart(); d3.select(this.refs.container).call(this.chart .data(this.props.data) .axisLineVal(this.props.axisLineVal) .axisValueMax(this.props.axisLineMax) .axisValueMin(this.props.axisLineMin) .dataUnitSuffix(this.props.dataUnitSuffix)); }, componentWillUnmount: function () { window.removeEventListener('resize', this.onWindowResize); this.chart.destroy(); }, componentDidUpdate: function (prevProps) { this.chart.pauseUpdate(); if (prevProps.data !== this.props.data) { this.chart.data(this.props.data); } if (prevProps.axisLineVal !== this.props.axisLineVal) { this.chart.axisLineVal(this.props.axisLineVal); } if (prevProps.axisLineMax !== this.props.axisLineMax) { this.chart.axisValueMax(this.props.axisLineMax); } if (prevProps.axisLineMin !== this.props.axisLineMin) { this.chart.axisValueMin(this.props.axisLineMin); } if (prevProps.dataUnitSuffix !== this.props.dataUnitSuffix) { this.chart.dataUnitSuffix(this.props.dataUnitSuffix); } this.chart.continueUpdate(); }, render: function () { return (
); } }); module.exports = LineChart; var Chart = function (options) { // Data related variables for which we have getters and setters. var _data = null; var _axisLineVal, _axisValueMin, _axisValueMax, _dataUnitSuffix; // Pause var _pauseUpdate = false; // Containters var $el, $svg; // Var declaration. var margin = {top: 16, right: 32, bottom: 32, left: 24}; var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; // width and height refer to the data canvas. To know the svg size the margins // must be added. var _width, _height; // Draw functions. var line; // Update functions. var updateData, upateSize; // X scale. Range updated in function. var x = d3.time.scale(); // Y scale. Range updated in function. var y = d3.scale.linear(); // Used for the zoom translate bounds. var minX; // Zoom var zoom = d3.behavior .zoom() .scaleExtent([1, 1]); // Line function for the delimit the area. line = d3.svg.line() .x(d => x(d.timestep)) .y(d => y(d.value)); // Define xAxis function. var xAxis = d3.svg.axis() .scale(x) .orient('bottom') .tickSize(0) .tickFormat(d3.time.format('%H:%M')); // .ticks(3); function _calcSize () { _width = parseInt($el.style('width'), 10) - margin.left - margin.right; _height = parseInt($el.style('height'), 10) - margin.top - margin.bottom; } function chartFn (selection) { $el = selection; var layers = { line: function () { // lines. let the_line = $dataCanvas.selectAll('.data-line') .data([_data]); // Handle new. the_line.enter() .append('path') .attr('clip-path', 'url(#clip)'); // Update current. the_line .attr('d', d => line(d)) .attr('class', d => `data-line`); // Remove old. the_line.exit() .remove(); }, minMax: function () { let [sDate, eDate] = x.domain(); let f = (o) => { let timestamp = o.timestep.getTime(); return timestamp >= sDate.getTime() && timestamp <= eDate.getTime(); }; // Min Max. let sorted = _(_data).filter(f).sortBy('value').value(); if (!sorted.length) { return; } let min = sorted[0]; let max = _.last(sorted); let edgeG = $dataCanvas.selectAll('.edges') .data([0]) .enter().append('g') .attr('class', 'edges'); edgeG.append('text') .attr('text-anchor', 'middle') .attr('dy', '-0.25em') .attr('class', 'edge edge-max'); edgeG.append('text') .attr('text-anchor', 'middle') .attr('dy', '1em') .attr('class', 'edge edge-min'); $dataCanvas.select('.edge.edge-max') .datum(max) .attr('x', d => x(d.timestep)) .attr('y', d => y(d.value)) .text(d => d.value + _dataUnitSuffix); $dataCanvas.select('.edge.edge-min') .datum(min) .attr('x', d => x(d.timestep)) .attr('y', d => y(d.value)) .text(d => d.value + _dataUnitSuffix); }, xAxis: function () { // Append Axis. // X axis. let xAx = $svg.selectAll('.x.axis') .data([0]); xAx.enter().append('g') .attr('class', 'x axis') .append('text') .attr('class', 'label') .attr('text-anchor', 'start'); xAx .attr('transform', `translate(${margin.left},${_height + margin.top + 16})`) .call(xAxis); }, yAxis: function () { // Append Axis. // Y axis let yAx = $svg.selectAll('.y.axis') .data([0]); let yAxEnter = yAx.enter().append('g') .attr('class', 'y axis'); yAxEnter.append('text') .attr('class', 'label') .attr('text-anchor', 'end'); yAxEnter.append('line') .attr('class', 'line'); yAx.select('.label') .attr('y', y(_axisLineVal) + margin.top) .attr('x', _width + margin.left + margin.right) .attr('dy', '1em') .text(_axisLineVal + _dataUnitSuffix); yAx.select('.line') .attr('x1', 0) .attr('y1', y(_axisLineVal) + margin.top) .attr('x2', _width + margin.left + margin.right) .attr('y2', y(_axisLineVal) + margin.top); }, days: function () { // Compute days for the days steps. let dateCopyDay = d => { let n = new Date(d.getTime()); n.setHours(0); n.setMinutes(0); n.setSeconds(0); n.setMilliseconds(0); return n; }; let eDay = dateCopyDay(_.last(_data).timestep); let dt = dateCopyDay(_data[0].timestep); let daySteps = [dt]; while (true) { dt = d3.time.day.offset(dateCopyDay(dt), 1); daySteps.push(dt); if (dt.getTime() >= eDay.getTime()) { break; } } let selection = $dataCanvas.selectAll('.days') .data([0]); selection.enter().append('g') .attr('class', 'days'); let $days = selection.selectAll('.day-tick') .data(daySteps); $days.enter() .append('text') .attr('text-anchor', 'middle') .attr('class', 'day-tick'); $days .attr('x', d => x(d)) .attr('y', _height + margin.top) .text(d => `${d.getDate()} ${months[d.getMonth()]}`); } }; upateSize = function () { $svg .attr('width', _width + margin.left + margin.right) .attr('height', _height + margin.top + margin.bottom); $dataCanvas .attr('width', _width) .attr('height', _height); $svg.select('#clip rect') // Since this is going to be applied inside the dataCanvas // we have to compensate for the translate. .attr('x', -margin.left) .attr('y', -margin.top) // Add some top and bottom space to avoid clipping the path. .attr('width', _width + margin.left) .attr('height', _height + margin.top + margin.bottom); // DEBUG: // To view the area taken by the #clip rect. // $dataCanvas.select('.data-canvas-shadow') // .attr('x', -margin.left) // .attr('y', -margin.top) // .attr('width', _width + margin.left) // .attr('height', _height + margin.top + margin.bottom); // Update scale ranges. x.range([0, _width]); y.range([_height, 0]); // Recalculate the minX and zoom since scale changed. minX = x(_data[0].timestep); zoom.x(x); // Redraw. layers.line(); layers.minMax(); layers.days(); layers.xAxis(); layers.yAxis(); }; updateData = function () { if (!_data || _pauseUpdate) { return; } // Update scale domains. let eDate = _.last(_data).timestep; let sDate = d3.time.day.offset(eDate, -1); x.domain([sDate, eDate]); // Since the data is stacked the last element will contain the // highest values) y.domain([_axisValueMin, _axisValueMax]); // Recalculate the minX and zoom since scale changed. minX = x(_data[0].timestep); zoom.x(x); // Redraw. layers.line(); layers.minMax(); layers.days(); layers.xAxis(); layers.yAxis(); }; // ----------------------------------------------------------------- // INIT. $svg = $el.append('svg') .attr('class', 'chart') .style('display', 'block'); // Datacanvas var $dataCanvas = $svg.append('g') .attr('class', 'data-canvas') .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); $svg.append('defs') .append('clipPath') .attr('id', 'clip') .append('rect'); // DEBUG: // To view the area taken by the #clip rect. // $dataCanvas.append('rect') // .attr('class', 'data-canvas-shadow') // .style('fill', '#000') // .style('opacity', 0.16); $svg .attr('cursor', 'move') .call(zoom) .on('mousewheel.zoom', null) .on('DOMMouseScroll.zoom', null); zoom.on('zoom', function () { // Bound translate. let [tx, ty] = zoom.translate(); tx = Math.max(tx, 0); tx = Math.min(tx, Math.abs(minX) - margin.right); tx = Math.round(tx); zoom.translate([tx, ty]); layers.line(); layers.minMax(); layers.days(); layers.xAxis(); }); _calcSize(); upateSize(); updateData(); } chartFn.checkSize = function () { _calcSize(); upateSize(); return chartFn; }; chartFn.destroy = function () { // Cleanup. }; // -------------------------------------------- // Getters and setters. chartFn.data = function (d) { if (!arguments.length) return _data; _data = _.cloneDeep(d); if (typeof updateData === 'function') updateData(); return chartFn; }; chartFn.axisLineVal = function (d) { if (!arguments.length) return _axisLineVal; _axisLineVal = d; if (typeof updateData === 'function') updateData(); return chartFn; }; chartFn.axisValueMin = function (d) { if (!arguments.length) return _axisValueMin; _axisValueMin = d; if (typeof updateData === 'function') updateData(); return chartFn; }; chartFn.axisValueMax = function (d) { if (!arguments.length) return _axisValueMax; _axisValueMax = d; if (typeof updateData === 'function') updateData(); return chartFn; }; chartFn.dataUnitSuffix = function (d) { if (!arguments.length) return _dataUnitSuffix; _dataUnitSuffix = d; if (typeof updateData === 'function') updateData(); return chartFn; }; chartFn.pauseUpdate = function () { _pauseUpdate = true; return chartFn; }; chartFn.continueUpdate = function () { _pauseUpdate = false; if (typeof updateData === 'function') updateData(); return chartFn; }; return chartFn; };