Skip to content

Commit

Permalink
feat(annotations): render line annotations via LineAnnotation spec (#126
Browse files Browse the repository at this point in the history
)
  • Loading branch information
emmacunningham authored Apr 4, 2019
1 parent 34d676a commit 98ff170
Show file tree
Hide file tree
Showing 20 changed files with 2,773 additions and 21 deletions.
1 change: 1 addition & 0 deletions .storybook/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function loadStories() {
require('../stories/rotations.tsx');
require('../stories/styling.tsx');
require('../stories/grid.tsx');
require('../stories/annotations.tsx');
}

configure(loadStories, module);
43 changes: 43 additions & 0 deletions src/components/_annotation.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.elasticChartsAnnotation {
@include euiFontSizeXS;
pointer-events: none;
position: absolute;
z-index: $euiZLevel9;
max-width: $euiSizeXL * 10;
overflow: hidden;
overflow-wrap: break-word;
transition: opacity $euiAnimSpeedNormal;
user-select: none;
}

.elasticChartsAnnotation--hidden, .elasticChartsAnnotation__tooltip--hidden {
opacity: 0;
}

.elasticChartsAnnotation__tooltip {
@include euiBottomShadow($color: $euiColorFullShade);
@include euiFontSizeXS;
pointer-events: none;
position: absolute;
z-index: $euiZLevel9;
background-color: rgba(tintOrShade($euiColorFullShade, 25%, 80%), 0.9);
color: $euiColorGhost;
border-radius: $euiBorderRadius;
max-width: $euiSizeXL * 10;
overflow: hidden;
overflow-wrap: break-word;
transition: opacity $euiAnimSpeedNormal;
user-select: none;
}

.elasticChartsAnnotation__header {
margin: 0;
background: rgba(shade($euiColorGhost, 20%), 0.9);
color: $euiColorFullShade;
padding: 0 8px;
}

.elasticChartsAnnotation__details {
margin: 0;
padding: 0 8px;
}
1 change: 1 addition & 0 deletions src/components/_chart.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
@import 'tooltip';
@import 'crosshair';
@import 'highlighter';
@import 'annotation';
102 changes: 102 additions & 0 deletions src/components/annotation_tooltips.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { inject, observer } from 'mobx-react';
import React from 'react';
import { AnnotationTypes } from '../lib/series/specs';
import { AnnotationId } from '../lib/utils/ids';
import { AnnotationLineProps } from '../state/annotation_utils';
import { ChartStore } from '../state/chart_state';

interface AnnotationTooltipProps {
chartStore?: ChartStore;
}

class AnnotationTooltipComponent extends React.Component<AnnotationTooltipProps> {
static displayName = 'AnnotationTooltip';

renderTooltip() {
const annotationTooltipState = this.props.chartStore!.annotationTooltipState.get();
if (!annotationTooltipState || !annotationTooltipState.isVisible) {
return <div className="elasticChartsAnnotation__tooltip elasticChartsAnnotation__tooltip--hidden" />;
}

const transform = annotationTooltipState.transform;
const chartDimensions = this.props.chartStore!.chartDimensions;

const style = {
transform,
top: chartDimensions.top,
left: chartDimensions.left,
};

return (
<div className="elasticChartsAnnotation__tooltip" style={{ ...style }}>
<p className="elasticChartsAnnotation__header">{annotationTooltipState.header}</p>
<div className="elasticChartsAnnotation__details">
{annotationTooltipState.details}
</div>
</div>
);
}

renderAnnotationLineMarkers(annotationLines: AnnotationLineProps[], id: AnnotationId): JSX.Element[] {
const { chartDimensions } = this.props.chartStore!;

const markers: JSX.Element[] = [];

annotationLines.forEach((line: AnnotationLineProps, index: number) => {
if (!line.marker) {
return;
}

const { transform, icon, color } = line.marker;

const style = {
color,
transform,
top: chartDimensions.top,
left: chartDimensions.left,
};

const markerElement = (
<div className="elasticChartsAnnotation" style={{ ...style }} key={`annotation-${id}-${index}`}>
{icon}
</div>
);

markers.push(markerElement);
});

return markers;
}

renderAnnotationMarkers(): JSX.Element[] {
const { annotationDimensions, annotationSpecs } = this.props.chartStore!;
const markers: JSX.Element[] = [];

annotationDimensions.forEach((annotationLines: AnnotationLineProps[], id: AnnotationId) => {
const annotationSpec = annotationSpecs.get(id);
if (!annotationSpec) {
return;
}

switch (annotationSpec.annotationType) {
case AnnotationTypes.Line:
const lineMarkers = this.renderAnnotationLineMarkers(annotationLines, id);
markers.push(...lineMarkers);
break;
}
});

return markers;
}

render() {
return (
<React.Fragment>
{this.renderAnnotationMarkers()}
{this.renderTooltip()}
</React.Fragment>
);
}
}

