Merge pull request #2680 from stormpython/fix/#1962

Add Negative Axes to Line, Bar, and Area Charts/Fix Pie Issues with 0 values
This commit is contained in:
Spencer 2015-02-05 14:53:47 -07:00
commit 1d83784ba3
24 changed files with 1055 additions and 234 deletions

View file

@ -237,5 +237,15 @@ define(function (require) {
};
inherits(errors.NoResults, KbnError);
/**
* error thrown when no results are returned from an elasticsearch query
*/
errors.PieContainsAllZeros = function PieContainsAllZeros() {
KbnError.call(this,
'No results displayed because all values equal 0',
errors.PieContainsAllZeros);
};
inherits(errors.PieContainsAllZeros, KbnError);
return errors;
});

View file

@ -21,6 +21,7 @@ define(function (require) {
return new Data(data, attr);
}
var self = this;
var offset;
if (attr.mode === 'stacked') {
@ -53,15 +54,141 @@ define(function (require) {
this._normalizeOrdered();
this._attr = _.defaults(attr || {}, {
// d3 stack function
stack: d3.layout.stack()
.x(function (d) { return d.x; })
.y(function (d) { return d.y; })
.y(function (d) {
if (offset === 'expand') {
return Math.abs(d.y);
}
return d.y;
})
.offset(offset || 'zero')
});
if (attr.mode === 'stacked' && attr.type === 'histogram') {
this._attr.stack.out(function (d, y0, y) {
return self._stackNegAndPosVals(d, y0, y);
});
}
}
/**
* Returns true for positive numbers
*/
Data.prototype._isPositive = function (num) {
return num >= 0;
};
/**
* Returns true for negative numbers
*/
Data.prototype._isNegative = function (num) {
return num < 0;
};
/**
* Adds two input values
*/
Data.prototype._addVals = function (a, b) {
return a + b;
};
/**
* Returns the results of the addition of numbers in a filtered array.
*/
Data.prototype._sumYs = function (arr, callback) {
var filteredArray = arr.filter(callback);
return (filteredArray.length) ? filteredArray.reduce(this._addVals) : 0;
};
/**
* Calculates the d.y0 value for stacked data in D3.
*/
Data.prototype._calcYZero = function (y, arr) {
if (y >= 0) return this._sumYs(arr, this._isPositive);
return this._sumYs(arr, this._isNegative);
};
/**
*
*/
Data.prototype._getCounts = function (i, j) {
var data = this.chartData();
var dataLengths = {};
dataLengths.charts = data.length;
dataLengths.stacks = data[i].series.length;
dataLengths.values = data[i].series[j].values.length;
return dataLengths;
};
/**
*
*/
Data.prototype._createCache = function () {
var cache = {
index: {
chart: 0,
stack: 0,
value: 0
},
yValsArr: []
};
cache.count = this._getCounts(cache.index.chart, cache.index.stack);
return cache;
};
/**
* Stacking function passed to the D3 Stack Layout `.out` API.
* See: https://github.com/mbostock/d3/wiki/Stack-Layout
* It is responsible for calculating the correct d.y0 value for
* mixed datasets containing both positive and negative values.
*/
Data.prototype._stackNegAndPosVals = function (d, y0, y) {
var data = this.chartData();
// Storing counters and data characteristics needed to stack values properly
if (!this._cache) {
this._cache = this._createCache();
}
d.y0 = this._calcYZero(y, this._cache.yValsArr);
++this._cache.index.stack;
// last stack, or last value, reset the stack count and y value array
var lastStack = (this._cache.index.stack >= this._cache.count.stacks);
if (lastStack) {
this._cache.index.stack = 0;
++this._cache.index.value;
this._cache.yValsArr = [];
// still building the stack collection, push v value to array
} else if (y !== 0) {
this._cache.yValsArr.push(y);
}
// last value, prepare for the next chart, if one exists
var lastValue = (this._cache.index.value >= this._cache.count.values);
if (lastValue) {
this._cache.index.value = 0;
++this._cache.index.chart;
// no more charts, reset the queue and finish
if (this._cache.index.chart >= this._cache.count.charts) {
this._cache = this._createCache();
return;
}
// get stack and value count for next chart
this._cache.count.stacks = data[this._cache.index.chart].series.length; // number of stack layers
this._cache.count.values = data[this._cache.index.chart].series[this._cache.index.stack].values.length; // number of values
}
};
Data.prototype.getDataType = function () {
var data = this.getVisData();
var type;
@ -160,15 +287,12 @@ define(function (require) {
* @returns {Array} Value objects
*/
Data.prototype.flatten = function () {
var data = this.chartData();
var series = _.chain(data).pluck('series').pluck().value();
var values = [];
series.forEach(function (d) {
values.push(_.chain(d).flatten().pluck('values').value());
});
return values;
return _(this.chartData())
.pluck('series')
.flatten()
.pluck('values')
.flatten()
.value();
};
/**
@ -176,16 +300,66 @@ define(function (require) {
* TODO: need to make this more generic
*
* @method shouldBeStacked
* @param series {Array} Array of data objects
* @returns {boolean}
*/
Data.prototype.shouldBeStacked = function (series) {
Data.prototype.shouldBeStacked = function () {
var isHistogram = (this._attr.type === 'histogram');
var isArea = (this._attr.type === 'area');
var isOverlapping = (this._attr.mode === 'overlap');
var grouped = (this._attr.mode === 'grouped');
// Series should be an array
return (isHistogram || isArea && !isOverlapping && series.length > 1);
var stackedHisto = isHistogram && !grouped;
var stackedArea = isArea && !isOverlapping;
return stackedHisto || stackedArea;
};
/**
* Validates that the Y axis min value defined by user input
* is a number.
*
* @param val {Number} Y axis min value
* @returns {Number} Y axis min value
*/
Data.prototype.validateUserDefinedYMin = function (val) {
if (!_.isNumber(val)) {
throw new Error('validateUserDefinedYMin expects a number');
}
return val;
};
/**
* Calculates the min y value from this.dataArray
* for each object in the dataArray.
*
* @method getYMinValue
* @returns {Number} Min y axis value
*/
Data.prototype.getYMinValue = function () {
var self = this;
var arr = [];
if (this._attr.mode === 'percentage' || this._attr.mode === 'wiggle' ||
this._attr.mode === 'silhouette') {
return 0;
}
var flat = this.flatten();
// if there is only one data point and its less than zero,
// return 0 as the yMax value.
if (!flat.length || flat.length === 1 && flat[0].y > 0) {
return 0;
}
var min = Infinity;
// for each object in the dataArray,
// push the calculated y value to the initialized array (arr)
_.each(this.chartData(), function (chart) {
min = Math.min(min, self._getYExtent(chart, 'min'));
});
return min;
};
/**
@ -200,22 +374,27 @@ define(function (require) {
Data.prototype.getYMaxValue = function () {
var self = this;
var arr = [];
var grouped = (self._attr.mode === 'grouped');
if (self._attr.mode === 'percentage') {
return 1;
}
var flat = this.flatten();
// if there is only one data point and its less than zero,
// return 0 as the yMax value.
if (!flat.length || flat.length === 1 && flat[0].y < 0) {
return 0;
}
var max = -Infinity;
// for each object in the dataArray,
// push the calculated y value to the initialized array (arr)
_.forEach(this.flatten(), function (series) {
if (self.shouldBeStacked(series) && !grouped) {
return arr.push(self._getYMax(series, self._getYStack));
}
return arr.push(self._getYMax(series, self._getY));
_.each(this.chartData(), function (chart) {
max = Math.max(max, self._getYExtent(chart, 'max'));
});
return _.max(arr);
return max;
};
/**
@ -235,10 +414,21 @@ define(function (require) {
* Returns the max Y axis value for a `series` array based on
* a specified callback function (calculation).
*/
Data.prototype._getYMax = function (series, calculation) {
return d3.max(this.stackData(series), function (data) {
return d3.max(data, calculation);
});
Data.prototype._getYExtent = function (chart, extent) {
var calculation = this._getY;
if (this.shouldBeStacked()) {
this.stackData(_.pluck(chart.series, 'values'));
calculation = this._getYStack;
}
var points = chart.series
.reduce(function (points, series) {
return points.concat(series.values);
}, [])
.map(calculation);
return d3[extent](points);
};
/**

View file

@ -37,6 +37,7 @@ define(function (require) {
}),
yAxis: new YAxis({
el : vis.el,
yMin : data.getYMinValue(),
yMax : data.getYMaxValue(),
_attr: vis._attr
})

View file

@ -15,8 +15,9 @@ define(function (require) {
*/
function YAxis(args) {
this.el = args.el;
this.yMin = args.yMin;
this.yMax = args.yMax;
this._attr = _.defaults(args._attr || {}, {});
this._attr = args._attr || {};
}
_(YAxis.prototype).extend(ErrorHandler.prototype);
@ -39,9 +40,37 @@ define(function (require) {
* @returns {D3.Scale.QuantitiveScale|*} D3 yScale function
*/
YAxis.prototype.getYScale = function (height) {
// yMin and yMax can never be equal for the axis
// to render. Defaults yMin to 0 if yMin === yMax
// and yMin is greater than or equal to zero, else
// defaults yMax to zero.
if (this.yMin === this.yMax) {
if (this.yMin > 0) {
this.yMin = 0;
} else if (this.yMin === 0) {
this.yMin = -1;
this.yMax = 1;
} else {
this.yMax = 0;
}
}
if (!this._attr.defaultYExtents) {
// if yMin and yMax are both positive, then yMin should be zero
if (this.yMin > 0 && this.yMax > 0) {
this.yMin = 0;
}
// if yMin and yMax are both negative, then yMax should be zero
if (this.yMin < 0 && this.yMax < 0) {
this.yMax = 0;
}
}
// save reference to y scale
this.yScale = d3.scale.linear()
.domain([0, this.yMax])
.domain([this.yMin, this.yMax])
.range([height, 0])
.nice(this.tickScale(height));
@ -76,7 +105,7 @@ define(function (require) {
if (isPercentage) {
tickFormat = d3.format('%');
} else if (this.yMax <= 100 && !isPercentage) {
} else if (this.yMax <= 100 && this.yMin >= -100 && !isPercentage) {
tickFormat = d3.format('n');
} else {
tickFormat = this.formatAxisLabel;

View file

@ -25,7 +25,7 @@ define(function (require) {
Vis.Super.apply(this, arguments);
this.el = $el.get ? $el.get(0) : $el;
this.ChartClass = chartTypes[config.type];
this._attr = _.defaults(config || {}, {});
this._attr = _.defaults({}, config || {}, {});
this.eventTypes = {
enabled: []
};
@ -80,6 +80,7 @@ define(function (require) {
// Because we have to wait for the DOM element to initialize, we do not
// want to throw an error when the DOM `el` is zero
if (error instanceof errors.ContainerTooSmall ||
error instanceof errors.PieContainsAllZeros ||
error instanceof errors.NotEnoughData ||
error instanceof errors.NoResults) {
this.handler.error(error.message);

View file

@ -58,7 +58,6 @@ define(function (require) {
var color = this.handler.data.getColorFunc();
var xScale = this.handler.xAxis.xScale;
var yScale = this.handler.yAxis.yScale;
var height = yScale.range()[0];
var defaultOpacity = this._attr.defaultOpacity;
var area = d3.svg.area()
@ -70,14 +69,16 @@ define(function (require) {
})
.y0(function (d) {
if (isOverlapping) {
return height;
return yScale(0);
}
return yScale(d.y0);
})
.y1(function (d) {
if (isOverlapping) {
return yScale(d.y);
}
return yScale(d.y0 + d.y);
});
@ -268,6 +269,8 @@ define(function (require) {
var margin = this._attr.margin;
var elWidth = this._attr.width = $elem.width();
var elHeight = this._attr.height = $elem.height();
var yMin = this.handler.yAxis.yMin;
var yScale = this.handler.yAxis.yScale;
var minWidth = 20;
var minHeight = 20;
var div;
@ -308,6 +311,19 @@ define(function (require) {
// add path
path = self.addPath(svg, layers);
if (yMin < 0 && self._attr.mode !== 'wiggle' && self._attr.mode !== 'silhouette') {
// Draw line at yScale 0 value
svg.append('line')
.attr('class', 'zero-line')
.attr('x1', 0)
.attr('y1', yScale(0))
.attr('x2', width)
.attr('y2', yScale(0))
.style('stroke', '#ddd')
.style('stroke-width', 1);
}
// add circles
circles = self.addCircles(svg, layers);
@ -316,6 +332,7 @@ define(function (require) {
// chart base line
var line = svg.append('line')
.attr('class', 'base-line')
.attr('x1', 0)
.attr('y1', height)
.attr('x2', width)

View file

@ -113,6 +113,9 @@ define(function (require) {
var data = this.chartData;
var xScale = this.handler.xAxis.xScale;
var yScale = this.handler.yAxis.yScale;
var height = yScale.range()[0];
var yMin = this.handler.yAxis.yScale.domain()[0];
var self = this;
var barWidth;
if (data.ordered && data.ordered.date) {
@ -132,9 +135,23 @@ define(function (require) {
return barWidth || xScale.rangeBand();
})
.attr('y', function (d) {
if (d.y < 0) {
return yScale(d.y0);
}
return yScale(d.y0 + d.y);
})
.attr('height', function (d) {
if (d.y < 0) {
return Math.abs(yScale(d.y0 + d.y) - yScale(d.y0));
}
// for split bars or for one series,
// last series will have d.y0 = 0
if (d.y0 === 0 && yMin > 0) {
return yScale(yMin) - yScale(d.y);
}
return yScale(d.y0) - yScale(d.y0 + d.y);
});
@ -151,6 +168,7 @@ define(function (require) {
ColumnChart.prototype.addGroupedBars = function (bars) {
var xScale = this.handler.xAxis.xScale;
var yScale = this.handler.yAxis.yScale;
var yMin = this.handler.yAxis.yMin;
var data = this.chartData;
var n = data.series.length;
var height = yScale.range()[0];
@ -184,9 +202,22 @@ define(function (require) {
return xScale.rangeBand() / n;
})
.attr('y', function (d) {
if (d.y < 0) {
return yScale(0);
}
return yScale(d.y);
})
.attr('height', function (d) {
if (d.y < 0) {
return Math.abs(yScale(0) - yScale(d.y));
}
// if there is a negative yMin value, use yScale(0) instead of height
if (yMin < 0) {
return yScale(0) - yScale(d.y);
}
return height - yScale(d.y);
});
@ -230,6 +261,8 @@ define(function (require) {
var margin = this._attr.margin;
var elWidth = this._attr.width = $elem.width();
var elHeight = this._attr.height = $elem.height();
var yMin = this.handler.yAxis.yMin;
var yScale = this.handler.yAxis.yScale;
var minWidth = 20;
var minHeight = 20;
var div;
@ -265,6 +298,7 @@ define(function (require) {
self.addBarEvents(bars, svg);
var line = svg.append('line')
.attr('class', 'base-line')
.attr('x1', 0)
.attr('y1', height)
.attr('x2', width)
@ -272,6 +306,19 @@ define(function (require) {
.style('stroke', '#ddd')
.style('stroke-width', 1);
if (yMin < 0) {
// Draw line at yScale 0 value
svg.append('line')
.attr('class', 'zero-line')
.attr('x1', 0)
.attr('y1', yScale(0))
.attr('x2', width)
.attr('y2', yScale(0))
.style('stroke', '#ddd')
.style('stroke-width', 1);
}
return svg;
});
};

View file

@ -96,7 +96,7 @@ define(function (require) {
.enter()
.append('circle')
.attr('class', function circleClass(d) {
return self.colorToClass(color(d.label));
return 'circle ' + self.colorToClass(color(d.label));
})
.attr('fill', function (d) {
return color(d.label);
@ -217,8 +217,8 @@ define(function (require) {
var margin = this._attr.margin;
var elWidth = this._attr.width = $elem.width();
var elHeight = this._attr.height = $elem.height();
var xScale = this.handler.xAxis.xScale;
var chartToSmallError = 'The height and/or width of this container is too small for this chart.';
var yMin = this.handler.yAxis.yMin;
var yScale = this.handler.yAxis.yScale;
var minWidth = 20;
var minHeight = 20;
var startLineX = 0;
@ -261,6 +261,19 @@ define(function (require) {
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
if (yMin < 0) {
// Draw line at yScale 0 value
svg.append('line')
.attr('class', 'zero-line')
.attr('x1', 0)
.attr('y1', yScale(0))
.attr('x2', width)
.attr('y2', yScale(0))
.style('stroke', '#ddd')
.style('stroke-width', 1);
}
self.addClipPath(svg, width, height);
lines = self.addLines(svg, data.series);
circles = self.addCircles(svg, layers);
@ -269,6 +282,7 @@ define(function (require) {
var line = svg
.append('line')
.attr('class', 'base-line')
.attr('x1', startLineX)
.attr('y1', height)
.attr('x2', width)
@ -276,6 +290,7 @@ define(function (require) {
.style('stroke', '#ddd')
.style('stroke-width', lineStrokeWidth);
return svg;
});
};

View file

@ -24,8 +24,11 @@ define(function (require) {
}
PieChart.Super.apply(this, arguments);
// Check whether pie chart should be rendered.
this._validatePieData();
this._attr = _.defaults(handler._attr || {}, {
isDonut: handler._attr.isDonut || false,
isDonut: handler._attr.isDonut || false
});
}
@ -61,6 +64,7 @@ define(function (require) {
var partition = d3.layout.partition()
.sort(null)
.value(function (d) {
if (d.size === 0) return;
return Math.abs(d.size);
});
var x = d3.scale.linear()
@ -109,7 +113,6 @@ define(function (require) {
.style('stroke', '#fff')
.style('fill', function (d) {
if (d.depth === 0) { return 'none'; }
return color(format(d, d.name));
});
@ -120,6 +123,29 @@ define(function (require) {
return path;
};
/**
* Checks whether all pie slices have zero values.
* If so, an error is thrown.
*/
PieChart.prototype._validatePieData = function () {
this.chartData.slices = (function withoutZeroSlices(slices) {
if (!slices.children) return slices;
slices = _.clone(slices);
slices.children = slices.children.reduce(function (children, child) {
if (child.size !== 0) {
children.push(withoutZeroSlices(child));
}
return children;
}, []);
return slices;
}(this.chartData.slices));
if (this.chartData.slices.children.length === 0) {
throw new errors.PieContainsAllZeros();
}
};
/**
* Renders d3 visualization
*

View file

@ -11,4 +11,10 @@
Show Legend
</label>
</div>
<div class="vis-option-item">
<label>
<input type="checkbox" ng-model="vis.params.defaultYExtents" ng-checked="vis.params.defaultYExtents">
Scale Y-Axis to Data Bounds
</label>
</div>
</div>

View file

@ -16,7 +16,8 @@ define(function (require) {
shareYAxis: true,
addTooltip: true,
addLegend: true,
mode: 'stacked'
mode: 'stacked',
defaultYExtents: false
},
modes: ['stacked', 'overlap', 'percentage', 'wiggle', 'silhouette'],
editor: require('text!plugins/vis_types/vislib/editors/area.html')

View file

@ -5,5 +5,4 @@
</label>
<select class="form-control" ng-model="vis.params.mode" ng-options="mode for mode in vis.type.params.modes"></select>
</div>
<vislib-basic-options></vislib-basic-options>

View file

@ -14,7 +14,8 @@ define(function (require) {
shareYAxis: true,
addTooltip: true,
addLegend: true,
mode: 'stacked'
mode: 'stacked',
defaultYExtents: false
},
modes: ['stacked', 'percentage', 'grouped'],
editor: require('text!plugins/vis_types/vislib/editors/histogram.html')

View file

@ -14,6 +14,7 @@ define(function (require) {
shareYAxis: true,
addTooltip: true,
addLegend: true,
defaultYExtents: false
},
editor: require('text!plugins/vis_types/vislib/editors/basic.html')
},

View file

@ -24,7 +24,6 @@ define(function (require) {
Renderbot = Private(require('plugins/vis_types/_renderbot'));
VislibRenderbot = Private(require('plugins/vis_types/vislib/_vislib_renderbot'));
normalizeChartData = Private(require('components/agg_response/index'));
});
}
@ -85,7 +84,7 @@ define(function (require) {
}
}, mockVisType)
};
var $el = 'element';
var $el = $('<div>testing</div>');
var createVisSpy;
var getParamsStub;
var renderbot;

View file

@ -0,0 +1,152 @@
define(function (require) {
var moment = require('moment');
return {
'label': '',
'xAxisLabel': '@timestamp per 30 sec',
'ordered': {
'date': true,
'min': 1411761457636,
'max': 1411762357636,
'interval': 30000
},
'yAxisLabel': 'Count of documents',
'series': [
{
'values': [
{
'x': 1411761450000,
'y': -41
},
{
'x': 1411761480000,
'y': -18
},
{
'x': 1411761510000,
'y': -22
},
{
'x': 1411761540000,
'y': -17
},
{
'x': 1411761570000,
'y': -17
},
{
'x': 1411761600000,
'y': -21
},
{
'x': 1411761630000,
'y': -16
},
{
'x': 1411761660000,
'y': -17
},
{
'x': 1411761690000,
'y': -15
},
{
'x': 1411761720000,
'y': -19
},
{
'x': 1411761750000,
'y': -11
},
{
'x': 1411761780000,
'y': -13
},
{
'x': 1411761810000,
'y': -24
},
{
'x': 1411761840000,
'y': -20
},
{
'x': 1411761870000,
'y': -20
},
{
'x': 1411761900000,
'y': -21
},
{
'x': 1411761930000,
'y': -17
},
{
'x': 1411761960000,
'y': -20
},
{
'x': 1411761990000,
'y': -13
},
{
'x': 1411762020000,
'y': -14
},
{
'x': 1411762050000,
'y': -25
},
{
'x': 1411762080000,
'y': -17
},
{
'x': 1411762110000,
'y': -14
},
{
'x': 1411762140000,
'y': -22
},
{
'x': 1411762170000,
'y': -14
},
{
'x': 1411762200000,
'y': -19
},
{
'x': 1411762230000,
'y': -22
},
{
'x': 1411762260000,
'y': -17
},
{
'x': 1411762290000,
'y': -8
},
{
'x': 1411762320000,
'y': -15
},
{
'x': 1411762350000,
'y': -4
}
]
}
],
'hits': 533,
'xAxisFormatter': function (thing) {
return moment(thing);
},
'tooltipFormatter': function (d) {
return d;
}
};
});

View file

@ -0,0 +1,152 @@
define(function (require) {
var moment = require('moment');
return {
'label': '',
'xAxisLabel': '@timestamp per 30 sec',
'ordered': {
'date': true,
'min': 1411761457636,
'max': 1411762357636,
'interval': 30000
},
'yAxisLabel': 'Count of documents',
'series': [
{
'values': [
{
'x': 1411761450000,
'y': 41
},
{
'x': 1411761480000,
'y': 18
},
{
'x': 1411761510000,
'y': -22
},
{
'x': 1411761540000,
'y': -17
},
{
'x': 1411761570000,
'y': -17
},
{
'x': 1411761600000,
'y': -21
},
{
'x': 1411761630000,
'y': -16
},
{
'x': 1411761660000,
'y': 17
},
{
'x': 1411761690000,
'y': 15
},
{
'x': 1411761720000,
'y': 19
},
{
'x': 1411761750000,
'y': 11
},
{
'x': 1411761780000,
'y': -13
},
{
'x': 1411761810000,
'y': -24
},
{
'x': 1411761840000,
'y': -20
},
{
'x': 1411761870000,
'y': -20
},
{
'x': 1411761900000,
'y': -21
},
{
'x': 1411761930000,
'y': 17
},
{
'x': 1411761960000,
'y': 20
},
{
'x': 1411761990000,
'y': -13
},
{
'x': 1411762020000,
'y': -14
},
{
'x': 1411762050000,
'y': 25
},
{
'x': 1411762080000,
'y': -17
},
{
'x': 1411762110000,
'y': -14
},
{
'x': 1411762140000,
'y': -22
},
{
'x': 1411762170000,
'y': -14
},
{
'x': 1411762200000,
'y': 19
},
{
'x': 1411762230000,
'y': 22
},
{
'x': 1411762260000,
'y': 17
},
{
'x': 1411762290000,
'y': 8
},
{
'x': 1411762320000,
'y': -15
},
{
'x': 1411762350000,
'y': -4
}
]
}
],
'hits': 533,
'xAxisFormatter': function (thing) {
return moment(thing);
},
'tooltipFormatter': function (d) {
return d;
}
};
});

View file

@ -230,20 +230,68 @@ define(function (require) {
colOut = colIn.flatten();
});
it('should return an array of arrays', function () {
expect(_.isArray(serOut)).to.be(true);
it('should return an array of value objects from every series', function () {
expect(serOut.every(_.isObject)).to.be(true);
});
it('should return array length 3', function () {
expect(serOut[0][0].length).to.be(3);
function testLength(inputData) {
return function () {
var data = new dataFactory(inputData, {});
var len = _.reduce(data.chartData(), function (sum, chart) {
return sum + chart.series.reduce(function (sum, series) {
return sum + series.values.length;
}, 0);
}, 0);
expect(data.flatten()).to.have.length(len);
};
}
it('should return all points from every series', testLength(seriesData));
it('should return all points from every series', testLength(rowsData));
it('should return all points from every series', testLength(colsData));
});
describe('getYMinValue method', function () {
var Data;
var dataSeries;
var stackedDataSeries;
var visData;
var stackedVisData;
var series;
var stackedSeries;
var minValue;
var stackedMinValue;
beforeEach(function () {
module('DataFactory');
});
it('should return array length 3', function () {
expect(rowOut[0][0].length).to.be(3);
beforeEach(function () {
inject(function (d3, Private) {
Data = Private(require('components/vislib/lib/data'));
dataSeries = require('vislib_fixtures/mock_data/date_histogram/_series');
stackedDataSeries = require('vislib_fixtures/mock_data/stacked/_stacked');
visData = new Data(dataSeries, {});
stackedVisData = new Data(stackedDataSeries, { type: 'histogram' });
series = _.pluck(visData.chartData(), 'series');
stackedSeries = _.pluck(stackedVisData.chartData(), 'series');
minValue = 4;
stackedMinValue = 15;
});
});
it('should return array length 3', function () {
expect(colOut[0][0].length).to.be(3);
// The first value in the time series is less than the min date in the
// date range. It also has the largest y value. This value should be excluded
// when calculating the Y max value since it falls outside of the range.
it('should return the Y domain min value', function () {
expect(visData.getYMinValue()).to.be(minValue);
expect(stackedVisData.getYMinValue()).to.be(stackedMinValue);
});
it('should have a minimum date value that is greater than the max value within the date range', function () {
expect(_.min(series.values, function (d) { return d.x; })).to.be.greaterThan(minValue);
expect(_.min(stackedSeries.values, function (d) { return d.x; })).to.be.greaterThan(stackedMinValue);
});
});
@ -268,9 +316,9 @@ define(function (require) {
dataSeries = require('vislib_fixtures/mock_data/date_histogram/_series');
stackedDataSeries = require('vislib_fixtures/mock_data/stacked/_stacked');
visData = new Data(dataSeries, {});
stackedVisData = new Data(stackedDataSeries, {});
series = visData.flatten();
stackedSeries = stackedVisData.flatten();
stackedVisData = new Data(stackedDataSeries, { type: 'histogram' });
series = _.pluck(visData.chartData(), 'series');
stackedSeries = _.pluck(stackedVisData.chartData(), 'series');
maxValue = 41;
stackedMaxValue = 115;
});
@ -279,13 +327,9 @@ define(function (require) {
// The first value in the time series is less than the min date in the
// date range. It also has the largest y value. This value should be excluded
// when calculating the Y max value since it falls outside of the range.
it('should return the Y domain max value', function () {
series.forEach(function (data) {
expect(visData._getYMax(data, visData._getY)).to.be(maxValue);
});
stackedSeries.forEach(function (data) {
expect(stackedVisData._getYMax(data, visData._getYStack)).to.be(stackedMaxValue);
});
it('should return the Y domain min value', function () {
expect(visData.getYMaxValue()).to.be(maxValue);
expect(stackedVisData.getYMaxValue()).to.be(stackedMaxValue);
});
it('should have a minimum date value that is greater than the max value within the date range', function () {

View file

@ -20,9 +20,7 @@ define(function (require) {
vis = Private(require('vislib_fixtures/_vis_fixture'))();
require('css!components/vislib/styles/main');
vis.on('brush', function (e) {
console.log(e);
});
vis.on('brush', _.noop);
vis.render(data);
});
@ -36,12 +34,8 @@ define(function (require) {
describe('addEvent method', function () {
it('should return a function', function () {
vis.handler.charts.forEach(function (chart) {
var clickEvent = function (e) {
console.log(e);
};
var addEvent = chart.events.addEvent;
expect(_.isFunction(addEvent('click', clickEvent))).to.be(true);
expect(_.isFunction(addEvent('click', _.noop))).to.be(true);
});
});
});

View file

@ -1,18 +1,42 @@
define(function (require) {
var angular = require('angular');
var _ = require('lodash');
var d3 = require('d3');
var $ = require('jquery');
angular.module('YAxisFactory', ['kibana']);
var YAxis;
var Data;
var el;
var yAxis;
var yAxisDiv;
describe('Vislib yAxis Class Test Suite', function () {
var YAxis;
var Data;
var yAxis;
var el;
var yAxisDiv;
var dataObj;
var timeSeries = [
1408734060000,
1408734090000,
1408734120000,
1408734150000,
1408734180000,
1408734210000,
1408734240000,
1408734270000,
1408734300000,
1408734330000
];
var defaultGraphData = [
[ 8, 23, 30, 28, 36, 30, 26, 22, 29, 24 ],
[ 2, 13, 20, 18, 26, 20, 16, 12, 19, 14 ]
];
function makeSeriesData(data) {
return timeSeries.map(function (timestamp, i) {
return {
x: timestamp,
y: data[i] || 0
};
});
}
function createData(seriesData) {
var data = {
hits: 621,
label: 'test',
@ -22,129 +46,51 @@ define(function (require) {
max: 1408734982458,
min: 1408734082458
},
series: [
{
values: [
{
x: 1408734060000,
y: 8
},
{
x: 1408734090000,
y: 23
},
{
x: 1408734120000,
y: 30
},
{
x: 1408734150000,
y: 28
},
{
x: 1408734180000,
y: 36
},
{
x: 1408734210000,
y: 30
},
{
x: 1408734240000,
y: 26
},
{
x: 1408734270000,
y: 22
},
{
x: 1408734300000,
y: 29
},
{
x: 1408734330000,
y: 24
}
]
},
{
values: [
{
x: 1408734060000,
y: 8
},
{
x: 1408734090000,
y: 23
},
{
x: 1408734120000,
y: 30
},
{
x: 1408734150000,
y: 28
},
{
x: 1408734180000,
y: 36
},
{
x: 1408734210000,
y: 30
},
{
x: 1408734240000,
y: 26
},
{
x: 1408734270000,
y: 22
},
{
x: 1408734300000,
y: 29
},
{
x: 1408734330000,
y: 24
}
]
}
],
series: seriesData.map(function (series) {
return { values: makeSeriesData(series) };
}),
xAxisLabel: 'Date Histogram',
yAxisLabel: 'Count'
};
beforeEach(module('YAxisFactory'));
beforeEach(inject(function (d3, Private) {
var node = $('<div>').css({
height: 40,
width: 40
})
.appendTo('body')
.addClass('y-axis-wrapper')
.get(0);
el = d3.select(node).datum(data);
yAxisDiv = el.append('div')
.attr('class', 'y-axis-div');
var dataObj = new Data(data, {
defaultYMin: true
});
yAxis = new YAxis({
el: node,
yMin: dataObj.getYMinValue(),
yMax: dataObj.getYMaxValue(),
_attr: {
margin: { top: 0, right: 0, bottom: 0, left: 0 }
}
});
}
describe('Vislib yAxis Class Test Suite', function () {
var d3Provider;
beforeEach(module('kibana'));
beforeEach(inject(function (Private, _d3_) {
d3Provider = _d3_;
Data = Private(require('components/vislib/lib/data'));
YAxis = Private(require('components/vislib/lib/y_axis'));
expect($('.y-axis-wrapper')).to.have.length(0);
var $node = $('<div>').css({
height: 40,
width: 40
})
.appendTo('body')
.addClass('y-axis-wrapper');
var node = $node.get(0);
el = d3.select(node).datum(data);
yAxisDiv = el.append('div')
.attr('class', 'y-axis-div');
dataObj = new Data(data, {});
yAxis = new YAxis({
el: node,
yMax: dataObj.getYMaxValue(),
_attr: {
margin: { top: 0, right: 0, bottom: 0, left: 0 }
}
});
}));
afterEach(function () {
@ -154,6 +100,7 @@ define(function (require) {
describe('render Method', function () {
beforeEach(function () {
createData(defaultGraphData);
expect(d3.select(yAxis.el).selectAll('.y-axis-div')).to.have.length(1);
yAxis.render();
});
@ -173,26 +120,89 @@ define(function (require) {
describe('getYScale Method', function () {
var yScale;
var graphData;
var height = 50;
beforeEach(function () {
yScale = yAxis.getYScale(height);
});
function checkDomain(min, max) {
var domain = yScale.domain();
expect(domain[0]).to.be.lessThan(min + 1);
expect(domain[1]).to.be.greaterThan(max - 1);
return domain;
}
it('should return a function', function () {
expect(_.isFunction(yScale)).to.be(true);
});
it('should return the correct domain', function () {
expect(yScale.domain()[0]).to.be(0);
// Should be greater than 36 since we are using .nice()
expect(yScale.domain()[1]).to.be.greaterThan(36);
});
it('should return the correct range', function () {
function checkRange() {
expect(yScale.range()[0]).to.be(height);
// The yScale range should always start from 0
expect(yScale.range()[1]).to.be(0);
}
describe('API', function () {
beforeEach(function () {
createData(defaultGraphData);
yScale = yAxis.getYScale(height);
});
it('should return a function', function () {
expect(_.isFunction(yScale)).to.be(true);
});
});
describe('positive values', function () {
beforeEach(function () {
graphData = defaultGraphData;
createData(graphData);
yScale = yAxis.getYScale(height);
});
it('should have domain between 0 and max value', function () {
var min = 0;
var max = _.max(_.flatten(graphData));
var domain = checkDomain(min, max);
expect(domain[1]).to.be.greaterThan(0);
checkRange();
});
});
describe('negative values', function () {
beforeEach(function () {
graphData = [
[ -8, -23, -30, -28, -36, -30, -26, -22, -29, -24 ],
[ -22, -8, -30, -4, 0, 0, -3, -22, -14, -24 ]
];
createData(graphData);
yScale = yAxis.getYScale(height);
});
it('should have domain between min value and 0', function () {
var min = _.min(_.flatten(graphData));
var max = 0;
var domain = checkDomain(min, max);
expect(domain[0]).to.be.lessThan(0);
checkRange();
});
});
describe('positive and negative values', function () {
beforeEach(function () {
graphData = [
[ 8, 23, 30, 28, 36, 30, 26, 22, 29, 24 ],
[ 22, 8, -30, -4, 0, 0, 3, -22, 14, 24 ]
];
createData(graphData);
yScale = yAxis.getYScale(height);
});
it('should have domain between min and max values', function () {
var min = _.min(_.flatten(graphData));
var max = _.max(_.flatten(graphData));
var domain = checkDomain(min, max);
expect(domain[0]).to.be.lessThan(0);
expect(domain[1]).to.be.greaterThan(0);
checkRange();
});
});
});
@ -201,6 +211,7 @@ define(function (require) {
var val;
beforeEach(function () {
createData(defaultGraphData);
val = yAxis.formatAxisLabel(num);
});
@ -212,6 +223,7 @@ define(function (require) {
describe('getYAxis method', function () {
var mode, yMax, yScale;
beforeEach(function () {
createData(defaultGraphData);
mode = yAxis._attr.mode;
yMax = yAxis.yMax;
yScale = yAxis.getYScale;
@ -244,12 +256,20 @@ define(function (require) {
});
describe('draw Method', function () {
beforeEach(function () {
createData(defaultGraphData);
});
it('should be a function', function () {
expect(_.isFunction(yAxis.draw())).to.be(true);
});
});
describe('tickScale Method', function () {
beforeEach(function () {
createData(defaultGraphData);
});
it('should return the correct number of ticks', function () {
expect(yAxis.tickScale(1000)).to.be(11);
expect(yAxis.tickScale(40)).to.be(3);
@ -257,4 +277,4 @@ define(function (require) {
});
});
});
});
});

View file

@ -130,12 +130,8 @@ define(function (require) {
beforeEach(function () {
listeners = [];
listener1 = function (e) {
console.log(e, 'listener1');
};
listener2 = function (e) {
console.log(e, 'listener2');
};
listener1 = function () {};
listener2 = function () {};
listeners.push(listener1);
listeners.push(listener2);
@ -198,12 +194,8 @@ define(function (require) {
beforeEach(function () {
listeners = [];
listener1 = function (e) {
console.log(e, 'listener1');
};
listener2 = function (e) {
console.log(e, 'listener2');
};
listener1 = function () {};
listener2 = function () {};
listeners.push(listener1);
listeners.push(listener2);

View file

@ -4,20 +4,26 @@ define(function (require) {
var $ = require('jquery');
// Data
var series = require('vislib_fixtures/mock_data/date_histogram/_series');
var seriesPos = require('vislib_fixtures/mock_data/date_histogram/_series');
var seriesPosNeg = require('vislib_fixtures/mock_data/date_histogram/_series_pos_neg');
var seriesNeg = require('vislib_fixtures/mock_data/date_histogram/_series_neg');
var termColumns = require('vislib_fixtures/mock_data/terms/_columns');
var rangeRows = require('vislib_fixtures/mock_data/range/_rows');
var stackedSeries = require('vislib_fixtures/mock_data/date_histogram/_stacked_series');
var dataArray = [
series,
seriesPos,
seriesPosNeg,
seriesNeg,
termColumns,
rangeRows,
stackedSeries,
];
var names = [
'series',
'series pos',
'series pos neg',
'series neg',
'term columns',
'range rows',
'stackedSeries',
@ -26,7 +32,8 @@ define(function (require) {
var visLibParams = {
type: 'area',
addLegend: true,
addTooltip: true
addTooltip: true,
defaultYExtents: false
};
angular.module('AreaChartFactory', ['kibana']);
@ -44,9 +51,7 @@ define(function (require) {
vis = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams);
require('css!components/vislib/styles/main');
vis.on('brush', function (e) {
console.log(e);
});
vis.on('brush', _.noop);
vis.render(data);
});
@ -218,6 +223,25 @@ define(function (require) {
expect(_.isFunction(chart.draw())).to.be(true);
});
});
it('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
var yAxis = chart.handler.yAxis;
expect(yAxis.yMin).to.not.be(undefined);
expect(yAxis.yMax).to.not.be(undefined);
});
});
it('should render a zero axis line', function () {
vis.handler.charts.forEach(function (chart) {
var yAxis = chart.handler.yAxis;
if (yAxis.yMin < 0 && yAxis.yMax > 0) {
expect($(chart.chartEl).find('line.zero-line').length).to.be(1);
}
});
});
});
describe('containerTooSmall error', function () {
@ -234,6 +258,23 @@ define(function (require) {
});
});
});
describe('defaultYExtents is true', function () {
beforeEach(function () {
vis._attr.defaultYExtents = true;
vis.render(data);
});
it('should return yAxis extents equal to data extents', function () {
vis.handler.charts.forEach(function (chart) {
var yAxis = chart.handler.yAxis;
var yVals = [vis.handler.data.getYMinValue(), vis.handler.data.getYMaxValue()];
expect(yAxis.yMin).to.equal(yVals[0]);
expect(yAxis.yMax).to.equal(yVals[1]);
});
});
});
});
});
});

View file

@ -6,22 +6,30 @@ define(function (require) {
// Data
var series = require('vislib_fixtures/mock_data/date_histogram/_series');
var seriesPosNeg = require('vislib_fixtures/mock_data/date_histogram/_series_pos_neg');
var seriesNeg = require('vislib_fixtures/mock_data/date_histogram/_series_neg');
var termsColumns = require('vislib_fixtures/mock_data/terms/_columns');
//var histogramRows = require('vislib_fixtures/mock_data/histogram/_rows');
var stackedSeries = require('vislib_fixtures/mock_data/date_histogram/_stacked_series');
var dataArray = [
series,
seriesPosNeg,
seriesNeg,
termsColumns,
//histogramRows,
stackedSeries
];
var names = [
'series',
'series with positive and negative values',
'series with negative values',
'terms columns',
//'histogram rows',
'stackedSeries'
];
var modes = [
'stacked',
'stacked',
'stacked',
'grouped',
//'percentage',
@ -153,6 +161,25 @@ define(function (require) {
expect(_.isFunction(chart.draw())).to.be(true);
});
});
it('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
var yAxis = chart.handler.yAxis;
expect(yAxis.yMin).to.not.be(undefined);
expect(yAxis.yMax).to.not.be(undefined);
});
});
it('should render a zero axis line', function () {
vis.handler.charts.forEach(function (chart) {
var yAxis = chart.handler.yAxis;
if (yAxis.yMin < 0 && yAxis.yMax > 0) {
expect($(chart.chartEl).find('line.zero-line').length).to.be(1);
}
});
});
});
describe('containerTooSmall error', function () {
@ -169,6 +196,23 @@ define(function (require) {
});
});
});
describe('defaultYExtents is true', function () {
beforeEach(function () {
vis._attr.defaultYExtents = true;
vis.render(data);
});
it('should return yAxis extents equal to data extents', function () {
vis.handler.charts.forEach(function (chart) {
var yAxis = chart.handler.yAxis;
var yVals = [vis.handler.data.getYMinValue(), vis.handler.data.getYMaxValue()];
expect(yAxis.yMin).to.equal(yVals[0]);
expect(yAxis.yMax).to.equal(yVals[1]);
});
});
});
});
});
});

View file

@ -4,18 +4,24 @@ define(function (require) {
var $ = require('jquery');
// Data
var series = require('vislib_fixtures/mock_data/date_histogram/_series');
var seriesPos = require('vislib_fixtures/mock_data/date_histogram/_series');
var seriesPosNeg = require('vislib_fixtures/mock_data/date_histogram/_series_pos_neg');
var seriesNeg = require('vislib_fixtures/mock_data/date_histogram/_series_neg');
var histogramColumns = require('vislib_fixtures/mock_data/histogram/_columns');
var rangeRows = require('vislib_fixtures/mock_data/range/_rows');
var termSeries = require('vislib_fixtures/mock_data/terms/_series');
var dateHistogramArray = [
series,
seriesPos,
seriesPosNeg,
seriesNeg,
histogramColumns,
rangeRows,
termSeries,
];
var names = [
'series',
'series pos',
'series pos neg',
'series neg',
'histogram columns',
'range rows',
'term series',
@ -41,9 +47,7 @@ define(function (require) {
vis = Private(require('vislib_fixtures/_vis_fixture'))(visLibParams);
require('css!components/vislib/styles/main');
vis.on('brush', function (e) {
console.log(e);
});
vis.on('brush', _.noop);
vis.render(data);
});
@ -133,6 +137,25 @@ define(function (require) {
expect(chart.draw()).to.be.a(Function);
});
});
it('should return a yMin and yMax', function () {
vis.handler.charts.forEach(function (chart) {
var yAxis = chart.handler.yAxis;
expect(yAxis.yMin).to.not.be(undefined);
expect(yAxis.yMax).to.not.be(undefined);
});
});
it('should render a zero axis line', function () {
vis.handler.charts.forEach(function (chart) {
var yAxis = chart.handler.yAxis;
if (yAxis.yMin < 0 && yAxis.yMax > 0) {
expect($(chart.chartEl).find('line.zero-line').length).to.be(1);
}
});
});
});
describe('containerTooSmall error', function () {
@ -150,6 +173,22 @@ define(function (require) {
});
});
describe('defaultYExtents is true', function () {
beforeEach(function () {
vis._attr.defaultYExtents = true;
vis.render(data);
});
it('should return yAxis extents equal to data extents', function () {
vis.handler.charts.forEach(function (chart) {
var yAxis = chart.handler.yAxis;
var yVals = [vis.handler.data.getYMinValue(), vis.handler.data.getYMaxValue()];
expect(yAxis.yMin).to.equal(yVals[0]);
expect(yAxis.yMax).to.equal(yVals[1]);
});
});
});
});
});
});