Merge pull request #3464 from stormpython/fixed_scales

Add ability to select y-axis min and max values
This commit is contained in:
Rashid Khan 2015-05-18 12:20:48 -07:00
commit 0128aa99ad
21 changed files with 269 additions and 202 deletions

View file

@ -6,7 +6,7 @@ define(function (require) {
var moment = require('moment');
require('directives/input_datetime');
require('directives/greater_than');
require('directives/inequality');
require('components/timepicker/quick_ranges');
require('components/timepicker/refresh_intervals');
require('components/timepicker/time_units');

View file

@ -70,7 +70,7 @@ define(function (require) {
if (_.isString(this.type)) this.type = visTypes.byName[this.type];
this.listeners = _.assign({}, state.listeners, this.type.listeners);
this.params = _.defaults({}, state.params || {}, this.type.params.defaults || {});
this.params = _.defaults({}, _.cloneDeep(state.params || {}), this.type.params.defaults || {});
this.aggs = new AggConfigs(this, state.aggs);
};

View file

@ -18,7 +18,9 @@ define(function (require) {
opts = opts || {};
return function (vis) {
var isUserDefinedYAxis = vis._attr.setYExtents;
var data;
if (opts.zeroFill) {
data = new Data(injectZeros(vis.data), vis._attr);
} else {
@ -41,12 +43,13 @@ define(function (require) {
alerts: new Alerts(vis, data, opts.alerts),
yAxis: new YAxis({
el : vis.el,
yMin : data.getYMin(),
yMax : data.getYMax(),
_attr: vis._attr,
yAxisFormatter: data.get('yAxisFormatter')
yMin : isUserDefinedYAxis ? vis._attr.yAxis.min : data.getYMin(),
yMax : isUserDefinedYAxis ? vis._attr.yAxis.max : data.getYMax(),
yAxisFormatter: data.get('yAxisFormatter'),
_attr: vis._attr
})
});
};
}
@ -79,4 +82,3 @@ define(function (require) {
};
};
});

View file

@ -15,8 +15,8 @@ define(function (require) {
*/
function YAxis(args) {
this.el = args.el;
this.yMin = args.yMin;
this.yMax = args.yMax;
this.scale = null;
this.domain = [args.yMin, args.yMax];
this.yAxisFormatter = args.yAxisFormatter;
this._attr = args._attr || {};
}
@ -33,29 +33,59 @@ define(function (require) {
d3.select(this.el).selectAll('.y-axis-div').call(this.draw());
};
YAxis.prototype.throwCustomError = function (message) {
YAxis.prototype._isPercentage = function () {
return (this._attr.mode === 'percentage');
};
YAxis.prototype._isUserDefined = function () {
return (this._attr.setYExtents);
};
YAxis.prototype._isYExtents = function () {
return (this._attr.defaultYExtents);
};
YAxis.prototype._validateUserExtents = function (domain) {
var self = this;
return domain.map(function (val) {
val = parseInt(val, 10);
if (isNaN(val)) throw new Error(val + ' is not a valid number');
if (self._isPercentage() && self._attr.setYExtents) return val / 100;
return val;
});
};
YAxis.prototype._getExtents = function (domain) {
var min = domain[0];
var max = domain[1];
if (this._attr.scale === 'log') return this._logDomain(min, max); // Negative values cannot be displayed with a log scale.
if (!this._isYExtents() && !this._isUserDefined()) return [Math.min(0, min), Math.max(0, max)];
if (this._isUserDefined()) return this._validateUserExtents(domain);
return domain;
};
YAxis.prototype._throwCustomError = function (message) {
throw new Error(message);
};
YAxis.prototype.throwCannotLogScaleNegVals = function () {
YAxis.prototype._throwCannotLogScaleNegVals = function () {
throw new errors.CannotLogScaleNegVals();
};
YAxis.prototype.throwNoResultsError = function () {
throw new errors.NoResults();
};
/**
* Returns the appropriate D3 scale
*
* @param fnName {String} D3 scale
* @returns {*}
*/
YAxis.prototype.getScaleType = function (fnName) {
YAxis.prototype._getScaleType = function (fnName) {
if (fnName === 'square root') fnName = 'sqrt'; // Rename 'square root' to 'sqrt'
fnName = fnName || 'linear';
if (typeof d3.scale[fnName] !== 'function') return this.throwCustomError('YAxis.getScaleType: ' + fnName + ' is not a function');
if (typeof d3.scale[fnName] !== 'function') return this._throwCustomError('YAxis.getScaleType: ' + fnName + ' is not a function');
return d3.scale[fnName]();
};
@ -69,29 +99,9 @@ define(function (require) {
* @param yMax
* @returns {*[]}
*/
YAxis.prototype.returnLogDomain = function (yMin, yMax) {
if (yMin < 0 || yMax < 0) return this.throwCannotLogScaleNegVals();
return [Math.max(1, yMin), yMax];
};
YAxis.prototype._setDefaultYExtents = function () {
return this._attr.defaultYExtents;
};
/**
* Returns the domain, i.e. the extent of the y axis
*
* @param scale {String} Kibana scale
* @param yMin {Number} Y-axis minimum value
* @param yMax {Number} Y-axis maximum value
* @returns {*[]}
*/
YAxis.prototype.getDomain = function (scale, yMin, yMax) {
if (this._setDefaultYExtents()) return [yMin, yMax];
if (scale === 'log') return this.returnLogDomain(yMin, yMax); // Negative values cannot be displayed with a log scale.
if (yMin === 0 && yMax === 0) return this.throwNoResultsError(); // yMin and yMax can never both be equal to zero
return [Math.min(0, yMin), Math.max(0, yMax)];
YAxis.prototype._logDomain = function (min, max) {
if (min < 0 || max < 0) return this._throwCannotLogScaleNegVals();
return [Math.max(1, min), max];
};
/**
@ -102,14 +112,16 @@ define(function (require) {
* @returns {D3.Scale.QuantitiveScale|*} D3 yScale function
*/
YAxis.prototype.getYScale = function (height) {
this.yScale = this.getScaleType(this._attr.scale)
.domain(this.getDomain(this._attr.scale, this.yMin, this.yMax))
var scale = this._getScaleType(this._attr.scale);
var domain = this._getExtents(this.domain);
this.yScale = scale
.domain(domain)
.range([height, 0]);
// Nicing the scale, rounds values down or up to make the scale look better
// When defaultYExtents are selected, the extents (i.e. min and max) should
// be shown without any rounding.
if (!this._attr.defaultYExtents) return this.yScale.nice();
if (!this._isUserDefined()) this.yScale.nice(); // round extents when not user defined
// Prevents bars from going off the chart when the y extents are within the domain range
if (this._attr.type === 'histogram') this.yScale.clamp(true);
return this.yScale;
};
@ -120,6 +132,10 @@ define(function (require) {
return d3.format('n');
};
YAxis.prototype._validateYScale = function (yScale) {
if (!yScale || _.isNaN(yScale)) throw new Error('yScale is ' + yScale);
};
/**
* Creates the d3 y axis function
*
@ -129,16 +145,12 @@ define(function (require) {
*/
YAxis.prototype.getYAxis = function (height) {
var yScale = this.getYScale(height);
// y scale should never be `NaN`
if (!yScale || _.isNaN(yScale)) {
throw new Error('yScale is ' + yScale);
}
this._validateYScale(yScale);
// Create the d3 yAxis function
this.yAxis = d3.svg.axis()
.scale(yScale)
.tickFormat(this.tickFormat())
.tickFormat(this.tickFormat(this.domain))
.ticks(this.tickScale(height))
.orient('left');
@ -177,7 +189,6 @@ define(function (require) {
var isWiggleOrSilhouette = (mode === 'wiggle' || mode === 'silhouette');
return function (selection) {
selection.each(function () {
var el = this;

View file

@ -1,21 +0,0 @@
define(function (require) {
require('modules')
.get('kibana')
.directive('greaterThan', function () {
return {
require: 'ngModel',
link: function ($scope, $el, $attr, ngModel) {
var val = $attr.greaterThan || 0;
ngModel.$parsers.push(validator);
ngModel.$formatters.push(validator);
function validator(value) {
var valid = false;
if (!isNaN(value)) valid = value > val;
ngModel.$setValidity('greaterThan', valid);
return value;
}
}
};
});
});

View file

@ -0,0 +1,40 @@
define(function (require) {
function makeDirectiveDef(id, compare) {
return function ($parse) {
return {
require: 'ngModel',
link: function ($scope, $el, $attr, ngModel) {
var getBound = function () { return $parse($attr[id])(); };
var defaultVal = {
'greaterThan': -Infinity,
'lessThan': Infinity
}[id];
ngModel.$parsers.push(validate);
ngModel.$formatters.push(validate);
$scope.$watch(getBound, function () {
validate(ngModel.$viewValue);
});
function validate(val) {
var bound = !isNaN(getBound()) ? +getBound() : defaultVal;
var valid = !isNaN(bound) && !isNaN(val) && compare(val, bound);
ngModel.$setValidity(id, valid);
return val;
}
}
};
};
}
require('modules')
.get('kibana')
.directive('greaterThan', makeDirectiveDef('greaterThan', function (a, b) {
return a > b;
}))
.directive('lessThan', makeDirectiveDef('lessThan', function (a, b) {
return a < b;
}));
});

View file

@ -1,14 +1,49 @@
<div>
<div class="vis-option-item">
<label>
<input type="checkbox" ng-model="vis.params.defaultYExtents">
Scale Y-Axis to Data Bounds
</label>
</div>
<div class="vis-option-item" ng-show="vis.hasSchemaAgg('segment', 'date_histogram')">
<label>
<input type="checkbox" ng-model="vis.params.addTimeMarker">
<input type="checkbox" ng-model="vis.params.addTimeMarker" ng-checked="vis.params.addTimeMarker">
Current time marker
</label>
</div>
<div class="vis-option-item">
<label>
<input type="checkbox" ng-model="vis.params.setYExtents">
Set Y-Axis Extents
</label>
<div ng-if="vis.params.setYExtents">
<label>
y-max
<input name="yMax"
class="form-control"
type="number"
step="0.1"
greater-than="{{vis.params.yAxis.min}}"
ng-model="vis.params.yAxis.max"
ng-required="vis.params.setYExtents">
</label>
<div ng-show="vis.params.yAxis.min > vis.params.yAxis.max">
<span class="text-danger">Min must not exceed max</span>
</div>
<label>
y-min
<input name="yMin"
class="form-control"
type="number"
step="0.1"
less-than="{{vis.params.yAxis.max}}"
greater-than="{{vis.params.scale === 'log' ? 0 : ''}}"
ng-model="vis.params.yAxis.min"
ng-required="vis.params.setYExtents">
</label>
</div>
<div ng-show="vis.params.setYExtents && vis.params.scale === 'log' && vis.params.yAxis.min <= 0">
<span class="text-danger">Min must exceed 0 when a log scale is selected</span>
</div>
<div class="vis-option-item">
<label>
<input type="checkbox" ng-model="vis.params.defaultYExtents" ng-disabled="vis.params.setYExtents">
Scale Y-Axis to Data Bounds
</label>
</div>
</div>
</div>

View file

@ -2,6 +2,7 @@ define(function (require) {
var _ = require('lodash');
var $ = require('jquery');
var module = require('modules').get('kibana');
require('directives/inequality');
module.directive('pointSeriesOptions', function ($parse, $compile) {
return {

View file

@ -8,6 +8,7 @@ define(function (require) {
var VislibRenderbot = Private(require('plugins/vis_types/vislib/_vislib_renderbot'));
require('plugins/vis_types/controls/vislib_basic_options');
require('plugins/vis_types/controls/point_series_options');
require('plugins/vis_types/controls/line_interpolation_option');
require('plugins/vis_types/controls/point_series_options');

View file

@ -18,11 +18,13 @@ define(function (require) {
addLegend: true,
smoothLines: false,
scale: 'linear',
mode: 'stacked',
interpolate: 'linear',
defaultYExtents: false,
mode: 'stacked',
times: [],
addTimeMarker: false
addTimeMarker: false,
defaultYExtents: false,
setYExtents: false,
yAxis: {}
},
scales: ['linear', 'log', 'square root'],
modes: ['stacked', 'overlap', 'percentage', 'wiggle', 'silhouette'],

View file

@ -7,5 +7,5 @@
</label>
</div>
<line-interpolation-option></line-interpolation-option>
<vislib-basic-options></vislib-basic-options>
<point-series-options></point-series-options>
<vislib-basic-options></vislib-basic-options>

View file

@ -5,5 +5,5 @@
</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>
<point-series-options></point-series-options>
<vislib-basic-options></vislib-basic-options>

View file

@ -19,5 +19,5 @@
</label>
</div>
</div>
<vislib-basic-options></vislib-basic-options>
<point-series-options></point-series-options>
<vislib-basic-options></vislib-basic-options>

View file

@ -16,9 +16,11 @@ define(function (require) {
addLegend: true,
scale: 'linear',
mode: 'stacked',
defaultYExtents: false,
times: [],
addTimeMarker: false
addTimeMarker: false,
defaultYExtents: false,
setYExtents: false,
yAxis: {}
},
scales: ['linear', 'log', 'square root'],
modes: ['stacked', 'percentage', 'grouped'],

View file

@ -17,12 +17,14 @@ define(function (require) {
showCircles: true,
smoothLines: false,
interpolate: 'linear',
scale: 'linear',
drawLinesBetweenPoints: true,
radiusRatio: 9,
scale: 'linear',
defaultYExtents: false,
times: [],
addTimeMarker: false
addTimeMarker: false,
defaultYExtents: false,
setYExtents: false,
yAxis: {}
},
scales: ['linear', 'log', 'square root'],
editor: require('text!plugins/vis_types/vislib/editors/line.html')

View file

@ -1,6 +1,6 @@
define(function (require) {
var angular = require('angular');
require('directives/greater_than');
require('directives/inequality');
describe('greater_than model validator directive', function () {
var $compile, $rootScope;
@ -27,16 +27,16 @@ define(function (require) {
expect(element.hasClass('ng-valid')).to.be.ok();
});
it('should be invalid for 0', function () {
it('should be valid for 0', function () {
$rootScope.value = '0';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
expect(element.hasClass('ng-valid')).to.be.ok();
});
it('should be invalid for negatives', function () {
it('should be valid for negatives', function () {
$rootScope.value = '-10';
$rootScope.$digest();
expect(element.hasClass('ng-invalid')).to.be.ok();
expect(element.hasClass('ng-valid')).to.be.ok();
});
});

View file

@ -1,6 +1,7 @@
define(function (require) {
return function VisLibFixtures(Private) {
var $ = require('jquery');
var _ = require('lodash');
return function (visLibParams) {
var Vis = Private(require('components/vislib/vis'));
@ -12,12 +13,15 @@ define(function (require) {
$el.width(1024);
$el.height(300);
var config = visLibParams || {
var config = _.defaults(visLibParams || {}, {
shareYAxis: true,
addTooltip: true,
addLegend: true,
defaultYExtents: false,
setYExtents: false,
yAxis: {},
type: 'histogram'
};
});
return new Vis($el, config);
};

View file

@ -77,7 +77,10 @@ define(function (require) {
yMin: dataObj.getYMin(),
yMax: dataObj.getYMax(),
_attr: {
margin: { top: 0, right: 0, bottom: 0, left: 0 }
margin: { top: 0, right: 0, bottom: 0, left: 0 },
defaultYMin: true,
setYExtents: false,
yAxis: {}
}
}));
};
@ -126,6 +129,7 @@ define(function (require) {
describe('getYScale Method', function () {
var yScale;
var graphData;
var domain;
var height = 50;
function checkDomain(min, max) {
@ -151,6 +155,20 @@ define(function (require) {
});
});
describe('should return log values', function () {
var domain;
var extents;
it('should return 1', function () {
yAxis._attr.scale = 'log';
extents = [0, 400];
domain = yAxis._getExtents(extents);
// Log scales have a yMin value of 1
expect(domain[0]).to.be(1);
});
});
describe('positive values', function () {
beforeEach(function () {
graphData = defaultGraphData;
@ -178,7 +196,6 @@ define(function (require) {
yScale = yAxis.getYScale(height);
});
it('should have domain between min value and 0', function () {
var min = _.min(_.flatten(graphData));
var max = 0;
@ -186,7 +203,6 @@ define(function (require) {
expect(domain[0]).to.be.lessThan(0);
checkRange();
});
});
describe('positive and negative values', function () {
@ -199,7 +215,6 @@ define(function (require) {
yScale = yAxis.getYScale(height);
});
it('should have domain between min and max values', function () {
var min = _.min(_.flatten(graphData));
var max = _.max(_.flatten(graphData));
@ -210,20 +225,60 @@ define(function (require) {
});
});
describe('should not return a nice scale when defaultYExtents is true', function () {
describe('validate user defined values', function () {
beforeEach(function () {
createData(defaultGraphData);
yAxis._attr.defaultYExtents = true;
yAxis.getYAxis(height);
yAxis.render();
yAxis._attr.mode = 'stacked';
yAxis._attr.setYExtents = false;
yAxis._attr.yAxis = {};
});
it('not return a nice scale', function () {
var min = _.min(_.flatten(defaultGraphData));
var max = _.max(_.flatten(defaultGraphData));
var domain = yAxis.yAxis.scale().domain();
expect(domain[0]).to.be(min);
expect(domain[1]).to.be(max);
it('should throw a NaN error', function () {
var min = 'Not a number';
var max = 12;
expect(function () {
yAxis._validateUserExtents(min, max);
}).to.throwError();
});
it('should return a decimal value', function () {
yAxis._attr.mode = 'percentage';
yAxis._attr.setYExtents = true;
domain = [];
domain[0] = yAxis._attr.yAxis.min = 20;
domain[1] = yAxis._attr.yAxis.max = 80;
var newDomain = yAxis._validateUserExtents(domain);
expect(newDomain[0]).to.be(domain[0] / 100);
expect(newDomain[1]).to.be(domain[1] / 100);
});
it('should return the user defined value', function () {
domain = [20, 50];
var newDomain = yAxis._validateUserExtents(domain);
expect(newDomain[0]).to.be(domain[0]);
expect(newDomain[1]).to.be(domain[1]);
});
});
describe('should throw an error when', function () {
it('min === max', function () {
var min = 12;
var max = 12;
expect(function () {
yAxis._validateAxisExtents(min, max);
}).to.throwError();
});
it('min > max', function () {
var min = 30;
var max = 10;
expect(function () {
yAxis._validateAxisExtents(min, max);
}).to.throwError();
});
});
});
@ -233,105 +288,39 @@ define(function (require) {
it('should return a function', function () {
fnNames.forEach(function (fnName) {
var isFunction = (typeof yAxis.getScaleType(fnName) === 'function');
expect(isFunction).to.be(true);
expect(yAxis._getScaleType(fnName)).to.be.a(Function);
});
// if no value is provided to the function, scale should default to a linear scale
expect(typeof yAxis.getScaleType()).to.be('function');
expect(yAxis._getScaleType()).to.be.a(Function);
});
it('should throw an error if function name is undefined', function () {
expect(function () {
yAxis.getScaleType('square');
yAxis._getScaleType('square');
}).to.throwError();
});
});
describe('returnLogDomain method', function () {
describe('_logDomain method', function () {
it('should throw an error', function () {
expect(function () {
yAxis.returnLogDomain(-10, -5);
yAxis._logDomain(-10, -5);
}).to.throwError();
expect(function () {
yAxis.returnLogDomain(-10, 5);
yAxis._logDomain(-10, 5);
}).to.throwError();
expect(function () {
yAxis.returnLogDomain(0, -5);
yAxis._logDomain(0, -5);
}).to.throwError();
});
it('should return a yMin value of 1', function () {
var yMin = yAxis.returnLogDomain(0, 200)[0];
var yMin = yAxis._logDomain(0, 200)[0];
expect(yMin).to.be(1);
});
});
describe('getDomain method', function () {
beforeEach(function () {
// Need to set this to false before each test since its
// status changes in one of the tests below. Having this set to
// true causes other tests to fail that need this attr to be set to false.
yAxis._attr.defaultYExtents = false;
});
it('should return a log domain', function () {
var scale = 'log';
var yMin = 0;
var yMax = 400;
var domain = yAxis.getDomain(scale, yMin, yMax);
// Log scales have a yMin value of 1
expect(domain[0]).to.be(1);
});
it('should return the default y axis extents (i.e. the min and max values)', function () {
var scale = 'linear';
var yMin = 25;
var yMax = 150;
var domain;
yAxis._attr.defaultYExtents = true;
domain = yAxis.getDomain(scale, yMin, yMax);
expect(domain[0]).to.be(yMin);
expect(domain[1]).to.be(yMax);
});
it('should throw a no results error if yMin and yMax values are both 0', function () {
expect(function () {
yAxis.getDomain('linear', 0, 0);
}).to.throwError();
});
it('should return the correct min and max values', function () {
var extents = [
[-5, 20],
[-30, -10],
[5, 20]
];
extents.forEach(function (extent) {
var domain = yAxis.getDomain('linear', extent[0], extent[1]);
if (extent[0] < 0 && extent[1] > 0) {
expect(domain[0]).to.be(extent[0]);
expect(domain[1]).to.be(extent[1]);
}
if (extent[0] < 0 && extent[1] < 0) {
expect(domain[0]).to.be(extent[0]);
expect(domain[1]).to.be(0);
}
if (extent[0] > 0 && extent[1] > 0) {
expect(domain[0]).to.be(0);
expect(domain[1]).to.be(extent[1]);
}
});
});
});
describe('getYAxis method', function () {
var mode, yMax, yScale;
beforeEach(function () {

View file

@ -32,8 +32,7 @@ define(function (require) {
var visLibParams = {
type: 'area',
addLegend: true,
addTooltip: true,
defaultYExtents: false
addTooltip: true
};
angular.module('AreaChartFactory', ['kibana']);
@ -228,8 +227,8 @@ define(function (require) {
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);
expect(yAxis.domain[0]).to.not.be(undefined);
expect(yAxis.domain[1]).to.not.be(undefined);
});
});
@ -270,8 +269,8 @@ define(function (require) {
var yAxis = chart.handler.yAxis;
var yVals = [vis.handler.data.getYMin(), vis.handler.data.getYMax()];
expect(yAxis.yMin).to.equal(yVals[0]);
expect(yAxis.yMax).to.equal(yVals[1]);
expect(yAxis.domain[0]).to.equal(yVals[0]);
expect(yAxis.domain[1]).to.equal(yVals[1]);
});
});
});

View file

@ -166,8 +166,8 @@ define(function (require) {
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);
expect(yAxis.domain[0]).to.not.be(undefined);
expect(yAxis.domain[1]).to.not.be(undefined);
});
});
@ -208,8 +208,8 @@ define(function (require) {
var yAxis = chart.handler.yAxis;
var yVals = [vis.handler.data.getYMin(), vis.handler.data.getYMax()];
expect(yAxis.yMin).to.equal(yVals[0]);
expect(yAxis.yMax).to.equal(yVals[1]);
expect(yAxis.domain[0]).to.equal(yVals[0]);
expect(yAxis.domain[1]).to.equal(yVals[1]);
});
});
});

View file

@ -143,8 +143,8 @@ define(function (require) {
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);
expect(yAxis.domain[0]).to.not.be(undefined);
expect(yAxis.domain[1]).to.not.be(undefined);
});
});
@ -185,8 +185,8 @@ define(function (require) {
var yAxis = chart.handler.yAxis;
var yVals = [vis.handler.data.getYMin(), vis.handler.data.getYMax()];
expect(yAxis.yMin).to.equal(yVals[0]);
expect(yAxis.yMax).to.equal(yVals[1]);
expect(yAxis.domain[0]).to.equal(yVals[0]);
expect(yAxis.domain[1]).to.equal(yVals[1]);
});
});
});