Skip to content

Commit

Permalink
feat: add projection click listener
Browse files Browse the repository at this point in the history
  • Loading branch information
markov00 committed Nov 24, 2020
1 parent 642abc0 commit 12f0299
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 2 deletions.
15 changes: 15 additions & 0 deletions api/charts.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,20 @@ export interface Postfixes {
y1AccessorFormat?: string;
}

// @public
export type ProjectedValues = {
x: PrimitiveValue;
y: Array<{
value: PrimitiveValue;
groupId: string;
}>;
smVerticalValue: PrimitiveValue;
smHorizontalValue: PrimitiveValue;
};

// @public
export type ProjectionClickListener = (values: ProjectedValues) => void;

// Warning: (ae-missing-release-tag) "RectAnnotation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
Expand Down Expand Up @@ -1605,6 +1619,7 @@ export interface SettingsSpec extends Spec {
onLegendItemPlusClick?: LegendItemListener;
// (undocumented)
onPointerUpdate?: PointerUpdateListener;
onProjectionClick?: ProjectionClickListener;
// (undocumented)
onRenderChange?: RenderChangeListener;
orderOrdinalBinsBy?: OrderBy;
Expand Down
4 changes: 4 additions & 0 deletions src/chart_types/xy_chart/state/chart_state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { createOnElementClickCaller } from './selectors/on_element_click_caller'
import { createOnElementOutCaller } from './selectors/on_element_out_caller';
import { createOnElementOverCaller } from './selectors/on_element_over_caller';
import { createOnPointerMoveCaller } from './selectors/on_pointer_move_caller';
import { createOnProjectionClickCaller } from './selectors/on_projection_click_caller';

/** @internal */
export class XYAxisChartState implements InternalChartState {
Expand All @@ -62,13 +63,15 @@ export class XYAxisChartState implements InternalChartState {
onElementOutCaller: (state: GlobalChartState) => void;
onBrushEndCaller: (state: GlobalChartState) => void;
onPointerMoveCaller: (state: GlobalChartState) => void;
onProjectionClickCaller: (state: GlobalChartState) => void;

constructor() {
this.onElementClickCaller = createOnElementClickCaller();
this.onElementOverCaller = createOnElementOverCaller();
this.onElementOutCaller = createOnElementOutCaller();
this.onBrushEndCaller = createOnBrushEndCaller();
this.onPointerMoveCaller = createOnPointerMoveCaller();
this.onProjectionClickCaller = createOnProjectionClickCaller();
this.chartType = ChartTypes.XYAxis;
this.legendId = htmlIdGenerator()('legend');
}
Expand Down Expand Up @@ -148,6 +151,7 @@ export class XYAxisChartState implements InternalChartState {
this.onElementClickCaller(globalState);
this.onBrushEndCaller(globalState);
this.onPointerMoveCaller(globalState);
this.onProjectionClickCaller(globalState);
}

getDebugState(globalState: GlobalChartState) {
Expand Down
14 changes: 13 additions & 1 deletion src/chart_types/xy_chart/state/selectors/get_cursor_pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { GlobalChartState } from '../../../../state/chart_state';
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs';
import { computeChartDimensionsSelector } from './compute_chart_dimensions';
import { getProjectedScaledValues } from './get_projected_scaled_values';
import { getHighlightedGeomsSelector } from './get_tooltip_values_highlighted_geoms';
import { isBrushAvailableSelector } from './is_brush_available';

Expand All @@ -34,10 +35,18 @@ export const getPointerCursorSelector = createCachedSelector(
getHighlightedGeomsSelector,
getSettingsSpecSelector,
getCurrentPointerPositionSelector,
getProjectedScaledValues,
computeChartDimensionsSelector,
isBrushAvailableSelector,
],
(highlightedGeometries, settingsSpec, currentPointerPosition, { chartDimensions }, isBrushAvailable): string => {
(
highlightedGeometries,
settingsSpec,
currentPointerPosition,
projectedValues,
{ chartDimensions },
isBrushAvailable,
): string => {
const { x, y } = currentPointerPosition;
// get positions relative to chart
const xPos = x - chartDimensions.left;
Expand All @@ -53,6 +62,9 @@ export const getPointerCursorSelector = createCachedSelector(
if (highlightedGeometries.length > 0 && (settingsSpec.onElementClick || settingsSpec.onElementOver)) {
return 'pointer';
}
if (projectedValues !== null && settingsSpec.onProjectionClick) {
return 'pointer';
}
return isBrushAvailable ? 'crosshair' : 'default';
},
)(getChartIdSelector);
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 createCachedSelector from 're-reselect';

import { ProjectedValues } from '../../../../specs/settings';
import { getChartIdSelector } from '../../../../state/selectors/get_chart_id';
import { computeSeriesGeometriesSelector } from './compute_series_geometries';
import { getGeometriesIndexKeysSelector } from './get_geometries_index_keys';
import { getOrientedProjectedPointerPositionSelector } from './get_oriented_projected_pointer_position';

/** @internal */
export const getProjectedScaledValues = createCachedSelector(
[getOrientedProjectedPointerPositionSelector, computeSeriesGeometriesSelector, getGeometriesIndexKeysSelector],
(
{ x, y, verticalPanelValue, horizontalPanelValue },
{ scales: { xScale, yScales } },
geometriesIndexKeys,
): ProjectedValues | undefined => {
if (!xScale) {
return;
}

const xValue = xScale.invertWithStep(x, geometriesIndexKeys);
if (!xValue) {
return;
}

return {
x: xValue.value,
y: [...yScales.entries()].map(([groupId, yScale]) => {
return { value: yScale.invert(y), groupId };
}),
smVerticalValue: verticalPanelValue,
smHorizontalValue: horizontalPanelValue,
};
},
)(getChartIdSelector);
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* 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 createCachedSelector from 're-reselect';
import { Selector } from 'reselect';

import { ChartTypes } from '../../..';
import { GlobalChartState, PointerState } from '../../../../state/chart_state';
import { getLastClickSelector } from '../../../../state/selectors/get_last_click';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_specs';
import { isClicking } from '../../../../state/utils';
import { getProjectedScaledValues } from './get_projected_scaled_values';

/**
* Will call the onElementClick listener every time the following preconditions are met:
* - the onElementClick listener is available
* - we have at least one highlighted geometry
* - the pointer state goes from down state to up state
* @internal
*/
export function createOnProjectionClickCaller(): (state: GlobalChartState) => void {
let prevClick: PointerState | null = null;
let selector: Selector<GlobalChartState, void> | null = null;
return (state: GlobalChartState) => {
if (selector === null && state.chartType === ChartTypes.XYAxis) {
selector = createCachedSelector(
[getLastClickSelector, getSettingsSpecSelector, getProjectedScaledValues],
(lastClick, { onProjectionClick }, values): void => {
if (!onProjectionClick) {
return;
}

if (values !== undefined && isClicking(prevClick, lastClick)) {
onProjectionClick(values);
}
prevClick = lastClick;
},
)({
keySelector: ({ chartId }) => chartId,
});
}
if (selector) {
selector(state);
}
};
}
40 changes: 39 additions & 1 deletion src/specs/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,38 @@ export type XYChartElementEvent = [GeometryValue, XYChartSeriesIdentifier];
export type PartitionElementEvent = [Array<LayerValue>, SeriesIdentifier];
export type HeatmapElementEvent = [Cell, SeriesIdentifier];

/**
* @public
* An object that contains the scaled mouse position based on
* the current chart configuration.
*/
export type ProjectedValues = {
/**
* The independent variable of the chart
*/
x: PrimitiveValue;
/**
* The set of dependent variable, each one with its own groupId
*/
y: Array<{ value: PrimitiveValue; groupId: string }>;
/**
* The categorical value used for the vertical placement of the chart
* in a small multiple layout
*/
smVerticalValue: PrimitiveValue;
/**
* The categorical value used for the horizontal placement of the chart
* in a small multiple layout
*/
smHorizontalValue: PrimitiveValue;
};

/**
* @public
* The listener type for click on the projection area.
*/
export type ProjectionClickListener = (values: ProjectedValues) => void;

export type ElementClickListener = (
elements: Array<XYChartElementEvent | PartitionElementEvent | HeatmapElementEvent>,
) => void;
Expand All @@ -77,7 +109,7 @@ export interface BasePointerEvent {
type: PointerEventType;
}
/**
* Event used to syncronize pointers/mouse positions between Charts.
* Event used to synchronize pointers/mouse positions between Charts.
*
* fired as callback argument for `PointerUpdateListener`
*/
Expand Down Expand Up @@ -328,6 +360,12 @@ export interface SettingsSpec extends Spec {
* Compares title, position and first & last tick labels
*/
hideDuplicateAxes: boolean;
/**
* Attach a listener for click on the projection area.
* The listener will be called with the current x value snapped to the closest
* X axis point, and an array of Y values for every groupId used in the chart.
*/
onProjectionClick?: ProjectionClickListener;
onElementClick?: ElementClickListener;
onElementOver?: ElementOverListener;
onElementOut?: BasicListener;
Expand Down
1 change: 1 addition & 0 deletions stories/interactions/1_bar_clicks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const onElementListeners = {
onElementClick: action('onElementClick'),
onElementOver: action('onElementOver'),
onElementOut: action('onElementOut'),
onProjectionClick: action('onProjectionClick'),
};

export const Example = () => {
Expand Down

0 comments on commit 12f0299

Please sign in to comment.