Skip to content

Commit

Permalink
fix(interactions): line cursor above the chart, band cursor below (#1453
Browse files Browse the repository at this point in the history
) (#1457)
  • Loading branch information
nickofthyme authored Nov 2, 2021
1 parent b416ed5 commit ca004a6
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 66 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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 { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation';
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_specs';
import { Rotation } from '../../../../utils/common';
import { LIGHT_THEME } from '../../../../utils/themes/light_theme';
import { Theme } from '../../../../utils/themes/theme';
import { getCursorBandPositionSelector } from '../../state/selectors/get_cursor_band';

interface CursorBandProps {
theme: Theme;
chartRotation: Rotation;
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 {
theme: {
crosshair: { band },
},
cursorPosition,
tooltipType,
fromExternalEvent,
} = this.props;
const isBand = (cursorPosition?.width ?? 0) > 0 && (cursorPosition?.height ?? 0) > 0;
if (!isBand || !cursorPosition || !canRenderBand(tooltipType, band.visible, fromExternalEvent)) {
return null;
}
const { x, y, width, height } = cursorPosition;
const { fill } = band;
return (
<svg className="echCrosshair__cursor" width="100%" height="100%">
<rect {...{ x, y, width, height, fill }} />
</svg>
);
}
}

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

return {
theme: getChartThemeSelector(state),
chartRotation: getChartRotationSelector(state),
cursorPosition: cursorBandPosition,
tooltipType,
fromExternalEvent,
};
};

