Add Wordcloud Component #27

Merged
snedadah merged 29 commits from shahanneda/add-wordcloud-2 into main 2022-08-04 02:17:21 -04:00
2 changed files with 220 additions and 0 deletions
Showing only changes of commit 2e6ab1c039 - Show all commits

View File

@ -0,0 +1,19 @@
.word:hover {
snedadah marked this conversation as resolved
Review

I noticed when hovering over words that the cursor becomes the text cursor, I think in this context it might be nice to keep it as the default cursor?

I noticed when hovering over words that the cursor becomes the text cursor, I think in this context it might be nice to keep it as the default cursor?
text-shadow: var(--primary-accent) 0 0 calc(20rem / 16);
text-anchor: "middle";
}
.tooltip {
font-family: "Inconsolata", monospace;
font-weight: bold;
top: 0;
left: 0;
position: absolute;
background-color: white;
color: var(--navy);
box-shadow: var(--card-background) 0px calc(1rem / 16) calc(2rem / 16);
pointer-events: none;
padding: calc(10rem / 16);
font-size: calc(18rem / 16);
border-radius: calc(10rem / 16);
}

201
components/WordCloud.tsx Normal file
View File

@ -0,0 +1,201 @@
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;
a258wang marked this conversation as resolved
Review

It looks like we're copying a lot of the keys from WordCloudProps here, would it be cleaner if we used Omit<WordCloudProps, ...> & { ... } instead of copying so much?

Omit: https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys
Intersection types (&): https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types

It looks like we're copying a lot of the keys from `WordCloudProps` here, would it be cleaner if we used `Omit<WordCloudProps, ...> & { ... }` instead of copying so much? Omit: https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys Intersection types (&): https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types
Review

Good point on using Omit. I originally did not want to use the same thing since that requires me to move the default parameters inside this component. However, I think it is better this way.

Good point on using Omit. I originally did not want to use the same thing since that requires me to move the default parameters inside this component. However, I think it is better this way.
// 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,
}) => {
snedadah marked this conversation as resolved
Review

I'm a little confused what this comment means, but if I'm understanding it correctly then would it be more succinct to say "These props are used for memoization"?

Also I would recommend explicitly listing which props instead of saying "These next props" -- this makes the comment a bit more future-proof in case someone decides to change the order of the props or add new props or something in the future.

I'm a little confused what this comment means, but if I'm understanding it correctly then would it be more succinct to say "These props are used for memoization"? Also I would recommend explicitly listing which props instead of saying "These next props" -- this makes the comment a bit more future-proof in case someone decides to change the order of the props or add new props or something in the future.
Review

Good point.

Good point.
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]}
snedadah marked this conversation as resolved
Review

I tried using rem for these values but it's not possible AFAIK since VisxWordCloud accepts number only as its props.

I tried using `rem` for these values but it's not possible AFAIK since VisxWordCloud accepts number only as its props.
transform={`translate(${w.x ?? 0}, ${w.y ?? 0})`}
fontSize={w.size}
fontFamily={w.font}
fontWeight={fontWeight}
Review

I'm not sure if we should add quotes around Inconsolata 🤔

I'm not sure if we should add quotes around `Inconsolata` 🤔
Review

we need the quotes here, this is in TSX, that issue is only for CSS.

we need the quotes here, this is in TSX, that issue is only for CSS.
Review

Ahh, my concern is that this string eventually becomes an inline style:

<text transform="translate(8, 0)" fill="var(--primary-accent)" font-size="150" font-family="Inconsolata, monospace" font-weight="500" class="WordCloud_word__4_u0u" text-anchor="middle"><tspan x="0" dy="0em">Python</tspan></text>

Looking at staging the font seems to be okay, but I'm not sure if that's because it's inline or if it's because Inconsolata doesn't have any spaces or if it's because of something else. But also I just tried adding quotation marks to the HTMl and it didn't seem to do anything, so yeah we're probably fine. 😄

Ahh, my concern is that this string eventually becomes an inline style: ```html <text transform="translate(8, 0)" fill="var(--primary-accent)" font-size="150" font-family="Inconsolata, monospace" font-weight="500" class="WordCloud_word__4_u0u" text-anchor="middle"><tspan x="0" dy="0em">Python</tspan></text> ``` Looking at staging the font seems to be okay, but I'm not sure if that's because it's inline or if it's because `Inconsolata` doesn't have any spaces or if it's because of something else. But also I just tried adding quotation marks to the HTMl and it didn't seem to do anything, so yeah we're probably fine. 😄
className={styles.word}
textAnchor="middle"
a258wang marked this conversation as resolved
Review

Any particular reason why a rectagular spiral, and not an archimedean spiral? (Maybe it'd be nice to be able to control this as a prop?)

Any particular reason why a rectagular spiral, and not an archimedean spiral? (Maybe it'd be nice to be able to control this as a prop?)
Review

Added it as a prop, our figma design matches the rectangular which is why I set that one.

Added it as a prop, our figma design matches the rectangular which is why I set that one.
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
);
a258wang marked this conversation as resolved
Review

NIT: name this variable as event or evt?

NIT: name this variable as `event` or `evt`?
Review

I think in the instance of events, it is very common practice to use e, and IMO confusing to use evt (extreme value thereom!? 😆 ). In fact in the react docs (and almost all other docs) they use e. https://reactjs.org/docs/handling-events.html

I think in the instance of events, it is very common practice to use e, and IMO confusing to use `evt` (extreme value thereom!? 😆 ). In fact in the react docs (and almost all other docs) they use e. https://reactjs.org/docs/handling-events.html
}
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);