import { localPoint } from "@visx/event"; import { Point } from "@visx/point"; import { scaleLog } from "@visx/scale"; import { Text } from "@visx/text"; import { TooltipWithBounds, useTooltip, withTooltip } from "@visx/tooltip"; import { Wordcloud as VisxWordcloud } from "@visx/wordcloud"; import React from "react"; import { Color } from "utils/Color"; import styles from "./WordCloud.module.css"; interface WordCloudProps { data: Array; /** Width of the graph, in px */ width?: number; /** Height of the graph, in px */ height?: number; /** Minimum padding between words, in px */ wordPadding?: number; /** Weight of the font of the words */ fontWeight?: number; /** The desired font size of the smallest word, in px.*/ minFontSize?: number; /** The desired font size of the largest word, in px. */ maxFontSize?: number; /** A random seed in the range [0, 1) used for placing the words, change this value to get an alternate placement of words */ randomSeed?: number; /** Type of spiral used for rendering the words, either rectangular or archimedean */ spiral?: "rectangular" | "archimedean"; /** ClassName of the wrapper of the wordcloud */ className?: string; } interface WordData { text: string; value: number; } const wordColors = [Color.primaryAccent, Color.primaryAccentLight]; const TOOLTIP_HORIZONTAL_SHIFT_SCALER = 12.0; export const WordCloud = withTooltip( ({ data, width, height, wordPadding, fontWeight, minFontSize, maxFontSize, randomSeed, spiral, className, }: WordCloudProps) => { const { tooltipData, tooltipLeft, tooltipTop, tooltipOpen, showTooltip, hideTooltip, } = useTooltip(); return (
{ showTooltip({ tooltipData: data, tooltipLeft: left, tooltipTop: top, }); }} hideTooltip={hideTooltip} tooltipLeft={tooltipLeft} tooltipTop={tooltipTop} randomSeed={randomSeed} spiral={spiral} /> {tooltipOpen && tooltipData ? ( {tooltipData.text} ({tooltipData.value}) ) : null}
); } ); /** The internal wordcloud component that actually lays out the word needs to be separate from the tooltip to prevent extra rerendering. */ type WordCloudWordsProps = Omit & { showTooltip: ( data: WordData, tooltipLeft: number, tooltipTop: number ) => void; hideTooltip: () => void; // tooltipLeft and tooltipTop are used for preventing unnessary renders tooltipLeft?: number; tooltipTop?: number; }; const WordCloudWords: React.FC = ({ data, width = 1000, height = 500, wordPadding = 30, fontWeight = 500, minFontSize = 20, maxFontSize = 150, randomSeed = 0.5, spiral = "rectangular", showTooltip, hideTooltip, }) => { const fontScale = scaleLog({ domain: [ Math.min(...data.map((w) => w.value)), Math.max(...data.map((w) => w.value)), ], range: [minFontSize, maxFontSize], }); const fontSizeSetter = (datum: WordData) => fontScale(datum.value); const fixedValueGenerator = () => randomSeed; return ( {(cloudWords) => cloudWords.map((word, index) => { return ( ) => { const eventSvgCoords = localPoint( // ownerSVGElement is given by visx docs but not recognized by typescript // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore e.target.ownerSVGElement as Element, e ) as Point; if (word.text) { showTooltip( { text: word.text, value: data[index].value }, eventSvgCoords.x - word.text.length * TOOLTIP_HORIZONTAL_SHIFT_SCALER, eventSvgCoords.y ); } }) as React.MouseEventHandler } onMouseLeave={(_) => hideTooltip()} > {word.text} ); }) } ); }; const shouldNotRerender = ( prevProps: WordCloudWordsProps, nextProps: WordCloudWordsProps ) => { if ( // if width changes, rerender, else don't rerender for a tooltip change prevProps.width === nextProps.width && (prevProps.tooltipLeft !== nextProps.tooltipLeft || prevProps.tooltipTop !== nextProps.tooltipTop || nextProps.tooltipLeft === undefined || nextProps.tooltipTop === undefined) ) { return true; // do not re-render } return false; // will re-render }; const WordCloudWordsMemoized = React.memo(WordCloudWords, shouldNotRerender);