@hpcc-js/chart 3.6.5 → 3.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +43 -43
- package/README.md +93 -93
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +1 -1
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +4 -4
- package/src/Area.md +176 -176
- package/src/Area.ts +12 -12
- package/src/Axis.css +34 -34
- package/src/Axis.ts +781 -781
- package/src/Bar.md +90 -90
- package/src/Bar.ts +9 -9
- package/src/Bubble.css +16 -16
- package/src/Bubble.md +69 -69
- package/src/Bubble.ts +196 -196
- package/src/BubbleXY.ts +14 -14
- package/src/Bullet.css +60 -60
- package/src/Bullet.md +104 -104
- package/src/Bullet.ts +176 -176
- package/src/Column.css +44 -44
- package/src/Column.md +90 -90
- package/src/Column.ts +684 -684
- package/src/Contour.md +88 -88
- package/src/Contour.ts +97 -97
- package/src/D3Cloud.ts +403 -403
- package/src/Gantt.md +119 -119
- package/src/Gantt.ts +14 -14
- package/src/Gauge.md +148 -148
- package/src/Gauge.ts +368 -368
- package/src/HalfPie.md +62 -62
- package/src/HalfPie.ts +26 -26
- package/src/Heat.md +42 -42
- package/src/Heat.ts +283 -283
- package/src/HexBin.css +9 -9
- package/src/HexBin.md +88 -88
- package/src/HexBin.ts +144 -144
- package/src/Line.css +6 -6
- package/src/Line.md +170 -170
- package/src/Line.ts +14 -14
- package/src/Pie.css +50 -50
- package/src/Pie.md +88 -88
- package/src/Pie.ts +546 -546
- package/src/QuarterPie.md +61 -61
- package/src/QuarterPie.ts +35 -35
- package/src/QuartileCandlestick.md +129 -129
- package/src/QuartileCandlestick.ts +349 -349
- package/src/Radar.css +15 -15
- package/src/Radar.md +104 -104
- package/src/Radar.ts +336 -336
- package/src/RadialBar.css +25 -25
- package/src/RadialBar.md +91 -91
- package/src/RadialBar.ts +217 -217
- package/src/Scatter.css +42 -42
- package/src/Scatter.md +163 -163
- package/src/Scatter.ts +412 -412
- package/src/StatChart.md +117 -117
- package/src/StatChart.ts +261 -261
- package/src/Step.md +163 -163
- package/src/Step.ts +12 -12
- package/src/Summary.css +56 -56
- package/src/Summary.md +219 -219
- package/src/Summary.ts +322 -322
- package/src/SummaryC.md +154 -154
- package/src/SummaryC.ts +240 -240
- package/src/WordCloud.css +3 -3
- package/src/WordCloud.md +144 -144
- package/src/WordCloud.ts +268 -268
- package/src/XYAxis.css +41 -41
- package/src/XYAxis.md +149 -149
- package/src/XYAxis.ts +809 -809
- package/src/__package__.ts +3 -3
- package/src/__tests__/heat.ts +71 -71
- package/src/__tests__/index.ts +3 -3
- package/src/__tests__/pie.ts +20 -20
- package/src/__tests__/stat.ts +16 -16
- package/src/__tests__/test3.ts +68 -68
- package/src/index.ts +28 -28
- package/src/test.ts +70 -70
- package/src/timeFormats.ts +26 -26
package/src/D3Cloud.ts
CHANGED
|
@@ -1,403 +1,403 @@
|
|
|
1
|
-
// Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/
|
|
2
|
-
// Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf
|
|
3
|
-
|
|
4
|
-
import { dispatch } from "d3-dispatch";
|
|
5
|
-
|
|
6
|
-
const cloudRadians = Math.PI / 180;
|
|
7
|
-
const cw = 1 << 11 >> 5;
|
|
8
|
-
const ch = 1 << 11;
|
|
9
|
-
|
|
10
|
-
export function d3Cloud() {
|
|
11
|
-
const event = dispatch("word", "end");
|
|
12
|
-
const cloud: any = {};
|
|
13
|
-
|
|
14
|
-
let size = [256, 256];
|
|
15
|
-
let text = cloudText;
|
|
16
|
-
let font = cloudFont;
|
|
17
|
-
let fontSize = cloudFontSize;
|
|
18
|
-
let fontStyle = cloudFontNormal;
|
|
19
|
-
let fontWeight = cloudFontNormal;
|
|
20
|
-
let rotate = cloudRotate;
|
|
21
|
-
let padding = cloudPadding;
|
|
22
|
-
let words = [];
|
|
23
|
-
let spiral = archimedeanSpiral;
|
|
24
|
-
let timeInterval = Infinity;
|
|
25
|
-
let timer = null;
|
|
26
|
-
let random = Math.random;
|
|
27
|
-
let canvas = cloudCanvas;
|
|
28
|
-
|
|
29
|
-
cloud.canvas = function (_?) {
|
|
30
|
-
return arguments.length ? (canvas = functor(_), cloud) : canvas;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
cloud.start = function () {
|
|
34
|
-
const contextAndRatio = getContext(canvas());
|
|
35
|
-
const board = zeroArray((size[0] >> 5) * size[1]);
|
|
36
|
-
let bounds = null;
|
|
37
|
-
const n = words.length;
|
|
38
|
-
let i = -1;
|
|
39
|
-
const tags = [];
|
|
40
|
-
const data = words.map(function (d, i) {
|
|
41
|
-
d.text = text.call(this, d, i);
|
|
42
|
-
d.font = font.call(this, d, i);
|
|
43
|
-
d.style = fontStyle.call(this, d, i);
|
|
44
|
-
d.weight = fontWeight.call(this, d, i);
|
|
45
|
-
d.rotate = rotate.call(this, d, i);
|
|
46
|
-
d.size = ~~fontSize.call(this, d, i);
|
|
47
|
-
d.padding = padding.call(this, d, i);
|
|
48
|
-
return d;
|
|
49
|
-
}).sort(function (a, b) { return b.size - a.size; });
|
|
50
|
-
|
|
51
|
-
if (timer) clearInterval(timer);
|
|
52
|
-
timer = setInterval(step, 0);
|
|
53
|
-
step();
|
|
54
|
-
|
|
55
|
-
return cloud;
|
|
56
|
-
|
|
57
|
-
function step() {
|
|
58
|
-
const start = Date.now();
|
|
59
|
-
while (Date.now() - start < timeInterval && ++i < n && timer) {
|
|
60
|
-
const d = data[i];
|
|
61
|
-
d.x = (size[0] * (random() + .5)) >> 1;
|
|
62
|
-
d.y = (size[1] * (random() + .5)) >> 1;
|
|
63
|
-
cloudSprite(contextAndRatio, d, data, i);
|
|
64
|
-
if (d.hasText && place(board, d, bounds)) {
|
|
65
|
-
tags.push(d);
|
|
66
|
-
event.call("word", cloud, d);
|
|
67
|
-
if (bounds) cloudBounds(bounds, d);
|
|
68
|
-
else bounds = [{ x: d.x + d.x0, y: d.y + d.y0 }, { x: d.x + d.x1, y: d.y + d.y1 }];
|
|
69
|
-
// Temporary hack
|
|
70
|
-
d.x -= size[0] >> 1;
|
|
71
|
-
d.y -= size[1] >> 1;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (i >= n) {
|
|
75
|
-
cloud.stop();
|
|
76
|
-
event.call("end", cloud, tags, bounds);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
cloud.stop = function () {
|
|
82
|
-
if (timer) {
|
|
83
|
-
clearInterval(timer);
|
|
84
|
-
timer = null;
|
|
85
|
-
}
|
|
86
|
-
return cloud;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
function getContext(canvas) {
|
|
90
|
-
canvas.width = canvas.height = 1;
|
|
91
|
-
const ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
|
|
92
|
-
canvas.width = (cw << 5) / ratio;
|
|
93
|
-
canvas.height = ch / ratio;
|
|
94
|
-
|
|
95
|
-
const context = canvas.getContext("2d");
|
|
96
|
-
context.fillStyle = context.strokeStyle = "red";
|
|
97
|
-
context.textAlign = "center";
|
|
98
|
-
|
|
99
|
-
return { context, ratio };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function place(board, tag, bounds) {
|
|
103
|
-
const startX = tag.x;
|
|
104
|
-
const startY = tag.y;
|
|
105
|
-
const maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]);
|
|
106
|
-
const s = spiral(size);
|
|
107
|
-
const dt = random() < .5 ? 1 : -1;
|
|
108
|
-
let t = -dt;
|
|
109
|
-
let dxdy;
|
|
110
|
-
let dx;
|
|
111
|
-
let dy;
|
|
112
|
-
|
|
113
|
-
// eslint-disable-next-line no-cond-assign
|
|
114
|
-
while (dxdy = s(t += dt)) {
|
|
115
|
-
dx = ~~dxdy[0];
|
|
116
|
-
dy = ~~dxdy[1];
|
|
117
|
-
|
|
118
|
-
if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
|
|
119
|
-
|
|
120
|
-
tag.x = startX + dx;
|
|
121
|
-
tag.y = startY + dy;
|
|
122
|
-
|
|
123
|
-
if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
|
|
124
|
-
tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
|
|
125
|
-
// TODO only check for collisions within current bounds.
|
|
126
|
-
if (!bounds || !cloudCollide(tag, board, size[0])) {
|
|
127
|
-
if (!bounds || collideRects(tag, bounds)) {
|
|
128
|
-
const sprite = tag.sprite;
|
|
129
|
-
const w = tag.width >> 5;
|
|
130
|
-
const sw = size[0] >> 5;
|
|
131
|
-
const lx = tag.x - (w << 4);
|
|
132
|
-
const sx = lx & 0x7f;
|
|
133
|
-
const msx = 32 - sx;
|
|
134
|
-
const h = tag.y1 - tag.y0;
|
|
135
|
-
let x = (tag.y + tag.y0) * sw + (lx >> 5);
|
|
136
|
-
let last;
|
|
137
|
-
for (let j = 0; j < h; j++) {
|
|
138
|
-
last = 0;
|
|
139
|
-
for (let i = 0; i <= w; i++) {
|
|
140
|
-
board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
|
|
141
|
-
}
|
|
142
|
-
x += sw;
|
|
143
|
-
}
|
|
144
|
-
delete tag.sprite;
|
|
145
|
-
return true;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
cloud.timeInterval = function (_?) {
|
|
153
|
-
return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
cloud.words = function (_?) {
|
|
157
|
-
return arguments.length ? (words = _, cloud) : words;
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
cloud.size = function (_?) {
|
|
161
|
-
return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
cloud.font = function (_?) {
|
|
165
|
-
return arguments.length ? (font = functor(_), cloud) : font;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
cloud.fontStyle = function (_?) {
|
|
169
|
-
return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
cloud.fontWeight = function (_?) {
|
|
173
|
-
return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
cloud.rotate = function (_?) {
|
|
177
|
-
return arguments.length ? (rotate = functor(_), cloud) : rotate;
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
cloud.text = function (_?) {
|
|
181
|
-
return arguments.length ? (text = functor(_), cloud) : text;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
cloud.spiral = function (_?) {
|
|
185
|
-
return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
cloud.fontSize = function (_?) {
|
|
189
|
-
return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
cloud.padding = function (_?) {
|
|
193
|
-
return arguments.length ? (padding = functor(_), cloud) : padding;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
cloud.random = function (_?) {
|
|
197
|
-
return arguments.length ? (random = _, cloud) : random;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
cloud.on = function () {
|
|
201
|
-
const value = event.on.apply(event, arguments);
|
|
202
|
-
return value === event ? cloud : value;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
return cloud;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function cloudText(d) {
|
|
209
|
-
return d.text;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function cloudFont() {
|
|
213
|
-
return "serif";
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function cloudFontNormal() {
|
|
217
|
-
return "normal";
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function cloudFontSize(d) {
|
|
221
|
-
return Math.sqrt(d.value);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function cloudRotate() {
|
|
225
|
-
return (~~(Math.random() * 6) - 3) * 30;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function cloudPadding() {
|
|
229
|
-
return 1;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Fetches a monochrome sprite bitmap for the specified text.
|
|
233
|
-
// Load in batches for speed.
|
|
234
|
-
function cloudSprite(contextAndRatio, d, data, di) {
|
|
235
|
-
if (d.sprite) return;
|
|
236
|
-
const c = contextAndRatio.context;
|
|
237
|
-
const ratio = contextAndRatio.ratio;
|
|
238
|
-
|
|
239
|
-
c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
|
|
240
|
-
let x = 0;
|
|
241
|
-
let y = 0;
|
|
242
|
-
let maxh = 0;
|
|
243
|
-
const n = data.length;
|
|
244
|
-
--di;
|
|
245
|
-
while (++di < n) {
|
|
246
|
-
d = data[di];
|
|
247
|
-
c.save();
|
|
248
|
-
c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
|
|
249
|
-
let w = c.measureText(d.text + "m").width * ratio;
|
|
250
|
-
let h = d.size << 1;
|
|
251
|
-
if (d.rotate) {
|
|
252
|
-
const sr = Math.sin(d.rotate * cloudRadians);
|
|
253
|
-
const cr = Math.cos(d.rotate * cloudRadians);
|
|
254
|
-
const wcr = w * cr;
|
|
255
|
-
const wsr = w * sr;
|
|
256
|
-
const hcr = h * cr;
|
|
257
|
-
const hsr = h * sr;
|
|
258
|
-
w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
|
|
259
|
-
h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
|
|
260
|
-
} else {
|
|
261
|
-
w = (w + 0x1f) >> 5 << 5;
|
|
262
|
-
}
|
|
263
|
-
if (h > maxh) maxh = h;
|
|
264
|
-
if (x + w >= (cw << 5)) {
|
|
265
|
-
x = 0;
|
|
266
|
-
y += maxh;
|
|
267
|
-
maxh = 0;
|
|
268
|
-
}
|
|
269
|
-
if (y + h >= ch) break;
|
|
270
|
-
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
|
|
271
|
-
if (d.rotate) c.rotate(d.rotate * cloudRadians);
|
|
272
|
-
c.fillText(d.text, 0, 0);
|
|
273
|
-
if (d.padding) {
|
|
274
|
-
c.lineWidth = 2 * d.padding;
|
|
275
|
-
c.strokeText(d.text, 0, 0);
|
|
276
|
-
}
|
|
277
|
-
c.restore();
|
|
278
|
-
d.width = w;
|
|
279
|
-
d.height = h;
|
|
280
|
-
d.xoff = x;
|
|
281
|
-
d.yoff = y;
|
|
282
|
-
d.x1 = w >> 1;
|
|
283
|
-
d.y1 = h >> 1;
|
|
284
|
-
d.x0 = -d.x1;
|
|
285
|
-
d.y0 = -d.y1;
|
|
286
|
-
d.hasText = true;
|
|
287
|
-
x += w;
|
|
288
|
-
}
|
|
289
|
-
const pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data;
|
|
290
|
-
const sprite = [];
|
|
291
|
-
while (--di >= 0) {
|
|
292
|
-
d = data[di];
|
|
293
|
-
if (!d.hasText) continue;
|
|
294
|
-
const w = d.width;
|
|
295
|
-
const w32 = w >> 5;
|
|
296
|
-
let h = d.y1 - d.y0;
|
|
297
|
-
// Zero the buffer
|
|
298
|
-
for (let i = 0; i < h * w32; i++) sprite[i] = 0;
|
|
299
|
-
x = d.xoff;
|
|
300
|
-
if (x == null) return;
|
|
301
|
-
y = d.yoff;
|
|
302
|
-
let seen = 0;
|
|
303
|
-
let seenRow = -1;
|
|
304
|
-
for (let j = 0; j < h; j++) {
|
|
305
|
-
for (let i = 0; i < w; i++) {
|
|
306
|
-
const k = w32 * j + (i >> 5);
|
|
307
|
-
const m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
|
|
308
|
-
sprite[k] |= m;
|
|
309
|
-
seen |= m;
|
|
310
|
-
}
|
|
311
|
-
if (seen) seenRow = j;
|
|
312
|
-
else {
|
|
313
|
-
d.y0++;
|
|
314
|
-
h--;
|
|
315
|
-
j--;
|
|
316
|
-
y++;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
d.y1 = d.y0 + seenRow;
|
|
320
|
-
d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Use mask-based collision detection.
|
|
325
|
-
function cloudCollide(tag, board, sw) {
|
|
326
|
-
sw >>= 5;
|
|
327
|
-
const sprite = tag.sprite;
|
|
328
|
-
const w = tag.width >> 5;
|
|
329
|
-
const lx = tag.x - (w << 4);
|
|
330
|
-
const sx = lx & 0x7f;
|
|
331
|
-
const msx = 32 - sx;
|
|
332
|
-
const h = tag.y1 - tag.y0;
|
|
333
|
-
let x = (tag.y + tag.y0) * sw + (lx >> 5);
|
|
334
|
-
let last;
|
|
335
|
-
for (let j = 0; j < h; j++) {
|
|
336
|
-
last = 0;
|
|
337
|
-
for (let i = 0; i <= w; i++) {
|
|
338
|
-
if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
|
|
339
|
-
& board[x + i]) return true;
|
|
340
|
-
}
|
|
341
|
-
x += sw;
|
|
342
|
-
}
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function cloudBounds(bounds, d) {
|
|
347
|
-
const b0 = bounds[0];
|
|
348
|
-
const b1 = bounds[1];
|
|
349
|
-
if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
|
|
350
|
-
if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
|
|
351
|
-
if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
|
|
352
|
-
if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function collideRects(a, b) {
|
|
356
|
-
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function archimedeanSpiral(size) {
|
|
360
|
-
const e = size[0] / size[1];
|
|
361
|
-
return function (t) {
|
|
362
|
-
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function rectangularSpiral(size) {
|
|
367
|
-
const dy = 4;
|
|
368
|
-
const dx = dy * size[0] / size[1];
|
|
369
|
-
let x = 0;
|
|
370
|
-
let y = 0;
|
|
371
|
-
return function (t) {
|
|
372
|
-
const sign = t < 0 ? -1 : 1;
|
|
373
|
-
// See triangular numbers: T_n = n * (n + 1) / 2.
|
|
374
|
-
switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
|
|
375
|
-
case 0: x += dx; break;
|
|
376
|
-
case 1: y += dy; break;
|
|
377
|
-
case 2: x -= dx; break;
|
|
378
|
-
default: y -= dy; break;
|
|
379
|
-
}
|
|
380
|
-
return [x, y];
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// TODO reuse arrays?
|
|
385
|
-
function zeroArray(n) {
|
|
386
|
-
const a = [];
|
|
387
|
-
let i = -1;
|
|
388
|
-
while (++i < n) a[i] = 0;
|
|
389
|
-
return a;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function cloudCanvas() {
|
|
393
|
-
return document.createElement("canvas");
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function functor(d) {
|
|
397
|
-
return typeof d === "function" ? d : function () { return d; };
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const spirals = {
|
|
401
|
-
archimedean: archimedeanSpiral,
|
|
402
|
-
rectangular: rectangularSpiral
|
|
403
|
-
};
|
|
1
|
+
// Word cloud layout by Jason Davies, https://www.jasondavies.com/wordcloud/
|
|
2
|
+
// Algorithm due to Jonathan Feinberg, http://static.mrfeinberg.com/bv_ch03.pdf
|
|
3
|
+
|
|
4
|
+
import { dispatch } from "d3-dispatch";
|
|
5
|
+
|
|
6
|
+
const cloudRadians = Math.PI / 180;
|
|
7
|
+
const cw = 1 << 11 >> 5;
|
|
8
|
+
const ch = 1 << 11;
|
|
9
|
+
|
|
10
|
+
export function d3Cloud() {
|
|
11
|
+
const event = dispatch("word", "end");
|
|
12
|
+
const cloud: any = {};
|
|
13
|
+
|
|
14
|
+
let size = [256, 256];
|
|
15
|
+
let text = cloudText;
|
|
16
|
+
let font = cloudFont;
|
|
17
|
+
let fontSize = cloudFontSize;
|
|
18
|
+
let fontStyle = cloudFontNormal;
|
|
19
|
+
let fontWeight = cloudFontNormal;
|
|
20
|
+
let rotate = cloudRotate;
|
|
21
|
+
let padding = cloudPadding;
|
|
22
|
+
let words = [];
|
|
23
|
+
let spiral = archimedeanSpiral;
|
|
24
|
+
let timeInterval = Infinity;
|
|
25
|
+
let timer = null;
|
|
26
|
+
let random = Math.random;
|
|
27
|
+
let canvas = cloudCanvas;
|
|
28
|
+
|
|
29
|
+
cloud.canvas = function (_?) {
|
|
30
|
+
return arguments.length ? (canvas = functor(_), cloud) : canvas;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
cloud.start = function () {
|
|
34
|
+
const contextAndRatio = getContext(canvas());
|
|
35
|
+
const board = zeroArray((size[0] >> 5) * size[1]);
|
|
36
|
+
let bounds = null;
|
|
37
|
+
const n = words.length;
|
|
38
|
+
let i = -1;
|
|
39
|
+
const tags = [];
|
|
40
|
+
const data = words.map(function (d, i) {
|
|
41
|
+
d.text = text.call(this, d, i);
|
|
42
|
+
d.font = font.call(this, d, i);
|
|
43
|
+
d.style = fontStyle.call(this, d, i);
|
|
44
|
+
d.weight = fontWeight.call(this, d, i);
|
|
45
|
+
d.rotate = rotate.call(this, d, i);
|
|
46
|
+
d.size = ~~fontSize.call(this, d, i);
|
|
47
|
+
d.padding = padding.call(this, d, i);
|
|
48
|
+
return d;
|
|
49
|
+
}).sort(function (a, b) { return b.size - a.size; });
|
|
50
|
+
|
|
51
|
+
if (timer) clearInterval(timer);
|
|
52
|
+
timer = setInterval(step, 0);
|
|
53
|
+
step();
|
|
54
|
+
|
|
55
|
+
return cloud;
|
|
56
|
+
|
|
57
|
+
function step() {
|
|
58
|
+
const start = Date.now();
|
|
59
|
+
while (Date.now() - start < timeInterval && ++i < n && timer) {
|
|
60
|
+
const d = data[i];
|
|
61
|
+
d.x = (size[0] * (random() + .5)) >> 1;
|
|
62
|
+
d.y = (size[1] * (random() + .5)) >> 1;
|
|
63
|
+
cloudSprite(contextAndRatio, d, data, i);
|
|
64
|
+
if (d.hasText && place(board, d, bounds)) {
|
|
65
|
+
tags.push(d);
|
|
66
|
+
event.call("word", cloud, d);
|
|
67
|
+
if (bounds) cloudBounds(bounds, d);
|
|
68
|
+
else bounds = [{ x: d.x + d.x0, y: d.y + d.y0 }, { x: d.x + d.x1, y: d.y + d.y1 }];
|
|
69
|
+
// Temporary hack
|
|
70
|
+
d.x -= size[0] >> 1;
|
|
71
|
+
d.y -= size[1] >> 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (i >= n) {
|
|
75
|
+
cloud.stop();
|
|
76
|
+
event.call("end", cloud, tags, bounds);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
cloud.stop = function () {
|
|
82
|
+
if (timer) {
|
|
83
|
+
clearInterval(timer);
|
|
84
|
+
timer = null;
|
|
85
|
+
}
|
|
86
|
+
return cloud;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function getContext(canvas) {
|
|
90
|
+
canvas.width = canvas.height = 1;
|
|
91
|
+
const ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
|
|
92
|
+
canvas.width = (cw << 5) / ratio;
|
|
93
|
+
canvas.height = ch / ratio;
|
|
94
|
+
|
|
95
|
+
const context = canvas.getContext("2d");
|
|
96
|
+
context.fillStyle = context.strokeStyle = "red";
|
|
97
|
+
context.textAlign = "center";
|
|
98
|
+
|
|
99
|
+
return { context, ratio };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function place(board, tag, bounds) {
|
|
103
|
+
const startX = tag.x;
|
|
104
|
+
const startY = tag.y;
|
|
105
|
+
const maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]);
|
|
106
|
+
const s = spiral(size);
|
|
107
|
+
const dt = random() < .5 ? 1 : -1;
|
|
108
|
+
let t = -dt;
|
|
109
|
+
let dxdy;
|
|
110
|
+
let dx;
|
|
111
|
+
let dy;
|
|
112
|
+
|
|
113
|
+
// eslint-disable-next-line no-cond-assign
|
|
114
|
+
while (dxdy = s(t += dt)) {
|
|
115
|
+
dx = ~~dxdy[0];
|
|
116
|
+
dy = ~~dxdy[1];
|
|
117
|
+
|
|
118
|
+
if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;
|
|
119
|
+
|
|
120
|
+
tag.x = startX + dx;
|
|
121
|
+
tag.y = startY + dy;
|
|
122
|
+
|
|
123
|
+
if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
|
|
124
|
+
tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
|
|
125
|
+
// TODO only check for collisions within current bounds.
|
|
126
|
+
if (!bounds || !cloudCollide(tag, board, size[0])) {
|
|
127
|
+
if (!bounds || collideRects(tag, bounds)) {
|
|
128
|
+
const sprite = tag.sprite;
|
|
129
|
+
const w = tag.width >> 5;
|
|
130
|
+
const sw = size[0] >> 5;
|
|
131
|
+
const lx = tag.x - (w << 4);
|
|
132
|
+
const sx = lx & 0x7f;
|
|
133
|
+
const msx = 32 - sx;
|
|
134
|
+
const h = tag.y1 - tag.y0;
|
|
135
|
+
let x = (tag.y + tag.y0) * sw + (lx >> 5);
|
|
136
|
+
let last;
|
|
137
|
+
for (let j = 0; j < h; j++) {
|
|
138
|
+
last = 0;
|
|
139
|
+
for (let i = 0; i <= w; i++) {
|
|
140
|
+
board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
|
|
141
|
+
}
|
|
142
|
+
x += sw;
|
|
143
|
+
}
|
|
144
|
+
delete tag.sprite;
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
cloud.timeInterval = function (_?) {
|
|
153
|
+
return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
cloud.words = function (_?) {
|
|
157
|
+
return arguments.length ? (words = _, cloud) : words;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
cloud.size = function (_?) {
|
|
161
|
+
return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
cloud.font = function (_?) {
|
|
165
|
+
return arguments.length ? (font = functor(_), cloud) : font;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
cloud.fontStyle = function (_?) {
|
|
169
|
+
return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
cloud.fontWeight = function (_?) {
|
|
173
|
+
return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
cloud.rotate = function (_?) {
|
|
177
|
+
return arguments.length ? (rotate = functor(_), cloud) : rotate;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
cloud.text = function (_?) {
|
|
181
|
+
return arguments.length ? (text = functor(_), cloud) : text;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
cloud.spiral = function (_?) {
|
|
185
|
+
return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
cloud.fontSize = function (_?) {
|
|
189
|
+
return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
cloud.padding = function (_?) {
|
|
193
|
+
return arguments.length ? (padding = functor(_), cloud) : padding;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
cloud.random = function (_?) {
|
|
197
|
+
return arguments.length ? (random = _, cloud) : random;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
cloud.on = function () {
|
|
201
|
+
const value = event.on.apply(event, arguments);
|
|
202
|
+
return value === event ? cloud : value;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return cloud;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function cloudText(d) {
|
|
209
|
+
return d.text;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function cloudFont() {
|
|
213
|
+
return "serif";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function cloudFontNormal() {
|
|
217
|
+
return "normal";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function cloudFontSize(d) {
|
|
221
|
+
return Math.sqrt(d.value);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function cloudRotate() {
|
|
225
|
+
return (~~(Math.random() * 6) - 3) * 30;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function cloudPadding() {
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Fetches a monochrome sprite bitmap for the specified text.
|
|
233
|
+
// Load in batches for speed.
|
|
234
|
+
function cloudSprite(contextAndRatio, d, data, di) {
|
|
235
|
+
if (d.sprite) return;
|
|
236
|
+
const c = contextAndRatio.context;
|
|
237
|
+
const ratio = contextAndRatio.ratio;
|
|
238
|
+
|
|
239
|
+
c.clearRect(0, 0, (cw << 5) / ratio, ch / ratio);
|
|
240
|
+
let x = 0;
|
|
241
|
+
let y = 0;
|
|
242
|
+
let maxh = 0;
|
|
243
|
+
const n = data.length;
|
|
244
|
+
--di;
|
|
245
|
+
while (++di < n) {
|
|
246
|
+
d = data[di];
|
|
247
|
+
c.save();
|
|
248
|
+
c.font = d.style + " " + d.weight + " " + ~~((d.size + 1) / ratio) + "px " + d.font;
|
|
249
|
+
let w = c.measureText(d.text + "m").width * ratio;
|
|
250
|
+
let h = d.size << 1;
|
|
251
|
+
if (d.rotate) {
|
|
252
|
+
const sr = Math.sin(d.rotate * cloudRadians);
|
|
253
|
+
const cr = Math.cos(d.rotate * cloudRadians);
|
|
254
|
+
const wcr = w * cr;
|
|
255
|
+
const wsr = w * sr;
|
|
256
|
+
const hcr = h * cr;
|
|
257
|
+
const hsr = h * sr;
|
|
258
|
+
w = (Math.max(Math.abs(wcr + hsr), Math.abs(wcr - hsr)) + 0x1f) >> 5 << 5;
|
|
259
|
+
h = ~~Math.max(Math.abs(wsr + hcr), Math.abs(wsr - hcr));
|
|
260
|
+
} else {
|
|
261
|
+
w = (w + 0x1f) >> 5 << 5;
|
|
262
|
+
}
|
|
263
|
+
if (h > maxh) maxh = h;
|
|
264
|
+
if (x + w >= (cw << 5)) {
|
|
265
|
+
x = 0;
|
|
266
|
+
y += maxh;
|
|
267
|
+
maxh = 0;
|
|
268
|
+
}
|
|
269
|
+
if (y + h >= ch) break;
|
|
270
|
+
c.translate((x + (w >> 1)) / ratio, (y + (h >> 1)) / ratio);
|
|
271
|
+
if (d.rotate) c.rotate(d.rotate * cloudRadians);
|
|
272
|
+
c.fillText(d.text, 0, 0);
|
|
273
|
+
if (d.padding) {
|
|
274
|
+
c.lineWidth = 2 * d.padding;
|
|
275
|
+
c.strokeText(d.text, 0, 0);
|
|
276
|
+
}
|
|
277
|
+
c.restore();
|
|
278
|
+
d.width = w;
|
|
279
|
+
d.height = h;
|
|
280
|
+
d.xoff = x;
|
|
281
|
+
d.yoff = y;
|
|
282
|
+
d.x1 = w >> 1;
|
|
283
|
+
d.y1 = h >> 1;
|
|
284
|
+
d.x0 = -d.x1;
|
|
285
|
+
d.y0 = -d.y1;
|
|
286
|
+
d.hasText = true;
|
|
287
|
+
x += w;
|
|
288
|
+
}
|
|
289
|
+
const pixels = c.getImageData(0, 0, (cw << 5) / ratio, ch / ratio).data;
|
|
290
|
+
const sprite = [];
|
|
291
|
+
while (--di >= 0) {
|
|
292
|
+
d = data[di];
|
|
293
|
+
if (!d.hasText) continue;
|
|
294
|
+
const w = d.width;
|
|
295
|
+
const w32 = w >> 5;
|
|
296
|
+
let h = d.y1 - d.y0;
|
|
297
|
+
// Zero the buffer
|
|
298
|
+
for (let i = 0; i < h * w32; i++) sprite[i] = 0;
|
|
299
|
+
x = d.xoff;
|
|
300
|
+
if (x == null) return;
|
|
301
|
+
y = d.yoff;
|
|
302
|
+
let seen = 0;
|
|
303
|
+
let seenRow = -1;
|
|
304
|
+
for (let j = 0; j < h; j++) {
|
|
305
|
+
for (let i = 0; i < w; i++) {
|
|
306
|
+
const k = w32 * j + (i >> 5);
|
|
307
|
+
const m = pixels[((y + j) * (cw << 5) + (x + i)) << 2] ? 1 << (31 - (i % 32)) : 0;
|
|
308
|
+
sprite[k] |= m;
|
|
309
|
+
seen |= m;
|
|
310
|
+
}
|
|
311
|
+
if (seen) seenRow = j;
|
|
312
|
+
else {
|
|
313
|
+
d.y0++;
|
|
314
|
+
h--;
|
|
315
|
+
j--;
|
|
316
|
+
y++;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
d.y1 = d.y0 + seenRow;
|
|
320
|
+
d.sprite = sprite.slice(0, (d.y1 - d.y0) * w32);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Use mask-based collision detection.
|
|
325
|
+
function cloudCollide(tag, board, sw) {
|
|
326
|
+
sw >>= 5;
|
|
327
|
+
const sprite = tag.sprite;
|
|
328
|
+
const w = tag.width >> 5;
|
|
329
|
+
const lx = tag.x - (w << 4);
|
|
330
|
+
const sx = lx & 0x7f;
|
|
331
|
+
const msx = 32 - sx;
|
|
332
|
+
const h = tag.y1 - tag.y0;
|
|
333
|
+
let x = (tag.y + tag.y0) * sw + (lx >> 5);
|
|
334
|
+
let last;
|
|
335
|
+
for (let j = 0; j < h; j++) {
|
|
336
|
+
last = 0;
|
|
337
|
+
for (let i = 0; i <= w; i++) {
|
|
338
|
+
if (((last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0))
|
|
339
|
+
& board[x + i]) return true;
|
|
340
|
+
}
|
|
341
|
+
x += sw;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function cloudBounds(bounds, d) {
|
|
347
|
+
const b0 = bounds[0];
|
|
348
|
+
const b1 = bounds[1];
|
|
349
|
+
if (d.x + d.x0 < b0.x) b0.x = d.x + d.x0;
|
|
350
|
+
if (d.y + d.y0 < b0.y) b0.y = d.y + d.y0;
|
|
351
|
+
if (d.x + d.x1 > b1.x) b1.x = d.x + d.x1;
|
|
352
|
+
if (d.y + d.y1 > b1.y) b1.y = d.y + d.y1;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function collideRects(a, b) {
|
|
356
|
+
return a.x + a.x1 > b[0].x && a.x + a.x0 < b[1].x && a.y + a.y1 > b[0].y && a.y + a.y0 < b[1].y;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function archimedeanSpiral(size) {
|
|
360
|
+
const e = size[0] / size[1];
|
|
361
|
+
return function (t) {
|
|
362
|
+
return [e * (t *= .1) * Math.cos(t), t * Math.sin(t)];
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function rectangularSpiral(size) {
|
|
367
|
+
const dy = 4;
|
|
368
|
+
const dx = dy * size[0] / size[1];
|
|
369
|
+
let x = 0;
|
|
370
|
+
let y = 0;
|
|
371
|
+
return function (t) {
|
|
372
|
+
const sign = t < 0 ? -1 : 1;
|
|
373
|
+
// See triangular numbers: T_n = n * (n + 1) / 2.
|
|
374
|
+
switch ((Math.sqrt(1 + 4 * sign * t) - sign) & 3) {
|
|
375
|
+
case 0: x += dx; break;
|
|
376
|
+
case 1: y += dy; break;
|
|
377
|
+
case 2: x -= dx; break;
|
|
378
|
+
default: y -= dy; break;
|
|
379
|
+
}
|
|
380
|
+
return [x, y];
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// TODO reuse arrays?
|
|
385
|
+
function zeroArray(n) {
|
|
386
|
+
const a = [];
|
|
387
|
+
let i = -1;
|
|
388
|
+
while (++i < n) a[i] = 0;
|
|
389
|
+
return a;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function cloudCanvas() {
|
|
393
|
+
return document.createElement("canvas");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function functor(d) {
|
|
397
|
+
return typeof d === "function" ? d : function () { return d; };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const spirals = {
|
|
401
|
+
archimedean: archimedeanSpiral,
|
|
402
|
+
rectangular: rectangularSpiral
|
|
403
|
+
};
|