Add Wordcloud Component #27
|
@ -0,0 +1,19 @@
|
|||
.word:hover {
|
||||
snedadah marked this conversation as resolved
|
||||
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);
|
||||
}
|
|
@ -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
a258wang
commented
It looks like we're copying a lot of the keys from Omit: https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys 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
snedadah
commented
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
a258wang
commented
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.
snedadah
commented
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
snedadah
commented
I tried using 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}
|
||||
a258wang
commented
I'm not sure if we should add quotes around I'm not sure if we should add quotes around `Inconsolata` 🤔
snedadah
commented
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.
a258wang
commented
Ahh, my concern is that this string eventually becomes an inline style:
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 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
a258wang
commented
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?)
snedadah
commented
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
a258wang
commented
NIT: name this variable as NIT: name this variable as `event` or `evt`?
snedadah
commented
I think in the instance of events, it is very common practice to use e, and IMO confusing to use 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);
|
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?