UNPKG

nvd3-fork

Version:

FORK! of NVD3, a reusable charting library written in d3.js

468 lines (430 loc) 14.7 kB
'use strict'; nv.models.differenceChart = function () { 'use strict'; var container = void 0; var multiChart = nv.models.multiChart(); var focus = nv.models.focus(nv.models.line()); // const dispatch = d3.dispatch(); // yAccessor for multi chart // Not modifiable by end user. They can // overload yAccessor which is used during the processData step var yForMultiChart = function yForMultiChart(d) { // check if the data is for an area chart // which has y0 and y1 values if (isDefined(d.y0)) { return d.y0; } // otherwise assume it's for a line chart return d.y; }; var xForMultiChart = function xForMultiChart(d) { return d.x; }; var xAccessor = function xAccessor(d) { return d.x; }; var keyForXValue = 'x'; var yAccessor = function yAccessor(d) { return d.y; }; var duration = 300; var keyForActualLessThanPredicted = null; var keyForActualGreaterThanPredicted = null; var height = null; var width = null; var margin = { top: 30, right: 50, bottom: 20, left: 70 }; var focusMargin = { top: 0, right: 0, bottom: 0, left: 0 }; var showPredictedLine = true; var interpolate = 'linear'; var strokeWidth = 1; var xScale = d3.time.scale(); var tickFormat = d3.time.format.multi([['%I:%M', function (d) { return d.getMinutes(); }], ['%I %p', function (d) { return d.getHours(); }], ['%a %d', function (d) { return d.getDay() && d.getDate() != 1; }], ['%b %d', function (d) { return d.getDate() != 1; }], ['%B', function (d) { return d.getMonth(); }], ['%Y', function () { return true; }]]); function chart(selection) { selection.each(function (data) { container = d3.select(this); var dataWithoutDisabledSeries = (data || []).filter(function (dataset) { return !dataset.disabled; }); if (!data || !dataWithoutDisabledSeries.length) { nv.utils.noData(chart, container); return chart; } var processedData = processData(data); var availableHeight = nv.utils.availableHeight(height, container, margin) - focus.height(); var availableWidth = nv.utils.availableWidth(width, container, margin); container.attr('class', 'nv-differenceChart'); nv.utils.initSVG(container); chart.container = this; multiChart.margin(margin).color(d3.scale.category10().range()).y(yForMultiChart).width(width).height(availableHeight).interpolate(interpolate).useInteractiveGuideline(true); multiChart.interactiveLayer.tooltip.valueFormatter(function (value, i, datum) { if (datum.key === keyForActualGreaterThanPredicted || datum.key === keyForActualLessThanPredicted) { var diff = Math.abs(datum.data.y0 - datum.data.y1); if (diff === 0) { return '-'; } return diff; } return value; }); multiChart.stack1.areaY1(function (d) { return multiChart.stack1.scatter.yScale()(d.display.y); }); multiChart.stack1.transformData(function (d) { d.display = { y: d.y1, y0: d.y0 }; }); multiChart.xAxis.scale(xScale); multiChart.xAxis.tickFormat(tickFormat); var allValues = processedData.filter(function (dataset) { return !dataset.disabled; }).map(function (dataset) { return dataset.values; }); var dateExtent = d3.extent(d3.merge(allValues), function (d) { return xForMultiChart(d); }); multiChart.xAxis.domain(dateExtent).range([0, availableWidth]); var yExtent = d3.extent(d3.merge(allValues), function (d) { return yForMultiChart(d); }); multiChart.yDomain1(yExtent); multiChart.yAxis1.tickFormat(d3.format(',.1f')); multiChart.yAxis2.tickFormat(d3.format(',.1f')); focus.width(availableWidth); focus.margin(focusMargin); focus.xScale(xScale.copy()); focus.xAxis.tickFormat(tickFormat); focus.xAxis.rotateLabels(0); container.append('g').attr('class', 'nv-focusWrap').style('display', 'initial').attr('transform', 'translate(' + margin.left + ', ' + (availableHeight + focus.margin().top) + ')').datum(processedData.filter(function (dataset) { return dataset.type === 'line'; })).call(focus); container.datum(processedData).call(multiChart); focus.dispatch.on('onBrush', function (extent) { var filteredData = processedData.map(function (datum) { var leftIndex = -1; var rightIndex = -1; datum.values.some(function (val, index) { if (leftIndex === -1 && val.x >= extent[0]) { leftIndex = index; } if (rightIndex === -1 && val.x >= extent[1]) { rightIndex = index; return true; } return false; }); var filteredValues = datum.values.slice(leftIndex, rightIndex); var iterations = 0; // don't want to end up with an empty dataset as this will // break the viewfinder. while (filteredValues.length < 2 && iterations < 5) { leftIndex -= 1; rightIndex += 1; filteredValues = datum.values.slice(leftIndex, rightIndex); iterations++; } return Object.assign({}, datum, { values: filteredValues }); }); container.datum(filteredData); multiChart.xAxis.domain(extent); multiChart.update(); }); chart.update = function () { container.selectAll('*').remove(); if (duration === 0) { container.call(chart); } else { container.transition().duration(duration).call(chart); } }; return chart; }); } chart.options = nv.utils.optionsFunc.bind(chart); chart._options = Object.create({}, { width: { get: function get() { return width; }, set: function set(_) { width = _; } }, height: { get: function get() { return height; }, set: function set(_) { height = _; } }, strokeWidth: { get: function get() { return strokeWidth; }, set: function set(_) { strokeWidth = _; } }, x: { get: function get() { return xAccessor; }, set: function set(_) { xAccessor = _; } }, keyForXValue: { get: function get() { return keyForXValue; }, set: function set(_) { keyForXValue = _; } }, y: { get: function get() { return yAccessor; }, set: function set(_) { yAccessor = _; } }, xScale: { get: function get() { return xScale; }, set: function set(_) { xScale = _; } }, keyForActualLessThanPredicted: { get: function get() { return keyForActualLessThanPredicted; }, set: function set(_) { keyForActualLessThanPredicted = _; } }, keyForActualGreaterThanPredicted: { get: function get() { return keyForActualGreaterThanPredicted; }, set: function set(_) { keyForActualGreaterThanPredicted = _; } }, showPredictedLine: { get: function get() { return showPredictedLine; }, set: function set(_) { showPredictedLine = _; } }, tickFormat: { get: function get() { return tickFormat; }, set: function set(_) { tickFormat = _; } }, interpolate: { get: function get() { return interpolate; }, set: function set(_) { interpolate = _; } }, focusMargin: { get: function get() { return focusMargin; }, set: function set(_) { focusMargin.top = _.top !== undefined ? _.top : focusMargin.top; focusMargin.right = _.right !== undefined ? _.right : focusMargin.right; focusMargin.bottom = _.bottom !== undefined ? _.bottom : focusMargin.bottom; focusMargin.left = _.left !== undefined ? _.left : focusMargin.left; } }, margin: { get: function get() { return margin; }, set: function set(_) { margin.top = _.top !== undefined ? _.top : margin.top; margin.right = _.right !== undefined ? _.right : margin.right; margin.bottom = _.bottom !== undefined ? _.bottom : margin.bottom; margin.left = _.left !== undefined ? _.left : margin.left; } } }); function processData(data) { var clonedData = data.slice(0); var allProcessed = clonedData.every(function (dataset) { return dataset.processed; }); var actualData = clonedData.filter(function (dataSet) { return dataSet.type === 'actual'; }); var predictedData = clonedData.filter(function (dataSet) { return dataSet.type === 'expected'; }); if (allProcessed) { return clonedData; } else if (!actualData.length || !predictedData.length) { return []; } var defaultKeyForActualLessThanPredicted = predictedData[0].key + ' minus ' + actualData[0].key + ' (Predicted > Actual)'; var defaultKeyForActualGreaterThanPredicted = predictedData[0].key + ' minus ' + actualData[0].key + ' (Predicted < Actual)'; // processedData is mapped as follows: // [0] => Savings (actual under predicted) area // [1] => 'Loss' (actual over predicted) area // [2] => Actual profile // [3] => Predicted profile var processedData = [{ key: keyForActualLessThanPredicted || defaultKeyForActualLessThanPredicted, type: 'area', values: [], yAxis: 1, color: 'rgba(44,160,44,.9)', processed: true, noHighlightSeries: true }, { key: keyForActualGreaterThanPredicted || defaultKeyForActualGreaterThanPredicted, type: 'area', values: [], yAxis: 1, color: 'rgba(234,39,40,.9)', processed: true, noHighlightSeries: true }, { key: actualData[0].key, type: 'line', values: [], yAxis: 1, color: '#666666', processed: true, strokeWidth: strokeWidth }]; if (showPredictedLine) { processedData[3] = { key: predictedData[0].key, type: 'line', values: [], yAxis: 1, color: '#aec7e8', processed: true, strokeWidth: strokeWidth }; } var actualDataAsMap = actualData[0].values.reduce(function (result, datum, idx) { result[xAccessor(datum)] = yAccessor(datum); return result; }, {}); var predictedDataAsMap = predictedData[0].values.reduce(function (result, datum, idx) { result[xAccessor(datum)] = yAccessor(datum); return result; }, {}); Object.keys(actualDataAsMap).forEach(function (stringifiedXValue, idx) { var actualUsage = actualDataAsMap[stringifiedXValue]; var predictedUsage = predictedDataAsMap[stringifiedXValue]; var fakeDatumToGetProperXValue = {}; // NB - stringifiedXValue will not be the correct data type // e.g. you might want to use a number/date. Pass the stringified // version back through xAccessor. fakeDatumToGetProperXValue[keyForXValue] = stringifiedXValue; var correctlyFormattedXValue = xAccessor(fakeDatumToGetProperXValue); var predictedActualDelta = predictedUsage - actualUsage; // The below code generates data for the difference chart. // We have four series: two for the area (processedData[0] and processedData[1]) charts // and two for the line charts ([2] and [3]). The way we achieve difference chart // is that for each datapoint, we calculate whether it represents a 'savings' // (actual less than predicted) or a 'loss' (actual greater than predicted). // The two areas are different colours (e.g. out of the box, a loss is red and a // saving is green). // If it's a loss, then we add an area datapoint in the loss dataset ranging from actual to predicted // (the area represents the magnitude of the loss). // At the same time, for the savings dataset, we make the datapoint equivalent to actual usage so that // a dot renders rather than a proper area. This basically makes the savings area invisible // when there is a loss. // // The opposite occurs when predicted is greater than savings (a saving). if (isNaN(predictedActualDelta)) { // if there is no predicted value for this point, just use actual usage processedData[1].values[idx] = { x: correctlyFormattedXValue, y0: actualUsage, y1: actualUsage }; processedData[0].values[idx] = { x: correctlyFormattedXValue, y0: actualUsage, y1: actualUsage }; } else if (predictedActualDelta < 0) { // actual greater than predicted - this is a loss // add area for loss between actualUsage (y0) and predictedUsage(y1) processedData[1].values[idx] = { x: correctlyFormattedXValue, y0: actualUsage, y1: predictedUsage }; // for the saving data series, render a dot (y0 and y1) at actualUsage - need // this rather than NaN because otherwise if the next datapoint is a saving, // D3 won't be able to link the two areas together processedData[0].values[idx] = { x: correctlyFormattedXValue, y0: actualUsage, y1: actualUsage }; } else { processedData[0].values[idx] = { x: correctlyFormattedXValue, y0: actualUsage, y1: predictedUsage }; processedData[1].values[idx] = { x: correctlyFormattedXValue, y0: actualUsage, y1: actualUsage }; } // Set actual processedData[2].values[idx] = { x: correctlyFormattedXValue, y: actualUsage }; // Set predicted if (showPredictedLine) { processedData[3].values[idx] = { x: correctlyFormattedXValue, y: predictedUsage }; } }); return processedData; } function isDefined(thingToCheck) { // NB: void 0 === undefined return thingToCheck !== void 0; } chart.xAxis = multiChart.xAxis; chart.yAxis = multiChart.yAxis1; chart.multiChart = multiChart; chart.focus = focus; chart.processData = processData; nv.utils.inheritOptions(chart, multiChart); nv.utils.initOptions(chart); return chart; };