export const AnnotationTooltip = inject('chartStore')(observer(AnnotationTooltipComponent));
2 changes: 2 additions & 0 deletions src/components/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Provider } from 'mobx-react';
import React, { CSSProperties, Fragment } from 'react';
import { SpecsParser } from '../specs/specs_parser';
import { ChartStore } from '../state/chart_state';
import { AnnotationTooltip } from './annotation_tooltips';
import { ChartResizer } from './chart_resizer';
import { Crosshair } from './crosshair';
import { Highlighter } from './highlighter';
Expand Down Expand Up @@ -50,6 +51,7 @@ export class Chart extends React.Component<ChartProps> {
{renderer === 'svg' && <SVGChart />}
{renderer === 'canvas' && <ReactChart />}
<Tooltips />
<AnnotationTooltip />
<Legend />
<LegendButton />
<Highlighter />
Expand Down
39 changes: 39 additions & 0 deletions src/components/react_canvas/annotation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { Group, Line } from 'react-konva';
import { AnnotationLineStyle } from '../../lib/themes/theme';
import { Dimensions } from '../../lib/utils/dimensions';
import { AnnotationLineProps } from '../../state/annotation_utils';

interface AnnotationProps {
chartDimensions: Dimensions;
debug: boolean;
lines: AnnotationLineProps[];
lineStyle: AnnotationLineStyle;
}

export class Annotation extends React.PureComponent<AnnotationProps> {
render() {
return this.renderAnnotation();
}
private renderAnnotationLine = (lineConfig: AnnotationLineProps, i: number) => {
const { line } = this.props.lineStyle;
const { position } = lineConfig;

const lineProps = {
points: position,
...line,
};

return <Line key={`tick-${i}`} {...lineProps} />;
}

private renderAnnotation = () => {
const { chartDimensions, lines } = this.props;

return (
<Group x={chartDimensions.left} y={chartDimensions.top}>
{lines.map(this.renderAnnotationLine)}
</Group>
);
}
}
57 changes: 46 additions & 11 deletions src/components/react_canvas/reactive_chart.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { inject, observer } from 'mobx-react';
import React from 'react';
import { Layer, Rect, Stage } from 'react-konva';
import { AnnotationLineStyle } from '../../lib/themes/theme';
import { AnnotationId } from '../../lib/utils/ids';
import { AnnotationDimensions } from '../../state/annotation_utils';
import { ChartStore, Point } from '../../state/chart_state';
import { BrushExtent } from '../../state/utils';
import { Annotation } from './annotation';
import { AreaGeometries } from './area_geometries';
import { Axis } from './axis';
import { BarGeometries } from './bar_geometries';
Expand Down Expand Up @@ -167,6 +171,33 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
return gridComponents;
}

renderAnnotations = () => {
const { annotationDimensions, annotationSpecs, chartDimensions, debug } = this.props.chartStore!;

const annotationComponents: JSX.Element[] = [];
annotationDimensions.forEach((annotation: AnnotationDimensions, id: AnnotationId) => {
const spec = annotationSpecs.get(id);
if (!spec) {
return;
}

// We merge custom style w/ the default on addAnnotationSpec, so this is guaranteed
// to be complete by the time we get to rendering
const lineStyle = spec.style as AnnotationLineStyle;

annotationComponents.push(
<Annotation
key={`annotation-${id}`}
chartDimensions={chartDimensions}
debug={debug}
lines={annotation}
lineStyle={lineStyle}
/>,
);
});
return annotationComponents;
}

