Skip to content

Commit 5a31a1c

Browse files
committed
Improvement - VueUiWordCloud - Improve pointer precision
1 parent 04db352 commit 5a31a1c

File tree

3 files changed

+42
-43
lines changed

3 files changed

+42
-43
lines changed

src/components/vue-ui-word-cloud.vue

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -500,18 +500,27 @@ function useTooltip(word) {
500500
<g
501501
:transform="`translate(${(svg.width <= 0 ? 10 : svg.width) / 2}, ${(svg.height <= 0 ? 10 : svg.height) / 2})`">
502502
<g v-for="(word, index) in positionedWords">
503-
<text
503+
<rect
504+
v-if="word.minX !== undefined"
504505
data-cy="datapoint-word"
506+
:x="word.x + word.minX"
507+
:y="word.y + (word.minY * 1.25)"
508+
:width="word.maxX - word.minX"
509+
:height="word.maxY - word.minY"
510+
fill="transparent"
511+
pointer-events="visiblePainted"
512+
@mouseover="useTooltip(word)"
513+
@mouseleave="selectedWord = null; isTooltip = false"
514+
/>
515+
<text
505516
:fill="word.color"
506517
:font-weight="FINAL_CONFIG.style.chart.words.bold ? 'bold' : 'normal'" :key="index"
507518
:x="word.x" :y="word.y" :font-size="word.fontSize"
508519
:transform="`translate(${word.width / 2}, ${word.height / 2})`"
509520
:class="{'animated': FINAL_CONFIG.useCssAnimation, 'word-selected': selectedWord && selectedWord === word.id && mutableConfig.showTooltip, 'word-not-selected': selectedWord && selectedWord !== word.id && mutableConfig.showTooltip }"
510521
text-anchor="middle"
511522
dominant-baseline="central"
512-
@mouseover="useTooltip(word)"
513-
@mouseleave="selectedWord = null; isTooltip = false"
514-
:style="`animation-delay:${index * FINAL_CONFIG.animationDelayMs}ms !important;`"
523+
:style="`animation-delay:${index * FINAL_CONFIG.animationDelayMs}ms !important; pointer-events:none;`"
515524
>
516525
{{ word.name }}
517526
</text>

src/wordcloud.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,28 @@
1010
* - position words inside a given area using a spiral placement algorithm (the actual function of this file used in the component @generateWordCloud)
1111
*/
1212

13+
function getTightBoundingBox(canvas, ctx) {
14+
const { width, height } = canvas;
15+
const img = ctx.getImageData(0, 0, width, height);
16+
const data = img.data;
17+
let minX = width, minY = height, maxX = 0, maxY = 0;
18+
let found = false;
19+
for (let y = 0; y < height; y += 1) {
20+
for (let x = 0; x < width; x += 1) {
21+
const alpha = data[(y * width + x) * 4 + 3];
22+
if (alpha > 1) {
23+
found = true;
24+
if (x < minX) minX = x;
25+
if (x > maxX) maxX = x;
26+
if (y < minY) minY = y;
27+
if (y > maxY) maxY = y;
28+
}
29+
}
30+
}
31+
if (!found) return [0, 0, 0, 0];
32+
return [minX, minY, maxX, maxY];
33+
}
34+
1335
/**
1436
* Generates a bitmap and mask representing a word, given font size and padding.
1537
* @param {Object} params
@@ -52,9 +74,11 @@ export function getWordBitmap({
5274
if (data[(y * textW + x) * 4 + 3] > 1) wordMask.push([x, y]);
5375
}
5476
}
77+
78+
const [minX, minY, maxX, maxY] = getTightBoundingBox(canvas, ctx);
5579
ctx.restore();
5680

57-
return { w: textW, h: textH, wordMask };
81+
return { w: textW, h: textH, wordMask, minX, minY, maxX, maxY };
5882
}
5983

6084
/**
@@ -170,7 +194,7 @@ export function positionWords({
170194
let fontSize = targetFontSize;
171195

172196
while (!placed && fontSize >= minFontSize) {
173-
let { w, h, wordMask } = getWordBitmap({
197+
let { w, h, wordMask, minX, minY, maxX, maxY } = getWordBitmap({
174198
word: wordRaw,
175199
fontSize,
176200
pad: proximity,
@@ -193,7 +217,7 @@ export function positionWords({
193217
const py = Math.round(cy + r * Math.sin(theta * Math.PI / 180) - h / 2);
194218
if (px < 0 || py < 0 || px + w > maskW || py + h > maskH) continue;
195219
if (canPlaceAt({ mask, maskW, maskH, wx:px, wy:py, wordMask})) {
196-
positionedWords.push({ ...wordRaw, x: px - maskW / 2, y: py - maskH / 2, fontSize, width: w, height: h, angle: 0 });
220+
positionedWords.push({ ...wordRaw, x: px - maskW / 2, y: py - maskH / 2, fontSize, width: w, height: h, angle: 0, minX, minY, maxX, maxY });
197221
markMask({ mask, maskW, maskH, wx: px, wy: py, wordMask });
198222
placed = true;
199223
break;
@@ -206,7 +230,7 @@ export function positionWords({
206230

207231
if (!placed && fontSize < minFontSize) {
208232
fontSize = minFontSize;
209-
const { w, h, wordMask } = getWordBitmap({
233+
const { w, h, wordMask, minX, minY, maxX, maxY } = getWordBitmap({
210234
word: wordRaw,
211235
fontSize,
212236
pad: proximity,
@@ -225,7 +249,7 @@ export function positionWords({
225249
const py = Math.round(cy + r * Math.sin(theta * Math.PI / 180) - h / 2);
226250
if (px < 0 || py < 0 || px + w > maskW || py + h > maskH) continue;
227251
if (canPlaceAt({ mask, maskW, maskH, wx: px, wy: py, wordMask })) {
228-
positionedWords.push({ ...wordRaw, x: px - maskW / 2, y: py - maskH / 2, fontSize, width: w, height: h, angle: 0 });
252+
positionedWords.push({ ...wordRaw, x: px - maskW / 2, y: py - maskH / 2, fontSize, width: w, height: h, angle: 0, minX, minY, maxX, maxY });
229253
markMask({ mask, maskW, maskH, wx: px, wy: py, wordMask });
230254
placed = true;
231255
break;

tests/wordcloud.test.js

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,6 @@ describe("getWordBitmap", () => {
9999
});
100100
expect(ctx.font.startsWith("bold ")).toBe(true);
101101
});
102-
103-
test("pads the word bitmap correctly", () => {
104-
const canvas = createMockCanvas();
105-
const ctx = createMockContext2D();
106-
const word = { name: "pad" };
107-
const fontSize = 12;
108-
const pad = 5;
109-
const svg = { style: {} };
110-
const res1 = getWordBitmap({ word, fontSize, pad: 0, canvas, ctx, svg });
111-
const res2 = getWordBitmap({ word, fontSize, pad, canvas, ctx, svg });
112-
expect(res2.w).toBeGreaterThan(res1.w);
113-
expect(res2.h).toBeGreaterThan(res1.h);
114-
});
115102
});
116103

117104
describe("canPlaceAt", () => {
@@ -339,25 +326,4 @@ describe("positionWords", () => {
339326
expect(result[0].fontSize).toBe(svg.minFontSize);
340327
expect(result[1].fontSize).toBe(svg.minFontSize);
341328
});
342-
343-
test("proximity increases bounding box size", () => {
344-
const words = [
345-
{ name: "A", value: 15 },
346-
{ name: "B", value: 10 },
347-
];
348-
const svg = {
349-
width: 100,
350-
height: 100,
351-
minFontSize: 10,
352-
maxFontSize: 20,
353-
style: {},
354-
};
355-
const result1 = positionWords({ words, svg, proximity: 0 });
356-
const result2 = positionWords({ words, svg, proximity: 10 });
357-
358-
expect(result2[0].width).toBeGreaterThan(result1[0].width);
359-
expect(result2[1].width).toBeGreaterThan(result1[1].width);
360-
expect(result2[0].height).toBeGreaterThan(result1[0].height);
361-
expect(result2[1].height).toBeGreaterThan(result1[1].height);
362-
});
363329
});

0 commit comments

Comments
 (0)