Skip to content

Commit

Permalink
new(demo): add Annotation demo (#909)
Browse files Browse the repository at this point in the history
* new(demo): add annotation page + sandbox

* docs(annotation): add AnnotationTile, add to Gallery, update /docs/annotation

* demo(annotation): factor out ExampleControls, add more controls

* demo(annotation): update prop names

* types(demo/annotation): fix ExampleControls provided types

* docs(annotation): add react-annotation + ResizeObserver info

* docs(annotation): improve markdown syntax
  • Loading branch information
williaster committed Nov 6, 2020
1 parent 5387453 commit 1119398
Show file tree
Hide file tree
Showing 13 changed files with 577 additions and 5 deletions.
52 changes: 51 additions & 1 deletion packages/visx-annotation/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,57 @@
</a>
</p>

Annotations enable you to label points, thresholds, or regions of a visualization to provide additional context for your chart consumer.
SVG `Annotation`s enable you to label points, thresholds, or regions of a visualization to provide additional context to for your chart consumer. This package is heavily inspired by [Susie Lu](https://github.com/susielu/)'s [`react-annotation`](https://github.com/susielu/react-annotation) library.

Each annotation consists of three (optional) parts:

1) `Subject` (`CircleSubject`, `LineSubject`, more 🔜) – what part of a chart is being annotated (point, line, region)

2) `Label` – the text component for the annotation. Handles SVG text wrapping using `@visx/text`, and supports `title` and `subtitle` customization as well as vertical & horizontal anchors / alignment

3) `Connector` – line connecting a subject and label


The `Annotation` or `EditableAnnotation` component wrappers allow you to compose these components and simplify their individual positioning:

```tsx
<EditableAnnotation
x={subjectX}
y={subjectY}
dx={labelDx} // x offset of label from subject
dy={labelDy} // y offset of label from subject
onDragEnd={({ x, y, dx, dy }) => ...}
>
<Connector />
<CircleSubject />
<Label title="Context about this point" subtitle="More deets">
</EditableAnnotation>
```

Components can also be used in isolation, in which case you must specify exact positions for each item:

```tsx
() => (
<g>
<Connector x={subjectX} y={subjectY} dx={labelDx} dy={labelDy} />
<CircleSubject x={subjectX} y={subjectY} />
<Label x={subjectX + labelDx} y={subjectY + labelDy} title="...">
</g>
)
```

##### ⚠️ `ResizeObserver` dependency

The `Label` component relies on `ResizeObserver`s for auto-sizing. If you need a polyfill, you can either polute the `window` object or inject it cleanly through props:


