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

fix(brush): force brush tool per panel #1071

Merged
merged 5 commits into from
Mar 11, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
127 changes: 81 additions & 46 deletions src/chart_types/xy_chart/state/selectors/get_brush_area.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ import { GlobalChartState } from '../../../../state/chart_state';
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs';
import { Rotation } from '../../../../utils/common';
import { clamp, Rotation } from '../../../../utils/common';
import { Dimensions } from '../../../../utils/dimensions';
import { Point } from '../../../../utils/point';
import { isVerticalRotation } from '../utils/common';
import { computeChartDimensionsSelector } from './compute_chart_dimensions';
import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales';

const MIN_AREA_SIZE = 1;

const getMouseDownPosition = (state: GlobalChartState) => state.interactions.pointer.down;
const getMouseDownPosition = (state: GlobalChartState) => state.interactions.pointer.down?.position;
const getCurrentPointerPosition = (state: GlobalChartState) => state.interactions.pointer.current.position;

/** @internal */
Expand All @@ -43,81 +44,115 @@ export const getBrushAreaSelector = createCachedSelector(
getChartRotationSelector,
computeChartDimensionsSelector,
getSettingsSpecSelector,
computeSmallMultipleScalesSelector,
],
(mouseDownPosition, end, chartRotation, { chartDimensions }, { brushAxis }): Dimensions | null => {
if (!mouseDownPosition) {
(start, end, chartRotation, { chartDimensions }, { brushAxis }, smallMultipleScales): Dimensions | null => {
if (!start) {
return null;
}
const start = {
x: mouseDownPosition.position.x,
y: mouseDownPosition.position.y,
};
const plotStartPointPx = getPlotAreaRestrictedPoint(start, chartDimensions);
const plotEndPointPx = getPlotAreaRestrictedPoint(end, chartDimensions);
const panelPoints = getPointsConstraintToSinglePanel(plotStartPointPx, plotEndPointPx, smallMultipleScales);

switch (brushAxis) {
case BrushAxis.Y:
return getBrushForYAxis(chartDimensions, chartRotation, start, end);
return getBrushForYAxis(chartRotation, panelPoints);
case BrushAxis.Both:
return getBrushForBothAxis(chartDimensions, start, end);
return getBrushForBothAxis(panelPoints);
case BrushAxis.X:
default:
return getBrushForXAxis(chartDimensions, chartRotation, start, end);
return getBrushForXAxis(chartRotation, panelPoints);
}
},
)(getChartIdSelector);

/** @internal */
export function getBrushForXAxis(chartDimensions: Dimensions, chartRotation: Rotation, start: Point, end: Point) {
const rotated = isVerticalRotation(chartRotation);
export type PanelPoints = {
start: Point;
end: Point;
hPanelStart: number;
hPanelWidth: number;
vPanelStart: number;
vPanelHeight: number;
};

export function getPointsConstraintToSinglePanel(
startPlotPoint: Point,
endPlotPoint: Point,
{ horizontal, vertical }: SmallMultipleScales,
): PanelPoints {
const hPanel = horizontal.invert(startPlotPoint.x);
const vPanel = vertical.invert(startPlotPoint.y);

const hPanelStart = horizontal.scale(hPanel) ?? 0;
const hPanelEnd = hPanelStart + horizontal.bandwidth;

const vPanelStart = vertical.scale(vPanel) ?? 0;
const vPanelEnd = vPanelStart + vertical.bandwidth;

const x = clamp(endPlotPoint.x, hPanelStart, hPanelEnd);
const y = clamp(endPlotPoint.y, vPanelStart, vPanelEnd);
return {
left: rotated ? 0 : getLeftPoint(chartDimensions, start),
top: rotated ? getTopPoint(chartDimensions, start) : 0,
height: rotated ? getMinimalHeight(start, end) : chartDimensions.height,
width: rotated ? chartDimensions.width : getMinimalWidth(start, end),
start: startPlotPoint,
end: { x, y },
hPanelStart,
hPanelWidth: horizontal.bandwidth,
vPanelStart,
vPanelHeight: vertical.bandwidth,
};
}

/** @internal */
export function getBrushForYAxis(chartDimensions: Dimensions, chartRotation: Rotation, start: Point, end: Point) {
const rotated = isVerticalRotation(chartRotation);
export function getPlotAreaRestrictedPoint({ x, y }: Point, { left, top }: Dimensions) {
return {
left: rotated ? getLeftPoint(chartDimensions, start) : 0,
top: rotated ? 0 : getTopPoint(chartDimensions, start),
height: rotated ? chartDimensions.height : getMinimalHeight(start, end),
width: rotated ? getMinimalWidth(start, end) : chartDimensions.width,
x: x - left,
y: y - top,
};
}

/** @internal */
export function getBrushForBothAxis(chartDimensions: Dimensions, start: Point, end: Point) {
export function getBrushForXAxis(
chartRotation: Rotation,
{ hPanelStart, vPanelStart, hPanelWidth, vPanelHeight, start, end }: PanelPoints,
) {
const rotated = isVerticalRotation(chartRotation);

return {
left: getLeftPoint(chartDimensions, start),
top: getTopPoint(chartDimensions, start),
height: getMinimalHeight(start, end),
width: getMinimalWidth(start, end),
left: rotated ? hPanelStart : start.x,
top: rotated ? start.y : vPanelStart,
height: rotated ? getMinSize(start.y, end.y) : vPanelHeight,
width: rotated ? hPanelWidth : getMinSize(start.x, end.x),
};
}
Comment on lines +122 to 134
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These diffs are horribly misaligned 🙄 ....git 😞

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, I've refactored a bit the code cleaning up it a bit, but the diff is a bit messy, better to check both file side by side

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might just be me misunderstanding, but is the brush area adjusting for height less than the height of the panel?

small_multiples


/** @internal */
export function getLeftPoint({ left }: Dimensions, { x }: Point) {
return x - left;
}
export function getBrushForYAxis(
chartRotation: Rotation,
{ hPanelStart, vPanelStart, hPanelWidth, vPanelHeight, start, end }: PanelPoints,
) {
const rotated = isVerticalRotation(chartRotation);

/** @internal */
export function getTopPoint({ top }: Dimensions, { y }: Point) {
return y - top;
return {
left: rotated ? start.x : hPanelStart,
top: rotated ? vPanelStart : start.y,
height: rotated ? vPanelHeight : getMinSize(start.y, end.y),
width: rotated ? getMinSize(start.x, end.x) : hPanelWidth,
};
}

function getMinimalHeight(start: Point, end: Point, min = MIN_AREA_SIZE) {
const height = end.y - start.y;
if (Math.abs(height) < min) {
return min;
}
return height;
/** @internal */
export function getBrushForBothAxis({ start, end }: PanelPoints) {
return {
left: start.x,
top: start.y,
height: getMinSize(start.y, end.y),
width: getMinSize(start.x, end.x),
};
}

function getMinimalWidth(start: Point, end: Point, min = MIN_AREA_SIZE) {
const width = end.x - start.x;
if (Math.abs(width) < min) {
return min;
function getMinSize(a: number, b: number, minSize = MIN_AREA_SIZE) {
const size = b - a;
if (Math.abs(size) < minSize) {
return minSize;
}
return width;
return size;
}
96 changes: 64 additions & 32 deletions src/chart_types/xy_chart/state/selectors/on_brush_end_caller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,16 @@ import { Scale } from '../../../../scales';
import { GroupBrushExtent, XYBrushArea } from '../../../../specs';
import { BrushAxis } from '../../../../specs/constants';
import { DragState, GlobalChartState } from '../../../../state/chart_state';
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs';
import { maxValueWithUpperLimit, minValueWithLowerLimit, Rotation } from '../../../../utils/common';
import { Dimensions } from '../../../../utils/dimensions';
import { hasDragged, DragCheckProps } from '../../../../utils/events';
import { GroupId } from '../../../../utils/ids';
import { isVerticalRotation } from '../utils/common';
import { computeChartDimensionsSelector } from './compute_chart_dimensions';
import { getLeftPoint, getTopPoint } from './get_brush_area';
import { computeSmallMultipleScalesSelector, SmallMultipleScales } from './compute_small_multiple_scales';
import { getPlotAreaRestrictedPoint, getPointsConstraintToSinglePanel, PanelPoints } from './get_brush_area';
import { getComputedScalesSelector } from './get_computed_scales';
import { isBrushAvailableSelector } from './is_brush_available';
import { isHistogramModeEnabledSelector } from './is_histogram_mode_enabled';
Expand Down Expand Up @@ -62,6 +64,7 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void {
getComputedScalesSelector,
computeChartDimensionsSelector,
isHistogramModeEnabledSelector,
computeSmallMultipleScalesSelector,
],
(
lastDrag,
Expand All @@ -76,6 +79,7 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void {
computedScales,
{ chartDimensions },
histogramMode,
smallMultipleScales,
): void => {
const nextProps = {
lastDrag,
Expand All @@ -93,55 +97,70 @@ export function createOnBrushEndCaller(): (state: GlobalChartState) => void {
rotation,
histogramMode,
xScale,
smallMultipleScales,
minBrushDelta,
roundHistogramBrushValues,
allowBrushingLastHistogramBucket,
);
}
if (brushAxis === BrushAxis.Y || brushAxis === BrushAxis.Both) {
brushArea.y = getYBrushExtents(chartDimensions, lastDrag, rotation, yScales, minBrushDelta);
brushArea.y = getYBrushExtents(
chartDimensions,
lastDrag,
rotation,
yScales,
smallMultipleScales,
minBrushDelta,
);
}
if (brushArea.x !== undefined || brushArea.y !== undefined) {
onBrushEnd(brushArea);
}
}
prevProps = nextProps;
},
)({
keySelector: (state: GlobalChartState) => state.chartId,
});
)(getChartIdSelector);
}
if (selector) {
selector(state);
}
};
}

function scalePanelPointsToPanelCoordinates(
scaleXPoint: boolean,
{ start, end, vPanelStart, hPanelStart, vPanelHeight, hPanelWidth }: PanelPoints,
) {
// scale screen coordinates down to panel scale
const startPos = scaleXPoint ? start.x - hPanelStart : start.y - vPanelStart;
const endPos = scaleXPoint ? end.x - hPanelStart : end.y - vPanelStart;
const panelMax = scaleXPoint ? hPanelWidth : vPanelHeight;
return {
minPos: Math.min(startPos, endPos),
maxPos: Math.max(startPos, endPos),
panelMax,
};
}

function getXBrushExtent(
chartDimensions: Dimensions,
lastDrag: DragState,
rotation: Rotation,
histogramMode: boolean,
xScale: Scale,
smallMultipleScales: SmallMultipleScales,
minBrushDelta?: number,
roundHistogramBrushValues?: boolean,
allowBrushingLastHistogramBucket?: boolean,
): [number, number] | undefined {
let startPos = getLeftPoint(chartDimensions, lastDrag.start.position);
let endPos = getLeftPoint(chartDimensions, lastDrag.end.position);
let chartMax = chartDimensions.width;

if (isVerticalRotation(rotation)) {
startPos = getTopPoint(chartDimensions, lastDrag.start.position);
endPos = getTopPoint(chartDimensions, lastDrag.end.position);
chartMax = chartDimensions.height;
}

let minPos = minValueWithLowerLimit(startPos, endPos, 0);
let maxPos = maxValueWithUpperLimit(startPos, endPos, chartMax);
const isXHorizontal = !isVerticalRotation(rotation);
// scale screen coordinates down to panel scale
const scaledPanelPoints = getMinMaxPos(chartDimensions, lastDrag, smallMultipleScales, isXHorizontal);
let { minPos, maxPos } = scaledPanelPoints;
// reverse the positions if chart is mirrored
if (rotation === -90 || rotation === 180) {
minPos = chartMax - minPos;
maxPos = chartMax - maxPos;
minPos = scaledPanelPoints.panelMax - minPos;
maxPos = scaledPanelPoints.panelMax - maxPos;
}
if (minBrushDelta !== undefined ? Math.abs(maxPos - minPos) < minBrushDelta : maxPos === minPos) {
// if 0 size brush, avoid computing the value
Expand All @@ -163,28 +182,41 @@ function getXBrushExtent(
return [minValue, maxValue];
}

function getMinMaxPos(
chartDimensions: Dimensions,
lastDrag: DragState,
smallMultipleScales: SmallMultipleScales,
scaleXPoint: boolean,
) {
const panelPoints = getPanelPoints(chartDimensions, lastDrag, smallMultipleScales);
// scale screen coordinates down to panel scale
return scalePanelPointsToPanelCoordinates(scaleXPoint, panelPoints);
}

function getPanelPoints(chartDimensions: Dimensions, lastDrag: DragState, smallMultipleScales: SmallMultipleScales) {
const plotStartPointPx = getPlotAreaRestrictedPoint(lastDrag.start.position, chartDimensions);
const plotEndPointPx = getPlotAreaRestrictedPoint(lastDrag.end.position, chartDimensions);
return getPointsConstraintToSinglePanel(plotStartPointPx, plotEndPointPx, smallMultipleScales);
}

function getYBrushExtents(
chartDimensions: Dimensions,
lastDrag: DragState,
rotation: Rotation,
yScales: Map<GroupId, Scale>,
smallMultipleScales: SmallMultipleScales,
minBrushDelta?: number,
): GroupBrushExtent[] | undefined {
const yValues: GroupBrushExtent[] = [];
yScales.forEach((yScale, groupId) => {
let startPos = getTopPoint(chartDimensions, lastDrag.start.position);
let endPos = getTopPoint(chartDimensions, lastDrag.end.position);
let chartMax = chartDimensions.height;
if (isVerticalRotation(rotation)) {
startPos = getLeftPoint(chartDimensions, lastDrag.start.position);
endPos = getLeftPoint(chartDimensions, lastDrag.end.position);
chartMax = chartDimensions.width;
}
let minPos = minValueWithLowerLimit(startPos, endPos, 0);
let maxPos = maxValueWithUpperLimit(startPos, endPos, chartMax);
if (rotation === -90 || rotation === 180) {
minPos = chartMax - minPos;
maxPos = chartMax - maxPos;
const isXVertical = isVerticalRotation(rotation);
// scale screen coordinates down to panel scale
const scaledPanelPoints = getMinMaxPos(chartDimensions, lastDrag, smallMultipleScales, isXVertical);
let { minPos, maxPos } = scaledPanelPoints;

if (rotation === 90 || rotation === 180) {
minPos = scaledPanelPoints.panelMax - minPos;
maxPos = scaledPanelPoints.panelMax - maxPos;
}
if (minBrushDelta !== undefined ? Math.abs(maxPos - minPos) < minBrushDelta : maxPos === minPos) {
// if 0 size brush, avoid computing the value
Expand Down
2 changes: 1 addition & 1 deletion stories/interactions/9a_brush_selection_linear.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const Example = () => {
);
return (
<Chart className="story-chart">
<Settings onBrushEnd={action('onBrushEnd')} rotation={getChartRotationKnob()} brushAxis={brushAxisSelect} />
<Settings rotation={getChartRotationKnob()} brushAxis={brushAxisSelect} onBrushEnd={action('brush')} />
<Axis id="bottom" position={Position.Bottom} title="bottom" showOverlappingTicks />
<Axis id="left" title="left" position={Position.Left} tickFormat={(d) => Number(d).toFixed(2)} />
<Axis id="top" position={Position.Top} title="top" showOverlappingTicks />
Expand Down
1 change: 1 addition & 0 deletions stories/small_multiples/1_grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const Example = () => {
},
},
}}
onBrushEnd={(d) => console.log(d)}
/>
<Axis id="time" title="horizontal" position={Position.Bottom} gridLine={{ visible: false }} />
<Axis
Expand Down
Loading