mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
2b04dde70f
commit
d8fda1e201
10 changed files with 287 additions and 36 deletions
|
@ -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,
|
||||
|
|
|
@ -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('-');
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
177
src/ui/public/utils/__tests__/brush_event.js
Normal file
177
src/ui/public/utils/__tests__/brush_event.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue