Skip to content

Commit

Permalink
feat(interactions): crosshair
Browse files Browse the repository at this point in the history
Removed any listeners from the canvas elements. Using a map for each geometry on the chart: mappings x value and any relative Y values associated. We invert the mouse coordinates using the xScale to get the right X key to use on that map. This increase the performance on highly/medium dense charts. Two new props are added to the `Settings` specs: `tooltipType` to specify the tooltip type (vertical cursor, crosshair, follow, none) and the `tooltipSnap` to snap the cursor to the grid for linear charts.

close #80, close #58, close #88
  • Loading branch information
markov00 committed Mar 22, 2019
1 parent 43578e0 commit 5ddd1a8
Show file tree
Hide file tree
Showing 65 changed files with 6,174 additions and 1,185 deletions.
2 changes: 2 additions & 0 deletions src/components/_chart.scss
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
@import 'legend';
@import 'tooltip';
@import 'crosshair';
@import 'highlighter';
16 changes: 16 additions & 0 deletions src/components/_crosshair.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.elasticChartsCrosshair {
position: absolute;
pointer-events: none;
}

.elasticChartsCrosshair__band {
position: absolute;
pointer-events: none;
}

.elasticChartsCrosshair__line {
position: absolute;
pointer-events: none;
z-index: $euiZLevel8;
background: 'transparent';
}
11 changes: 11 additions & 0 deletions src/components/_highlighter.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.elasticChartsHighlighter {
position: absolute;
z-index: 1000;
pointer-events: none;
top: 0;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
}
37 changes: 24 additions & 13 deletions src/components/_tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,49 @@
@include euiBottomShadow($color: $euiColorFullShade);
@include euiFontSizeXS;
pointer-events: none;
position: fixed;
position: absolute;
z-index: $euiZLevel9;
background-color: tintOrShade($euiColorFullShade, 25%, 90%);
background-color: rgba(tintOrShade($euiColorFullShade, 25%, 80%), 0.9);
color: $euiColorGhost;
border-radius: $euiBorderRadius;
max-width: $euiSizeXL * 10;
overflow: hidden;
overflow-wrap: break-word;
opacity: 0.95;
transition: opacity $euiAnimSpeedNormal;
user-select: none;
> :last-child {
margin-bottom: $euiSizeS;
}

> * {
margin: $euiSizeS $euiSizeS 0;
}

table {
td,
th {
padding: $euiSizeXS;
padding: 3px;
}
}
table {
width: 100%;
}
}
.elasticChartsTooltip__label {
// max-width: $euiSizeXL * 3;
font-weight: $euiFontWeightMedium;
color: shade($euiColorGhost, 20%);
border-left: 3px solid red;
}
.elasticChartsTooltip__rowHighlighted {
background-color: shade($euiColorGhost, 20%);
color: shade($euiColorGhost, 90%);
.elasticChartsTooltip__label {
color: shade($euiColorGhost, 90%);
}
}

.elasticChartsTooltip--hidden {
opacity: 0;
}

