-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.html
231 lines (189 loc) · 12.8 KB
/
index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Word Cloud Generator</title>
<meta name="description"
content="Word cloud generator for visually presenting your text data. Just paste your text, select your preferences, and watch the magic happen.">
<meta property="og:type" content="website">
<meta property="og:url" content="https://nanx.me/wordcloud/">
<meta property="og:title" content="Word Cloud Generator">
<meta property="og:description" content="Visualize your text data as a word cloud.">
<meta property="og:image" content="https://nanx.me/wordcloud/social-preview.jpg">
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://nanx.me/wordcloud/">
<meta property="twitter:title" content="Word Cloud Generator">
<meta property="twitter:description" content="Visualize your text data as a word cloud.">
<meta property="twitter:image" content="https://nanx.me/wordcloud/social-preview.jpg">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css"
integrity="sha512-t4GWSVZO1eC8BM339Xd7Uphw5s17a86tIZIj8qRxhnKub6WoyhnrxeCIMeAqBPgdZGlCcG2PrZjMc+Wr78+5Xg=="
crossorigin="anonymous" referrerpolicy="no-referrer" />
<link rel="stylesheet" href="style.css" />
<link rel="shortcut icon" href="https://nanx.me/image/favicon.png">
</head>
<body class="container py-3">
<div id="vis" class="mx-auto" style="width: 960px;"></div>
<form id="form">
<p id="status" class="position-absolute top-0 end-0"></p>
<div class="text-center mb-4">
<div id="presets" class="mb-4"></div>
<div id="custom-area">
<p class="mb-2">
<label for="text" class="form-label">Paste your text below!</label>
</p>
<p>
<textarea id="text" class="form-control" rows="3">
How the Word Cloud Generator Works
The layout algorithm for positioning words without overlap is available on GitHub under an open source license as d3-cloud. Note that this is the only the layout algorithm and any code for converting text into words and rendering the final output requires additional development.
As word placement can be quite slow for more than a few hundred words, the layout algorithm can be run asynchronously, with a configurable time step size. This makes it possible to animate words as they are placed without stuttering. It is recommended to always use a time step even without animations as it prevents the browser’s event loop from blocking while placing the words.
The layout algorithm itself is incredibly simple. For each word, starting with the most “important”:
Attempt to place the word at some starting point: usually near the middle, or somewhere on a central horizontal line.
If the word intersects with any previously placed words, move it one step along an increasing spiral. Repeat until no intersections are found.
The hard part is making it perform efficiently! According to Jonathan Feinberg, Wordle uses a combination of hierarchical bounding boxes and quadtrees to achieve reasonable speeds.
Glyphs in JavaScript
There isn’t a way to retrieve precise glyph shapes via the DOM, except perhaps for SVG fonts. Instead, we draw each word to a hidden canvas element, and retrieve the pixel data.
Retrieving the pixel data separately for each word is expensive, so we draw as many words as possible and then retrieve their pixels in a batch operation.
Sprites and Masks
My initial implementation performed collision detection using sprite masks. Once a word is placed, it doesn't move, so we can copy it to the appropriate position in a larger sprite representing the whole placement area.
The advantage of this is that collision detection only involves comparing a candidate sprite with the relevant area of this larger sprite, rather than comparing with each previous word separately.
Somewhat surprisingly, a simple low-level hack made a tremendous difference: when constructing the sprite I compressed blocks of 32 1-bit pixels into 32-bit integers, thus reducing the number of checks (and memory) by 32 times.
In fact, this turned out to beat my hierarchical bounding box with quadtree implementation on everything I tried it on (even very large areas and font sizes). I think this is primarily because the sprite version only needs to perform a single collision test per candidate area, whereas the bounding box version has to compare with every other previously placed word that overlaps slightly with the candidate area.
Another possibility would be to merge a word’s tree with a single large tree once it is placed. I think this operation would be fairly expensive though compared with the analagous sprite mask operation, which is essentially ORing a whole block.
</textarea>
</p>
<button id="go" type="submit" class="btn btn-primary custom-btn-primary me-3">Refresh Word
Cloud</button>
<button id="download-svg" class="btn btn-outline-secondary custom-btn-outline">Download SVG</button>
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<!-- Left-side Form Inputs -->
<div class="form-floating mb-3">
<select id="color-palette" class="form-select" aria-label="Select color palette">
<optgroup label="Tableau Palettes">
<option value="tableau10">New Tableau 10</option>
<option value="tableau10Reversed">New Tableau 10 (Reversed)</option>
</optgroup>
<optgroup label="Viridis Palettes">
<option value="viridis">Viridis</option>
<option value="viridisReversed">Viridis (Reversed)</option>
</optgroup>
<optgroup label="ColorBrewer Palettes">
<option value="dark2">ColorBrewer Dark2</option>
<option value="dark2Reversed">ColorBrewer Dark2 (Reversed)</option>
<option value="set2">ColorBrewer Set2</option>
<option value="set2Reversed">ColorBrewer Set2 (Reversed)</option>
</optgroup>
<optgroup label="ggplot2 Palettes">
<option value="ggplot2">ggplot2 Hue</option>
<option value="ggplot2Reversed">ggplot2 Hue (Reversed)</option>
</optgroup>
<optgroup label="D3 Palettes">
<option value="category10">D3 Category 10</option>
<option value="category10Reversed">D3 Category 10 (Reversed)</option>
<option value="category20">D3 Category 20</option>
<option value="category20Reversed">D3 Category 20 (Reversed)</option>
<option value="category20b">D3 Category 20b</option>
<option value="category20bReversed">D3 Category 20b (Reversed)</option>
<option value="category20c">D3 Category 20c</option>
<option value="category20cReversed">D3 Category 20c (Reversed)</option>
</optgroup>
<optgroup label="Other Palettes">
<option value="okabeIto">Okabe-Ito</option>
<option value="okabeItoReversed">Okabe-Ito (Reversed)</option>
<option value="gephi">Gephi</option>
<option value="gephiReversed">Gephi (Reversed)</option>
<option value="flatUI">FlatUI</option>
<option value="flatUIReversed">FlatUI (Reversed)</option>
</optgroup>
</select>
<label for="color-palette">Color:</label>
</div>
<div class="form-floating mb-3">
<input type="text" id="font" value="Arial" style="min-width: 30ch;" class="form-control">
<label for="font">Font:</label>
</div>
<div class="d-flex align-items-center mb-3">
<label class="me-2">Spiral:</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="spiral" id="archimedean" value="archimedean"
checked>
<label class="form-check-label" for="archimedean">Archimedean</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="spiral" id="rectangular" value="rectangular">
<label class="form-check-label" for="rectangular">Rectangular</label>
</div>
</div>
<div class="d-flex align-items-center mb-3">
<label class="me-2">Scale:</label>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="scale" id="scale-log" value="log" checked>
<label class="form-check-label" for="scale-log">log n</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="scale" id="scale-sqrt" value="sqrt">
<label class="form-check-label" for="scale-sqrt">√n</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="scale" id="scale-linear" value="linear">
<label class="form-check-label" for="scale-linear">n</label>
</div>
</div>
</div>
<div class="col">
<!-- Center Form Inputs -->
<div id="angles">
<div class="form-floating mb-3">
<input type="number" id="angle-count" value="2" min="1" class="form-control">
<label for="angle-count">Number of orientations:</label>
</div>
<div class="form-floating mb-3">
<input type="number" id="angle-from" value="-90" min="-90" max="90" class="form-control">
<label for="angle-from">From:</label>
</div>
<div class="form-floating mb-3">
<input type="number" id="angle-to" value="0" min="-90" max="90" class="form-control">
<label for="angle-to">To:</label>
</div>
</div>
</div>
<div class="col">
<!-- Right-side Form Inputs -->
<div class="form-floating mb-3">
<input type="number" value="250" min="1" id="max" class="form-control">
<label for="max">Number of words:</label>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" role="switch" id="per-line">
<label class="form-check-label" for="per-line">One word per line</label>
</div>
</div>
</div>
</form>
<hr class="clear-both my-4">
<div class="text-secondary">
<p class="float-start">
Copyright ©
<a href="http://www.jasondavies.com/">Jason Davies</a> |
<a href="https://www.jasondavies.com/privacy/">Privacy Policy</a>.
The generated word clouds may be used for any purpose.
</p>
<p class="float-end">
<a href="https://www.jasondavies.com/wordcloud/about/">How the Word Cloud Generator Works</a>.
</p>
<p class="float-start mt-2">
This application is a fork of the original implementation by Jason Davies. The current version, which
includes additional features and revised defaults, was modified and updated by <a href="http://nanx.me">Nan
Xiao</a>. You can view the source code and detailed changes on <a
href="https://github.com/nanxstats/wordcloud">GitHub</a>.
</p>
</div>
<script src="d3.min.js"></script>
<script src="color-scales.js"></script>
<script src="wordcloud.js"></script>
</body>
</html>