cs-2022-class-profile/components/Wordcloud.tsx

202 lines
5.4 KiB
TypeScript

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<WordData>;
width?: number;
height?: number;
wordPadding?: number;
fontWeight?: number;
minFontSize?: number;
maxFontSize?: number;
}
interface WordData {
text: string;
value: number;
}
const wordColors = [Color.primaryAccent, Color.primaryAccentLight];
const fixedValueGenerator = () => 0.5;
const TOOLTIP_HORIZONTAL_SHIFT_SCALER = 12.0;
export const WordCloud = withTooltip(
({
data,
width = 1000,
height = 500,
wordPadding = 30,
fontWeight = 500,
minFontSize = 20,
maxFontSize = 150,
}: WordCloudProps) => {
const {
tooltipData,
tooltipLeft,
tooltipTop,
tooltipOpen,
showTooltip,
hideTooltip,
} = useTooltip<WordData>();
return (
<>
<WordCloudWordsMemoized
width={width}
height={height}
data={data}
wordPadding={wordPadding}
fontWeight={fontWeight}
minFontSize={minFontSize}
maxFontSize={maxFontSize}
showTooltip={(data, left, top) => {
showTooltip({
tooltipData: data,
tooltipLeft: left,
tooltipTop: top,
});
}}
hideTooltip={hideTooltip}
tooltipLeft={tooltipLeft}
tooltipTop={tooltipTop}
/>
{tooltipOpen && tooltipData ? (
<TooltipWithBounds
// set this to random so it correctly updates with parent bounds
key={Math.random()}
top={tooltipTop}
left={tooltipLeft}
unstyled
applyPositionStyle
id={styles.tooltip}
>
{tooltipData.text} ({tooltipData.value})
</TooltipWithBounds>
) : null}
</>
);
}
);
type WordCloudWordsProps = WordCloudProps & {
data: Array<WordData>;
width: number;
height: number;
wordPadding: number;
fontWeight: number;
minFontSize: number;
maxFontSize: number;
showTooltip: (
data: WordData,
tooltipLeft: number,
tooltipTop: number
) => void;
hideTooltip: () => void;
// These next props are just used to stop the component from updating when it doesnt need to,
// but they are not needed to render the component
tooltipLeft?: number;
tooltipTop?: number;
};
const WordCloudWords: React.FC<WordCloudWordsProps> = ({
width,
height,
data,
wordPadding = 30,
fontWeight = 400,
minFontSize = 20,
maxFontSize = 100,
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);
return (
<>
<VisxWordcloud
words={data}
width={width}
height={height}
fontSize={fontSizeSetter}
font="Inconsolata, monospace"
padding={wordPadding}
spiral={"rectangular"}
rotate={0}
random={fixedValueGenerator}
>
{(cloudWords) =>
cloudWords.map((w, i) => {
return (
<Text
key={w.text}
fill={wordColors[i % wordColors.length]}
transform={`translate(${w.x ?? 0}, ${w.y ?? 0})`}
fontSize={w.size}
fontFamily={w.font}
fontWeight={fontWeight}
className={styles.word}
textAnchor="middle"
onMouseMove={
((e: React.MouseEvent<SVGTextElement, MouseEvent>) => {
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 (w.text) {
showTooltip(
{ text: w.text, value: data[i].value },
eventSvgCoords.x -
w.text.length * TOOLTIP_HORIZONTAL_SHIFT_SCALER,
eventSvgCoords.y
);
}
console.log(e, eventSvgCoords);
}) as React.MouseEventHandler<SVGTextElement>
}
onMouseLeave={(_) => hideTooltip()}
>
{w.text}
</Text>
);
})
}
</VisxWordcloud>
</>
);
};
const shouldNotRerender = (
prevProps: WordCloudWordsProps,
nextProps: WordCloudWordsProps
) => {
if (
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);