Skip to content

Commit ac580fe

Browse files
authored
Merge pull request #206 from graphieros/ft-wordcloud-perf
Ft wordcloud perf
2 parents b42356c + 67d72fc commit ac580fe

File tree

8 files changed

+54
-49
lines changed

8 files changed

+54
-49
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vue-data-ui",
33
"private": false,
4-
"version": "2.10.1",
4+
"version": "2.10.2-beta.2",
55
"type": "module",
66
"description": "A user-empowering data visualization Vue 3 components library for eloquent data storytelling",
77
"keywords": [

src/App.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const components = ref([ //------|
125125
* Modify the index to display a component
126126
* [0] = VueUiXy
127127
*/
128-
const selectedComponent = ref(components.value[54]);
128+
const selectedComponent = ref(components.value[37]);
129129
130130
/**
131131
* Legacy testing arena where some non chart components can be tested

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ function generateWordCloud() {
237237
words: scaledWords,
238238
svg: svg.value,
239239
proximity: FINAL_CONFIG.value.style.chart.words.proximity,
240+
strictPixelPadding: FINAL_CONFIG.value.strictPixelPadding
240241
});
241242
}
242243
@@ -499,18 +500,27 @@ function useTooltip(word) {
499500
<g
500501
:transform="`translate(${(svg.width <= 0 ? 10 : svg.width) / 2}, ${(svg.height <= 0 ? 10 : svg.height) / 2})`">
501502
<g v-for="(word, index) in positionedWords">
502-
<text
503+
<rect
504+
v-if="word.minX !== undefined"
503505
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
504516
:fill="word.color"
505517
:font-weight="FINAL_CONFIG.style.chart.words.bold ? 'bold' : 'normal'" :key="index"
506518
:x="word.x" :y="word.y" :font-size="word.fontSize"
507519
:transform="`translate(${word.width / 2}, ${word.height / 2})`"
508520
:class="{'animated': FINAL_CONFIG.useCssAnimation, 'word-selected': selectedWord && selectedWord === word.id && mutableConfig.showTooltip, 'word-not-selected': selectedWord && selectedWord !== word.id && mutableConfig.showTooltip }"
509521
text-anchor="middle"
510522
dominant-baseline="central"
511-
@mouseover="useTooltip(word)"
512-
@mouseleave="selectedWord = null; isTooltip = false"
513-
:style="`animation-delay:${index * FINAL_CONFIG.animationDelayMs}ms !important;`"
523+
:style="`animation-delay:${index * FINAL_CONFIG.animationDelayMs}ms !important; pointer-events:none;`"
514524
>
515525
{{ word.name }}
516526
</text>

src/useConfig.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3645,6 +3645,7 @@ export function useConfig() {
36453645
customPalette: [],
36463646
useCssAnimation: true,
36473647
animationDelayMs: 20,
3648+
strictPixelPadding: false, // If true, strict per-pixel padding is used (dilateWordMask); if false, just rectangular bounding box (or pad).
36483649
userOptions: USER_OPTIONS({
36493650
tooltip: true,
36503651
pdf: true,

src/wordcloud.js

Lines changed: 34 additions & 7 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
/**
@@ -135,13 +159,14 @@ export function positionWords({
135159
words,
136160
proximity = 0,
137161
svg,
162+
strictPixelPadding
138163
}) {
139164
const { width, height } = svg;
140165
const maskW = Math.round(width);
141166
const maskH = Math.round(height);
142167
const minFontSize = 1;
143168
const configMinFontSize = svg.minFontSize;
144-
const maxFontSize = svg.maxFontSize;
169+
const maxFontSize = Math.min(svg.maxFontSize, 100);
145170
const values = words.map(w => w.value);
146171
const minValue = Math.min(...values);
147172
const maxValue = Math.max(...values);
@@ -169,7 +194,7 @@ export function positionWords({
169194
let fontSize = targetFontSize;
170195

171196
while (!placed && fontSize >= minFontSize) {
172-
let { w, h, wordMask } = getWordBitmap({
197+
let { w, h, wordMask, minX, minY, maxX, maxY } = getWordBitmap({
173198
word: wordRaw,
174199
fontSize,
175200
pad: proximity,
@@ -178,7 +203,9 @@ export function positionWords({
178203
svg
179204
});
180205

181-
wordMask = dilateWordMask({ wordMask, w, h, dilation: 2 });
206+
if (strictPixelPadding) {
207+
wordMask = dilateWordMask({ wordMask, w, h, dilation: 1 });
208+
}
182209

183210
let r = 0;
184211
let attempts = 0;
@@ -190,7 +217,7 @@ export function positionWords({
190217
const py = Math.round(cy + r * Math.sin(theta * Math.PI / 180) - h / 2);
191218
if (px < 0 || py < 0 || px + w > maskW || py + h > maskH) continue;
192219
if (canPlaceAt({ mask, maskW, maskH, wx:px, wy:py, wordMask})) {
193-
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 });
194221
markMask({ mask, maskW, maskH, wx: px, wy: py, wordMask });
195222
placed = true;
196223
break;
@@ -203,7 +230,7 @@ export function positionWords({
203230

204231
if (!placed && fontSize < minFontSize) {
205232
fontSize = minFontSize;
206-
const { w, h, wordMask } = getWordBitmap({
233+
const { w, h, wordMask, minX, minY, maxX, maxY } = getWordBitmap({
207234
word: wordRaw,
208235
fontSize,
209236
pad: proximity,
@@ -222,7 +249,7 @@ export function positionWords({
222249
const py = Math.round(cy + r * Math.sin(theta * Math.PI / 180) - h / 2);
223250
if (px < 0 || py < 0 || px + w > maskW || py + h > maskH) continue;
224251
if (canPlaceAt({ mask, maskW, maskH, wx: px, wy: py, wordMask })) {
225-
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 });
226253
markMask({ mask, maskW, maskH, wx: px, wy: py, wordMask });
227254
placed = true;
228255
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
});

types/vue-data-ui.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5480,6 +5480,7 @@ declare module "vue-data-ui" {
54805480
userOptions?: ChartUserOptions;
54815481
useCssAnimation?: boolean;
54825482
animationDelayMs?: number;
5483+
strictPixelPadding?: boolean;
54835484
style?: {
54845485
fontFamily?: string;
54855486
chart?: {

0 commit comments

Comments
 (0)