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

Annotation label <foreignobject> render #1383

Merged
merged 5 commits into from
Jan 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
87 changes: 87 additions & 0 deletions packages/visx-annotation/src/components/HtmlLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useContext, useMemo } from 'react';
import cx from 'classnames';
import useMeasure from 'react-use-measure';
import Group from '@visx/group/lib/Group';
import AnnotationContext from '../context/AnnotationContext';
import AnchorLine from './LabelAnchorLine';
import { LabelProps } from './Label';

const wrapperStyle = { display: 'inline-block' };

export type HtmlLabelProps = Pick<
LabelProps,
| 'anchorLineStroke'
| 'className'
| 'horizontalAnchor'
| 'resizeObserverPolyfill'
| 'showAnchorLine'
| 'verticalAnchor'
| 'x'
| 'y'
> & {
/** Pass in a custom element as the label to style as you like. Renders inside a <foreignObject>, be aware that most non-browser SVG renderers will not render HTML <foreignObject>s. See: https://github.com/airbnb/visx/issues/1173#issuecomment-1014380545. */
children?: React.ReactNode;
};
export default function HtmlLabel({
anchorLineStroke = '#222',
children,
className,
horizontalAnchor: propsHorizontalAnchor,
resizeObserverPolyfill,
showAnchorLine = true,
verticalAnchor: propsVerticalAnchor,
x: propsX,
y: propsY,
}: HtmlLabelProps) {
// we must measure the rendered title + subtitle to compute container height
const [labelRef, titleBounds] = useMeasure({
polyfill: resizeObserverPolyfill,
});
const { width, height } = titleBounds;

// if props are provided, they take precedence over context
const { x = 0, y = 0, dx = 0, dy = 0 } = useContext(AnnotationContext);

// offset container position based on horizontal + vertical anchor
const horizontalAnchor =
propsHorizontalAnchor || (Math.abs(dx) < Math.abs(dy) ? 'middle' : dx > 0 ? 'start' : 'end');
const verticalAnchor =
propsVerticalAnchor || (Math.abs(dx) > Math.abs(dy) ? 'middle' : dy > 0 ? 'start' : 'end');

const containerCoords = useMemo(() => {
let adjustedX: number = propsX == null ? x + dx : propsX;
let adjustedY: number = propsY == null ? y + dy : propsY;

if (horizontalAnchor === 'middle') adjustedX -= width / 2;
if (horizontalAnchor === 'end') adjustedX -= width;
if (verticalAnchor === 'middle') adjustedY -= height / 2;
if (verticalAnchor === 'end') adjustedY -= height;

return { x: adjustedX, y: adjustedY };
}, [propsX, x, dx, propsY, y, dy, horizontalAnchor, verticalAnchor, width, height]);

return (
<Group
top={containerCoords.y}
left={containerCoords.x}
pointerEvents="none"
className={cx('visx-annotationlabel', className)}
>
{showAnchorLine && (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one thing I noticed while playing with this is that the line renders behind the HTML, do you think we should flip this and render the anchor line after foreignObject?

<AnchorLine
anchorLineOrientation={Math.abs(dx) > Math.abs(dy) ? 'vertical' : 'horizontal'}
anchorLineStroke={anchorLineStroke}
verticalAnchor={verticalAnchor}
horizontalAnchor={horizontalAnchor}
width={width}
height={height}
/>
)}
<foreignObject width={width} height={height} overflow="visible">
<div ref={labelRef} style={wrapperStyle}>
Copy link
Collaborator

@williaster williaster Jan 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I was playing with this I found myself adding a bunch of styles to a div in children, I wonder if we should just expose this so users don't need an extra DOM element? something like containerStyle?: React.CSSProperties and then we can spread it in like style={containerStyle ? { ...wrapperStyle, containerStyle } : wrapperStyle

{children}
</div>
</foreignObject>
</Group>
);
}
37 changes: 16 additions & 21 deletions packages/visx-annotation/src/components/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Text, { TextProps } from '@visx/text/lib/Text';
import { useText } from '@visx/text';
import useMeasure, { Options as UseMeasureOptions } from 'react-use-measure';
import AnnotationContext from '../context/AnnotationContext';
import AnchorLine from './LabelAnchorLine';