/** @internal */
export const CursorBand = connect(mapStateToProps)(CursorBandComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import React from 'react';
import { connect } from 'react-redux';

import { Line, Rect } from '../../../../geoms/types';
import { Line } from '../../../../geoms/types';
import { getTooltipType } from '../../../../specs';
import { TooltipType } from '../../../../specs/constants';
import { GlobalChartState } from '../../../../state/chart_state';
Expand All @@ -23,62 +23,22 @@ import { Theme } from '../../../../utils/themes/theme';
import { getCursorBandPositionSelector } from '../../state/selectors/get_cursor_band';
import { getCursorLinePositionSelector } from '../../state/selectors/get_cursor_line';

interface CrosshairProps {
interface CursorCrossLineProps {
theme: Theme;
chartRotation: Rotation;
cursorPosition?: Rect;
cursorCrossLinePosition?: Line;
tooltipType: TooltipType;
fromExternalEvent?: boolean;
zIndex: number;
}

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

function canRenderHelpLine(type: TooltipType, visible: boolean) {
return visible && type === TooltipType.Crosshairs;
}

class CrosshairComponent extends React.Component<CrosshairProps> {
static displayName = 'Crosshair';
class CursorCrossLineComponent extends React.Component<CursorCrossLineProps> {
static displayName = 'CursorCrossLine';

renderCursor() {
const {
zIndex,
theme: {
crosshair: { band, line },
},
cursorPosition,
tooltipType,
fromExternalEvent,
} = this.props;

if (!cursorPosition || !canRenderBand(tooltipType, band.visible, fromExternalEvent)) {
return null;
}
const { x, y, width, height } = cursorPosition;
const isLine = width === 0 || height === 0;
const { strokeWidth, stroke, dash } = line;
const { fill } = band;
const strokeDasharray = (dash ?? []).join(' ');
return (
<svg
className="echCrosshair__cursor"
width="100%"
height="100%"
style={{ zIndex: cursorPosition && isLine ? zIndex : undefined }}
>
{isLine && <line {...{ x1: x, x2: x + width, y1: y, y2: y + height, strokeWidth, stroke, strokeDasharray }} />}
{!isLine && <rect {...{ x, y, width, height, fill }} />}
</svg>
);
}

renderCrossLine() {
render() {
const {
zIndex,
theme: {
crosshair: { crossLine },
},
Expand All @@ -98,29 +58,19 @@ class CrosshairComponent extends React.Component<CrosshairProps> {
};

return (
<svg className="echCrosshair__crossLine" width="100%" height="100%" style={{ zIndex }}>
<svg className="echCrosshair__crossLine" width="100%" height="100%">
<line {...cursorCrossLinePosition} {...style} />
</svg>
);
}

render() {
return (
<>
{this.renderCursor()}
{this.renderCrossLine()}
</>
);
}
}

const mapStateToProps = (state: GlobalChartState): CrosshairProps => {
const mapStateToProps = (state: GlobalChartState): CursorCrossLineProps => {
if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) {
return {
theme: LIGHT_THEME,
chartRotation: 0,
tooltipType: TooltipType.None,
zIndex: 0,
};
}
const settings = getSettingsSpecSelector(state);
Expand All @@ -131,13 +81,10 @@ const mapStateToProps = (state: GlobalChartState): CrosshairProps => {
return {
theme: getChartThemeSelector(state),
chartRotation: getChartRotationSelector(state),
cursorPosition: cursorBandPosition,
cursorCrossLinePosition: getCursorLinePositionSelector(state),
tooltipType,
fromExternalEvent,
zIndex: state.zIndex,
};
};

/** @internal */
export const Crosshair = connect(mapStateToProps)(CrosshairComponent);
export const CursorCrossLine = connect(mapStateToProps)(CursorCrossLineComponent);
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 { getChartRotationSelector } from '../../../../state/selectors/get_chart_rotation';
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_specs';
import { Rotation } from '../../../../utils/common';
import { LIGHT_THEME } from '../../../../utils/themes/light_theme';
import { Theme } from '../../../../utils/themes/theme';
import { getCursorBandPositionSelector } from '../../state/selectors/get_cursor_band';

interface CursorLineProps {
theme: Theme;
chartRotation: Rotation;
cursorPosition?: Rect;
tooltipType: TooltipType;
fromExternalEvent?: boolean;
isLine: boolean;
}

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

class CursorLineComponent extends React.Component<CursorLineProps> {
static displayName = 'CursorLine';

render() {
const {
theme: {
crosshair: { band, line },
},
cursorPosition,
tooltipType,
fromExternalEvent,
isLine,
} = this.props;

if (!cursorPosition || !canRenderBand(tooltipType, band.visible, fromExternalEvent) || !isLine) {
return null;
}
const { x, y, width, height } = cursorPosition;
const { strokeWidth, stroke, dash } = line;
const strokeDasharray = (dash ?? []).join(' ');
return (
<svg className="echCrosshair__cursor" width="100%" height="100%">
<line {...{ x1: x, x2: x + width, y1: y, y2: y + height, strokeWidth, stroke, strokeDasharray }} />
</svg>
);
}
}

const mapStateToProps = (state: GlobalChartState): CursorLineProps => {
if (getInternalIsInitializedSelector(state) !== InitStatus.Initialized) {
return {
theme: LIGHT_THEME,
chartRotation: 0,
tooltipType: TooltipType.None,
isLine: false,
};
}
const settings = getSettingsSpecSelector(state);
const cursorBandPosition = getCursorBandPositionSelector(state);
const fromExternalEvent = cursorBandPosition?.fromExternalEvent;
const tooltipType = getTooltipType(settings, fromExternalEvent);
const isLine = cursorBandPosition?.width === 0 || cursorBandPosition?.height === 0;

return {
theme: getChartThemeSelector(state),
chartRotation: getChartRotationSelector(state),
cursorPosition: cursorBandPosition,
tooltipType,
fromExternalEvent,
isLine,
};
};

/** @internal */
export const CursorLine = connect(mapStateToProps)(CursorLineComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import { InitStatus } from '../../../state/selectors/get_internal_is_intialized'
import { htmlIdGenerator } from '../../../utils/common';
import { XYChart } from '../renderer/canvas/xy_chart';
import { Annotations } from '../renderer/dom/annotations';
import { Crosshair } from '../renderer/dom/crosshair';
import { CursorBand } from '../renderer/dom/cursor_band';
import { CursorCrossLine } from '../renderer/dom/cursor_crossline';
import { CursorLine } from '../renderer/dom/cursor_line';
import { Highlighter } from '../renderer/dom/highlighter';
import { computeChartDimensionsSelector } from './selectors/compute_chart_dimensions';
import { computeLegendSelector } from './selectors/compute_legend';
Expand Down Expand Up @@ -112,8 +114,10 @@ export class XYAxisChartState implements InternalChartState {
chartRenderer(containerRef: BackwardRef, forwardCanvasRef: RefObject<HTMLCanvasElement>) {
return (
<>
<Crosshair />
<CursorBand />
<XYChart forwardCanvasRef={forwardCanvasRef} />
<CursorLine />
<CursorCrossLine />
<Tooltip getChartContainerRef={containerRef} />
<Annotations getChartContainerRef={containerRef} chartAreaRef={forwardCanvasRef} />
<Highlighter />
Expand Down
12 changes: 9 additions & 3 deletions packages/charts/src/components/__snapshots__/chart.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ exports[`Chart should render the legend name test 1`] = `
<Connect(ChartContainer) getChartContainerRef={[Function (anonymous)]} forwardStageRef={{...}}>
<ChartContainer getChartContainerRef={[Function (anonymous)]} forwardStageRef={{...}} status=\\"Initialized\\" initialized={true} isChartEmpty={false} pointerCursor=\\"default\\" isBrushingAvailable={false} isBrushing={false} internalChartRenderer={[Function (anonymous)]} settings={{...}} onPointerMove={[Function (anonymous)]} onMouseUp={[Function (anonymous)]} onMouseDown={[Function (anonymous)]} onKeyPress={[Function (anonymous)]}>
<div className=\\"echChartPointerContainer\\" style={{...}} onMouseMove={[Function (anonymous)]} onMouseLeave={[Function (anonymous)]} onMouseDown={[Function (anonymous)]} onMouseUp={[Function (anonymous)]}>
<Connect(Crosshair)>
<Crosshair theme={{...}} chartRotation={0} cursorPosition={[undefined]} cursorCrossLinePosition={[undefined]} tooltipType=\\"vertical\\" fromExternalEvent={[undefined]} zIndex={0} dispatch={[Function: dispatch]} />
</Connect(Crosshair)>
<Connect(CursorBand)>
<CursorBand theme={{...}} chartRotation={0} cursorPosition={[undefined]} tooltipType=\\"vertical\\" fromExternalEvent={[undefined]} dispatch={[Function: dispatch]} />
</Connect(CursorBand)>
<Connect(XYChart) forwardCanvasRef={{...}}>
<XYChart forwardCanvasRef={{...}} initialized={true} isChartEmpty={false} debug={true} geometries={{...}} geometriesIndex={{...}} theme={{...}} chartContainerDimensions={{...}} highlightedLegendItem={[undefined]} rotation={0} renderingArea={{...}} chartTransform={{...}} axesSpecs={{...}} perPanelAxisGeoms={{...}} perPanelGridLines={{...}} axesStyles={{...}} annotationDimensions={{...}} annotationSpecs={{...}} panelGeoms={{...}} a11ySettings={{...}} onChartRendered={[Function (anonymous)]}>
<figure aria-labelledby={[undefined]} aria-describedby=\\"chart1--defaultSummary\\">
Expand All @@ -95,6 +95,12 @@ exports[`Chart should render the legend name test 1`] = `
</Connect(ScreenReaderSummaryComponent)>
</XYChart>
</Connect(XYChart)>
<Connect(CursorLine)>
<CursorLine theme={{...}} chartRotation={0} cursorPosition={[undefined]} tooltipType=\\"vertical\\" fromExternalEvent={[undefined]} isLine={false} dispatch={[Function: dispatch]} />
</Connect(CursorLine)>
<Connect(CursorCrossLine)>
<CursorCrossLine theme={{...}} chartRotation={0} cursorCrossLinePosition={[undefined]} tooltipType=\\"vertical\\" dispatch={[Function: dispatch]} />
</Connect(CursorCrossLine)>
<Connect(Tooltip) getChartContainerRef={[Function (anonymous)]}>
<Tooltip getChartContainerRef={[Function (anonymous)]} visible={false} zIndex={0} info={{...}} position={{...}} headerFormatter={[undefined]} settings={{...}} rotation={0} chartId=\\"chart1\\" backgroundColor=\\"transparent\\" onPointerMove={[Function (anonymous)]} />
</Connect(Tooltip)>
Expand Down

0 comments on commit ca004a6

Please sign in to comment.