Skip to content

Commit

Permalink
feat: sort values in actions by closest to cursor (opensearch-project…
Browse files Browse the repository at this point in the history
…#1023)

orders array of values in `onElement___` listeners closest to the cursor point.
  • Loading branch information
nickofthyme authored Feb 11, 2021
1 parent e2d8cb4 commit 4f99c63
Show file tree
Hide file tree
Showing 7 changed files with 229 additions and 15 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { Store } from 'redux';

import { MockGlobalSpec, MockSeriesSpec } from '../../../../mocks/specs/specs';
import { MockStore } from '../../../../mocks/store/store';
import { ScaleType } from '../../../../scales/constants';
import { onPointerMove } from '../../../../state/actions/mouse';
import { GlobalChartState } from '../../../../state/chart_state';
import { getElementAtCursorPositionSelector } from './get_elements_at_cursor_pos';

const data = [
{ x: 0, y: 2 },
{ x: 0, y: 2.2 },
{ x: 1, y: 2 },
{ x: 2, y: 3 },
];

describe('getElementAtCursorPositionSelector', () => {
let store: Store<GlobalChartState>;

describe('Area', () => {
beforeEach(() => {
store = MockStore.default({ width: 300, height: 300, top: 0, left: 0 }, 'chartId');
MockStore.addSpecs(
[
MockGlobalSpec.settingsNoMargins(),
MockSeriesSpec.area({
data,
xScaleType: ScaleType.Ordinal,
}),
],
store,
);
});

it('should correctly sort matched points near y = 2', () => {
store.dispatch(onPointerMove({ x: 0, y: 100 }, 0));
const elements = getElementAtCursorPositionSelector(store.getState());
expect(elements).toHaveLength(2);
expect(elements.map(({ value }) => value.datum.y)).toEqual([2, 2.2]);
});

it('should correctly sort matched points near y = 2.2', () => {
store.dispatch(onPointerMove({ x: 0, y: 80 }, 0));
const elements = getElementAtCursorPositionSelector(store.getState());
expect(elements).toHaveLength(2);
expect(elements.map(({ value }) => value.datum.y)).toEqual([2.2, 2]);
});
});

describe('Bubble', () => {
beforeEach(() => {
store = MockStore.default({ width: 300, height: 300, top: 0, left: 0 }, 'chartId');
MockStore.addSpecs(
[
MockGlobalSpec.settingsNoMargins(),
MockSeriesSpec.bubble({
data,
xScaleType: ScaleType.Ordinal,
}),
],
store,
);
});

it('should correctly sort matched points near y = 2', () => {
store.dispatch(onPointerMove({ x: 0, y: 100 }, 0));
const elements = getElementAtCursorPositionSelector(store.getState());
expect(elements).toHaveLength(3);
expect(elements.map(({ value }) => value.datum.y)).toEqual([2, 2.2, 2]);
});

it('should correctly sort matched points near y = 2.2', () => {
store.dispatch(onPointerMove({ x: 0, y: 80 }, 0));
const elements = getElementAtCursorPositionSelector(store.getState());
expect(elements).toHaveLength(4);
expect(elements.map(({ value }) => value.datum.y)).toEqual([2.2, 2, 2, 3]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { isValidPointerOverEvent } from '../../../../utils/events';
import { IndexedGeometry } from '../../../../utils/geometry';
import { ChartDimensions } from '../../utils/dimensions';
import { IndexedGeometryMap } from '../../utils/indexed_geometry_map';
import { sortClosestToPoint } from '../utils/common';
import { ComputedScales } from '../utils/types';
import { computeChartDimensionsSelector } from './compute_chart_dimensions';
import { getComputedScalesSelector } from './get_computed_scales';
Expand Down Expand Up @@ -71,10 +72,12 @@ function getElementAtCursorPosition(
return [];
}
// get the elements at cursor position
return geometriesIndex.find(
xValue?.value,
orientedProjectedPointerPosition,
orientedProjectedPointerPosition.horizontalPanelValue,
orientedProjectedPointerPosition.verticalPanelValue,
);
return geometriesIndex
.find(
xValue?.value,
orientedProjectedPointerPosition,
orientedProjectedPointerPosition.horizontalPanelValue,
orientedProjectedPointerPosition.verticalPanelValue,
)
.sort(sortClosestToPoint(orientedProjectedPointerPosition));
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,19 @@ import { LegendItem } from '../../../../common/legend';
import { ScaleType } from '../../../../scales/constants';
import { SpecTypes } from '../../../../specs';
import { BARCHART_1Y1G } from '../../../../utils/data_samples/test_dataset';
import { Point } from '../../../../utils/point';
import { AreaSeriesSpec, SeriesTypes, LineSeriesSpec, BarSeriesSpec } from '../../utils/specs';
import {
isHorizontalRotation,
isVerticalRotation,
isLineAreaOnlyChart,
isChartAnimatable,
isAllSeriesDeselected,
sortClosestToPoint,
} from './common';

describe('Type Checks', () => {
test('is horizontal chart rotation', () => {
it('is horizontal chart rotation', () => {
expect(isHorizontalRotation(0)).toBe(true);
expect(isHorizontalRotation(180)).toBe(true);
expect(isHorizontalRotation(-90)).toBe(false);
Expand All @@ -42,15 +44,15 @@ describe('Type Checks', () => {
expect(isVerticalRotation(0)).toBe(false);
expect(isVerticalRotation(180)).toBe(false);
});
test('is vertical chart rotation', () => {
it('is vertical chart rotation', () => {
expect(isVerticalRotation(-90)).toBe(true);
expect(isVerticalRotation(90)).toBe(true);
expect(isVerticalRotation(0)).toBe(false);
expect(isVerticalRotation(180)).toBe(false);
});

describe('#isLineAreaOnlyChart', () => {
test('is an area or line only map', () => {
it('is an area or line only map', () => {
const area: AreaSeriesSpec = {
chartType: ChartTypes.XYAxis,
specType: SpecTypes.Series,
Expand Down Expand Up @@ -106,7 +108,7 @@ describe('Type Checks', () => {
});

describe('#isChartAnimatable', () => {
test('can enable the chart animation if we have a valid number of elements', () => {
it('can enable the chart animation if we have a valid number of elements', () => {
const geometriesCounts = {
points: 0,
bars: 0,
Expand All @@ -131,7 +133,7 @@ describe('Type Checks', () => {
});
});

test('displays no data available if chart is empty', () => {
it('displays no data available if chart is empty', () => {
const legendItems1: LegendItem[] = [
{
color: '#1EA593',
Expand All @@ -158,7 +160,7 @@ describe('Type Checks', () => {
];
expect(isAllSeriesDeselected(legendItems1)).toBe(true);
});
test('displays data availble if chart is not empty', () => {
it('displays data availble if chart is not empty', () => {
const legendItems2: LegendItem[] = [
{
color: '#1EA593',
Expand All @@ -185,4 +187,90 @@ describe('Type Checks', () => {
];
expect(isAllSeriesDeselected(legendItems2)).toBe(false);
});

describe('#sortClosestToPoint', () => {
describe('positive cursor', () => {
const cursor: Point = { x: 10, y: 10 };

it('should sort points with same x', () => {
const points: Point[] = [
{ x: 10, y: -10 },
{ x: 10, y: 12 },
{ x: 10, y: 11 },
{ x: 10, y: 10 },
{ x: 10, y: 5 },
{ x: 10, y: -12 },
];
expect(points.sort(sortClosestToPoint(cursor))).toEqual([
{ x: 10, y: 10 },
{ x: 10, y: 11 },
{ x: 10, y: 12 },
{ x: 10, y: 5 },
{ x: 10, y: -10 },
{ x: 10, y: -12 },
]);
});

it('should sort points with different x', () => {
const points: Point[] = [
{ x: 9, y: -10 },
{ x: -6, y: 12 },
{ x: 3, y: 11 },
{ x: 9, y: 10 },
{ x: 1, y: 5 },
{ x: -9, y: -12 },
];
expect(points.sort(sortClosestToPoint(cursor))).toEqual([
{ x: 9, y: 10 },
{ x: 3, y: 11 },
{ x: 1, y: 5 },
{ x: -6, y: 12 },
{ x: 9, y: -10 },
{ x: -9, y: -12 },
]);
});
});

describe('negative cursor', () => {
const cursor: Point = { x: -10, y: -10 };

it('should sort points with same x', () => {
const points: Point[] = [
{ x: 10, y: -10 },
{ x: 10, y: 12 },
{ x: 10, y: 11 },
{ x: 10, y: 10 },
{ x: 10, y: 5 },
{ x: 10, y: -12 },
];
expect(points.sort(sortClosestToPoint(cursor))).toEqual([
{ x: 10, y: -10 },
{ x: 10, y: -12 },
{ x: 10, y: 5 },
{ x: 10, y: 10 },
{ x: 10, y: 11 },
{ x: 10, y: 12 },
]);
});

it('should sort points with different x', () => {
const points: Point[] = [
{ x: 9, y: -10 },
{ x: -6, y: 12 },
{ x: 3, y: 11 },
{ x: 9, y: 10 },
{ x: 1, y: 5 },
{ x: -9, y: -12 },
];
expect(points.sort(sortClosestToPoint(cursor))).toEqual([
{ x: -9, y: -12 },
{ x: 1, y: 5 },
{ x: 9, y: -10 },
{ x: -6, y: 12 },
{ x: 3, y: 11 },
{ x: 9, y: 10 },
]);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
*/

import { LegendItem } from '../../../../common/legend';
import { Rotation } from '../../../../utils/common';
import { getDistance, Rotation } from '../../../../utils/common';
import { Point } from '../../../../utils/point';
import { BasicSeriesSpec, SeriesTypes } from '../../utils/specs';
import { GeometriesCounts } from './types';

Expand Down Expand Up @@ -64,3 +65,11 @@ export function isAllSeriesDeselected(legendItems: LegendItem[]): boolean {
}
return true;
}

/**
* Sorts points in order from closest to farthest from cursor
* @internal
*/
export const sortClosestToPoint = (cursor: Point) => (a: Point, b: Point): number => {
return getDistance(cursor, a) - getDistance(cursor, b);
};
5 changes: 4 additions & 1 deletion packages/osd-charts/src/utils/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,10 @@ export type ValueAccessor = (d: Datum) => number;
export type LabelAccessor = (value: PrimitiveValue) => string;
export type ShowAccessor = (value: PrimitiveValue) => boolean;

/** @internal */
/**
* Returns planar distance bewtween two points
* @internal
*/
export function getDistance(a: Point, b: Point): number {
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
}
Expand Down
15 changes: 14 additions & 1 deletion packages/osd-charts/stories/interactions/3_line_point_clicks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const Example = () => (
<Axis id="left2" title="Left axis" position={Position.Left} tickFormat={(d) => Number(d).toFixed(2)} />

<LineSeries
id="line"
id="line 1"
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x"
Expand All @@ -47,5 +47,18 @@ export const Example = () => (
{ x: 3, y: 6 },
]}
/>
<LineSeries
id="line 2"
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor="x"
yAccessors={['y']}
data={[
{ x: 0, y: 2 },
{ x: 1, y: 6.8 },
{ x: 2, y: 3.001 },
{ x: 3, y: 6.1 },
]}
/>
</Chart>
);

0 comments on commit 4f99c63

Please sign in to comment.