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

feat(brush): add multi axis brushing #625

Merged
merged 12 commits into from
Apr 28, 2020
66 changes: 66 additions & 0 deletions integration/page_objects/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,51 @@ class CommonPage {
await page.mouse.click(element.left + mousePosition.x, element.top + mousePosition.y);
}

/**
* Drag mouse relative to element
*
* @param mousePosition
* @param selector
*/
async dragMouseRelativeToDOMElement(
start: { x: number; y: number },
end: { x: number; y: number },
selector: string,
) {
const element = await this.getBoundingClientRect(selector);
await page.mouse.move(element.left + start.x, element.top + start.y);
await page.mouse.down();
await page.mouse.move(element.left + end.x, element.top + end.y);
}
nickofthyme marked this conversation as resolved.
Show resolved Hide resolved

/**
* Drop mouse
*
* @param mousePosition
* @param selector
*/
async dropMouse() {
await page.mouse.up();
}

/**
* Drag and drop mouse relative to element
*
* @param mousePosition
* @param selector
*/
async dragAndDropMouseRelativeToDOMElement(
start: { x: number; y: number },
end: { x: number; y: number },
selector: string,
) {
const element = await this.getBoundingClientRect(selector);
await page.mouse.move(element.left + start.x, element.top + start.y);
await page.mouse.down();
await page.mouse.move(element.left + end.x, element.top + end.y);
await page.mouse.up();
}

/**
* Expect an element given a url and selector from storybook
*
Expand Down Expand Up @@ -201,6 +246,27 @@ class CommonPage {
});
}

/**
* Expect a chart given a url from storybook with mouse move
*
* @param url Storybook url from knobs section
* @param start - the start postion of mouse relative to chart
* @param end - the end postion of mouse relative to chart
* @param options
*/
async expectChartWithDragAtUrlToMatchScreenshot(
markov00 marked this conversation as resolved.
Show resolved Hide resolved
url: string,
start: { x: number; y: number },
end: { x: number; y: number },
options?: Omit<ScreenshotElementAtUrlOptions, 'action'>,
) {
const action = async () => await this.dragMouseRelativeToDOMElement(start, end, this.chartSelector);
await this.expectChartAtUrlToMatchScreenshot(url, {
...options,
action,
});
}

