var graphs = graphs || {}; graphs.chart = function module(){ var margin = {top: 20, right: 20, bottom: 40, left: 20}, width = 960, height = 500, data, ease = 'quad-out', dateLabel = 'date', valueLabel = 'value', chartW, chartH, xScale, yScale, xAxis, brush, chartBrush, onBrush = null, gradientColorSchema = { left: '#39C7EA', right: '#4CDCBA' }, defaultTimeFormat = '%m/%d/%Y', xTickMonthFormat = d3.time.format('%b'), svg; /** * This function creates the graph using the selection and data provided * * @param {D3Selection} _selection A d3 selection that represents * the container(s) where the chart(s) will be rendered * @param {Object} _data The data to attach and generate the chart */ function exports(_selection) { _selection.each(function(_data){ chartW = width - margin.left - margin.right; chartH = height - margin.top - margin.bottom; data = cleanData(cloneData(_data)); buildScales(); buildAxis(); buildSVG(this); buildGradient(); buildBrush(); drawArea(); drawAxis(); drawBrush(); // This last step is optional, just needed when // a given selection would need to be shown setBrush(0, 0.5); }); } /** * Creates the d3 x and y axis, setting orientations */ function buildAxis() { xAxis = d3.svg.axis() .scale(xScale) .orient('bottom') .tickFormat(xTickMonthFormat); } /** * Creates the brush element and attaches a listener * @return {void} */ function buildBrush() { brush = d3.svg.brush() .x(xScale) .on('brush', handleBrush); } /** * Builds containers for the chart, the axis and a wrapper for all of them * NOTE: The order of drawing of this group elements is really important, * as everything else will be drawn on top of them * @private */ function buildContainerGroups() { var container = svg.append('g') .classed('container-group', true) .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); container .append('g') .classed('chart-group', true); container .append('g') .classed('metadata-group', true); container .append('g') .classed('x-axis-group', true); container .append('g') .classed('brush-group', true); } /** * Creates the gradient on the area * @return {void} */ function buildGradient() { let metadataGroup = svg.select('.metadata-group'); metadataGroup.append('linearGradient') .attr('id', 'brush-area-gradient') .attr('gradientUnits', 'userSpaceOnUse') .attr('x1', 0) .attr('x2', xScale(data[data.length - 1].date)) .attr('y1', 0) .attr('y2', 0) .selectAll('stop') .data([ {offset: '0%', color: gradientColorSchema.left}, {offset: '100%', color: gradientColorSchema.right} ]) .enter().append('stop') .attr('offset', ({offset}) => offset) .attr('stop-color', ({color}) => color); } /** * Creates the x and y scales of the graph * @private */ function buildScales() { xScale = d3.time.scale() .domain(d3.extent(data, function(d) { return d.date; } )) .range([0, chartW]); yScale = d3.scale.linear() .domain([0, d3.max(data, function(d) { return d.value; })]) .range([chartH, 0]); } /** * Builds the SVG element that will contain the chart * * @param {HTMLElement} container DOM element that will work as the container of the graph */ function buildSVG(container) { if (!svg) { svg = d3.select(container) .append('svg') .classed('chart brush-chart', true); buildContainerGroups(); } svg .transition() .ease(ease) .attr({ width: width, height: height }); } /** * Cleaning data adding the proper format * * @param {array} data Data */ function cleanData(data) { var parseDate = d3.time.format(defaultTimeFormat).parse; return data.map(function (d) { d.date = parseDate(d[dateLabel]); d.value = +d[valueLabel]; return d; }); } /** * Clones the passed array of data * @param {Object[]} dataToClone Data to clone * @return {Object[]} Cloned data */ function cloneData(dataToClone) { return JSON.parse(JSON.stringify(dataToClone)); } /** * Draws the x axis on the svg object within its group */ function drawAxis() { svg.select('.x-axis-group') .append('g') .attr('class', 'x axis') .attr('transform', 'translate(0,' + chartH + ')') .call(xAxis); } /** * Draws the area that is going to represent the data * * @return {void} */ function drawArea() { // Create and configure the area generator var area = d3.svg.area() .x(function(d) { return xScale(d.date); }) .y0(chartH) .y1(function(d) { return yScale(d.value); }) .interpolate('basis'); // Create the area path svg.select('.chart-group') .append('path') .datum(data) .attr('class', 'brush-area') .attr('d', area); } /** * Draws the Brush components on its group * @return {void} */ function drawBrush() { chartBrush = svg.select('.brush-group') .call(brush); // Update the height of the brushing rectangle chartBrush.selectAll('rect') .classed('brush-rect', true) .attr('height', chartH); } /** * When a brush event happens, we can extract info from the extension * of the brush. * * @return {void} */ function handleBrush() { var brushExtent = d3.event.target.extent(); if (typeof onBrush === 'function') { onBrush.call(null, brushExtent); } } /** * Sets a new brush extent within the passed percentage positions * @param {Number} a Percentage of data that the brush start with * @param {Number} b Percentage of data that the brush ends with */ function setBrush(a, b) { var transitionDuration = 500, transitionDelay = 1000, x0 = xScale.invert(a * chartW), x1 = xScale.invert(b * chartW); brush.extent([x0, x1]); // now draw the brush to match our extent brush(d3.select('.brush-group').transition().duration(transitionDuration)); // now fire the brushstart, brushmove, and brushend events // set transition the delay and duration to 0 to draw right away brush.event(d3.select('.brush-group').transition().delay(transitionDelay).duration(transitionDuration)); } exports.margin = function(_x) { if (!arguments.length) return margin; margin = _x; return this; }; exports.width = function(_x) { if (!arguments.length) return width; width = _x; return this; }; exports.height = function(_x) { if (!arguments.length) return height; height = _x; return this; }; exports.onBrush = function(_x) { if (!arguments.length) return onBrush; onBrush = _x; return this; }; return exports; };