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(heatmap): Sync cursor from xy charts to Heatmap #1721

Merged
merged 27 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6619bba
[ML] Heatmap cursor callback
qn895 May 10, 2022
d48ecab
[ML] Fix linting
qn895 May 10, 2022
9d5657f
Address comments
qn895 Jun 15, 2022
491b826
Fix linting
qn895 Jun 15, 2022
2971a36
Merge remote-tracking branch 'upstream/master' into cursor-sync-up-to…
qn895 Jun 15, 2022
0475b5d
[ML] Sync cursor from other chart to heatmap
qn895 May 12, 2022
a323fad
Add example to story
qn895 Jun 20, 2022
fc97dc5
Correct syncing from continuous xy to nearest/closest bands
qn895 Jun 21, 2022
90fe61d
Clean up stories
qn895 Jun 21, 2022
d5810b5
Merge remote-tracking branch 'upstream/master' into cursor-sync-up-to…
qn895 Jun 21, 2022
5a18d9a
Remove unused cursor line
qn895 Jun 21, 2022
51f395b
Clean up
qn895 Jun 21, 2022
40451ed
Fix types
qn895 Jun 22, 2022
49ce28a
Remove redundant stuff
qn895 Jun 22, 2022
71d7f62
Merge branch 'master' into cursor-sync-xy-charts-to-heatmap
nickofthyme Jun 29, 2022
d9a2784
Merge branch 'master' into cursor-sync-xy-charts-to-heatmap
nickofthyme Jun 29, 2022
aee993d
refactor: revert changes to existing story
markov00 Jul 5, 2022
a9da4d5
feat: expose the cursor band picker in the view model
markov00 Jul 5, 2022
da3cd3b
Add current cursor band
markov00 Jul 5, 2022
9626421
Remove unused file
markov00 Jul 5, 2022
bb88979
Update example
markov00 Jul 5, 2022
7ab3bf3
Merge branch 'master' into cursor-sync-xy-charts-to-heatmap
markov00 Jul 25, 2022
6b89f3f
fix: example and merge changes
markov00 Jul 25, 2022
057c219
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Jul 28, 2022
56799b4
fix time rounding on cursor band picking
markov00 Aug 3, 2022
d40d9a5
Merge branch 'master' into cursor-sync-xy-charts-to-heatmap
markov00 Aug 3, 2022
470beba
test(vrt): update screenshots [skip ci]
elastic-datavis[bot] Aug 3, 2022
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
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.
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ export type PickHighlightedArea = (
y: Array<NonNullable<PrimitiveValue>>,
) => Rect | null;

/** @internal */
export type PickCursorBand = (x: NonNullable<PrimitiveValue>) => Rect | undefined;

/** @internal */
export type PickGridCell = (x: Pixels, y: Pixels) => GridCell | undefined;

Expand All @@ -114,6 +117,7 @@ export type ShapeViewModel = {
pickDragShape: PickDragShapeFunction;
pickHighlightedArea: PickHighlightedArea;
pickGridCell: PickGridCell;
pickCursorBand: PickCursorBand;
};