.elasticChartsTooltip__header {
margin: 0;
background: rgba(shade($euiColorGhost, 20%), 0.9);
color: $euiColorFullShade;
padding: 0 8px;
}
.elasticChartsTooltip__table {
margin: 4px;
}
10 changes: 7 additions & 3 deletions src/components/chart.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import classNames from 'classnames';
import { Provider } from 'mobx-react';
import React, { Fragment } from 'react';
import React, { CSSProperties, Fragment } from 'react';
import { SpecsParser } from '../specs/specs_parser';
import { ChartStore } from '../state/chart_state';
import { ChartResizer } from './chart_resizer';
import { Crosshair } from './crosshair';
import { Highlighter } from './highlighter';
import { Legend } from './legend';
import { LegendButton } from './legend_button';
import { ReactiveChart as ReactChart } from './react_canvas/reactive_chart';
Expand All @@ -27,10 +29,10 @@ export class Chart extends React.Component<ChartProps> {
}
render() {
const { renderer, size, className } = this.props;
let containerStyle;
let containerStyle: CSSProperties;
if (size) {
containerStyle = {
position: 'relative' as 'relative',
position: 'relative',
width: size[0],
height: size[1],
};
Expand All @@ -44,11 +46,13 @@ export class Chart extends React.Component<ChartProps> {
<SpecsParser>{this.props.children}</SpecsParser>
<div style={containerStyle} className={chartClass}>
<ChartResizer />
<Crosshair />
{renderer === 'svg' && <SVGChart />}
{renderer === 'canvas' && <ReactChart />}
<Tooltips />
<Legend />
<LegendButton />
<Highlighter />
</div>
</Fragment>
</Provider>
Expand Down
3 changes: 2 additions & 1 deletion src/components/chart_resizer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class Resizer extends React.Component<ResizerProps> {
onResize = (entries: ResizeObserverEntry[]) => {
entries.forEach((entry) => {
const { width, height } = entry.contentRect;
this.props.chartStore!.updateParentDimensions(width, height, 0, 0);
const { top, left } = entry.target.getBoundingClientRect();
this.props.chartStore!.updateParentDimensions(width, height, top, left);
});
}

Expand Down
89 changes: 89 additions & 0 deletions src/components/crosshair.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { inject, observer } from 'mobx-react';
import React, { CSSProperties } from 'react';
import { TooltipType } from '../lib/utils/interactions';
import { ChartStore } from '../state/chart_state';
import { isHorizontalRotation } from '../state/utils';

interface CrosshairProps {
chartStore?: ChartStore;
}

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

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

render() {
const { isCrosshairVisible } = this.props.chartStore!;
if (!isCrosshairVisible.get()) {
return <div className="elasticChartsCrosshair" />;
}

return (
<div className="elasticChartsCrosshair">
{this.renderBand()}
{this.renderLine()}
</div>
);
}

renderBand() {
const {
chartTheme: {
crosshair: { band },
},
cursorBandPosition,
tooltipType,
} = this.props.chartStore!;

if (!canRenderBand(tooltipType.get(), band.visible)) {
return null;
}
const style: CSSProperties = {
...cursorBandPosition,
background: band.fill,
};

return <div className="elasticChartsCrosshair__band" style={style} />;
}

renderLine() {
const {
chartTheme: {
crosshair: { line },
},
cursorLinePosition,
tooltipType,
chartRotation,
} = this.props.chartStore!;

if (!canRenderHelpLine(tooltipType.get(), line.visible)) {
return null;
}
const isHorizontalRotated = isHorizontalRotation(chartRotation);
let style: CSSProperties;
if (isHorizontalRotated) {
style = {
...cursorLinePosition,
borderTopWidth: line.strokeWidth,
borderTopColor: line.stroke,
borderTopStyle: line.dash ? 'dashed' : 'solid',
};
} else {
style = {
...cursorLinePosition,
borderLeftWidth: line.strokeWidth,
borderLeftColor: line.stroke,
borderLeftStyle: line.dash ? 'dashed' : 'solid',
};
}
return <div className="elasticChartsCrosshair__line" style={style} />;
}
}

export const Crosshair = inject('chartStore')(observer(CrosshairComponent));
52 changes: 52 additions & 0 deletions src/components/highlighter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { inject, observer } from 'mobx-react';
import React from 'react';
import { ChartStore } from '../state/chart_state';

interface HighlighterProps {
chartStore?: ChartStore;
}

class HighlighterComponent extends React.Component<HighlighterProps> {
static displayName = 'Highlighter';

render() {
const {
highlightedGeometries,
chartTransform,
chartDimensions,
chartRotation,
} = this.props.chartStore!;
const left = chartDimensions.left + chartTransform.x;
const top = chartDimensions.top + chartTransform.y;
return (
<svg className="elasticChartsHighlighter">
<g transform={`translate(${left}, ${top}) rotate(${chartRotation})`}>
{highlightedGeometries.map((highlightedGeometry, i) => {
const {
color,
geom: { x, y, width, height, isPoint },
} = highlightedGeometry;
if (isPoint) {
return (
<circle
key={i}
cx={x}
cy={y}
r={width}
stroke={color}
strokeWidth={4}
fill="transparent"
/>
);
}
return (
<rect key={i} x={x} y={y} width={width} height={height} fill="white" opacity={0.4} />
);
})}
</g>
</svg>
);
}
}

export const Highlighter = inject('chartStore')(observer(HighlighterComponent));
26 changes: 13 additions & 13 deletions src/components/legend.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import classNames from 'classnames';
import { inject, observer } from 'mobx-react';
import React from 'react';
Expand Down Expand Up @@ -35,7 +32,7 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
if (
!showLegend.get() ||
!initialized.get() ||
legendItems.length === 0 ||
legendItems.size === 0 ||
legendPosition === undefined
) {
return null;
Expand Down Expand Up @@ -70,19 +67,19 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
className="elasticChartsLegendListContainer"
responsive={false}
>
{legendItems.map((item, index) => {
{[...legendItems.values()].map((item) => {
const legendItemProps = {
key: index,
key: item.key,
className: 'elasticChartsLegendList__item',
onMouseEnter: this.onLegendItemMouseover(index),
onMouseEnter: this.onLegendItemMouseover(item.key),
onMouseLeave: this.onLegendItemMouseout,
};

const { color, label, isVisible } = item;

return (
<EuiFlexItem {...legendItemProps}>
{this.renderLegendElement({ color, label, isVisible }, index)}
{this.renderLegendElement({ color, label, isVisible }, item.key)}
</EuiFlexItem>
);
})}
Expand All @@ -92,16 +89,19 @@ class LegendComponent extends React.Component<ReactiveChartProps> {
);
}

private onLegendItemMouseover = (legendItemIndex: number) => () => {
this.props.chartStore!.onLegendItemOver(legendItemIndex);
private onLegendItemMouseover = (legendItemKey: string) => () => {
this.props.chartStore!.onLegendItemOver(legendItemKey);
}

private onLegendItemMouseout = () => {
this.props.chartStore!.onLegendItemOut();
}

private renderLegendElement = ({ color, label, isVisible }: Partial<LegendItem>, legendItemIndex: number) => {
const props = { color, label, isVisible, index: legendItemIndex };
private renderLegendElement = (
{ color, label, isVisible }: Partial<LegendItem>,
legendItemKey: string,
) => {
const props = { color, label, isVisible, legendItemKey };

return <LegendElement {...props} />;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/legend_button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class LegendButtonComponent extends React.Component<ReactiveChartProps> {
render() {
const { initialized, legendItems, legendCollapsed, showLegend } = this.props.chartStore!;

if (!showLegend.get() || !initialized.get() || legendItems.length === 0) {
if (!showLegend.get() || !initialized.get() || legendItems.size === 0) {
return null;
}
const isOpen = !legendCollapsed.get();
Expand Down
Loading

0 comments on commit 5ddd1a8

Please sign in to comment.