@memberjunction/ng-word-cloud 5.42.0 → 5.44.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-word-cloud",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.44.0",
|
|
4
4
|
"description": "MemberJunction: Reusable SVG-based word cloud component for Angular.",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@angular/compiler": "21.1.3",
|
|
25
25
|
"@angular/compiler-cli": "21.1.3",
|
|
26
|
+
"@memberjunction/ng-test-utils": "5.44.0",
|
|
26
27
|
"vitest": "^4.0.18"
|
|
27
28
|
},
|
|
28
29
|
"peerDependencies": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"word-cloud-layout.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/word-cloud-layout.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { computeWordCloudLayout } from '../lib/word-cloud.layout';
|
|
3
|
-
const defaultConfig = {
|
|
4
|
-
MinFontSize: 12,
|
|
5
|
-
MaxFontSize: 48,
|
|
6
|
-
Layout: 'spiral',
|
|
7
|
-
MaxItems: 100,
|
|
8
|
-
};
|
|
9
|
-
function makeItems(count, baseWeight = 1.0) {
|
|
10
|
-
return Array.from({ length: count }, (_, i) => ({
|
|
11
|
-
Text: `word${i}`,
|
|
12
|
-
Weight: baseWeight - (i * (baseWeight / count)),
|
|
13
|
-
}));
|
|
14
|
-
}
|
|
15
|
-
describe('computeWordCloudLayout', () => {
|
|
16
|
-
describe('empty and edge cases', () => {
|
|
17
|
-
it('should return empty layout for empty items array', () => {
|
|
18
|
-
const result = computeWordCloudLayout([], defaultConfig);
|
|
19
|
-
expect(result.Items).toHaveLength(0);
|
|
20
|
-
expect(result.ViewBox).toBe('0 0 100 100');
|
|
21
|
-
});
|
|
22
|
-
it('should return empty layout for null-ish items', () => {
|
|
23
|
-
const result = computeWordCloudLayout(null, defaultConfig);
|
|
24
|
-
expect(result.Items).toHaveLength(0);
|
|
25
|
-
});
|
|
26
|
-
it('should handle a single item', () => {
|
|
27
|
-
const items = [{ Text: 'hello', Weight: 1.0 }];
|
|
28
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
29
|
-
expect(result.Items).toHaveLength(1);
|
|
30
|
-
expect(result.Items[0].Text).toBe('hello');
|
|
31
|
-
expect(result.Items[0].FontSize).toBe(48); // max font for weight 1.0
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe('sorting and limiting', () => {
|
|
35
|
-
it('should sort items by weight descending (heaviest first)', () => {
|
|
36
|
-
const items = [
|
|
37
|
-
{ Text: 'low', Weight: 0.2 },
|
|
38
|
-
{ Text: 'high', Weight: 0.9 },
|
|
39
|
-
{ Text: 'mid', Weight: 0.5 },
|
|
40
|
-
];
|
|
41
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
42
|
-
expect(result.Items[0].Text).toBe('high');
|
|
43
|
-
expect(result.Items[1].Text).toBe('mid');
|
|
44
|
-
expect(result.Items[2].Text).toBe('low');
|
|
45
|
-
});
|
|
46
|
-
it('should limit items to MaxItems', () => {
|
|
47
|
-
const items = makeItems(50);
|
|
48
|
-
const config = { ...defaultConfig, MaxItems: 10 };
|
|
49
|
-
const result = computeWordCloudLayout(items, config);
|
|
50
|
-
expect(result.Items.length).toBeLessThanOrEqual(10);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
describe('font size mapping', () => {
|
|
54
|
-
it('should map weight 1.0 to MaxFontSize', () => {
|
|
55
|
-
const items = [{ Text: 'max', Weight: 1.0 }];
|
|
56
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
57
|
-
expect(result.Items[0].FontSize).toBe(48);
|
|
58
|
-
});
|
|
59
|
-
it('should map weight 0.0 to MinFontSize', () => {
|
|
60
|
-
const items = [{ Text: 'min', Weight: 0.0 }];
|
|
61
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
62
|
-
expect(result.Items[0].FontSize).toBe(12);
|
|
63
|
-
});
|
|
64
|
-
it('should map weight 0.5 to midpoint font size', () => {
|
|
65
|
-
const items = [{ Text: 'mid', Weight: 0.5 }];
|
|
66
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
67
|
-
expect(result.Items[0].FontSize).toBe(30); // 12 + 0.5 * (48 - 12) = 30
|
|
68
|
-
});
|
|
69
|
-
it('should clamp weights above 1.0', () => {
|
|
70
|
-
const items = [{ Text: 'over', Weight: 1.5 }];
|
|
71
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
72
|
-
expect(result.Items[0].FontSize).toBe(48);
|
|
73
|
-
});
|
|
74
|
-
it('should clamp weights below 0.0', () => {
|
|
75
|
-
const items = [{ Text: 'under', Weight: -0.5 }];
|
|
76
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
77
|
-
expect(result.Items[0].FontSize).toBe(12);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe('positioning', () => {
|
|
81
|
-
it('should place the first item near the center', () => {
|
|
82
|
-
const items = [{ Text: 'center', Weight: 1.0 }];
|
|
83
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
84
|
-
// First item should be at or very near (0, 0)
|
|
85
|
-
expect(Math.abs(result.Items[0].X)).toBeLessThan(10);
|
|
86
|
-
expect(Math.abs(result.Items[0].Y)).toBeLessThan(10);
|
|
87
|
-
});
|
|
88
|
-
it('should not have overlapping items', () => {
|
|
89
|
-
const items = makeItems(20);
|
|
90
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
91
|
-
// Verify no two items share the exact same position
|
|
92
|
-
for (let i = 0; i < result.Items.length; i++) {
|
|
93
|
-
for (let j = i + 1; j < result.Items.length; j++) {
|
|
94
|
-
const samePos = result.Items[i].X === result.Items[j].X &&
|
|
95
|
-
result.Items[i].Y === result.Items[j].Y;
|
|
96
|
-
expect(samePos).toBe(false);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
it('should place items progressively further from center', () => {
|
|
101
|
-
const items = makeItems(10, 1.0);
|
|
102
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
103
|
-
// The average distance from center should increase for later items
|
|
104
|
-
if (result.Items.length >= 2) {
|
|
105
|
-
const firstDist = Math.sqrt(result.Items[0].X ** 2 + result.Items[0].Y ** 2);
|
|
106
|
-
const lastDist = Math.sqrt(result.Items[result.Items.length - 1].X ** 2 +
|
|
107
|
-
result.Items[result.Items.length - 1].Y ** 2);
|
|
108
|
-
expect(lastDist).toBeGreaterThanOrEqual(firstDist);
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
describe('rotation', () => {
|
|
113
|
-
it('should rotate approximately 20% of words (every 5th at index 3)', () => {
|
|
114
|
-
const items = makeItems(20);
|
|
115
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
116
|
-
const rotated = result.Items.filter(i => i.Rotation === 90);
|
|
117
|
-
const unrotated = result.Items.filter(i => i.Rotation === 0);
|
|
118
|
-
// Roughly 20% should be rotated (deterministic based on index % 5 === 3)
|
|
119
|
-
expect(rotated.length).toBeGreaterThan(0);
|
|
120
|
-
expect(unrotated.length).toBeGreaterThan(rotated.length);
|
|
121
|
-
});
|
|
122
|
-
it('should only use 0 or 90 degree rotation', () => {
|
|
123
|
-
const items = makeItems(30);
|
|
124
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
125
|
-
for (const item of result.Items) {
|
|
126
|
-
expect([0, 90]).toContain(item.Rotation);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
describe('layout modes', () => {
|
|
131
|
-
it('should produce valid layout with spiral mode', () => {
|
|
132
|
-
const items = makeItems(15);
|
|
133
|
-
const result = computeWordCloudLayout(items, { ...defaultConfig, Layout: 'spiral' });
|
|
134
|
-
expect(result.Items.length).toBeGreaterThan(0);
|
|
135
|
-
expect(result.ViewBox).not.toBe('0 0 100 100'); // should have computed viewBox
|
|
136
|
-
});
|
|
137
|
-
it('should produce valid layout with rectangular mode', () => {
|
|
138
|
-
const items = makeItems(15);
|
|
139
|
-
const result = computeWordCloudLayout(items, { ...defaultConfig, Layout: 'rectangular' });
|
|
140
|
-
expect(result.Items.length).toBeGreaterThan(0);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
describe('viewBox computation', () => {
|
|
144
|
-
it('should produce a viewBox that encompasses all items', () => {
|
|
145
|
-
const items = makeItems(10);
|
|
146
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
147
|
-
const [vx, vy, vw, vh] = result.ViewBox.split(' ').map(Number);
|
|
148
|
-
for (const item of result.Items) {
|
|
149
|
-
expect(item.X).toBeGreaterThanOrEqual(vx);
|
|
150
|
-
expect(item.X).toBeLessThanOrEqual(vx + vw);
|
|
151
|
-
expect(item.Y).toBeGreaterThanOrEqual(vy);
|
|
152
|
-
expect(item.Y).toBeLessThanOrEqual(vy + vh);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
describe('index assignment', () => {
|
|
157
|
-
it('should assign sequential indices to layout items', () => {
|
|
158
|
-
const items = makeItems(5);
|
|
159
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
160
|
-
for (let i = 0; i < result.Items.length; i++) {
|
|
161
|
-
expect(result.Items[i].Index).toBe(i);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
describe('metadata preservation', () => {
|
|
166
|
-
it('should preserve Category from input items', () => {
|
|
167
|
-
const items = [
|
|
168
|
-
{ Text: 'test', Weight: 0.8, Category: 'tech' },
|
|
169
|
-
];
|
|
170
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
171
|
-
expect(result.Items[0].Category).toBe('tech');
|
|
172
|
-
});
|
|
173
|
-
it('should preserve Metadata from input items', () => {
|
|
174
|
-
const meta = { id: '123', source: 'test' };
|
|
175
|
-
const items = [
|
|
176
|
-
{ Text: 'test', Weight: 0.8, Metadata: meta },
|
|
177
|
-
];
|
|
178
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
179
|
-
expect(result.Items[0].Metadata).toEqual(meta);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
describe('large datasets', () => {
|
|
183
|
-
it('should handle 100 items without crashing', () => {
|
|
184
|
-
const items = makeItems(100);
|
|
185
|
-
const result = computeWordCloudLayout(items, defaultConfig);
|
|
186
|
-
expect(result.Items.length).toBeGreaterThan(0);
|
|
187
|
-
expect(result.Items.length).toBeLessThanOrEqual(100);
|
|
188
|
-
});
|
|
189
|
-
it('should complete layout within reasonable time', () => {
|
|
190
|
-
const items = makeItems(100);
|
|
191
|
-
const start = performance.now();
|
|
192
|
-
computeWordCloudLayout(items, defaultConfig);
|
|
193
|
-
const elapsed = performance.now() - start;
|
|
194
|
-
expect(elapsed).toBeLessThan(5000); // should complete in < 5 seconds
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
//# sourceMappingURL=word-cloud-layout.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"word-cloud-layout.test.js","sourceRoot":"","sources":["../../src/__tests__/word-cloud-layout.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { computeWordCloudLayout, WordCloudLayoutConfig } from '../lib/word-cloud.layout';\nimport { WordCloudItem } from '../lib/word-cloud.types';\n\nconst defaultConfig: WordCloudLayoutConfig = {\n MinFontSize: 12,\n MaxFontSize: 48,\n Layout: 'spiral',\n MaxItems: 100,\n};\n\nfunction makeItems(count: number, baseWeight = 1.0): WordCloudItem[] {\n return Array.from({ length: count }, (_, i) => ({\n Text: `word${i}`,\n Weight: baseWeight - (i * (baseWeight / count)),\n }));\n}\n\ndescribe('computeWordCloudLayout', () => {\n describe('empty and edge cases', () => {\n it('should return empty layout for empty items array', () => {\n const result = computeWordCloudLayout([], defaultConfig);\n expect(result.Items).toHaveLength(0);\n expect(result.ViewBox).toBe('0 0 100 100');\n });\n\n it('should return empty layout for null-ish items', () => {\n const result = computeWordCloudLayout(null as unknown as WordCloudItem[], defaultConfig);\n expect(result.Items).toHaveLength(0);\n });\n\n it('should handle a single item', () => {\n const items: WordCloudItem[] = [{ Text: 'hello', Weight: 1.0 }];\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items).toHaveLength(1);\n expect(result.Items[0].Text).toBe('hello');\n expect(result.Items[0].FontSize).toBe(48); // max font for weight 1.0\n });\n });\n\n describe('sorting and limiting', () => {\n it('should sort items by weight descending (heaviest first)', () => {\n const items: WordCloudItem[] = [\n { Text: 'low', Weight: 0.2 },\n { Text: 'high', Weight: 0.9 },\n { Text: 'mid', Weight: 0.5 },\n ];\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items[0].Text).toBe('high');\n expect(result.Items[1].Text).toBe('mid');\n expect(result.Items[2].Text).toBe('low');\n });\n\n it('should limit items to MaxItems', () => {\n const items = makeItems(50);\n const config = { ...defaultConfig, MaxItems: 10 };\n const result = computeWordCloudLayout(items, config);\n expect(result.Items.length).toBeLessThanOrEqual(10);\n });\n });\n\n describe('font size mapping', () => {\n it('should map weight 1.0 to MaxFontSize', () => {\n const items: WordCloudItem[] = [{ Text: 'max', Weight: 1.0 }];\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items[0].FontSize).toBe(48);\n });\n\n it('should map weight 0.0 to MinFontSize', () => {\n const items: WordCloudItem[] = [{ Text: 'min', Weight: 0.0 }];\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items[0].FontSize).toBe(12);\n });\n\n it('should map weight 0.5 to midpoint font size', () => {\n const items: WordCloudItem[] = [{ Text: 'mid', Weight: 0.5 }];\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items[0].FontSize).toBe(30); // 12 + 0.5 * (48 - 12) = 30\n });\n\n it('should clamp weights above 1.0', () => {\n const items: WordCloudItem[] = [{ Text: 'over', Weight: 1.5 }];\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items[0].FontSize).toBe(48);\n });\n\n it('should clamp weights below 0.0', () => {\n const items: WordCloudItem[] = [{ Text: 'under', Weight: -0.5 }];\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items[0].FontSize).toBe(12);\n });\n });\n\n describe('positioning', () => {\n it('should place the first item near the center', () => {\n const items: WordCloudItem[] = [{ Text: 'center', Weight: 1.0 }];\n const result = computeWordCloudLayout(items, defaultConfig);\n // First item should be at or very near (0, 0)\n expect(Math.abs(result.Items[0].X)).toBeLessThan(10);\n expect(Math.abs(result.Items[0].Y)).toBeLessThan(10);\n });\n\n it('should not have overlapping items', () => {\n const items = makeItems(20);\n const result = computeWordCloudLayout(items, defaultConfig);\n\n // Verify no two items share the exact same position\n for (let i = 0; i < result.Items.length; i++) {\n for (let j = i + 1; j < result.Items.length; j++) {\n const samePos = result.Items[i].X === result.Items[j].X &&\n result.Items[i].Y === result.Items[j].Y;\n expect(samePos).toBe(false);\n }\n }\n });\n\n it('should place items progressively further from center', () => {\n const items = makeItems(10, 1.0);\n const result = computeWordCloudLayout(items, defaultConfig);\n\n // The average distance from center should increase for later items\n if (result.Items.length >= 2) {\n const firstDist = Math.sqrt(result.Items[0].X ** 2 + result.Items[0].Y ** 2);\n const lastDist = Math.sqrt(\n result.Items[result.Items.length - 1].X ** 2 +\n result.Items[result.Items.length - 1].Y ** 2\n );\n expect(lastDist).toBeGreaterThanOrEqual(firstDist);\n }\n });\n });\n\n describe('rotation', () => {\n it('should rotate approximately 20% of words (every 5th at index 3)', () => {\n const items = makeItems(20);\n const result = computeWordCloudLayout(items, defaultConfig);\n const rotated = result.Items.filter(i => i.Rotation === 90);\n const unrotated = result.Items.filter(i => i.Rotation === 0);\n // Roughly 20% should be rotated (deterministic based on index % 5 === 3)\n expect(rotated.length).toBeGreaterThan(0);\n expect(unrotated.length).toBeGreaterThan(rotated.length);\n });\n\n it('should only use 0 or 90 degree rotation', () => {\n const items = makeItems(30);\n const result = computeWordCloudLayout(items, defaultConfig);\n for (const item of result.Items) {\n expect([0, 90]).toContain(item.Rotation);\n }\n });\n });\n\n describe('layout modes', () => {\n it('should produce valid layout with spiral mode', () => {\n const items = makeItems(15);\n const result = computeWordCloudLayout(items, { ...defaultConfig, Layout: 'spiral' });\n expect(result.Items.length).toBeGreaterThan(0);\n expect(result.ViewBox).not.toBe('0 0 100 100'); // should have computed viewBox\n });\n\n it('should produce valid layout with rectangular mode', () => {\n const items = makeItems(15);\n const result = computeWordCloudLayout(items, { ...defaultConfig, Layout: 'rectangular' });\n expect(result.Items.length).toBeGreaterThan(0);\n });\n });\n\n describe('viewBox computation', () => {\n it('should produce a viewBox that encompasses all items', () => {\n const items = makeItems(10);\n const result = computeWordCloudLayout(items, defaultConfig);\n const [vx, vy, vw, vh] = result.ViewBox.split(' ').map(Number);\n\n for (const item of result.Items) {\n expect(item.X).toBeGreaterThanOrEqual(vx);\n expect(item.X).toBeLessThanOrEqual(vx + vw);\n expect(item.Y).toBeGreaterThanOrEqual(vy);\n expect(item.Y).toBeLessThanOrEqual(vy + vh);\n }\n });\n });\n\n describe('index assignment', () => {\n it('should assign sequential indices to layout items', () => {\n const items = makeItems(5);\n const result = computeWordCloudLayout(items, defaultConfig);\n for (let i = 0; i < result.Items.length; i++) {\n expect(result.Items[i].Index).toBe(i);\n }\n });\n });\n\n describe('metadata preservation', () => {\n it('should preserve Category from input items', () => {\n const items: WordCloudItem[] = [\n { Text: 'test', Weight: 0.8, Category: 'tech' },\n ];\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items[0].Category).toBe('tech');\n });\n\n it('should preserve Metadata from input items', () => {\n const meta = { id: '123', source: 'test' };\n const items: WordCloudItem[] = [\n { Text: 'test', Weight: 0.8, Metadata: meta },\n ];\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items[0].Metadata).toEqual(meta);\n });\n });\n\n describe('large datasets', () => {\n it('should handle 100 items without crashing', () => {\n const items = makeItems(100);\n const result = computeWordCloudLayout(items, defaultConfig);\n expect(result.Items.length).toBeGreaterThan(0);\n expect(result.Items.length).toBeLessThanOrEqual(100);\n });\n\n it('should complete layout within reasonable time', () => {\n const items = makeItems(100);\n const start = performance.now();\n computeWordCloudLayout(items, defaultConfig);\n const elapsed = performance.now() - start;\n expect(elapsed).toBeLessThan(5000); // should complete in < 5 seconds\n });\n });\n});\n"]}
|