/** @internal */
Expand Down Expand Up @@ -144,4 +148,5 @@ export const nullShapeViewModel = (): ShapeViewModel => ({
pickDragShape: () => ({ x: 0, y: 0, width: 0, height: 0 }),
pickHighlightedArea: () => ({ x: 0, y: 0, width: 0, height: 0 }),
pickGridCell: () => undefined,
pickCursorBand: () => undefined,
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { Box, Font, maximiseFontSize } from '../../../../common/text_utils';
import { ScaleType } from '../../../../scales/constants';
import { LinearScale, OrdinalScale, RasterTimeScale } from '../../../../specs';
import { TextMeasure } from '../../../../utils/bbox/canvas_text_bbox_calculator';
import { addIntervalToTime } from '../../../../utils/chrono/elasticsearch';
import { addIntervalToTime, roundDateToESInterval } from '../../../../utils/chrono/elasticsearch';
import { clamp, Datum, isFiniteNumber } from '../../../../utils/common';
import { innerPad, pad } from '../../../../utils/dimensions';
import { Logger } from '../../../../utils/logger';
Expand All @@ -28,6 +28,7 @@ import { ColorScale } from '../../state/selectors/get_color_scale';
import {
Cell,
GridCell,
PickCursorBand,
PickDragFunction,
PickDragShapeFunction,
PickHighlightedArea,
Expand Down Expand Up @@ -333,6 +334,24 @@ export function shapeViewModel<D extends BaseDatum = Datum>(
return pickHighlightedArea(area.x, area.y);
};

const pickCursorBand: PickCursorBand = (x) => {
// TODO for Linear scale we need to round the numerical interval. see also https://github.com/elastic/elastic-charts/issues/1701
const roundedValue =
isRasterTimeScale(spec.xScale) && isFiniteNumber(x)
? roundDateToESInterval(x, spec.xScale.interval, 'start', spec.timeZone)
: x;

const index = xValues.indexOf(roundedValue);
return index < 0
? undefined
: {
width: cellWidth,
x: elementSizes.grid.left + (xScale(xValues[index]) ?? NaN),
y: elementSizes.grid.top,
height: elementSizes.grid.height,
};
};

// ordered left-right vertical lines
const xLines = Array.from({ length: xValues.length + 1 }, (d, i) => {
const xAxisExtension = i % elementSizes.xAxisTickCadence === 0 ? 5 : 0;
Expand Down Expand Up @@ -416,6 +435,7 @@ export function shapeViewModel<D extends BaseDatum = Datum>(
pickDragArea,
pickDragShape,
pickHighlightedArea,
pickCursorBand,
};
}

Expand All @@ -435,7 +455,7 @@ export function isRasterTimeScale(scale: RasterTimeScale | OrdinalScale | Linear
function getXTicks(
spec: HeatmapSpec,
style: HeatmapStyle['xAxisLabel'],
scale: ScaleBand<string | number>,
scale: ScaleBand<NonNullable<PrimitiveValue>>,
values: NonNullable<PrimitiveValue>[],
): Array<TextBox> {
const isTimeScale = isRasterTimeScale(spec.xScale);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import React from 'react';
import { connect } from 'react-redux';

import { Rect } from '../../../../geoms/types';
import { getTooltipType } from '../../../../specs';
import { TooltipType } from '../../../../specs/constants';
import { GlobalChartState } from '../../../../state/chart_state';
import { getChartThemeSelector } from '../../../../state/selectors/get_chart_theme';
import { getInternalIsInitializedSelector, InitStatus } from '../../../../state/selectors/get_internal_is_intialized';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
import { getTooltipSpecSelector } from '../../../../state/selectors/get_tooltip_spec';
import { LIGHT_THEME } from '../../../../utils/themes/light_theme';
import { Theme } from '../../../../utils/themes/theme';
import { getCursorBandPositionSelector } from '../../state/selectors/get_cursor_band';

interface CursorBandProps {
bandStyle: Theme['crosshair']['band'];
cursorPosition?: Rect;
tooltipType: TooltipType;
fromExternalEvent?: boolean;
}

function canRenderBand(type: TooltipType, visible: boolean, fromExternalEvent?: boolean) {
return visible && (type === TooltipType.Crosshairs || type === TooltipType.VerticalCursor || fromExternalEvent);
}

class CursorBandComponent extends React.Component<CursorBandProps> {
static displayName = 'CursorBand';

render() {
const { bandStyle, cursorPosition, tooltipType, fromExternalEvent } = this.props;
const isBand = (cursorPosition?.width ?? 0) > 0 && (cursorPosition?.height ?? 0) > 0;
if (!isBand || !cursorPosition || !canRenderBand(tooltipType, bandStyle.visible, fromExternalEvent)) {
return null;
}
const { x, y, width, height } = cursorPosition;
const { fill } = bandStyle;
return (
<svg className="echCrosshair__cursor" width="100%" height="100%">
<rect {...{ x, y, width, height, fill, opacity: 0.5 }} />
</svg>
);
}
}

const mapStateToProps = (state: GlobalChartState): CursorBandProps => {
if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) {
return {
bandStyle: LIGHT_THEME.crosshair.band,
tooltipType: TooltipType.None,
};
}
const settings = getSettingsSpecSelector(state);
const tooltip = getTooltipSpecSelector(state);
const cursorBandPosition = getCursorBandPositionSelector(state);
const fromExternalEvent = cursorBandPosition?.fromExternalEvent;
const tooltipType = getTooltipType(tooltip, settings, fromExternalEvent);

return {
bandStyle: getChartThemeSelector(state).crosshair.band,
cursorPosition: cursorBandPosition,
tooltipType,
fromExternalEvent,
};
};

/** @internal */
export const CursorBand = connect(mapStateToProps)(CursorBandComponent);
2 changes: 2 additions & 0 deletions packages/charts/src/chart_types/heatmap/state/chart_state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { getChartContainerDimensionsSelector } from '../../../state/selectors/ge
import { InitStatus } from '../../../state/selectors/get_internal_is_intialized';
import { Dimensions } from '../../../utils/dimensions';
import { Heatmap } from '../renderer/canvas/connected_component';
import { CursorBand } from '../renderer/dom/cursor_band';
import { HighlighterFromBrush } from '../renderer/dom/highlighter_brush';
import { computeChartElementSizesSelector } from './selectors/compute_chart_dimensions';
import { computeLegendSelector } from './selectors/compute_legend';
Expand Down Expand Up @@ -85,6 +86,7 @@ export class HeatmapState implements InternalChartState {
<>
<Tooltip getChartContainerRef={containerRef} />
<Heatmap forwardStageRef={forwardStageRef} />
<CursorBand />
<BrushTool />
<HighlighterFromBrush />
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { Rect } from '../../../../geoms/types';
import { isPointerOverEvent, PointerEvent, SettingsSpec } from '../../../../specs';
import { GlobalChartState, PointerState } from '../../../../state/chart_state';
import { createCustomCachedSelector } from '../../../../state/create_selector';
import { getSettingsSpecSelector } from '../../../../state/selectors/get_settings_spec';
import { isNil } from '../../../../utils/common';
import { ShapeViewModel } from '../../layout/types/viewmodel_types';
import { getHeatmapGeometries } from './geometries';

const getExternalPointerEventStateSelector = (state: GlobalChartState) => state.externalEvents.pointer;

const getPointerEventStateSelector = (state: GlobalChartState) => state.interactions.pointer.current;

/** @internal */
export const getCursorBandPositionSelector = createCustomCachedSelector(
[getHeatmapGeometries, getExternalPointerEventStateSelector, getPointerEventStateSelector, getSettingsSpecSelector],
getCursorBand,
);

function getCursorBand(
geoms: ShapeViewModel,
externalPointerEvent: PointerEvent | null,
currentPointer: PointerState,
settings: SettingsSpec,
): (Rect & { fromExternalEvent: boolean }) | undefined {
// external pointer events takes precedence over the current mouse pointer
if (settings.externalPointerEvents.tooltip.visible && isPointerOverEvent(externalPointerEvent)) {
const { x } = externalPointerEvent;
if (!isNil(x)) {
const band = geoms.pickCursorBand(x);
if (band) {
return {
...band,
fromExternalEvent: true,
};
}
}
}
// display the current hovered cell
if (currentPointer.position.x > -1) {
const point = currentPointer.position;
const end = { x: point.x + Number.EPSILON, y: point.y + Number.EPSILON };
const band = geoms.pickDragShape([point, end]);
if (band) {
return {
...band,
fromExternalEvent: false,
};
}
}
}
Loading