```tsx
import { ResizeObserver } from 'your-favorite-polyfill';

function App() {
return <Label resizeObserverPolyfill={ResizeObserver} {...} />
```
## Installation
Expand Down
2 changes: 1 addition & 1 deletion packages/visx-annotation/src/components/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export default function AnnotationLabel({
fontColor = '#222',
horizontalAnchor: propsHorizontalAnchor,
resizeObserverPolyfill,
showAnchorLine,
showAnchorLine = true,
showBackground = true,
subtitle,
subtitleDy = 4,
Expand Down
29 changes: 29 additions & 0 deletions packages/visx-demo/src/components/Gallery/AnnotationTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import Annotation, { AnnotationProps, greens } from '../../sandboxes/visx-annotation/Example';
import GalleryTile from '../GalleryTile';

export { default as packageJson } from '../../sandboxes/visx-area/package.json';

const tileStyles = { background: greens[0] };
const detailsStyles: React.CSSProperties = {
color: greens[2],
borderBottomRightRadius: 16,
borderBottomLeftRadius: 16,
};
const exampleProps = { compact: true };
const exampleRenderer: React.FC<AnnotationProps> = props =>
props.width > 0 && props.height > 0 ? <Annotation {...props} /> : null;

export default function AnnotationTile() {
return (
<GalleryTile<AnnotationProps>
title="Annotation"
description="<Annotation />"
exampleRenderer={exampleRenderer}
exampleUrl="/annotation"
detailsStyles={detailsStyles}
tileStyles={tileStyles}
exampleProps={exampleProps}
/>
);
}
3 changes: 3 additions & 0 deletions packages/visx-demo/src/components/Gallery/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Tilt from 'react-tilt';
import Link from 'next/link';
import { useRouter } from 'next/router';

import * as AnnotationTile from './AnnotationTile';
import * as AreaTile from './AreaTile';
import * as AxisTile from './AxisTile';
import * as BarGroupHorizontalTile from './BarGroupHorizontalTile';
Expand Down Expand Up @@ -63,6 +64,7 @@ const tiles = [
StreamGraphTile,
LegendsTile,
ThresholdTile,
AnnotationTile,
TreemapTile,
ZoomITile,
LineRadialTile,
Expand Down Expand Up @@ -117,6 +119,7 @@ export default function Gallery() {
query: routePackage === visxPackage ? undefined : { pkg: visxPackage },
}}
>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
className={cx('filter-button', {
emphasize: routePackage === visxPackage,
Expand Down
16 changes: 16 additions & 0 deletions packages/visx-demo/src/pages/annotation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import Show from '../components/Show';
import Annotation from '../sandboxes/visx-annotation/Example';
import AnnotationSource from '!!raw-loader!../sandboxes/visx-annotation/Example';
import packageJson from '../sandboxes/visx-annotation/package.json';

export default () => (
<Show
component={Annotation}
title="Annotation"
codeSandboxDirectoryName="visx-annotation"
packageJson={packageJson}
>
{AnnotationSource}
</Show>
);
29 changes: 26 additions & 3 deletions packages/visx-demo/src/pages/docs/annotation.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
import React from 'react';
import AnnotationReadme from '!!raw-loader!../../../../visx-annotation/Readme.md';
import LinePathAnnotation from '../../../../visx-annotation/src/deprecated/LinePathAnnotation';
import Annotation from '../../../../visx-annotation/src/components/Annotation';
import EditableAnnotation from '../../../../visx-annotation/src/components/EditableAnnotation';
import CircleSubject from '../../../../visx-annotation/src/components/CircleSubject';
import LineSubject from '../../../../visx-annotation/src/components/LineSubject';
import Connector from '../../../../visx-annotation/src/components/Connector';
import Label from '../../../../visx-annotation/src/components/Label';
import LinePathAnnotationDeprecated from '../../../../visx-annotation/src/deprecated/LinePathAnnotation';
import DocPage from '../../components/DocPage';
import AnnotationTile from '../../components/Gallery/AnnotationTile';

const components = [LinePathAnnotation];
const components = [
Annotation,
EditableAnnotation,
CircleSubject,
LineSubject,
Connector,
Label,
LinePathAnnotationDeprecated,
];

const examples = [AnnotationTile];

const AnnotationDocs = () => (
<DocPage components={components} readme={AnnotationReadme} visxPackage="annotation" />
<DocPage
examples={examples}
components={components}
readme={AnnotationReadme}
visxPackage="annotation"
/>
);

export default AnnotationDocs;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import annotationPackageJson from './visx-annotation/package.json';
import areaPackageJson from './visx-area/package.json';
import axisPackageJson from './visx-axis/package.json';
import bargroupPackageJson from './visx-bargroup/package.json';
Expand Down Expand Up @@ -42,6 +43,7 @@ import extractVisxDepsFromPackageJson from '../components/util/extractVisxDepsFr
import { VisxPackage } from '../types';

const examples = [
annotationPackageJson,
areaPackageJson,
axisPackageJson,
bargroupHorizontalPackageJson,
Expand Down
107 changes: 107 additions & 0 deletions packages/visx-demo/src/sandboxes/visx-annotation/Example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';
import { Label, Connector, CircleSubject, LineSubject } from '@visx/annotation';
import { LinePath } from '@visx/shape';
import ExampleControls from './ExampleControls';
import findNearestDatum from './findNearestDatum';

export type AnnotationProps = {
width: number;
height: number;
compact?: boolean;
};

export const orange = '#ff7e67';
export const greens = ['#ecf4f3', '#68b0ab', '#006a71'];

export default function Example({ width, height, compact = false }: AnnotationProps) {
return (
<ExampleControls width={width} height={height} compact={compact}>
{({
AnnotationComponent,
annotationPosition,
approxTooltipHeight,
connectorType,
data,
getDate,
getStockValue,
horizontalAnchor,
labelWidth,
setAnnotationPosition,
showAnchorLine,
subjectType,
subtitle,
title,
verticalAnchor,
xScale,
yScale,
}) => (
<svg width={width} height={height}>
<rect width={width} height={height} fill={greens[0]} />
<LinePath
stroke={greens[2]}
strokeWidth={2}
data={data}
x={d => xScale(getDate(d)) ?? 0}
y={d => yScale(getStockValue(d)) ?? 0}
/>
<AnnotationComponent
width={width}
height={height}
x={annotationPosition.x}
y={annotationPosition.y}
dx={annotationPosition.dx}
dy={annotationPosition.dy}
onDragEnd={({ event, ...nextPosition }) => {
// snap Annotation to the nearest data point
const nearestDatum = findNearestDatum({
accessor: subjectType === 'horizontal-line' ? getStockValue : getDate,
data,
scale: subjectType === 'horizontal-line' ? yScale : xScale,
value: subjectType === 'horizontal-line' ? nextPosition.y : nextPosition.x,
});
const x = xScale(getDate(nearestDatum)) ?? 0;
const y = yScale(getStockValue(nearestDatum)) ?? 0;

// flip label to keep in view
const shouldFlipDx =
(nextPosition.dx > 0 && x + nextPosition.dx + labelWidth > width) ||
(nextPosition.dx < 0 && x + nextPosition.dx - labelWidth <= 0);
const shouldFlipDy = // 100 is est. tooltip height
(nextPosition.dy > 0 && height - (y + nextPosition.dy) < approxTooltipHeight) ||
(nextPosition.dy < 0 && y + nextPosition.dy - approxTooltipHeight <= 0);
setAnnotationPosition({
x,
y,
dx: (shouldFlipDx ? -1 : 1) * nextPosition.dx,
dy: (shouldFlipDy ? -1 : 1) * nextPosition.dy,
});
}}
>
<Connector stroke={orange} type={connectorType} />
<Label
backgroundFill="white"
showAnchorLine={showAnchorLine}
anchorLineStroke={greens[2]}
backgroundProps={{ stroke: greens[1] }}
fontColor={greens[2]}
horizontalAnchor={horizontalAnchor}
subtitle={subtitle}
title={title}
verticalAnchor={verticalAnchor}
width={labelWidth}
/>
{subjectType === 'circle' && <CircleSubject stroke={orange} />}
{subjectType !== 'circle' && (
<LineSubject
orientation={subjectType === 'vertical-line' ? 'vertical' : 'horizontal'}
stroke={orange}
min={0}
max={subjectType === 'vertical-line' ? height : width}
/>
)}
</AnnotationComponent>
</svg>
)}
</ExampleControls>
);
}
Loading

0 comments on commit 1119398

Please sign in to comment.