Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lens] Allow user to drag and select a subset of the timeline in the chart (aka brush interaction) #62636

Merged
merged 25 commits into from
Apr 30, 2020
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
c213932
feat: brushing basic example for time histogram
mbondyra Apr 6, 2020
8c99c2d
test: added
mbondyra Apr 7, 2020
0e83e13
refactor: simplify the structure
mbondyra Apr 8, 2020
e868030
Merge branch 'master' into brushing-on-lens
elasticmachine Apr 8, 2020
4ff7e63
refactor: move to inline function
mbondyra Apr 8, 2020
e984281
refactor
mbondyra Apr 9, 2020
5806bd6
Merge branch 'master' into brushing-on-lens
elasticmachine Apr 13, 2020
8e23141
refactor
mbondyra Apr 14, 2020
5a6bfe9
Merge branch 'master' into brushing-on-lens
elasticmachine Apr 14, 2020
609b8c0
Merge remote-tracking branch 'origin/master' into HEAD
wylieconlon Apr 14, 2020
436cc5a
Always use time field from index pattern
wylieconlon Apr 14, 2020
b162d60
types
mbondyra Apr 15, 2020
f6228cb
use the meta.aggConfigParams for timefieldName
mbondyra Apr 16, 2020
1fb9a9d
Merge commit 'fdc962f348ed54c63e49c8f745753caadf394ad6' into brushing…
mbondyra Apr 16, 2020
3b59c4b
fix: test snapshot update
mbondyra Apr 17, 2020
61695ad
Update embeddable.tsx
mbondyra Apr 20, 2020
789baf1
Merge branch 'master' into brushing-on-lens
elasticmachine Apr 20, 2020
006c9cf
Merge commit 'ffc8741da2ffcf4d31ae3ccc143fd483a61cbb1b' into brushing…
mbondyra Apr 22, 2020
7ce6088
fix: moment remov
mbondyra Apr 22, 2020
0919adc
Merge branch 'master' into brushing-on-lens
elasticmachine Apr 24, 2020
793aaa1
Merge branch 'master' into brushing-on-lens
elasticmachine Apr 27, 2020
19ae66f
Merge commit '6e2691358fdb59af717a9aaa2b32887b9cdf2d2a' into brushing…
mbondyra Apr 30, 2020
a3491ee
fix: corrections for adapting to timepicker on every timefield
mbondyra Apr 30, 2020
6c94c86
fix: fix single bar condition
mbondyra Apr 30, 2020
674564a
types
mbondyra Apr 30, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,7 @@ export class Embeddable extends AbstractEmbeddable<LensEmbeddableInput, LensEmbe
public supportedTriggers() {
switch (this.savedVis.visualizationType) {
case 'lnsXY':
// TODO: case 'lnsDatatable':
return [VIS_EVENT_TO_TRIGGER.filter];

return [VIS_EVENT_TO_TRIGGER.filter, VIS_EVENT_TO_TRIGGER.brush];
case 'lnsMetric':
default:
return [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { createGetterSetter } from '../../../../../src/plugins/kibana_utils/public';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { createGetterSetter } from '../../../../src/plugins/kibana_utils/public';
import { UiActionsStart } from '../../../../src/plugins/ui_actions/public';

export const [getExecuteTriggerActions, setExecuteTriggerActions] = createGetterSetter<
UiActionsStart['executeTriggerActions']
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion x-pack/plugins/lens/public/xy_visualization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { xyVisualization } from './xy_visualization';
import { xyChart, getXyChartRenderer } from './xy_expression';
import { legendConfig, xConfig, layerConfig } from './types';
import { EditorFrameSetup, FormatFactory } from '../types';
import { setExecuteTriggerActions } from '../services';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { setExecuteTriggerActions } from './services';

export interface XyVisualizationPluginSetupPlugins {
expressions: ExpressionsSetup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export const buildExpression = (
.concat(layer.splitAccessor ? [layer.splitAccessor] : [])
.forEach(accessor => {
const operation = datasource.getOperationForColumnId(accessor);
if (operation && operation.label) {
if (operation?.label) {
columnToLabel[accessor] = operation.label;
}
});
Expand Down
171 changes: 171 additions & 0 deletions x-pack/plugins/lens/public/xy_visualization/xy_expression.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,145 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';

const executeTriggerActions = jest.fn();

const dateHistogramData: LensMultiTable = {
type: 'lens_multitable',
tables: {
timeLayer: {
type: 'kibana_datatable',
rows: [
{
xAccessorId: 1585758120000,
splitAccessorId: "Men's Clothing",
yAccessorId: 1,
},
{
xAccessorId: 1585758360000,
splitAccessorId: "Women's Accessories",
yAccessorId: 1,
},
{
xAccessorId: 1585758360000,
splitAccessorId: "Women's Clothing",
yAccessorId: 1,
},
{
xAccessorId: 1585759380000,
splitAccessorId: "Men's Clothing",
yAccessorId: 1,
},
{
xAccessorId: 1585759380000,
splitAccessorId: "Men's Shoes",
yAccessorId: 1,
},
{
xAccessorId: 1585759380000,
splitAccessorId: "Women's Clothing",
yAccessorId: 1,
},
{
xAccessorId: 1585760700000,
splitAccessorId: "Men's Clothing",
yAccessorId: 1,
},
{
xAccessorId: 1585760760000,
splitAccessorId: "Men's Clothing",
yAccessorId: 1,
},
{
xAccessorId: 1585760760000,
splitAccessorId: "Men's Shoes",
yAccessorId: 1,
},
{
xAccessorId: 1585761120000,
splitAccessorId: "Men's Shoes",
yAccessorId: 1,
},
],
columns: [
{
id: 'xAccessorId',
name: 'order_date per minute',
meta: {
type: 'date_histogram',
indexPatternId: 'indexPatternId',
aggConfigParams: {
field: 'order_date',
timeRange: { from: '2020-04-01T16:14:16.246Z', to: '2020-04-01T17:15:41.263Z' },
useNormalizedEsInterval: true,
scaleMetricValues: false,
interval: '1m',
drop_partials: false,
min_doc_count: 0,
extended_bounds: {},
},
},
formatHint: { id: 'date', params: { pattern: 'HH:mm' } },
},
{
id: 'splitAccessorId',
name: 'Top values of category.keyword',
meta: {
type: 'terms',
indexPatternId: 'indexPatternId',
aggConfigParams: {
field: 'category.keyword',
orderBy: 'yAccessorId',
order: 'desc',
size: 3,
otherBucket: false,
otherBucketLabel: 'Other',
missingBucket: false,
missingBucketLabel: 'Missing',
},
},
formatHint: {
id: 'terms',
params: {
id: 'string',
otherBucketLabel: 'Other',
missingBucketLabel: 'Missing',
parsedUrl: {
origin: 'http://localhost:5601',
pathname: '/jiy/app/kibana',
basePath: '/jiy',
},
},
},
},
{
id: 'yAccessorId',
name: 'Count of records',
meta: {
type: 'count',
indexPatternId: 'indexPatternId',
aggConfigParams: {},
},
formatHint: { id: 'number' },
},
],
},
},
dateRange: {
fromDate: new Date('2020-04-01T16:14:16.246Z'),
toDate: new Date('2020-04-01T17:15:41.263Z'),
},
};

const dateHistogramLayer: LayerArgs = {
layerId: 'timeLayer',
hide: false,
xAccessor: 'xAccessorId',
yScaleType: 'linear',
xScaleType: 'time',
isHistogram: true,
splitAccessor: 'splitAccessorId',
seriesType: 'bar_stacked',
accessors: ['yAccessorId'],
};

const createSampleDatatableWithRows = (rows: KibanaDatatableRow[]): KibanaDatatable => ({
type: 'kibana_datatable',
columns: [
Expand Down Expand Up @@ -438,6 +577,38 @@ describe('xy_expression', () => {
expect(component.find(Settings).prop('rotation')).toEqual(90);
});

test('onBrushEnd returns correct context data for date histogram data', () => {
const { args } = sampleArgs();

const wrapper = mountWithIntl(
<XYChart
data={dateHistogramData}
args={{
...args,
layers: [dateHistogramLayer],
}}
formatFactory={getFormatSpy}
timeZone="UTC"
chartTheme={{}}
executeTriggerActions={executeTriggerActions}
/>
);

wrapper
.find(Settings)
.first()
.prop('onBrushEnd')!(1585757732783, 1585758880838);

expect(executeTriggerActions).toHaveBeenCalledWith('SELECT_RANGE_TRIGGER', {
data: {
column: 0,
table: dateHistogramData.tables.timeLayer,
range: [1585757732783, 1585758880838],
},
timeFieldName: 'order_date',
});
});

test('onElementClick returns correct context data', () => {
const geometry: GeometryValue = { x: 5, y: 1, accessor: 'y1', mark: null };
const series = {
Expand Down
58 changes: 40 additions & 18 deletions x-pack/plugins/lens/public/xy_visualization/xy_expression.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,19 @@ import {
import { EuiIcon, EuiText, IconType, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { i18n } from '@kbn/i18n';
import { ValueClickTriggerContext } from '../../../../../src/plugins/embeddable/public';
import { KibanaDatatableColumn } from '../../../../../src/plugins/expressions/public';
import {
ValueClickTriggerContext,
RangeSelectTriggerContext,
} from '../../../../../src/plugins/embeddable/public';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../src/plugins/visualizations/public';
import { LensMultiTable, FormatFactory } from '../types';
import { XYArgs, SeriesType, visualizationTypes } from './types';
import { VisualizationContainer } from '../visualization_container';
import { isHorizontalChart } from './state_helpers';
import { getExecuteTriggerActions } from '../services';
import { UiActionsStart } from '../../../../../src/plugins/ui_actions/public';
import { parseInterval } from '../../../../../src/plugins/data/common';
import { getExecuteTriggerActions } from './services';

type InferPropType<T> = T extends React.FunctionComponent<infer P> ? P : T;
type SeriesSpec = InferPropType<typeof LineSeries> &
Expand Down Expand Up @@ -190,7 +194,7 @@ export function XYChart({
}

// use formatting hint of first x axis column to format ticks
const xAxisColumn = Object.values(data.tables)[0].columns.find(
const xAxisColumn: KibanaDatatableColumn | undefined = Object.values(data.tables)[0].columns.find(
({ id }) => id === layers[0].xAccessor
);
const xAxisFormatter = formatFactory(xAxisColumn && xAxisColumn.formatHint);
Expand All @@ -211,21 +215,21 @@ export function XYChart({
const shouldRotate = isHorizontalChart(layers);

const xTitle = (xAxisColumn && xAxisColumn.name) || args.xTitle;

// add minInterval only for single row value as it cannot be determined from dataset

const minInterval = layers.every(layer => data.tables[layer.layerId].rows.length <= 1)
? parseInterval(xAxisColumn?.meta?.aggConfigParams?.interval)?.asMilliseconds()
: undefined;

const xDomain =
data.dateRange && layers.every(l => l.xScaleType === 'time')
? {
min: data.dateRange.fromDate.getTime(),
max: data.dateRange.toDate.getTime(),
minInterval,
}
: undefined;
const isTimeViz = data.dateRange && layers.every(l => l.xScaleType === 'time');

const xDomain = isTimeViz
? {
min: data.dateRange?.fromDate.getTime(),
max: data.dateRange?.toDate.getTime(),
minInterval,
}
: undefined;

return (
<Chart>
<Settings
Expand All @@ -235,6 +239,28 @@ export function XYChart({
theme={chartTheme}
rotation={shouldRotate ? 90 : 0}
xDomain={xDomain}
onBrushEnd={(min: number, max: number) => {
// in the future we want to make it also for histogram
if (!xAxisColumn || !isTimeViz) {
return;
}

const firstLayerWithData =
layers[layers.findIndex(layer => data.tables[layer.layerId].rows.length)];
const table = data.tables[firstLayerWithData.layerId];

const xAxisFieldName = xAxisColumn?.meta?.aggConfigParams?.field;
const timeFieldName = xDomain && xAxisFieldName;
const context: RangeSelectTriggerContext = {
data: {
range: [min, max],
table,
column: table.columns.findIndex(el => el.id === firstLayerWithData.xAccessor),
},
timeFieldName,
};
executeTriggerActions(VIS_EVENT_TO_TRIGGER.brush, context);
}}
onElementClick={([[geometry, series]]) => {
// for xyChart series is always XYChartSeriesIdentifier and geometry is always type of GeometryValue
const xySeries = series as XYChartSeriesIdentifier;
Expand Down Expand Up @@ -271,10 +297,7 @@ export function XYChart({
});
}

const xAxisFieldName: string | undefined = table.columns.find(
col => col.id === layer.xAccessor
)?.meta?.aggConfigParams?.field;

const xAxisFieldName = xAxisColumn?.meta?.aggConfigParams?.field;
const timeFieldName = xDomain && xAxisFieldName;

const context: ValueClickTriggerContext = {
Expand All @@ -288,7 +311,6 @@ export function XYChart({
},
timeFieldName,
};

executeTriggerActions(VIS_EVENT_TO_TRIGGER.filter, context);
}}
/>
Expand Down