Skip to content

Commit efebe82

Browse files
committed
add FAQ page, update Options page, handle bad clouds using recursion, bug fixes
1 parent afd610f commit efebe82

10 files changed

+170
-54
lines changed

CHANGELOG.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,30 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [1.0.5](https://github.com/chrisrzhou/react-wordcloud/compare/v1.0.4...v1.0.5) (2019-03-16)
7+
8+
### New:
9+
10+
- Added `FAQ` page explaining common 'bugs', and updated `Options` page with more examples
11+
- Handled recursive attempts to layout 'bad' clouds. Provide a console warning when max attempts have been made to layout 'bad' clouds.
12+
- Changed default `minSize` and `options.fontSizes` value to make things less buggy.
13+
14+
### Bug fixes:
15+
16+
- Fixed a bug where `rotationAngles` was mutated.
17+
618
## [1.0.4](https://github.com/chrisrzhou/react-wordcloud/compare/v1.0.3...v1.0.4) (2019-03-16)
719

820
Improve and simplify React hooks code after detailed understanding of: https://overreacted.io/a-complete-guide-to-useeffect/
921

10-
Bug fixes:
22+
### Bug fixes:
1123

1224
- Handle words that don't fit in the boundary of the SVG by applying a font-size scale factor
1325
- Handle large number of words
1426

1527
## [1.0.3](https://github.com/chrisrzhou/react-wordcloud/compare/v1.0.2...v1.0.3) (2019-03-14)
1628

17-
Bug fixes:
29+
### Bug fixes:
1830

1931
- https://github.com/chrisrzhou/react-wordcloud/issues/5
2032
- https://github.com/chrisrzhou/react-wordcloud/issues/11

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,9 @@ Features supported:
7979
- Various NLP methods (stopwords, ngrams)
8080
- Wordcloud configurations
8181
- Export/save/share wordclouds
82+
83+
## Donate
84+
85+
My projects will always be (ads-)free. I constantly learn from the community, so these projects are a way of giving back to the community. If you liked this project or find it useful, feel free to buy me a cup of coffee ☕️ through a small donation!
86+
87+
[![paypal](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/chrisrzhou/5)

docs/faq.mdx

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
name: FAQ
3+
route: /faq
4+
---
5+
6+
import { Playground } from "docz";
7+
import ReactWordcloud from "./../src";
8+
9+
# FAQ
10+
11+
## Why are some words dropped?
12+
13+
`ReactWordcloud` applies a limit that can be controlled by the `maxWords` prop. This is set to `100` by default. You can raise the limit but be careful that this could affect performance.
14+
15+
## Why is the most frequent word is not showing up?
16+
17+
This issue happens when the most frequent word is also the **longest** word. For a given wordcloud size, if the longest and most frequent word does not fit in the wordcloud SVG container, the `d3-cloud` algorithm drops them out. This is a known issue discussed in: https://github.com/jasondavies/d3-cloud/issues/36
18+
19+
`react-wordcloud` tries to solve this issue by recursively rendering the wordcloud if it detects that words have been dropped out. Each recursion would decrease the applied font-size by a scale factor. The recursion will bail out after some maximum attempts is reached, and a console warning will be thrown to the user informating that the words cannot be rendered in the wordcloud. The following example below demonstrates this scenario:
20+
21+
<Playground>
22+
<ReactWordcloud
23+
size={[200, 200]}
24+
words={[
25+
{ text: "this_is_a_long_word_and_also_the_most_frequent", value: 45 },
26+
{ text: "some_word", value: 35 },
27+
{ text: "another_word", value: 20 }
28+
]}
29+
/>
30+
</Playground>
31+
32+
If you see this console warning, it is recommended that you address it in the following few common ways:
33+
34+
- Increase the wordcloud size (either using the `size` prop or the parent container).
35+
- Reduce the `options.fontSizes` values.
36+
- Avoid rendering long words at vertical angles (i.e. 90 degrees). Browser heights are more limited than widths, and the long words may not fit within the wordcloud height. You can control rotation angles using `rotationAngles` and `rotations` in the `options` props.

docs/usage/options.mdx

+62-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,65 @@ import words from "./words";
1212

1313
You can customize many visual and layout properties of `ReactWordcloud` by using the `options` prop.
1414

15+
## Colors
16+
17+
`ReactWordcloud` will randomnly apply colors from an array of color hex codes.
18+
19+
<Playground>
20+
<ReactWordcloud
21+
options={{
22+
colors: ["#1f77b4", "#9467bd", "#8c564b"]
23+
}}
24+
words={words}
25+
/>
26+
</Playground>
27+
28+
## Font Styles
29+
30+
Configure wordfont styles using the `fontFamily`, `fontSizes`, `fontStyle`, `fontWeight` options.
31+
32+
<Playground>
33+
<ReactWordcloud
34+
options={{
35+
fontFamily: "courier new",
36+
fontSizes: [10, 20],
37+
fontStyle: "italic",
38+
fontWeight: "bold"
39+
}}
40+
words={words}
41+
/>
42+
</Playground>
43+
44+
## Rotations
45+
46+
By default `ReactWordcloud` will apply random rotations if the `rotations` option is not specified. If `rotations` option is specified, it will use evenly-divided angles from the `rotationAngles` option based on the `rotations` value. The following example demonstrates using rotation angles of `0`, `45` and `90` degrees (i.e. `rotations = 3` and `rotationAngles=[0, 90]`).
47+
48+
<Playground>
49+
<ReactWordcloud
50+
options={{
51+
rotations: 3,
52+
rotationAngles: [0, 90]
53+
}}
54+
words={words}
55+
/>
56+
</Playground>
57+
58+
## Layout
59+
60+
Configure the wordcloud layout by using the `scale`, `spiral` options.
61+
62+
<Playground>
63+
<ReactWordcloud
64+
options={{
65+
scale: "log",
66+
spiral: "rectangular"
67+
}}
68+
words={words}
69+
/>
70+
</Playground>
71+
72+
## Interactive
73+
1574
Use the code editor to edit and play around with some of these options!
1675

1776
<Playground>
@@ -27,13 +86,13 @@ Use the code editor to edit and play around with some of these options!
2786
"#8c564b"
2887
],
2988
enableTooltip: true,
30-
fontFamily: "times new roman",
31-
fontSizes: [5, 40],
89+
fontFamily: "impact",
90+
fontSizes: [5, 60],
3291
fontStyle: "normal",
3392
fontWeight: "normal",
3493
padding: 1,
3594
rotations: 3,
36-
rotationAngles: [-90, 90],
95+
rotationAngles: [0, 90],
3796
scale: "sqrt",
3897
spiral: "archimedean",
3998
transitionDuration: 1000

docs/usage/transitions.mdx

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import words from "./words";
1919
const [wordsFlag, toggleWordsFlag] = useState(false);
2020
const updatedOptions = optionsFlag
2121
? defaultOptions
22-
: { ...defaultOptions, fontFamily: "times new roman" };
22+
: { ...defaultOptions, fontFamily: "impact" };
2323
const updatedWords = wordsFlag ? words : words.slice(0, 50);
2424
return (
2525
<>

doczrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export default {
88
menu: ['Props', 'Basic', 'Size', 'Callbacks', 'Transitions', 'Options'],
99
},
1010
'Wordcloud Generator',
11+
'FAQ',
1112
'CHANGELOG',
1213
{ name: 'Github', href: 'https://github.com/chrisrzhou/react-wordcloud' },
1314
],

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-wordcloud",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"description": "Simple React + D3 wordcloud component with powerful features.",
55
"main": "dist/index.js",
66
"module": "dist/index.module.js",

src/index.tsx

+41-35
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const { useEffect } = React;
1111

1212
const d3 = { cloud: d3Cloud };
1313

14+
const MAX_LAYOUT_ATTEMPTS = 10;
15+
const SHRINK_FACTOR = 0.95;
16+
1417
export const defaultCallbacks: Callbacks = {
1518
getWordTooltip: ({ text, value }: Word) => `${text} (${value})`,
1619
};
@@ -19,7 +22,7 @@ export const defaultOptions: Options = {
1922
colors: getDefaultColors(),
2023
enableTooltip: true,
2124
fontFamily: 'times new roman',
22-
fontSizes: [5, 40],
25+
fontSizes: [4, 32],
2326
fontStyle: 'normal',
2427
fontWeight: 'normal',
2528
padding: 1,
@@ -77,7 +80,6 @@ function Wordcloud({
7780

7881
// render viz
7982
useEffect(() => {
80-
const layout = d3.cloud();
8183
const mergedCallbacks = { ...defaultCallbacks, ...callbacks };
8284
const mergedOptions = { ...defaultOptions, ...options };
8385

@@ -93,57 +95,61 @@ function Wordcloud({
9395
spiral,
9496
scale,
9597
} = mergedOptions;
96-
const ctx = document.createElement('canvas').getContext('2d');
97-
ctx.font = `${fontSizes[1]}px ${fontFamily}`;
98-
99-
if (rotations !== undefined) {
100-
layout.rotate(() => rotate(rotations, rotationAngles));
101-
}
10298

10399
const sortedWords = words
104100
.concat()
105101
.sort((x, y) => descending(x.value, y.value))
106102
.slice(0, maxWords);
107103

108-
layout
104+
const layout = d3
105+
.cloud()
109106
.size(size)
110107
.padding(padding)
111108
.words(sortedWords)
109+
.rotate(() => {
110+
if (rotations === undefined) {
111+
// default rotation algorithm
112+
return (~~(Math.random() * 6) - 3) * 30;
113+
} else {
114+
return rotate(rotations, rotationAngles);
115+
}
116+
})
112117
.spiral(spiral)
113118
.text(getText)
114119
.font(fontFamily)
115120
.fontStyle(fontStyle)
116121
.fontWeight(fontWeight);
117122

118-
const draw = (fontSizes: MinMaxPair): void => {
123+
const draw = (fontSizes: MinMaxPair, attempts: number = 1): void => {
119124
layout
120125
.fontSize((word: Word) => {
121-
const fontScale = getFontScale(words, fontSizes, scale);
126+
const fontScale = getFontScale(sortedWords, fontSizes, scale);
122127
return fontScale(word.value);
123128
})
124-
.on('end', () => {
125-
// For each word, we derive the x/y width projections based on the
126-
// rotation angle. Calculate the scale factor of the respective
127-
// width projections against the svg container width and height.
128-
// Apply a universal font-size scaling (maximum value = 1) in render
129-
let widthX = 0;
130-
let widthY = 0;
131-
sortedWords.forEach(word => {
132-
const wordWidth = ctx.measureText(word.text).width * 1.1;
133-
const angle = (word.rotate / 180) * Math.PI;
134-
widthX = Math.max(wordWidth * Math.cos(angle), widthX);
135-
widthY = Math.max(wordWidth * Math.sin(angle), widthY);
136-
});
137-
const scaleFactorX = size[0] / widthX;
138-
const scaleFactorY = size[1] / widthY;
139-
const scaleFactor = Math.min(1, scaleFactorX, scaleFactorY);
140-
render(
141-
selection,
142-
sortedWords,
143-
mergedOptions,
144-
mergedCallbacks,
145-
scaleFactor,
146-
);
129+
.on('end', (computedWords: Word[]) => {
130+
if (
131+
sortedWords.length !== computedWords.length &&
132+
attempts <= MAX_LAYOUT_ATTEMPTS
133+
) {
134+
// KNOWN ISSUE: Unable to render long words with high frequency.
135+
// (https://github.com/jasondavies/d3-cloud/issues/36)
136+
// Recursively layout and decrease font-sizes by a SHRINK_FACTOR.
137+
// Bail out with a warning message after MAX_LAYOUT_ATTEMPTS.
138+
if (attempts === MAX_LAYOUT_ATTEMPTS) {
139+
console.warn(
140+
`Unable to layout ${sortedWords.length -
141+
computedWords.length} word(s) after ${attempts} attempts. Consider: (1) Increasing the container/component size. (2) Lowering the max font size. (3) Limiting the rotation angles.`,
142+
);
143+
}
144+
const minFontSize = Math.max(fontSizes[0] * SHRINK_FACTOR, 1);
145+
const maxFontSize = Math.max(
146+
fontSizes[1] * SHRINK_FACTOR,
147+
minFontSize,
148+
);
149+
draw([minFontSize, maxFontSize], attempts + 1);
150+
} else {
151+
render(selection, computedWords, mergedOptions, mergedCallbacks);
152+
}
147153
})
148154
.start();
149155
};
@@ -157,7 +163,7 @@ function Wordcloud({
157163
Wordcloud.defaultProps = {
158164
callbacks: defaultCallbacks,
159165
maxWords: 100,
160-
minSize: [200, 150],
166+
minSize: [300, 300],
161167
options: defaultOptions,
162168
};
163169

src/render.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export default function render(
1111
words: Word[],
1212
options: Options,
1313
callbacks: Callbacks,
14-
scaleFactor: number,
1514
): void {
1615
const {
1716
getWordColor,
@@ -22,7 +21,6 @@ export default function render(
2221
} = callbacks;
2322
const { colors, enableTooltip, fontStyle, fontWeight } = options;
2423
const { fontFamily, transitionDuration } = options;
25-
const scaledFontSize = getFontSize(scaleFactor);
2624

2725
function getFill(word: Word): string {
2826
return getWordColor ? getWordColor(word) : choose(colors);
@@ -63,7 +61,7 @@ export default function render(
6361
.attr('transform', 'translate(0, 0) rotate(0)')
6462
.transition()
6563
.duration(transitionDuration)
66-
.attr('font-size', scaledFontSize)
64+
.attr('font-size', getFontSize)
6765
.attr('transform', getTransform)
6866
.text(getText);
6967

@@ -73,7 +71,7 @@ export default function render(
7371
.duration(transitionDuration)
7472
.attr('fill', getFill)
7573
.attr('font-family', fontFamily)
76-
.attr('font-size', scaledFontSize)
74+
.attr('font-size', getFontSize)
7775
.attr('transform', getTransform)
7876
.text(getText);
7977

src/utils.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ export function getFontScale(
2222
const maxSize = d3.max(words, (word: Word) => word.value);
2323
const Scales = {
2424
[Scale.Linear]: d3.scaleLinear,
25-
[Scale.Log]: d3.scaleLinear,
26-
[Scale.Sqrt]: d3.scaleLinear,
25+
[Scale.Log]: d3.scaleLog,
26+
[Scale.Sqrt]: d3.scaleSqrt,
2727
};
28-
const fontScale = Scales[scale]()
28+
const fontScale = (Scales[scale] || d3.scaleLinear)()
2929
.domain([minSize, maxSize])
3030
.range(fontSizes);
3131
return fontScale;
@@ -35,10 +35,8 @@ export function getText(word: Word): string {
3535
return word.text;
3636
}
3737

38-
export function getFontSize(scaleFactor: number): (word: Word) => string {
39-
return function(word: Word): string {
40-
return `${word.size * scaleFactor}px`;
41-
};
38+
export function getFontSize(word: Word): string {
39+
return `${word.size}px`;
4240
}
4341

4442
export function getTransform(word: Word): string {
@@ -56,7 +54,7 @@ export function rotate(rotations: number, rotationAngles: MinMaxPair): number {
5654
if (rotations === 1) {
5755
angles = [rotationAngles[0]];
5856
} else {
59-
angles = rotationAngles;
57+
angles = [...rotationAngles];
6058
const increment = (rotationAngles[1] - rotationAngles[0]) / (rotations - 1);
6159
let angle = rotationAngles[0] + increment;
6260
while (angle < rotationAngles[1]) {

0 commit comments

Comments
 (0)