renderBrushTool = () => {
const { brushing, brushStart, brushEnd } = this.state;
const { chartDimensions, chartRotation, chartTransform } = this.props.chartStore!;
Expand Down Expand Up @@ -242,15 +273,15 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
const clippings = debug
? {}
: {
clipX: 0,
clipY: 0,
clipWidth: [90, -90].includes(chartRotation)
? chartDimensions.height
: chartDimensions.width,
clipHeight: [90, -90].includes(chartRotation)
? chartDimensions.width
: chartDimensions.height,
};
clipX: 0,
clipY: 0,
clipWidth: [90, -90].includes(chartRotation)
? chartDimensions.height
: chartDimensions.width,
clipHeight: [90, -90].includes(chartRotation)
? chartDimensions.width
: chartDimensions.height,
};

let brushProps = {};
const isBrushEnabled = this.props.chartStore!.isBrushEnabled();
Expand All @@ -261,7 +292,7 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
};
}

const gridClippings = {
const layerClippings = {
clipX: chartDimensions.left,
clipY: chartDimensions.top,
clipWidth: chartDimensions.width,
Expand Down Expand Up @@ -297,7 +328,7 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
}}
{...brushProps}
>
<Layer hitGraphEnabled={false} listening={false} {...gridClippings}>
<Layer hitGraphEnabled={false} listening={false} {...layerClippings}>
{this.renderGrids()}
</Layer>

Expand Down Expand Up @@ -325,6 +356,10 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
<Layer hitGraphEnabled={false} listening={false}>
{this.renderAxes()}
</Layer>

<Layer hitGraphEnabled={false} listening={false}>
{this.renderAnnotations()}
</Layer>
</Stage>
</div>
);
Expand Down
54 changes: 52 additions & 2 deletions src/lib/series/specs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GridLineConfig } from '../themes/theme';
import { AnnotationLineStyle, GridLineConfig } from '../themes/theme';
import { Accessor } from '../utils/accessor';
import { AxisId, GroupId, SpecId } from '../utils/ids';
import { AnnotationId, AxisId, GroupId, SpecId } from '../utils/ids';
import { ScaleContinuousType, ScaleType } from '../utils/scales/scales';
import { CurveType } from './curves';
import { DataSeriesColorsValues } from './series';
Expand Down Expand Up @@ -149,3 +149,53 @@ export enum Position {
Left = 'left',
Right = 'right',
}

export const AnnotationTypes = Object.freeze({
Line: 'line' as AnnotationType,
Rectangle: 'rectangle' as AnnotationType,
Text: 'text' as AnnotationType,
});

export type AnnotationType = 'line' | 'rectangle' | 'text';

export const AnnotationDomainTypes = Object.freeze({
XDomain: 'xDomain' as AnnotationDomainType,
YDomain: 'yDomain' as AnnotationDomainType,
});

export type AnnotationDomainType = 'xDomain' | 'yDomain';
export interface AnnotationDatum {
dataValue: any;
details?: string;
header?: string;
}

export interface LineAnnotationSpec {
/** The id of the annotation */
annotationId: AnnotationId;
/** Annotation type: line, rectangle, text */
annotationType: AnnotationType;
/** The ID of the axis group, generated via getGroupId method
* @default __global__
*/
groupId: GroupId; // defaults to __global__; needed for yDomain position
/** Annotation domain type: AnnotationDomainTypes.XDomain or AnnotationDomainTypes.YDomain */
domainType: AnnotationDomainType;
/** Data values defined with value, details, and header */
dataValues: AnnotationDatum[];
/** Custom line styles */
style?: Partial<AnnotationLineStyle>;
/** Custom marker */
marker?: JSX.Element;
/**
* Custom marker dimensions; will be computed internally
* Any user-supplied values will be overwritten
*/
markerDimensions?: {
width: number;
height: number;
};
}

// TODO: RectangleAnnotationSpec & TextAnnotationSpec
export type AnnotationSpec = LineAnnotationSpec;
Loading

0 comments on commit 98ff170

Please sign in to comment.