@gemx-dev/clarity-visualize 3.5.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +26 -0
  3. package/package.json +63 -0
  4. package/rollup.config.ts +79 -0
  5. package/src/clarity.ts +3 -0
  6. package/src/data.ts +103 -0
  7. package/src/enrich.ts +80 -0
  8. package/src/global.ts +9 -0
  9. package/src/heatmap.ts +355 -0
  10. package/src/index.ts +2 -0
  11. package/src/interaction.ts +504 -0
  12. package/src/layout.ts +879 -0
  13. package/src/styles/blobUnavailable/chineseSimplified.svg +5 -0
  14. package/src/styles/blobUnavailable/chineseTraditional.svg +5 -0
  15. package/src/styles/blobUnavailable/dutch.svg +5 -0
  16. package/src/styles/blobUnavailable/english.svg +5 -0
  17. package/src/styles/blobUnavailable/french.svg +5 -0
  18. package/src/styles/blobUnavailable/german.svg +5 -0
  19. package/src/styles/blobUnavailable/iconOnly.svg +4 -0
  20. package/src/styles/blobUnavailable/italian.svg +5 -0
  21. package/src/styles/blobUnavailable/japanese.svg +5 -0
  22. package/src/styles/blobUnavailable/korean.svg +5 -0
  23. package/src/styles/blobUnavailable/portuguese.svg +5 -0
  24. package/src/styles/blobUnavailable/russian.svg +5 -0
  25. package/src/styles/blobUnavailable/spanish.svg +5 -0
  26. package/src/styles/blobUnavailable/turkish.svg +5 -0
  27. package/src/styles/iframeUnavailable/chineseSimplified.svg +5 -0
  28. package/src/styles/iframeUnavailable/chineseTraditional.svg +5 -0
  29. package/src/styles/iframeUnavailable/dutch.svg +5 -0
  30. package/src/styles/iframeUnavailable/english.svg +5 -0
  31. package/src/styles/iframeUnavailable/french.svg +5 -0
  32. package/src/styles/iframeUnavailable/german.svg +5 -0
  33. package/src/styles/iframeUnavailable/iconOnly.svg +4 -0
  34. package/src/styles/iframeUnavailable/italian.svg +5 -0
  35. package/src/styles/iframeUnavailable/japanese.svg +5 -0
  36. package/src/styles/iframeUnavailable/korean.svg +5 -0
  37. package/src/styles/iframeUnavailable/portuguese.svg +5 -0
  38. package/src/styles/iframeUnavailable/russian.svg +5 -0
  39. package/src/styles/iframeUnavailable/spanish.svg +5 -0
  40. package/src/styles/iframeUnavailable/turkish.svg +5 -0
  41. package/src/styles/imageMasked/chineseSimplified.svg +5 -0
  42. package/src/styles/imageMasked/chineseTraditional.svg +5 -0
  43. package/src/styles/imageMasked/dutch.svg +5 -0
  44. package/src/styles/imageMasked/english.svg +5 -0
  45. package/src/styles/imageMasked/french.svg +5 -0
  46. package/src/styles/imageMasked/german.svg +5 -0
  47. package/src/styles/imageMasked/iconOnly.svg +4 -0
  48. package/src/styles/imageMasked/italian.svg +5 -0
  49. package/src/styles/imageMasked/japanese.svg +5 -0
  50. package/src/styles/imageMasked/korean.svg +5 -0
  51. package/src/styles/imageMasked/portuguese.svg +5 -0
  52. package/src/styles/imageMasked/russian.svg +5 -0
  53. package/src/styles/imageMasked/spanish.svg +5 -0
  54. package/src/styles/imageMasked/turkish.svg +5 -0
  55. package/src/styles/pointer/click.css +31 -0
  56. package/src/styles/pointer/pointerIcon.svg +18 -0
  57. package/src/styles/shared.css +6 -0
  58. package/src/visualizer.ts +297 -0
  59. package/tsconfig.json +21 -0
  60. package/tslint.json +33 -0
  61. package/types/index.d.ts +10 -0
  62. package/types/string-import.d.ts +9 -0
  63. package/types/visualize.d.ts +235 -0
