@@ -29,6 +29,7 @@ import PenAndPaper from '../atoms/PenAndPaper.vue';
29
29
import { useUserOptionState } from ' ../useUserOptionState' ;
30
30
import { useChartAccessibility } from ' ../useChartAccessibility' ;
31
31
import usePanZoom from ' ../usePanZoom' ;
32
+ import { positionWords } from ' ../wordcloud' ;
32
33
33
34
const { vue_ui_word_cloud: DEFAULT_CONFIG } = useConfig ();
34
35
@@ -128,7 +129,8 @@ const svg = ref({
128
129
width: FINAL_CONFIG .value .style .chart .width ,
129
130
height: FINAL_CONFIG .value .style .chart .height ,
130
131
maxFontSize: FINAL_CONFIG .value .style .chart .words .maxFontSize ,
131
- minFontSize: FINAL_CONFIG .value .style .chart .words .minFontSize
132
+ minFontSize: FINAL_CONFIG .value .style .chart .words .minFontSize ,
133
+ bold: FINAL_CONFIG .value .style .chart .words .bold
132
134
});
133
135
134
136
const debounceUpdateCloud = debounce (() => {
@@ -208,158 +210,10 @@ function measureTextSize(text, fontSize, fontFamily = "Arial") {
208
210
};
209
211
}
210
212
211
- function positionWords (words , width , height ) {
212
- const maskW = Math .round (width);
213
- const maskH = Math .round (height);
214
- const minFontSize = 1 ;
215
- const configMinFontSize = svg .value .minFontSize ;
216
- const maxFontSize = svg .value .maxFontSize ;
217
- const proximity = FINAL_CONFIG .value .style .chart .words .proximity || 0 ;
218
- const values = words .map (w => w .value );
219
- const minValue = Math .min (... values);
220
- const maxValue = Math .max (... values);
221
-
222
- const mask = new Uint8Array (maskW * maskH);
223
- const canvas = document .createElement (' canvas' );
224
- const ctx = canvas .getContext (' 2d' , { willReadFrequently: true });
225
- canvas .width = maskW;
226
- canvas .height = maskH;
227
-
228
- function getWordBitmap (word , fontSize , pad ) {
229
- ctx .save ();
230
- ctx .font = ` ${ svg .value .style && svg .value .style .bold ? ' bold ' : ' ' }${ fontSize} px Arial` ;
231
- const metrics = ctx .measureText (word .name );
232
- const textW = Math .ceil (metrics .width ) + 2 + (pad ? pad * 2 : 0 );
233
- const textH = Math .ceil (fontSize) + 2 + (pad ? pad * 2 : 0 );
234
-
235
- canvas .width = textW;
236
- canvas .height = textH;
237
- ctx .clearRect (0 , 0 , textW, textH);
238
- ctx .font = ` ${ svg .value .style && svg .value .style .bold ? ' bold ' : ' ' }${ fontSize} px Arial` ;
239
- ctx .textAlign = " center" ;
240
- ctx .textBaseline = " middle" ;
241
- ctx .fillStyle = " black" ;
242
- ctx .fillText (word .name , textW / 2 , textH / 2 );
243
- const image = ctx .getImageData (0 , 0 , textW, textH);
244
- const data = image .data ;
245
- const wordMask = [];
246
- for (let y = 0 ; y < textH; y += 1 ) {
247
- for (let x = 0 ; x < textW; x += 1 ) {
248
- if (data[(y * textW + x) * 4 + 3 ] > 1 ) wordMask .push ([x, y]);
249
- }
250
- }
251
- ctx .restore ();
252
- return { w: textW, h: textH, wordMask };
253
- }
254
-
255
- function canPlaceAt (mask , maskW , maskH , wx , wy , wordMask ) {
256
- for (let i = 0 ; i < wordMask .length ; i += 1 ) {
257
- const x = wx + wordMask[i][0 ];
258
- const y = wy + wordMask[i][1 ];
259
- if (x < 0 || y < 0 || x >= maskW || y >= maskH) return false ;
260
- if (mask[y * maskW + x]) return false ;
261
- }
262
- return true ;
263
- }
264
- function markMask (mask , maskW , maskH , wx , wy , wordMask ) {
265
- for (let i = 0 ; i < wordMask .length ; i += 1 ) {
266
- const x = wx + wordMask[i][0 ];
267
- const y = wy + wordMask[i][1 ];
268
- if (x >= 0 && y >= 0 && x < maskW && y < maskH) mask[y * maskW + x] = 1 ;
269
- }
270
- }
271
-
272
- const spiralStep = 6 , spiralRadiusStep = 2 ;
273
- const fallbackSpiralStep = 2 , fallbackSpiralRadiusStep = 1 ;
274
- const cx = Math .floor (maskW / 2 ), cy = Math .floor (maskH / 2 );
275
-
276
- const sorted = [... words].sort ((a , b ) => b .value - a .value );
277
- const positionedWords = [];
278
-
279
- function dilateWordMask (wordMask , w , h , dilation = 1 ) {
280
- const set = new Set (wordMask .map (([x , y ]) => ` ${ x} ,${ y} ` ));
281
- const result = new Set (set);
282
- for (let [x, y] of wordMask) {
283
- for (let dx = - dilation; dx <= dilation; dx += 1 ) {
284
- for (let dy = - dilation; dy <= dilation; dy += 1 ) {
285
- if (dx === 0 && dy === 0 ) continue ;
286
- const nx = x + dx, ny = y + dy;
287
- if (nx >= 0 && nx < w && ny >= 0 && ny < h) {
288
- result .add (` ${ nx} ,${ ny} ` );
289
- }
290
- }
291
- }
292
- }
293
- return Array .from (result).map (s => s .split (' ,' ).map (Number ));
294
- }
295
-
296
- for (const wordRaw of sorted) {
297
- let targetFontSize = configMinFontSize;
298
- if (maxValue !== minValue) {
299
- targetFontSize = (wordRaw .value - minValue) / (maxValue - minValue) * (maxFontSize - configMinFontSize) + configMinFontSize;
300
- }
301
- targetFontSize = Math .max (configMinFontSize, Math .min (maxFontSize, targetFontSize));
302
-
303
- let placed = false ;
304
- let fontSize = targetFontSize;
305
-
306
- while (! placed && fontSize >= minFontSize) {
307
- let { w, h, wordMask } = getWordBitmap (wordRaw, fontSize, proximity);
308
- wordMask = dilateWordMask (wordMask, w, h, 2 );
309
- let r = 0 , attempts = 0 ;
310
- while (r < Math .max (maskW, maskH) && ! placed && attempts < 10000 ) {
311
- for (let theta = 0 ; theta < 360 ; theta += spiralStep) {
312
- attempts += 1 ;
313
- const px = Math .round (cx + r * Math .cos (theta * Math .PI / 180 ) - w / 2 );
314
- const py = Math .round (cy + r * Math .sin (theta * Math .PI / 180 ) - h / 2 );
315
- if (px < 0 || py < 0 || px + w > maskW || py + h > maskH) continue ;
316
- if (canPlaceAt (mask, maskW, maskH, px, py, wordMask)) {
317
- positionedWords .push ({ ... wordRaw, x: px - maskW / 2 , y: py - maskH / 2 , fontSize, width: w, height: h, angle: 0 });
318
- markMask (mask, maskW, maskH, px, py, wordMask);
319
- placed = true ;
320
- break ;
321
- }
322
- }
323
- r += spiralRadiusStep;
324
- }
325
- if (! placed) fontSize -= 1 ;
326
- }
327
-
328
- if (! placed && fontSize < minFontSize) {
329
- fontSize = minFontSize;
330
- const { w , h , wordMask } = getWordBitmap (wordRaw, fontSize, proximity);
331
- let r = 0 , attempts = 0 , bestPlacement = null ;
332
- while (r < Math .max (maskW, maskH) && ! placed && attempts < 25000 ) {
333
- for (let theta = 0 ; theta < 360 ; theta += fallbackSpiralStep) {
334
- attempts += 1 ;
335
- const px = Math .round (cx + r * Math .cos (theta * Math .PI / 180 ) - w / 2 );
336
- const py = Math .round (cy + r * Math .sin (theta * Math .PI / 180 ) - h / 2 );
337
- if (px < 0 || py < 0 || px + w > maskW || py + h > maskH) continue ;
338
- if (canPlaceAt (mask, maskW, maskH, px, py, wordMask)) {
339
- positionedWords .push ({ ... wordRaw, x: px - maskW / 2 , y: py - maskH / 2 , fontSize, width: w, height: h, angle: 0 });
340
- markMask (mask, maskW, maskH, px, py, wordMask);
341
- placed = true ;
342
- break ;
343
- }
344
- }
345
- r += fallbackSpiralRadiusStep;
346
- }
347
- }
348
- }
349
- return positionedWords;
350
- }
351
-
352
213
const positionedWords = ref ([]);
353
214
354
215
watch (() => props .dataset , generateWordCloud, { immediate: true });
355
216
356
- const wordMin = computed (() => {
357
- return Math .round (Math .min (... drawableDataset .value .map (w => w .value )));
358
- })
359
- const wordMax = computed (() => {
360
- return Math .round (Math .max (... drawableDataset .value .map (w => w .value )));
361
- })
362
-
363
217
function generateWordCloud () {
364
218
const values = [... drawableDataset .value ].map (d => d .value );
365
219
const maxValue = Math .max (... values);
@@ -379,7 +233,11 @@ function generateWordCloud() {
379
233
};
380
234
});
381
235
382
- positionedWords .value = positionWords (scaledWords, svg .value .width , svg .value .height ).sort ((a , b ) => b .fontSize - a .fontSize );
236
+ positionedWords .value = positionWords ({
237
+ words: scaledWords,
238
+ svg: svg .value ,
239
+ proximity: FINAL_CONFIG .value .style .chart .words .proximity ,
240
+ });
383
241
}
384
242
385
243
const table = computed (() => {
0 commit comments