/**
* Loads storybook page from raw url, and waits for element
*
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions integration/tests/interactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,49 @@ describe('Tooltips', () => {
},
);
});
it('show rectangular brush selection', async () => {
await common.expectChartWithDragAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/interactions--brush-tool',
{ x: 100, y: 100 },
{ x: 250, y: 250 },
);
});
it('show y brush selection', async () => {
await common.expectChartWithDragAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/interactions--brush-tool&knob-brush axis=y&knob-chartRotation=0',
{ x: 100, y: 100 },
{ x: 250, y: 250 },
);
});
it('show x brush selection', async () => {
await common.expectChartWithDragAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/interactions--brush-tool&knob-brush axis=x&knob-chartRotation=0',
{ x: 100, y: 100 },
{ x: 250, y: 250 },
);
});

it('show rectangular brush selection -90 degree', async () => {
await common.expectChartWithDragAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/interactions--brush-tool&knob-brush axis=both&knob-chartRotation=-90',
{ x: 100, y: 100 },
{ x: 250, y: 250 },
);
});
it('show y brush selection -90 degree', async () => {
await common.expectChartWithDragAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/interactions--brush-tool&knob-brush axis=y&knob-chartRotation=-90',
{ x: 100, y: 100 },
{ x: 250, y: 250 },
);
});
it('show x brush selection -90 degree', async () => {
await common.expectChartWithDragAtUrlToMatchScreenshot(
'http://localhost:9001/?path=/story/interactions--brush-tool&knob-brush axis=x&knob-chartRotation=-90',
{ x: 100, y: 100 },
{ x: 250, y: 250 },
);
});
});
it('should render corrent tooltip for split and y accessors', async () => {
await common.expectChartWithMouseAtUrlToMatchScreenshot(
Expand Down
184 changes: 165 additions & 19 deletions src/chart_types/xy_chart/state/chart_state.interactions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { BarSeriesSpec, BasicSeriesSpec, AxisSpec, SeriesTypes } from '../utils/
import { Position } from '../../../utils/commons';
import { ScaleType } from '../../../scales';
import { chartStoreReducer, GlobalChartState } from '../../../state/chart_state';
import { SettingsSpec, DEFAULT_SETTINGS_SPEC, SpecTypes, TooltipType } from '../../../specs';
import { SettingsSpec, DEFAULT_SETTINGS_SPEC, SpecTypes, TooltipType, XYBrushArea, BrushAxis } from '../../../specs';
import { computeSeriesGeometriesSelector } from './selectors/compute_series_geometries';
import { getProjectedPointerPositionSelector } from './selectors/get_projected_pointer_position';
import {
Expand Down Expand Up @@ -782,7 +782,7 @@ function mouseOverTestSuite(scaleType: ScaleType) {
});
describe('brush', () => {
test('can respond to a brush end event', () => {
const brushEndListener = jest.fn<void, [number, number]>((): void => {
const brushEndListener = jest.fn<void, [XYBrushArea]>((): void => {
return;
});
const onBrushCaller = createOnBrushEndCaller();
Expand Down Expand Up @@ -827,8 +827,7 @@ function mouseOverTestSuite(scaleType: ScaleType) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[0][0]).toBe(0);
expect(brushEndListener.mock.calls[0][1]).toBe(2.5);
expect(brushEndListener.mock.calls[0][0]).toEqual({ x: [0, 2.5] });
}
const start2 = { x: 75, y: 0 };
const end2 = { x: 100, y: 0 };
Expand All @@ -840,8 +839,7 @@ function mouseOverTestSuite(scaleType: ScaleType) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[1][0]).toBe(2.5);
expect(brushEndListener.mock.calls[1][1]).toBe(3);
expect(brushEndListener.mock.calls[1][0]).toEqual({ x: [2.5, 3] });
}

const start3 = { x: 75, y: 0 };
Expand All @@ -853,8 +851,7 @@ function mouseOverTestSuite(scaleType: ScaleType) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[2][0]).toBe(2.5);
expect(brushEndListener.mock.calls[2][1]).toBe(3);
expect(brushEndListener.mock.calls[2][0]).toEqual({ x: [2.5, 3] });
}

const start4 = { x: 25, y: 0 };
Expand All @@ -866,12 +863,11 @@ function mouseOverTestSuite(scaleType: ScaleType) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[3][0]).toBe(0);
expect(brushEndListener.mock.calls[3][1]).toBe(0.5);
expect(brushEndListener.mock.calls[3][0]).toEqual({ x: [0, 0.5] });
}
});
test('can respond to a brush end event on rotated chart', () => {
const brushEndListener = jest.fn<void, [number, number]>((): void => {
const brushEndListener = jest.fn<void, [XYBrushArea]>((): void => {
return;
});
const onBrushCaller = createOnBrushEndCaller();
Expand Down Expand Up @@ -906,8 +902,7 @@ function mouseOverTestSuite(scaleType: ScaleType) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[0][0]).toBe(0);
expect(brushEndListener.mock.calls[0][1]).toBe(1);
expect(brushEndListener.mock.calls[0][0]).toEqual({ x: [0, 1] });
}
const start2 = { x: 0, y: 75 };
const end2 = { x: 0, y: 100 };
Expand All @@ -919,8 +914,7 @@ function mouseOverTestSuite(scaleType: ScaleType) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[1][0]).toBe(1);
expect(brushEndListener.mock.calls[1][1]).toBe(1);
expect(brushEndListener.mock.calls[1][0]).toEqual({ x: [1, 1] });
}

const start3 = { x: 0, y: 75 };
Expand All @@ -932,8 +926,7 @@ function mouseOverTestSuite(scaleType: ScaleType) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[2][0]).toBe(1);
expect(brushEndListener.mock.calls[2][1]).toBe(1); // max of chart
expect(brushEndListener.mock.calls[2][0]).toEqual({ x: [1, 1] }); // max of chart
}

const start4 = { x: 0, y: 25 };
Expand All @@ -945,8 +938,161 @@ function mouseOverTestSuite(scaleType: ScaleType) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[3][0]).toBe(0);
expect(brushEndListener.mock.calls[3][1]).toBe(0);
expect(brushEndListener.mock.calls[3][0]).toEqual({ x: [0, 0] });
}
});
test('can respond to a Y brush', () => {
const brushEndListener = jest.fn<void, [XYBrushArea]>((): void => {
return;
});
const onBrushCaller = createOnBrushEndCaller();
store.subscribe(() => {
onBrushCaller(store.getState());
});
const settings = getSettingsSpecSelector(store.getState());
const updatedSettings: SettingsSpec = {
...settings,
brushAxis: BrushAxis.Y,
theme: {
...settings.theme,
chartMargins: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
},
onBrushEnd: brushEndListener,
};
store.dispatch(upsertSpec(updatedSettings));
store.dispatch(
upsertSpec({
...spec,
data: [
[0, 1],
[1, 1],
[2, 2],
[3, 3],
],
} as BarSeriesSpec),
);
store.dispatch(specParsed());

const start1 = { x: 0, y: 0 };
const end1 = { x: 0, y: 75 };

store.dispatch(onMouseDown(start1, 0));
store.dispatch(onPointerMove(end1, 1));
store.dispatch(onMouseUp(end1, 3));
if (scaleType === ScaleType.Ordinal) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[0][0]).toEqual({
y: [
{
groupId: spec.groupId,
extent: [0.75, 3],
},
],
});
}
const start2 = { x: 0, y: 75 };
const end2 = { x: 0, y: 100 };

store.dispatch(onMouseDown(start2, 4));
store.dispatch(onPointerMove(end2, 5));
store.dispatch(onMouseUp(end2, 6));
if (scaleType === ScaleType.Ordinal) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[1][0]).toEqual({
y: [
{
groupId: spec.groupId,
extent: [0, 0.75],
},
],
});
}
});
test('can respond to rectangular brush', () => {
const brushEndListener = jest.fn<void, [XYBrushArea]>((): void => {
return;
});
const onBrushCaller = createOnBrushEndCaller();
store.subscribe(() => {
onBrushCaller(store.getState());
});
const settings = getSettingsSpecSelector(store.getState());
const updatedSettings: SettingsSpec = {
...settings,
brushAxis: BrushAxis.Both,
theme: {
...settings.theme,
chartMargins: {
left: 0,
right: 0,
top: 0,
bottom: 0,
},
},
onBrushEnd: brushEndListener,
};
store.dispatch(upsertSpec(updatedSettings));
store.dispatch(
upsertSpec({
...spec,
data: [
[0, 1],
[1, 1],
[2, 2],
[3, 3],
],
} as BarSeriesSpec),
);
store.dispatch(specParsed());

const start1 = { x: 0, y: 0 };
const end1 = { x: 75, y: 75 };

store.dispatch(onMouseDown(start1, 0));
store.dispatch(onPointerMove(end1, 1));
store.dispatch(onMouseUp(end1, 3));
if (scaleType === ScaleType.Ordinal) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[0][0]).toEqual({
x: [0, 2.5],
y: [
{
groupId: spec.groupId,
extent: [0.75, 3],
},
],
});
}
const start2 = { x: 75, y: 75 };
const end2 = { x: 100, y: 100 };

store.dispatch(onMouseDown(start2, 4));
store.dispatch(onPointerMove(end2, 5));
store.dispatch(onMouseUp(end2, 6));
if (scaleType === ScaleType.Ordinal) {
expect(brushEndListener).not.toBeCalled();
} else {
expect(brushEndListener).toBeCalled();
expect(brushEndListener.mock.calls[1][0]).toEqual({
x: [2.5, 3],
y: [
{
groupId: spec.groupId,
extent: [0, 0.75],
},
],
});
}
});
});
Expand Down
Loading