package/src/heatmap.ts ADDED
@@ -0,0 +1,355 @@
1
+ import {
2
+ Activity,
3
+ Constant,
4
+ Heatmap,
5
+ Setting,
6
+ ScrollMapInfo,
7
+ PlaybackState,
8
+ } from '@clarity-types/visualize';
9
+ import { Data } from 'clarity-js';
10
+ import { LayoutHelper } from './layout';
11
+
12
+ export class HeatmapHelper {
13
+ static COLORS = ['blue', 'cyan', 'lime', 'yellow', 'red'];
14
+ data: Activity = null;
15
+ scrollData: ScrollMapInfo[] = null;
16
+ max: number = null;
17
+ offscreenRing: HTMLCanvasElement = null;
18
+ gradientPixels: ImageData = null;
19
+ timeout = null;
20
+ observer: ResizeObserver = null;
21
+ state: PlaybackState = null;
22
+ layout: LayoutHelper = null;
23
+ scrollAvgFold: number = null;
24
+ addScrollMakers: boolean = false;
25
+
26
+ constructor(state: PlaybackState, layout: LayoutHelper) {
27
+ this.state = state;
28
+ this.layout = layout;
29
+ }
30
+
31
+ public reset = (): void => {
32
+ this.data = null;
33
+ this.scrollData = null;
34
+ this.max = null;
35
+ this.offscreenRing = null;
36
+ this.gradientPixels = null;
37
+ this.timeout = null;
38
+
39
+ // Reset resize observer
40
+ if (this.observer) {
41
+ this.observer.disconnect();
42
+ this.observer = null;
43
+ }
44
+
45
+ // Remove scroll and resize event listeners
46
+ if (this.state && this.state.window) {
47
+ let win = this.state.window;
48
+ win.removeEventListener('scroll', this.redraw, true);
49
+ win.removeEventListener('resize', this.redraw, true);
50
+ }
51
+ };
52
+
53
+ public clear = (): void => {
54
+ let doc = this.state.window.document;
55
+ let win = this.state.window;
56
+ let canvas = doc.getElementById(Constant.HeatmapCanvas) as HTMLCanvasElement;
57
+ let de = doc.documentElement;
58
+ if (canvas) {
59
+ canvas.width = de.clientWidth;
60
+ canvas.height = de.clientHeight;
61
+ canvas.style.left = win.pageXOffset + Constant.Pixel;
62
+ canvas.style.top = win.pageYOffset + Constant.Pixel;
63
+ canvas.getContext(Constant.Context).clearRect(0, 0, canvas.width, canvas.height);
64
+ }
65
+ this.reset();
66
+ };
67
+
68
+ public scroll = (activity: ScrollMapInfo[], avgFold: number, addMarkers: boolean): void => {
69
+ this.scrollData = this.scrollData || activity;
70
+ this.scrollAvgFold = avgFold != null ? avgFold : this.scrollAvgFold;
71
+ this.addScrollMakers = addMarkers != null ? addMarkers : this.addScrollMakers;
72
+
73
+ let canvas = this.overlay();
74
+ let context = canvas.getContext(Constant.Context);
75
+ let doc = this.state.window.document;
76
+ var body = doc.body;
77
+ var de = doc.documentElement;
78
+ var height = Math.max(
79
+ body.scrollHeight,
80
+ body.offsetHeight,
81
+ de.clientHeight,
82
+ de.scrollHeight,
83
+ de.offsetHeight,
84
+ );
85
+ canvas.height = Math.min(height, Setting.ScrollCanvasMaxHeight);
86
+ canvas.style.top = 0 + Constant.Pixel;
87
+ if (canvas.width > 0 && canvas.height > 0) {
88
+ if (this.scrollData) {
89
+ const grd = context.createLinearGradient(0, 0, 0, canvas.height);
90
+ for (const currentCombination of this.scrollData) {
91
+ const huePercentView =
92
+ 1 - currentCombination.cumulativeSum / this.scrollData[0].cumulativeSum;
93
+ const percentView = (currentCombination.scrollReachY / 100) * (height / canvas.height);
94
+ const hue = huePercentView * Setting.MaxHue;
95
+ if (percentView <= 1) {
96
+ grd.addColorStop(percentView, `hsla(${hue}, 100%, 50%, 0.6)`);
97
+ }
98
+ }
99
+
100
+ // Fill with gradient
101
+ context.fillStyle = grd;
102
+ context.fillRect(0, 0, canvas.width, canvas.height);
103
+ if (this.addScrollMakers) {
104
+ this.addInfoMarkers(
105
+ context,
106
+ this.scrollData,
107
+ canvas.width,
108
+ canvas.height,
109
+ this.scrollAvgFold,
110
+ );
111
+ }
112
+ }
113
+ }
114
+ };
115
+
116
+ private addInfoMarkers = (
117
+ context: CanvasRenderingContext2D,
118
+ scrollMapInfo: ScrollMapInfo[],
119
+ width: number,
120
+ height: number,
121
+ avgFold: number,
122
+ ): void => {
123
+ this.addMarker(context, width, Constant.AverageFold, avgFold, Setting.MarkerMediumWidth);
124
+ const markers = [75, 50, 25];
125
+ for (const marker of markers) {
126
+ const closest = scrollMapInfo.reduce(
127
+ (prev: ScrollMapInfo, curr: ScrollMapInfo): ScrollMapInfo => {
128
+ return Math.abs(curr.percUsers - marker) < Math.abs(prev.percUsers - marker)
129
+ ? curr
130
+ : prev;
131
+ },
132
+ );
133
+ if (
134
+ closest.percUsers >= marker - Setting.MarkerRange &&
135
+ closest.percUsers <= marker + Setting.MarkerRange
136
+ ) {
137
+ const markerLine = (closest.scrollReachY / 100) * height;
138
+ this.addMarker(context, width, `${marker}%`, markerLine, Setting.MarkerSmallWidth);
139
+ }
140
+ }
141
+ };
142
+
143
+ private addMarker = (
144
+ context: CanvasRenderingContext2D,
145
+ heatmapWidth: number,
146
+ label: string,
147
+ markerY: number,
148
+ markerWidth: number,
149
+ ): void => {
150
+ context.beginPath();
151
+ context.moveTo(0, markerY);
152
+ context.lineTo(heatmapWidth, markerY);
153
+ context.setLineDash([2, 2]);
154
+ context.lineWidth = Setting.MarkerLineHeight;
155
+ context.strokeStyle = Setting.MarkerColor;
156
+ context.stroke();
157
+ context.fillStyle = Setting.CanvasTextColor;
158
+ context.fillRect(0, markerY - Setting.MarkerHeight / 2, markerWidth, Setting.MarkerHeight);
159
+ context.fillStyle = Setting.MarkerColor;
160
+ context.font = Setting.CanvasTextFont;
161
+ context.fillText(label, Setting.MarkerPadding, markerY + Setting.MarkerPadding);
162
+ };
163
+
164
+ public click = (activity: Activity): void => {
165
+ this.data = this.data || activity;
166
+ let heat = this.transform();
167
+ console.log(`🚀 🐥 ~ HeatmapHelper ~ heat:`, heat);
168
+ let canvas = this.overlay();
169
+ let ctx = canvas.getContext(Constant.Context);
170
+
171
+ if (canvas.width > 0 && canvas.height > 0) {
172
+ // To speed up canvas rendering, we draw ring & gradient on an offscreen canvas, so we can use drawImage API
173
+ // Canvas performance tips: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
174
+ // Pre-render similar primitives or repeating objects on an offscreen canvas
175
+ let ring = this.getRing();
176
+ let gradient = this.getGradient();
177
+
178
+ // Render activity for each (x,y) coordinate in our data
179
+ for (let entry of heat) {
180
+ ctx.globalAlpha = entry.a;
181
+ ctx.drawImage(ring, entry.x - Setting.Radius, entry.y - Setting.Radius);
182
+ }
183
+
184
+ // Add color to the canvas based on alpha value of each pixel
185
+ let pixels = ctx.getImageData(0, 0, canvas.width, canvas.height);
186
+ for (let i = 0; i < pixels.data.length; i += 4) {
187
+ // For each pixel, we have 4 entries in data array: (r,g,b,a)
188
+ // To pick the right color from gradient pixels, we look at the alpha value of the pixel
189
+ // Alpha value ranges from 0-255
190
+ let alpha = pixels.data[i + 3];
191
+ if (alpha > 0) {
192
+ let offset = (alpha - 1) * 4;
193
+ pixels.data[i] = gradient.data[offset];
194
+ pixels.data[i + 1] = gradient.data[offset + 1];
195
+ pixels.data[i + 2] = gradient.data[offset + 2];
196
+ }
197
+ }
198
+ ctx.putImageData(pixels, 0, 0);
199
+ }
200
+ };
201
+
202
+ private overlay = (): HTMLCanvasElement => {
203
+ // Create canvas for visualizing heatmap
204
+ let doc = this.state.window.document;
205
+ let win = this.state.window;
206
+ let de = doc.documentElement;
207
+ let canvas = doc.getElementById(Constant.HeatmapCanvas) as HTMLCanvasElement;
208
+ if (canvas === null) {
209
+ canvas = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
210
+ canvas.id = Constant.HeatmapCanvas;
211
+ canvas.width = 0;
212
+ canvas.height = 0;
213
+ canvas.style.position = Constant.Absolute;
214
+ canvas.style.zIndex = `${Setting.ZIndex}`;
215
+ de.appendChild(canvas);
216
+ win.addEventListener('scroll', this.redraw, true);
217
+ win.addEventListener('resize', this.redraw, true);
218
+ this.observer = this.state.window['ResizeObserver'] ? new ResizeObserver(this.redraw) : null;
219
+
220
+ if (this.observer) {
221
+ this.observer.observe(doc.body);
222
+ }
223
+ }
224
+
225
+ // Ensure canvas is positioned correctly
226
+ canvas.width = de.clientWidth;
227
+ canvas.height = de.clientHeight;
228
+ canvas.style.left = win.pageXOffset + Constant.Pixel;
229
+ canvas.style.top = win.pageYOffset + Constant.Pixel;
230
+ canvas.getContext(Constant.Context).clearRect(0, 0, canvas.width, canvas.height);
231
+
232
+ return canvas;
233
+ };
234
+
235
+ private getRing = (): HTMLCanvasElement => {
236
+ if (this.offscreenRing === null) {
237
+ let doc = this.state.window.document;
238
+ this.offscreenRing = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
239
+ this.offscreenRing.width = Setting.Radius * 2;
240
+ this.offscreenRing.height = Setting.Radius * 2;
241
+ let ctx = this.offscreenRing.getContext(Constant.Context);
242
+ ctx.shadowOffsetX = Setting.Radius * 2;
243
+ ctx.shadowBlur = Setting.Radius / 2;
244
+ ctx.shadowColor = Constant.Black;
245
+ ctx.beginPath();
246
+ ctx.arc(-Setting.Radius, Setting.Radius, Setting.Radius / 2, 0, Math.PI * 2, true);
247
+ ctx.closePath();
248
+ ctx.fill();
249
+ }
250
+ return this.offscreenRing;
251
+ };
252
+
253
+ private getGradient = (): ImageData => {
254
+ if (this.gradientPixels === null) {
255
+ let doc = this.state.window.document;
256
+ let offscreenGradient = doc.createElement(Constant.Canvas) as HTMLCanvasElement;
257
+ offscreenGradient.width = 1;
258
+ offscreenGradient.height = Setting.Colors;
259
+ let ctx = offscreenGradient.getContext(Constant.Context);
260
+ let gradient = ctx.createLinearGradient(0, 0, 0, Setting.Colors);
261
+ let step = 1 / HeatmapHelper.COLORS.length;
262
+ for (let i = 0; i < HeatmapHelper.COLORS.length; i++) {
263
+ gradient.addColorStop(step * (i + 1), HeatmapHelper.COLORS[i]);
264
+ }
265
+ ctx.fillStyle = gradient;
266
+ ctx.fillRect(0, 0, 1, Setting.Colors);
267
+ this.gradientPixels = ctx.getImageData(0, 0, 1, Setting.Colors);
268
+ }
269
+ return this.gradientPixels;
270
+ };
271
+
272
+ private redraw = (event): void => {
273
+ if (this.data) {
274
+ if (this.timeout) {
275
+ clearTimeout(this.timeout);
276
+ }
277
+ this.timeout = setTimeout(this.click, Setting.Interval);
278
+ } else if (this.scrollData) {
279
+ if (event.type != 'scroll') {
280
+ if (this.timeout) {
281
+ clearTimeout(this.timeout);
282
+ }
283
+ this.timeout = setTimeout(this.scroll, Setting.Interval);
284
+ }
285
+ }
286
+ };
287
+
288
+ private transform = (): Heatmap[] => {
289
+ let output: Heatmap[] = [];
290
+ let points: { [key: string]: number } = {};
291
+ let localMax = 0;
292
+ let height =
293
+ this.state.window && this.state.window.document
294
+ ? this.state.window.document.documentElement.clientHeight
295
+ : 0;
296
+
297
+ for (let element of this.data) {
298
+ let el = this.layout.get(element.hash) as HTMLElement;
299
+ el && console.log(`🚀 🐥 ~ HeatmapHelper ~ el:`, el);
300
+ if (el && typeof el.getBoundingClientRect === 'function') {
301
+ let r = el.getBoundingClientRect();
302
+ let v = this.visible(el, r, height);
303
+ // Process clicks for only visible elements
304
+ if (this.max === null || v) {
305
+ for (let i = 0; i < element.points; i++) {
306
+ let x = Math.round(r.left + (element.x[i] / Data.Setting.ClickPrecision) * r.width);
307
+ let y = Math.round(r.top + (element.y[i] / Data.Setting.ClickPrecision) * r.height);
308
+ let k = `${x}${Constant.Separator}${y}${Constant.Separator}${v ? 1 : 0}`;
309
+ points[k] = k in points ? points[k] + element.clicks[i] : element.clicks[i];
310
+ localMax = Math.max(points[k], localMax);
311
+ }
312
+ }
313
+ }
314
+ }
315
+
316
+ // Set the max value from the firs t
317
+ this.max = this.max ? this.max : localMax;
318
+
319
+ // Once all points are accounted for, convert everything into absolute (x, y)
320
+ for (let coordinates of Object.keys(points)) {
321
+ let parts = coordinates.split(Constant.Separator);
322
+ let alpha = Math.min(points[coordinates] / this.max + Setting.AlphaBoost, 1);
323
+ if (parts[2] === '1') {
324
+ output.push({ x: parseInt(parts[0], 10), y: parseInt(parts[1], 10), a: alpha });
325
+ }
326
+ }
327
+
328
+ return output;
329
+ };
330
+
331
+ private visible = (el: HTMLElement, r: DOMRect, height: number): boolean => {
332
+ let doc: Document | ShadowRoot = this.state.window.document;
333
+ let visibility = r.height > height ? true : false;
334
+ if (visibility === false && r.width > 0 && r.height > 0) {
335
+ while (!visibility && doc) {
336
+ let shadowElement = null;
337
+ let elements = doc.elementsFromPoint(r.left + r.width / 2, r.top + r.height / 2);
338
+ for (let e of elements) {
339
+ // Ignore if top element ends up being the canvas element we added for heatmap visualization
340
+ if (
341
+ e.tagName === Constant.Canvas ||
342
+ (e.id && e.id.indexOf(Constant.ClarityPrefix) === 0)
343
+ ) {
344
+ continue;
345
+ }
346
+ visibility = e === el;
347
+ shadowElement = e.shadowRoot && e.shadowRoot != doc ? e.shadowRoot : null;
348
+ break;
349
+ }
350
+ doc = shadowElement;
351
+ }
352
+ }
353
+ return visibility && r.bottom >= 0 && r.top <= height;
354
+ };
355
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * as visualize from "./clarity";
2
+ export { Visualizer } from "./visualizer";