From 78295bf4717c99ba123c8cf8079c9f523824ab87 Mon Sep 17 00:00:00 2001 From: Dan Wood Date: Fri, 26 Nov 2021 11:51:44 +1100 Subject: [PATCH 1/5] Annotation label render Add option to pass in an element to render as a label as a child. Uses to allow user to pass in an HTML element. Related to this discussion: https://github.com/airbnb/visx/issues/1173 This has the benefit of letting the element handle text reflow, with the drawback being that the user need to manage min and max width of the container element they render. This approach seems much better imo, favouring composability over configuration which is much more in line with the philosophy of d3. --- .../visx-annotation/src/components/Label.tsx | 146 +++++++++++++++--- 1 file changed, 125 insertions(+), 21 deletions(-) diff --git a/packages/visx-annotation/src/components/Label.tsx b/packages/visx-annotation/src/components/Label.tsx index cc9e3e29b..d3438acd7 100644 --- a/packages/visx-annotation/src/components/Label.tsx +++ b/packages/visx-annotation/src/components/Label.tsx @@ -15,6 +15,8 @@ export type LabelProps = { backgroundPadding?: number | { top?: number; right?: number; bottom?: number; left?: number }; /** Additional props to be passed to background SVGRectElement. */ backgroundProps?: React.SVGProps; + /** Pass in a custom element as the label to style as you like. Renders inside a */ + children?: React.ReactNode; /** Optional className to apply to container in addition to 'visx-annotation-label'. */ className?: string; /** Color of title and subtitle text. */ @@ -67,7 +69,79 @@ function getCompletePadding(padding: LabelProps['backgroundPadding']) { return { ...DEFAULT_PADDING, ...padding }; } -export default function Label({ +export default function Label({ ...props }: LabelProps) { + if (props.children) { + return ; + } + return ; +} + +function ForeignObjectLabel({ + anchorLineStroke = '#222', + children, + className, + horizontalAnchor: propsHorizontalAnchor, + resizeObserverPolyfill, + showAnchorLine = true, + verticalAnchor: propsVerticalAnchor, + x: propsX, + y: propsY, +}: LabelProps) { + // we must measure the rendered title + subtitle to compute container height + const [labelRef, titleBounds] = useMeasure({ + polyfill: resizeObserverPolyfill, + }); + const { width, height } = titleBounds; + console.log(width, height); + + // 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 ( + + {showAnchorLine && ( + Math.abs(dy) ? 'vertical' : 'horizontal'} + anchorLineStroke={anchorLineStroke} + verticalAnchor={verticalAnchor} + horizontalAnchor={horizontalAnchor} + width={width} + height={height} + /> + )} + +
+ {children} +
+
+
+ ); +} + +function SvgLabel({ anchorLineStroke = '#222', backgroundFill = '#eaeaea', backgroundPadding, @@ -94,8 +168,12 @@ export default function Label({ 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 }); + const [titleRef, titleBounds] = useMeasure({ + polyfill: resizeObserverPolyfill, + }); + const [subtitleRef, subtitleBounds] = useMeasure({ + polyfill: resizeObserverPolyfill, + }); const padding = useMemo(() => getCompletePadding(backgroundPadding), [backgroundPadding]); @@ -182,10 +260,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 : ( )} {showAnchorLine && ( - <> - {anchorLineOrientation === 'horizontal' && verticalAnchor === 'start' && ( - - )} - {anchorLineOrientation === 'horizontal' && verticalAnchor === 'end' && ( - - )} - {anchorLineOrientation === 'vertical' && horizontalAnchor === 'start' && ( - - )} - {anchorLineOrientation === 'vertical' && horizontalAnchor === 'end' && ( - - )} - + Math.abs(dy) ? 'vertical' : 'horizontal'} + anchorLineStroke={anchorLineStroke} + verticalAnchor={verticalAnchor} + horizontalAnchor={horizontalAnchor} + width={width} + height={height} + /> )} {title && ( ); } + +interface AnchorLineProps { + anchorLineOrientation: 'horizontal' | 'vertical'; + verticalAnchor: TextProps['verticalAnchor']; + horizontalAnchor: TextProps['textAnchor']; + anchorLineStroke: string; + width: number; + height: number; +} +function AnchorLine({ + anchorLineOrientation, + anchorLineStroke, + verticalAnchor, + horizontalAnchor, + width, + height, +}: AnchorLineProps) { + const backgroundOutline = { stroke: anchorLineStroke, strokeWidth: 2 }; + + return ( + <> + {anchorLineOrientation === 'horizontal' && verticalAnchor === 'start' && ( + + )} + {anchorLineOrientation === 'horizontal' && verticalAnchor === 'end' && ( + + )} + {anchorLineOrientation === 'vertical' && horizontalAnchor === 'start' && ( + + )} + {anchorLineOrientation === 'vertical' && horizontalAnchor === 'end' && ( + + )} + + ); +} From ce5cd189f4bb31fb5f2f5d9b4edc35053ddc40dd Mon Sep 17 00:00:00 2001 From: Dan Wood Date: Fri, 26 Nov 2021 12:51:03 +1100 Subject: [PATCH 2/5] Fix child render for Safari Add witdth and height to . Worked for Chrome but was not displaying anything for Safari. --- packages/visx-annotation/src/components/Label.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/visx-annotation/src/components/Label.tsx b/packages/visx-annotation/src/components/Label.tsx index d3438acd7..6826807c7 100644 --- a/packages/visx-annotation/src/components/Label.tsx +++ b/packages/visx-annotation/src/components/Label.tsx @@ -132,7 +132,7 @@ function ForeignObjectLabel({ height={height} /> )} - +
{children}
From a3bfc41fa17d95df38780f80aab5095a4151c9cf Mon Sep 17 00:00:00 2001 From: Dan Wood Date: Fri, 26 Nov 2021 13:35:08 +1100 Subject: [PATCH 3/5] Fix test cases Not sure how idomatic all the `dive()`s are, but I've heard complaints about them so I'm guess it's the way to go. --- packages/visx-annotation/test/Label.test.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/visx-annotation/test/Label.test.tsx b/packages/visx-annotation/test/Label.test.tsx index d6ef35970..e71f57280 100644 --- a/packages/visx-annotation/test/Label.test.tsx +++ b/packages/visx-annotation/test/Label.test.tsx @@ -11,6 +11,7 @@ describe('
); } - -interface AnchorLineProps { - anchorLineOrientation: 'horizontal' | 'vertical'; - verticalAnchor: TextProps['verticalAnchor']; - horizontalAnchor: TextProps['textAnchor']; - anchorLineStroke: string; - width: number; - height: number; -} -function AnchorLine({ - anchorLineOrientation, - anchorLineStroke, - verticalAnchor, - horizontalAnchor, - width, - height, -}: AnchorLineProps) { - const backgroundOutline = { stroke: anchorLineStroke, strokeWidth: 2 }; - - return ( - <> - {anchorLineOrientation === 'horizontal' && verticalAnchor === 'start' && ( - - )} - {anchorLineOrientation === 'horizontal' && verticalAnchor === 'end' && ( - - )} - {anchorLineOrientation === 'vertical' && horizontalAnchor === 'start' && ( - - )} - {anchorLineOrientation === 'vertical' && horizontalAnchor === 'end' && ( - - )} - - ); -} diff --git a/packages/visx-annotation/src/components/LabelAnchorLine.tsx b/packages/visx-annotation/src/components/LabelAnchorLine.tsx new file mode 100644 index 000000000..e8ac11aff --- /dev/null +++ b/packages/visx-annotation/src/components/LabelAnchorLine.tsx @@ -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' && ( + + )} + {anchorLineOrientation === 'horizontal' && verticalAnchor === 'end' && ( + + )} + {anchorLineOrientation === 'vertical' && horizontalAnchor === 'start' && ( + + )} + {anchorLineOrientation === 'vertical' && horizontalAnchor === 'end' && ( + + )} + + ); +} diff --git a/packages/visx-annotation/src/index.ts b/packages/visx-annotation/src/index.ts index dbb21e0dc..7fa2f1613 100644 --- a/packages/visx-annotation/src/index.ts +++ b/packages/visx-annotation/src/index.ts @@ -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'; diff --git a/packages/visx-demo/src/pages/docs/annotation.tsx b/packages/visx-demo/src/pages/docs/annotation.tsx index 6f51bdf0b..ea856c76e 100644 --- a/packages/visx-demo/src/pages/docs/annotation.tsx +++ b/packages/visx-demo/src/pages/docs/annotation.tsx @@ -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'; @@ -17,6 +18,7 @@ const components = [ LineSubject, Connector, Label, + HtmlLabel, LinePathAnnotationDeprecated, ];