brush histogram 2 (#9140)

Backports PR #9039

**Commit 1:**
support scripted fields

* Original sha: 4a6e9fed25
* Authored by nreese <reese.nathan@gmail.com> on 2016-11-10T22:35:23Z
* Committed by CJ Cenizal <cj@cenizal.com> on 2016-11-10T23:30:53Z

**Commit 2:**
update column_chart test to check brush for ordered data

* Original sha: 97aab8a61d
* Authored by nreese <reese.nathan@gmail.com> on 2016-11-10T23:06:29Z
* Committed by CJ Cenizal <cj@cenizal.com> on 2016-11-10T23:32:28Z

**Commit 3:**
brush histogram

* Original sha: 3b6652f745
* Authored by nreese <reese.nathan@gmail.com> on 2016-10-18T20:42:23Z
* Committed by CJ Cenizal <cj@cenizal.com> on 2016-11-10T23:33:27Z

**Commit 4:**
incorporate changes request by cjcenizal

* Original sha: 0f7e70ab62
* Authored by nreese <reese.nathan@gmail.com> on 2016-11-01T22:08:38Z
* Committed by CJ Cenizal <cj@cenizal.com> on 2016-11-10T23:33:43Z

**Commit 5:**
fixed typo

* Original sha: 48e5bd6656
* Authored by nreese <reese.nathan@gmail.com> on 2016-11-02T03:39:41Z
* Committed by CJ Cenizal <cj@cenizal.com> on 2016-11-10T23:33:52Z

**Commit 6:**
support scripted fields

* Original sha: 4a2788109c
* Authored by nreese <reese.nathan@gmail.com> on 2016-11-10T22:35:23Z
* Committed by CJ Cenizal <cj@cenizal.com> on 2016-11-10T23:34:00Z

**Commit 7:**
update column_chart test to check brush for ordered data

* Original sha: 77c295a36b
* Authored by nreese <reese.nathan@gmail.com> on 2016-11-10T23:06:29Z
* Committed by CJ Cenizal <cj@cenizal.com> on 2016-11-10T23:35:31Z

**Commit 8:**
Fix init_x_axis unit tests.

* Original sha: 32d9ce167d
* Authored by CJ Cenizal <cj@cenizal.com> on 2016-11-11T22:13:06Z

**Commit 9:**
add brush_event test

* Original sha: 22807f2250
* Authored by nreese <reese.nathan@gmail.com> on 2016-11-16T03:56:35Z
* Committed by CJ Cenizal <cj@cenizal.com> on 2016-11-16T18:34:28Z

**Commit 10:**
Fix small timezone bug in brushEvent tests. Polish test descriptions.

* Original sha: 28c8a6a0ea
* Authored by CJ Cenizal <cj@cenizal.com> on 2016-11-16T19:20:43Z
This commit is contained in:
jasper 2016-11-18 13:27:52 -05:00 committed by CJ Cenizal
parent 2b04dde70f
commit d8fda1e201
10 changed files with 287 additions and 36 deletions

View file

@ -10,7 +10,7 @@ export default function visualizationLoader(savedVisualizations, Private) { // I
.then(function (savedVis) {
// $scope.state comes via $scope inheritence from the dashboard app. Don't love this.
savedVis.vis.listeners.click = filterBarClickHandler($scope.state);
savedVis.vis.listeners.brush = brushEvent;
savedVis.vis.listeners.brush = brushEvent($scope.state);
return {
savedObj: savedVis,

View file

@ -5,7 +5,6 @@ import 'ui/visualize';
import 'ui/doc_table';
import PluginsKibanaDashboardComponentsPanelLibLoadPanelProvider from 'plugins/kibana/dashboard/components/panel/lib/load_panel';
import FilterManagerProvider from 'ui/filter_manager';
import UtilsBrushEventProvider from 'ui/utils/brush_event';
import uiModules from 'ui/modules';
import panelTemplate from 'plugins/kibana/dashboard/components/panel/panel.html';
uiModules
@ -23,9 +22,6 @@ uiModules
};
});
const brushEvent = Private(UtilsBrushEventProvider);
const getPanelId = function (panel) {
return ['P', panel.panelIndex].join('-');
};

View file

@ -543,7 +543,7 @@ function discoverController($scope, config, courier, $route, $window, Notifier,
timefilter.time.to = moment(e.point.x + e.data.ordered.interval);
timefilter.time.mode = 'absolute';
},
brush: brushEvent
brush: brushEvent($scope.state)
},
aggs: visStateAggs
});