export type LabelProps = {
/** Stroke color of anchor line. */
Expand Down Expand Up @@ -93,9 +94,13 @@ export default function Label({
x: propsX,
y: propsY,
}: LabelProps) {
// we must measure the rendered title + subtitle to compute container height
const [titleRef, titleBounds] = useMeasure({ polyfill: resizeObserverPolyfill });
const [subtitleRef, subtitleBounds] = useMeasure({ polyfill: resizeObserverPolyfill });
// we must measure the rendered html content to compute container height
const [titleRef, titleBounds] = useMeasure({
polyfill: resizeObserverPolyfill,
});
const [subtitleRef, subtitleBounds] = useMeasure({
polyfill: resizeObserverPolyfill,
});

const padding = useMemo(() => getCompletePadding(backgroundPadding), [backgroundPadding]);

Expand Down Expand Up @@ -182,10 +187,6 @@ export default function Label({
[subtitleFontSize, subtitleFontWeight, subtitleFontFamily],
) as React.CSSProperties;

const anchorLineOrientation = Math.abs(dx) > Math.abs(dy) ? 'vertical' : 'horizontal';

const backgroundOutline = showAnchorLine ? { stroke: anchorLineStroke, strokeWidth: 2 } : null;

return !title && !subtitle ? null : (
<Group
top={containerCoords.y}
Expand All @@ -206,20 +207,14 @@ export default function Label({
/>
)}
{showAnchorLine && (
<>
{anchorLineOrientation === 'horizontal' && verticalAnchor === 'start' && (
<line {...backgroundOutline} x1={0} x2={width} y1={0} y2={0} />
)}
{anchorLineOrientation === 'horizontal' && verticalAnchor === 'end' && (
<line {...backgroundOutline} x1={0} x2={width} y1={height} y2={height} />
)}
{anchorLineOrientation === 'vertical' && horizontalAnchor === 'start' && (
<line {...backgroundOutline} x1={0} x2={0} y1={0} y2={height} />
)}
{anchorLineOrientation === 'vertical' && horizontalAnchor === 'end' && (
<line {...backgroundOutline} x1={width} x2={width} y1={0} y2={height} />
)}
</>
<AnchorLine
anchorLineOrientation={Math.abs(dx) > Math.abs(dy) ? 'vertical' : 'horizontal'}
anchorLineStroke={anchorLineStroke}
verticalAnchor={verticalAnchor}
horizontalAnchor={horizontalAnchor}
width={width}
height={height}
/>
)}
{title && (
<Text
Expand Down
39 changes: 39 additions & 0 deletions packages/visx-annotation/src/components/LabelAnchorLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { TextProps } from '@visx/text';

interface AnchorLineProps {
anchorLineOrientation: 'horizontal' | 'vertical';
verticalAnchor: TextProps['verticalAnchor'];
horizontalAnchor: TextProps['textAnchor'];
anchorLineStroke: string;
width: number;
height: number;
}

export default function AnchorLine({
anchorLineOrientation,
anchorLineStroke,
verticalAnchor,
horizontalAnchor,
width,
height,
}: AnchorLineProps) {
const backgroundOutline = { stroke: anchorLineStroke, strokeWidth: 2 };

return (
<>
{anchorLineOrientation === 'horizontal' && verticalAnchor === 'start' && (
<line {...backgroundOutline} x1={0} x2={width} y1={0} y2={0} />
)}
{anchorLineOrientation === 'horizontal' && verticalAnchor === 'end' && (
<line {...backgroundOutline} x1={0} x2={width} y1={height} y2={height} />
)}
{anchorLineOrientation === 'vertical' && horizontalAnchor === 'start' && (
<line {...backgroundOutline} x1={0} x2={0} y1={0} y2={height} />
)}
{anchorLineOrientation === 'vertical' && horizontalAnchor === 'end' && (
<line {...backgroundOutline} x1={width} x2={width} y1={0} y2={height} />
)}
</>
);
}
1 change: 1 addition & 0 deletions packages/visx-annotation/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as Connector } from './components/Connector';
export { default as Label } from './components/Label';
export { default as HtmlLabel } from './components/HtmlLabel';
export { default as CircleSubject } from './components/CircleSubject';
export { default as LineSubject } from './components/LineSubject';
export { default as Annotation } from './components/Annotation';
Expand Down
16 changes: 10 additions & 6 deletions packages/visx-annotation/test/Label.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('<Label />', () => {
it('should render title Text', () => {
expect(
shallow(<Label title="title test" resizeObserverPolyfill={ResizeObserver} />)
.dive()
.children()
.find(Text)
.prop('children'),
Expand All @@ -25,6 +26,7 @@ describe('<Label />', () => {
resizeObserverPolyfill={ResizeObserver}
/>,
)
.dive()
.children()
.find(Text)
.at(1)
Expand All @@ -33,16 +35,18 @@ describe('<Label />', () => {
});
it('should render a background', () => {
expect(
shallow(
<Label title="title test" showBackground resizeObserverPolyfill={ResizeObserver} />,
).find('rect'),
shallow(<Label title="title test" showBackground resizeObserverPolyfill={ResizeObserver} />)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 thanks for fixing these

.dive()
.find('rect'),
).toHaveLength(1);
});
it('should render an anchor line', () => {
expect(
shallow(
<Label title="title test" showAnchorLine resizeObserverPolyfill={ResizeObserver} />,
).find('line'),
shallow(<Label title="title test" showAnchorLine resizeObserverPolyfill={ResizeObserver} />)
.dive()
.find('AnchorLine')
.dive()
.find('line'),
).toHaveLength(1);
});
});
2 changes: 2 additions & 0 deletions packages/visx-demo/src/pages/docs/annotation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CircleSubject from '../../../../visx-annotation/src/components/CircleSubj
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 HtmlLabel from '../../../../visx-annotation/src/components/HtmlLabel';
import LinePathAnnotationDeprecated from '../../../../visx-annotation/src/deprecated/LinePathAnnotation';
import DocPage from '../../components/DocPage';
import AnnotationTile from '../../components/Gallery/AnnotationTile';
Expand All @@ -17,6 +18,7 @@ const components = [
LineSubject,
Connector,
Label,
HtmlLabel,
LinePathAnnotationDeprecated,
];

Expand Down