Skip to content

Commit

Permalink
feat(brush): add multi axis brushing
Browse files Browse the repository at this point in the history
This commit allow the consumer to configure the direction used for the brush tool. The direction is
by default along the X axis, but can be changed to be along the Y axis or have a rectangular
selection along both axis. For the Y axis values, we are returning an array of values, one for each
Y axis defined (this is determined by the groupId associated with different axis/series groups)

BREAKING CHANGE: The type used by the `BrushEndListener` is now changed to reflect this change. The
new type is on form of `{ x?: [number, number]; y?: Array<{ groupId: GroupId; values: [number,
number]; }> }` where x contains an array of [min,max] values, and the y property is an optional
array of objects, containing the GroupId and the values of the brush for that specific axis.

fix elastic#587
  • Loading branch information
markov00 committed Apr 10, 2020
1 parent de7df75 commit 6deb517
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 115 deletions.
61 changes: 18 additions & 43 deletions .playground/playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,61 +17,36 @@
* under the License. */

import React from 'react';
import {
Chart,
ScaleType,
Position,
Axis,
Settings,
PartitionElementEvent,
XYChartElementEvent,
BarSeries,
} from '../src';
import { Chart, ScaleType, Position, Axis, Settings, BarSeries, DataGenerator } from '../src';

export class Playground extends React.Component<{}, { isSunburstShown: boolean }> {
onClick = (elements: Array<PartitionElementEvent | XYChartElementEvent>) => {
// eslint-disable-next-line no-console
console.log(elements[0]);
};
export class Playground extends React.Component {
render() {
const dg = new DataGenerator();
const data = dg.generateBasicSeries(10);
const data2 = dg.generateBasicSeries(10);

return (
<>
<div className="chart">
<Chart size={[300, 200]}>
<Chart>
<Settings
onElementClick={this.onClick}
rotation={90}
theme={{
barSeriesStyle: {
displayValue: {
fontSize: 15,
fill: 'black',
offsetX: 5,
offsetY: -8,
},
},
brushAxis="both"
onBrushEnd={(values) => {
// eslint-disable-next-line no-console
console.log(values);
}}
/>
<Axis id="x" position={Position.Bottom} />
<Axis id="y1" position={Position.Left} />
<Axis id="y2" groupId="aaa" position={Position.Right} />
<BarSeries id="amount" xScaleType={ScaleType.Linear} xAccessor="x" yAccessors={['y']} data={data} />
<BarSeries
id="amount"
xScaleType={ScaleType.Ordinal}
id="amount2"
groupId="aaa"
xScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
data={[
{ x: 'trousers', y: 390, val: 1222 },
{ x: 'watches', y: 0, val: 1222 },
{ x: 'bags', y: 750, val: 1222 },
{ x: 'cocktail dresses', y: 854, val: 1222 },
]}
displayValueSettings={{
showValueLabel: true,
isValueContainedInElement: true,
hideClippedValue: true,
valueFormatter: (d) => {
return `${d} $`;
},
}}
data={data2}
/>
</Chart>
</div>
Expand Down
30 changes: 11 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 } 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,7 @@ 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] });
}
});
});
Expand Down
97 changes: 79 additions & 18 deletions src/chart_types/xy_chart/state/selectors/get_brush_area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ import { Dimensions } from '../../../../utils/dimensions';
import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation';
import { computeChartDimensionsSelector } from './compute_chart_dimensions';
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs';
import { BrushAxis } from '../../../../specs';
import { Rotation } from '../../../../utils/commons';
import { Point } from '../../../../utils/point';

const getMouseDownPosition = (state: GlobalChartState) => state.interactions.pointer.down;
const getCurrentPointerPosition = (state: GlobalChartState) => {
Expand All @@ -30,31 +34,88 @@ const getCurrentPointerPosition = (state: GlobalChartState) => {

/** @internal */
export const getBrushAreaSelector = createCachedSelector(
[getMouseDownPosition, getCurrentPointerPosition, getChartRotationSelector, computeChartDimensionsSelector],
(mouseDownPosition, cursorPosition, chartRotation, { chartDimensions }): Dimensions | null => {
[
getMouseDownPosition,
getCurrentPointerPosition,
getChartRotationSelector,
computeChartDimensionsSelector,
getSettingsSpecSelector,
],
(mouseDownPosition, cursorPosition, chartRotation, { chartDimensions }, { brushAxis }): Dimensions | null => {
if (!mouseDownPosition) {
return null;
}
const brushStart = {
x: mouseDownPosition.position.x,
y: mouseDownPosition.position.y,
};
if (chartRotation === 0 || chartRotation === 180) {
const area = {
left: brushStart.x - chartDimensions.left,
width: cursorPosition.x - brushStart.x,
top: 0,
height: chartDimensions.height,
};
return area;
} else {
const area = {
left: 0,
width: chartDimensions.width,
top: brushStart.y - chartDimensions.top,
height: cursorPosition.y - brushStart.y,
};
return area;
switch (brushAxis) {
case BrushAxis.Y:
return getBrushForYAxis(chartDimensions, chartRotation, cursorPosition, brushStart);
case BrushAxis.Both:
return getBrushForBothAxis(chartDimensions, cursorPosition, brushStart);
case BrushAxis.X:
default:
return getBrushForXAxis(chartDimensions, chartRotation, cursorPosition, brushStart);
}
},
)(getChartIdSelector);

function getBrushForXAxis(
chartDimensions: Dimensions,
chartRotation: Rotation,
cursorPosition: Point,
brushStart: Point,
) {
if (chartRotation === 0 || chartRotation === 180) {
const area = {
left: brushStart.x - chartDimensions.left,
width: cursorPosition.x - brushStart.x,
top: 0,
height: chartDimensions.height,
};
return area;
} else {
const area = {
left: 0,
width: chartDimensions.width,
top: brushStart.y - chartDimensions.top,
height: cursorPosition.y - brushStart.y,
};
return area;
}
}

function getBrushForYAxis(
chartDimensions: Dimensions,
chartRotation: Rotation,
cursorPosition: Point,
brushStart: Point,
) {
if (chartRotation === -90 || chartRotation === 90) {
const area = {
left: brushStart.x - chartDimensions.left,
width: cursorPosition.x - brushStart.x,
top: 0,
height: chartDimensions.height,
};
return area;
} else {
const area = {
left: 0,
width: chartDimensions.width,
top: brushStart.y - chartDimensions.top,
height: cursorPosition.y - brushStart.y,
};
return area;
}
}

function getBrushForBothAxis(chartDimensions: Dimensions, cursorPosition: Point, brushStart: Point) {
return {
left: brushStart.x - chartDimensions.left,
width: cursorPosition.x - brushStart.x,
top: brushStart.y - chartDimensions.top,
height: cursorPosition.y - brushStart.y,
};
}
Loading

0 comments on commit 6deb517

Please sign in to comment.