View file

@ -187,7 +187,7 @@ function VisEditor($scope, $route, timefilter, AppState, $location, kbnUrl, $tim
$scope.$on('$destroy', () => stateMonitor.destroy());
editableVis.listeners.click = vis.listeners.click = filterBarClickHandler($state);
editableVis.listeners.brush = vis.listeners.brush = brushEvent;
editableVis.listeners.brush = vis.listeners.brush = brushEvent($state);
// track state of editable vis vs. "actual" vis
$scope.stageEditableVis = transferVisState(editableVis, vis, true);

View file

@ -25,6 +25,8 @@ describe('initXAxis', function () {
}
}
};
const field = {};
const indexPattern = {};
it('sets the xAxisFormatter if the agg is not ordered', function () {
let chart = _.cloneDeep(baseChart);
@ -37,11 +39,19 @@ describe('initXAxis', function () {
it('makes the chart ordered if the agg is ordered', function () {
let chart = _.cloneDeep(baseChart);
chart.aspects.x.agg.type.ordered = true;
chart.aspects.x.agg.params = {
field: field
};
chart.aspects.x.agg.vis = {
indexPattern: indexPattern
};
initXAxis(chart);
expect(chart)
.to.have.property('xAxisLabel', 'label')
.and.have.property('xAxisFormatter', chart.aspects.x.agg.fieldFormatter())
.and.have.property('indexPattern', indexPattern)
.and.have.property('xAxisField', field)
.and.have.property('ordered');
expect(chart.ordered)
@ -53,11 +63,19 @@ describe('initXAxis', function () {
let chart = _.cloneDeep(baseChart);
chart.aspects.x.agg.type.ordered = true;
chart.aspects.x.agg.write = _.constant({ params: { interval: 10 } });
chart.aspects.x.agg.params = {
field: field
};
chart.aspects.x.agg.vis = {
indexPattern: indexPattern
};
initXAxis(chart);
expect(chart)
.to.have.property('xAxisLabel', 'label')
.and.have.property('xAxisFormatter', chart.aspects.x.agg.fieldFormatter())
.and.have.property('indexPattern', indexPattern)
.and.have.property('xAxisField', field)
.and.have.property('ordered');
expect(chart.ordered)

View file

@ -7,6 +7,9 @@ define(function () {
if (!x.agg || !x.agg.type.ordered) return;
chart.indexPattern = x.agg.vis.indexPattern;
chart.xAxisField = x.agg.params.field;
chart.ordered = {};
let xAggOutput = x.agg.write();
if (xAggOutput.params.interval) {

View file

@ -0,0 +1,177 @@
import _ from 'lodash';
import expect from 'expect.js';
import moment from 'moment';
import ngMock from 'ng_mock';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import UtilsBrushEventProvider from 'ui/utils/brush_event';
describe('brushEvent', function () {
let brushEventFn;
let timefilter;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private, $injector, _timefilter_) {
brushEventFn = Private(UtilsBrushEventProvider);
timefilter = _timefilter_;
}));
it('is a function that returns a function', function () {
expect(brushEventFn).to.be.a(Function);
expect(brushEventFn({})).to.be.a(Function);
});
describe('returned function', function () {
let $state;
let brushEvent;
const baseState = {
filters:[],
};
const baseEvent = {
data: {
fieldFormatter: _.constant({}),
},
};
beforeEach(ngMock.inject(function (Private, $injector) {
baseEvent.data.indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
$state = _.cloneDeep(baseState);
brushEvent = brushEventFn($state);
}));
it('should be a function', function () {
expect(brushEvent).to.be.a(Function);
});
it('ignores event when data.xAxisField not provided', function () {
const event = _.cloneDeep(baseEvent);
brushEvent(event);
expect($state)
.not.have.property('$newFilters');
});
describe('handles an event when the x-axis field is a date', function () {
let dateEvent;
const dateField = {
name: 'dateField',
type: 'date'
};
beforeEach(ngMock.inject(function (Private, $injector) {
dateEvent = _.cloneDeep(baseEvent);
dateEvent.data.xAxisField = dateField;
}));
it('by ignoring the event when range spans zero time', function () {
const event = _.cloneDeep(dateEvent);
event.range = [1388559600000, 1388559600000];
brushEvent(event);
expect($state)
.not.have.property('$newFilters');
});
it('by updating the timefilter', function () {
const event = _.cloneDeep(dateEvent);
const DAY_IN_MS = 24 * 60 * 60 * 1000;
event.range = [1388559600000, 1388559600000 + DAY_IN_MS];
brushEvent(event);
expect(timefilter.time.mode).to.be('absolute');
expect(moment.isMoment(timefilter.time.from))
.to.be(true);
// Set to a baseline timezone for comparison.
expect(timefilter.time.from.utcOffset(0).format('YYYY-MM-DD'))
.to.equal('2014-01-01');
expect(moment.isMoment(timefilter.time.to))
.to.be(true);
// Set to a baseline timezone for comparison.
expect(timefilter.time.to.utcOffset(0).format('YYYY-MM-DD'))
.to.equal('2014-01-02');
});
});
describe('handles an event when the x-axis field is a number', function () {
let numberEvent;
const numberField = {
name: 'numberField',
type: 'number'
};
beforeEach(ngMock.inject(function (Private, $injector) {
numberEvent = _.cloneDeep(baseEvent);
numberEvent.data.xAxisField = numberField;
}));
it('by ignoring the event when range does not span at least 2 values', function () {
const event = _.cloneDeep(numberEvent);
event.range = [1];
brushEvent(event);
expect($state)
.not.have.property('$newFilters');
});
it('by creating a new filter', function () {
const event = _.cloneDeep(numberEvent);
event.range = [1,2,3,4];
brushEvent(event);
expect($state)
.to.have.property('$newFilters');
expect($state.filters.length)
.to.equal(0);
expect($state.$newFilters.length)
.to.equal(1);
expect($state.$newFilters[0].range.numberField.gte)
.to.equal(1);
expect($state.$newFilters[0].range.numberField.lt)
.to.equal(4);
});
it('by updating the existing range filter', function () {
const event = _.cloneDeep(numberEvent);
event.range = [3,7];
$state.filters.push({
meta: {
key: 'numberField'
},
range: {gte: 1, lt: 4}
});
brushEvent(event);
expect($state)
.not.have.property('$newFilters');
expect($state.filters.length)
.to.equal(1);
expect($state.filters[0].range.numberField.gte)
.to.equal(3);
expect($state.filters[0].range.numberField.lt)
.to.equal(7);
});
it('by updating the existing scripted filter', function () {
const event = _.cloneDeep(numberEvent);
event.range = [3,7];
$state.filters.push({
meta: {
key: 'numberField'
},
script: {
script: {
params: {
gte: 1,
lt: 4
}
}
}
});
brushEvent(event);
expect($state)
.not.have.property('$newFilters');
expect($state.filters.length)
.to.equal(1);
expect($state.filters[0].script.script.params.gte)
.to.equal(3);
expect($state.filters[0].script.script.params.lt)
.to.equal(7);
});
});
});
});

View file

@ -1,13 +1,51 @@
import _ from 'lodash';
import moment from 'moment';
import buildRangeFilter from 'ui/filter_manager/lib/range';
export default function brushEventProvider(timefilter) {
return function (event) {
let from = moment(event.range[0]);
let to = moment(event.range[1]);
return $state => {
return event => {
if (!event.data.xAxisField) {
return;
}
if (to - from === 0) return;
switch (event.data.xAxisField.type) {
case 'date':
let from = moment(event.range[0]);
let to = moment(event.range[1]);
timefilter.time.from = from;
timefilter.time.to = to;
timefilter.time.mode = 'absolute';
if (to - from === 0) return;
timefilter.time.from = from;
timefilter.time.to = to;
timefilter.time.mode = 'absolute';
break;
case 'number':
if (event.range.length <= 1) return;
const existingFilter = $state.filters.find(filter => (
filter.meta && filter.meta.key === event.data.xAxisField.name
));
const min = event.range[0];
const max = event.range[event.range.length - 1];
const range = {gte: min, lt: max};
if (_.has(existingFilter, 'range')) {
existingFilter.range[event.data.xAxisField.name] = range;
} else if (_.has(existingFilter, 'script.script.params.gte')
&& _.has(existingFilter, 'script.script.params.lt')) {
existingFilter.script.script.params.gte = min;
existingFilter.script.script.params.lt = max;
} else {
const newFilter = buildRangeFilter(
event.data.xAxisField,
range,
event.data.indexPattern,
event.data.xAxisFormatter);
$state.$newFilters = [newFilter];
}
break;
}
};
};
};

View file

@ -8,7 +8,7 @@ import series from 'fixtures/vislib/mock_data/date_histogram/_series';
import seriesPosNeg from 'fixtures/vislib/mock_data/date_histogram/_series_pos_neg';
import seriesNeg from 'fixtures/vislib/mock_data/date_histogram/_series_neg';
import termsColumns from 'fixtures/vislib/mock_data/terms/_columns';
//const histogramRows = require('fixtures/vislib/mock_data/histogram/_rows');
import histogramRows from 'fixtures/vislib/mock_data/histogram/_rows';
import stackedSeries from 'fixtures/vislib/mock_data/date_histogram/_stacked_series';
import $ from 'jquery';
import FixturesVislibVisFixtureProvider from 'fixtures/vislib/_vis_fixture';
@ -20,7 +20,7 @@ const dataTypesArray = [
['series with positive and negative values', 'stacked', seriesPosNeg],
['series with negative values', 'stacked', seriesNeg],
['terms columns', 'grouped', termsColumns],
// ['histogram rows', 'percentage', histogramRows],
['histogram rows', 'percentage', histogramRows],
['stackedSeries', 'stacked', stackedSeries],
];
@ -116,12 +116,12 @@ dataTypesArray.forEach(function (dataType, i) {
};
}
it('should attach the brush if data is a set of ordered dates', function () {
it('should attach the brush if data is a set is ordered', function () {
vis.handler.charts.forEach(function (chart) {
const has = checkChart(chart);
const ordered = vis.handler.data.get('ordered');
const date = Boolean(ordered && ordered.date);
expect(has.brush).to.be(date);
const allowBrushing = Boolean(ordered);
expect(has.brush).to.be(allowBrushing);
});
});

View file

@ -162,10 +162,9 @@ export default function DispatchClass(Private, config) {
*/
allowBrushing() {
const xAxis = this.handler.xAxis;
// Don't allow brushing for time based charts from non-time-based indices
const hasTimeField = this.handler.vis._attr.hasTimeField;
return Boolean(hasTimeField && xAxis.ordered && xAxis.xScale && _.isFunction(xAxis.xScale.invert));
//Allow brushing for ordered axis - date histogram and histogram
return Boolean(xAxis.ordered);
};
/**
@ -186,31 +185,35 @@ export default function DispatchClass(Private, config) {
addBrushEvent(svg) {
if (!this.isBrushable()) return;
const self = this;
const xScale = this.handler.xAxis.xScale;
const brush = this.createBrush(xScale, svg);
function brushEnd() {
function simulateClickWithBrushEnabled(d, i) {
if (!validBrushClick(d3.event)) return;
const bar = d3.select(this);
const startX = d3.mouse(svg.node());
const startXInv = xScale.invert(startX[0]);
if (isQuantitativeScale(xScale)) {
const bar = d3.select(this);
const startX = d3.mouse(svg.node());
const startXInv = xScale.invert(startX[0]);
// Reset the brush value
brush.extent([startXInv, startXInv]);
// Reset the brush value
brush.extent([startXInv, startXInv]);
// Magic!
// Need to call brush on svg to see brush when brushing
// while on top of bars.
// Need to call brush on bar to allow the click event to be registered
svg.call(brush);
bar.call(brush);
// Magic!
// Need to call brush on svg to see brush when brushing
// while on top of bars.
// Need to call brush on bar to allow the click event to be registered
svg.call(brush);
bar.call(brush);
} else {
self.emit('click', self.eventResponse(d, i));
}
}
return this.addEvent('mousedown', brushEnd);
return this.addEvent('mousedown', simulateClickWithBrushEnabled);
};
/**
* Mouseover Behavior
*
@ -306,6 +309,22 @@ export default function DispatchClass(Private, config) {
};
}
/**
* Determine if d3.Scale is quantitative
*
* @param element {d3.Scale}
* @method isQuantitativeScale
* @returns {boolean}
*/
function isQuantitativeScale(scale) {
//Invert is a method that only exists on quantitative scales
if (scale.invert) {
return true;
} else {
return false;
}
}
function validBrushClick(event) {
return event.button === 0;
}