@gemx-dev/heatmap-react 3.5.92-dev.20 → 3.5.92-dev.22

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 (64) hide show
  1. package/dist/esm/hooks/viz-canvas/useClickmap.d.ts.map +1 -1
  2. package/dist/esm/hooks/viz-canvas/useScrollmap.d.ts.map +1 -1
  3. package/dist/esm/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
  4. package/dist/esm/index.js +230 -166
  5. package/dist/esm/index.mjs +230 -166
  6. package/dist/esm/libs/iframe-processor/orchestrator.d.ts.map +1 -1
  7. package/dist/esm/libs/iframe-processor/processors/height-observer/index.d.ts +2 -0
  8. package/dist/esm/libs/iframe-processor/processors/height-observer/index.d.ts.map +1 -1
  9. package/dist/esm/libs/iframe-processor/processors/height-observer/listeners.d.ts +11 -0
  10. package/dist/esm/libs/iframe-processor/processors/height-observer/listeners.d.ts.map +1 -0
  11. package/dist/esm/libs/iframe-processor/processors/height-observer/types.d.ts +6 -1
  12. package/dist/esm/libs/iframe-processor/processors/height-observer/types.d.ts.map +1 -1
  13. package/dist/esm/libs/visualizer/GXVisualizer.d.ts +12 -1
  14. package/dist/esm/libs/visualizer/GXVisualizer.d.ts.map +1 -1
  15. package/dist/esm/libs/visualizer/index.d.ts +1 -0
  16. package/dist/esm/libs/visualizer/index.d.ts.map +1 -1
  17. package/dist/esm/libs/visualizer/renderers/AttentionMapRenderer.d.ts +1 -1
  18. package/dist/esm/libs/visualizer/renderers/AttentionMapRenderer.d.ts.map +1 -1
  19. package/dist/esm/libs/visualizer/renderers/ScrollBucketRenderer.d.ts +44 -0
  20. package/dist/esm/libs/visualizer/renderers/ScrollBucketRenderer.d.ts.map +1 -0
  21. package/dist/esm/libs/visualizer/renderers/index.d.ts +1 -1
  22. package/dist/esm/libs/visualizer/renderers/index.d.ts.map +1 -1
  23. package/dist/esm/types/clarity.d.ts +18 -0
  24. package/dist/esm/types/clarity.d.ts.map +1 -1
  25. package/dist/esm/types/control.d.ts +2 -0
  26. package/dist/esm/types/control.d.ts.map +1 -1
  27. package/dist/esm/types/viz-scrollmap.d.ts +4 -0
  28. package/dist/esm/types/viz-scrollmap.d.ts.map +1 -1
  29. package/dist/esm/utils/retry.d.ts +1 -0
  30. package/dist/esm/utils/retry.d.ts.map +1 -1
  31. package/dist/umd/hooks/viz-canvas/useClickmap.d.ts.map +1 -1
  32. package/dist/umd/hooks/viz-canvas/useScrollmap.d.ts.map +1 -1
  33. package/dist/umd/hooks/viz-scale/useHeatmapScale.d.ts.map +1 -1
  34. package/dist/umd/index.js +2 -2
  35. package/dist/umd/libs/iframe-processor/orchestrator.d.ts.map +1 -1
  36. package/dist/umd/libs/iframe-processor/processors/height-observer/index.d.ts +2 -0
  37. package/dist/umd/libs/iframe-processor/processors/height-observer/index.d.ts.map +1 -1
  38. package/dist/umd/libs/iframe-processor/processors/height-observer/listeners.d.ts +11 -0
  39. package/dist/umd/libs/iframe-processor/processors/height-observer/listeners.d.ts.map +1 -0
  40. package/dist/umd/libs/iframe-processor/processors/height-observer/types.d.ts +6 -1
  41. package/dist/umd/libs/iframe-processor/processors/height-observer/types.d.ts.map +1 -1
  42. package/dist/umd/libs/visualizer/GXVisualizer.d.ts +12 -1
  43. package/dist/umd/libs/visualizer/GXVisualizer.d.ts.map +1 -1
  44. package/dist/umd/libs/visualizer/index.d.ts +1 -0
  45. package/dist/umd/libs/visualizer/index.d.ts.map +1 -1
  46. package/dist/umd/libs/visualizer/renderers/AttentionMapRenderer.d.ts +1 -1
  47. package/dist/umd/libs/visualizer/renderers/AttentionMapRenderer.d.ts.map +1 -1
  48. package/dist/umd/libs/visualizer/renderers/ScrollBucketRenderer.d.ts +44 -0
  49. package/dist/umd/libs/visualizer/renderers/ScrollBucketRenderer.d.ts.map +1 -0
  50. package/dist/umd/libs/visualizer/renderers/index.d.ts +1 -1
  51. package/dist/umd/libs/visualizer/renderers/index.d.ts.map +1 -1
  52. package/dist/umd/types/clarity.d.ts +18 -0
  53. package/dist/umd/types/clarity.d.ts.map +1 -1
  54. package/dist/umd/types/control.d.ts +2 -0
  55. package/dist/umd/types/control.d.ts.map +1 -1
  56. package/dist/umd/types/viz-scrollmap.d.ts +4 -0
  57. package/dist/umd/types/viz-scrollmap.d.ts.map +1 -1
  58. package/dist/umd/utils/retry.d.ts +1 -0
  59. package/dist/umd/utils/retry.d.ts.map +1 -1
  60. package/package.json +4 -4
  61. package/dist/esm/libs/visualizer/renderers/ClickHeatmapRenderer.d.ts +0 -30
  62. package/dist/esm/libs/visualizer/renderers/ClickHeatmapRenderer.d.ts.map +0 -1
  63. package/dist/umd/libs/visualizer/renderers/ClickHeatmapRenderer.d.ts +0 -30
  64. package/dist/umd/libs/visualizer/renderers/ClickHeatmapRenderer.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"useClickmap.d.ts","sourceRoot":"","sources":["../../../src/hooks/viz-canvas/useClickmap.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,WAAW;;CAmBvB,CAAC"}
1
+ {"version":3,"file":"useClickmap.d.ts","sourceRoot":"","sources":["../../../src/hooks/viz-canvas/useClickmap.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,WAAW;;CAiBvB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useScrollmap.d.ts","sourceRoot":"","sources":["../../../src/hooks/viz-canvas/useScrollmap.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,YAAY;;CAiCxB,CAAC"}
1
+ {"version":3,"file":"useScrollmap.d.ts","sourceRoot":"","sources":["../../../src/hooks/viz-canvas/useScrollmap.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,YAAY;;CAuDxB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"useHeatmapScale.d.ts","sourceRoot":"","sources":["../../../src/hooks/viz-scale/useHeatmapScale.ts"],"names":[],"mappings":"AAOA,UAAU,kBAAkB;IAC1B,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACnD,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACrD,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACnD;AAED,UAAU,mBAAmB;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C;AAED,eAAO,MAAM,eAAe,GAAI,OAAO,kBAAkB,KAAG,mBA4B3D,CAAC"}
1
+ {"version":3,"file":"useHeatmapScale.d.ts","sourceRoot":"","sources":["../../../src/hooks/viz-scale/useHeatmapScale.ts"],"names":[],"mappings":"AAMA,UAAU,kBAAkB;IAC1B,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;IACnD,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;IACrD,SAAS,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CACnD;AAED,UAAU,mBAAmB;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3C;AAED,eAAO,MAAM,eAAe,GAAI,OAAO,kBAAkB,KAAG,mBA4B3D,CAAC"}
package/dist/esm/index.js CHANGED
@@ -3528,10 +3528,8 @@ const useClickmap = () => {
3528
3528
  if (!vizRef || !clickmap || clickmap.length === 0 || !isDomLoaded)
3529
3529
  return;
3530
3530
  try {
3531
- requestIdleCallback(() => {
3532
- vizRef?.clearmap?.();
3533
- vizRef?.clickmap?.(clickmap);
3534
- }, { timeout: 300 });
3531
+ vizRef?.clearmap?.();
3532
+ requestIdleCallback(() => vizRef?.clickmap?.(clickmap), { timeout: 300 });
3535
3533
  }
3536
3534
  catch (error) {
3537
3535
  console.error(`🚀 🐥 ~ useClickmap ~ error:`, error);
@@ -3545,31 +3543,52 @@ const useScrollmap = () => {
3545
3543
  const scrollType = useHeatmapSettingContext((s) => s.scrollType);
3546
3544
  const scrollmap = useHeatmapDataContext((s) => s.scrollmap);
3547
3545
  const isDomLoaded = useHeatmapVizContext((s) => s.isDomLoaded);
3548
- useMemo(() => {
3549
- switch (scrollType) {
3550
- case EScrollType.Depth:
3551
- return scrollmap;
3552
- case EScrollType.Attention:
3553
- return scrollmap;
3554
- case EScrollType.Revenue:
3555
- return scrollmap;
3556
- default:
3557
- return scrollmap;
3546
+ const renderScrollmap = useCallback(() => {
3547
+ if (!vizRef || !scrollmap || scrollmap.length === 0 || !isDomLoaded)
3548
+ return;
3549
+ try {
3550
+ requestIdleCallback(() => vizRef?.scrollmap?.(scrollmap), { timeout: 300 });
3558
3551
  }
3559
- }, [scrollmap, scrollType]);
3560
- const start = useCallback(() => {
3552
+ catch (error) {
3553
+ console.error(`🚀 🐥 ~ useScrollmap ~ renderScrollmap:`, error);
3554
+ }
3555
+ }, [vizRef, scrollmap, isDomLoaded]);
3556
+ const renderScrollBucket = useCallback(() => {
3561
3557
  if (!vizRef || !scrollmap || scrollmap.length === 0 || !isDomLoaded)
3562
3558
  return;
3559
+ const bucketData = scrollmap.map((point) => ({
3560
+ position: point.scrollReachY,
3561
+ value: point.cumulativeSum,
3562
+ percent: point.percUsers,
3563
+ }));
3563
3564
  try {
3564
- requestIdleCallback(() => {
3565
- vizRef?.clearmap?.();
3566
- vizRef?.scrollmap?.(scrollmap);
3567
- }, { timeout: 300 });
3565
+ requestIdleCallback(() => vizRef?.scrollBucket?.(bucketData), { timeout: 300 });
3568
3566
  }
3569
3567
  catch (error) {
3570
- logger$3.error(`🚀 🐥 ~ useScrollmap ~ error:`, error);
3568
+ console.error(`🚀 🐥 ~ useScrollmap ~ renderScrollBucket:`, error);
3571
3569
  }
3572
3570
  }, [vizRef, scrollmap, isDomLoaded]);
3571
+ const start = useCallback(() => {
3572
+ if (!vizRef || !scrollmap || scrollmap.length === 0 || !isDomLoaded)
3573
+ return;
3574
+ try {
3575
+ vizRef?.clearmap?.();
3576
+ }
3577
+ catch (error) {
3578
+ console.error(`🚀 🐥 ~ useScrollmap ~ start:`, error);
3579
+ }
3580
+ switch (scrollType) {
3581
+ case EScrollType.Attention:
3582
+ case EScrollType.Revenue: {
3583
+ renderScrollBucket();
3584
+ break;
3585
+ }
3586
+ case EScrollType.Depth:
3587
+ default:
3588
+ renderScrollmap();
3589
+ break;
3590
+ }
3591
+ }, [vizRef, scrollmap, isDomLoaded, scrollType, renderScrollmap, renderScrollBucket]);
3573
3592
  return { start };
3574
3593
  };
3575
3594
 
@@ -4299,14 +4318,25 @@ function handleHeightChange(s) {
4299
4318
  s.throttleTimeout = setTimeout(() => {
4300
4319
  s.throttleTimeout = null;
4301
4320
  const currentHeight = getActualHeight(s);
4302
- if (currentHeight === s.lastHeight)
4321
+ if (currentHeight === s.lastHeight) {
4322
+ // Height returned to known state — cancel any stale pending debounce
4323
+ // to prevent it from firing with an outdated height and corrupting lastHeight.
4324
+ if (s.debounceTimeout) {
4325
+ clearTimeout(s.debounceTimeout);
4326
+ s.debounceTimeout = null;
4327
+ }
4303
4328
  return;
4329
+ }
4304
4330
  s.logger.log(`Height changed: ${s.lastHeight}px -> ${currentHeight}px`);
4305
4331
  if (s.debounceTimeout)
4306
4332
  clearTimeout(s.debounceTimeout);
4307
4333
  s.debounceTimeout = setTimeout(() => {
4308
4334
  s.debounceTimeout = null;
4309
- processHeightChange(s, currentHeight);
4335
+ // Re-read height at dispatch time to avoid using a stale closure value.
4336
+ const finalHeight = getActualHeight(s);
4337
+ if (finalHeight !== s.lastHeight) {
4338
+ processHeightChange(s, finalHeight);
4339
+ }
4310
4340
  }, s.debounceMs);
4311
4341
  }, s.throttleMs);
4312
4342
  }
@@ -4316,16 +4346,16 @@ function observe(s) {
4316
4346
  return;
4317
4347
  }
4318
4348
  s.observerCleanup?.();
4319
- s.lastHeight = s.iframe.contentDocument.documentElement.scrollHeight;
4349
+ s.lastHeight = getActualHeight(s);
4320
4350
  s.logger.log('Initial height:', s.lastHeight);
4321
4351
  s.observerCleanup = setup(s.iframe.contentDocument, () => handleHeightChange(s));
4322
4352
  }
4323
- function start$5(s, cfg) {
4353
+ function start$5(s, iframe, cfg) {
4324
4354
  if (s.running) {
4325
4355
  s.logger.warn('Observer is already running. Call stop() first.');
4326
4356
  return;
4327
4357
  }
4328
- s.iframe = cfg.iframe;
4358
+ s.iframe = iframe;
4329
4359
  s.config = cfg;
4330
4360
  s.throttleMs = cfg.throttleMs ?? 25;
4331
4361
  s.debounceMs = cfg.debounceMs ?? 500;
@@ -4382,7 +4412,7 @@ function createHeightObserver() {
4382
4412
  running: false,
4383
4413
  };
4384
4414
  return {
4385
- start: (cfg) => start$5(s, cfg),
4415
+ start: (iframe, cfg) => start$5(s, iframe, cfg),
4386
4416
  stop: () => stop$5(s),
4387
4417
  observe: () => observe(s),
4388
4418
  clear: () => clear(s),
@@ -5426,6 +5456,20 @@ async function process(s) {
5426
5456
  perf$3.endSession();
5427
5457
  s.logger.groupEnd();
5428
5458
  s.logger.log('Process completed:', result);
5459
+ s.heightObserver.stop();
5460
+ s.heightObserver.start(s.iframe, {
5461
+ iframe: s.iframe,
5462
+ debug: s.config.debug,
5463
+ throttleMs: 25,
5464
+ debounceMs: 500,
5465
+ onHeightChange: (result) => {
5466
+ s.config?.onSuccess?.(result);
5467
+ dispatchDimensionsEvent(result);
5468
+ },
5469
+ onError: (error) => {
5470
+ s.config?.onError?.(error);
5471
+ },
5472
+ });
5429
5473
  s.config.onSuccess?.(result);
5430
5474
  dispatchDimensionsEvent(result);
5431
5475
  }
@@ -5783,7 +5827,7 @@ const htmlCache = {
5783
5827
 
5784
5828
  const CANVAS_ID = 'clarity-heatmap-canvas';
5785
5829
  const ATTENTION_HUES = [240, 210, 180, 150, 120, 90, 60, 30, 0];
5786
- const CANVAS_MAX_HEIGHT = 65535;
5830
+ const CANVAS_MAX_HEIGHT$1 = 65535;
5787
5831
  const Z_INDEX = 2147483647;
5788
5832
  const DEFAULT_IFRAME_ID = 'clarity-snapshot-heatmap-iframe';
5789
5833
  const SECONDARY_IFRAME_ID = 'clarity-snapshot-heatmap-iframe-secondary';
@@ -5848,7 +5892,7 @@ class AttentionMapRenderer {
5848
5892
  const body = doc.body;
5849
5893
  const de = doc.documentElement;
5850
5894
  const contentHeight = Math.max(body.scrollHeight, body.offsetHeight, de.clientHeight, de.scrollHeight, de.offsetHeight);
5851
- canvas.height = Math.min(contentHeight, CANVAS_MAX_HEIGHT);
5895
+ canvas.height = Math.min(contentHeight, CANVAS_MAX_HEIGHT$1);
5852
5896
  canvas.style.top = '0px';
5853
5897
  if (canvas.width <= 0 || canvas.height <= 0 || !this.attentionMapData)
5854
5898
  return;
@@ -5870,7 +5914,7 @@ class AttentionMapRenderer {
5870
5914
  const body = doc.body;
5871
5915
  const de = doc.documentElement;
5872
5916
  const contentHeight = Math.max(body.scrollHeight, body.offsetHeight, de.clientHeight, de.scrollHeight, de.offsetHeight);
5873
- canvas.height = Math.min(contentHeight, CANVAS_MAX_HEIGHT);
5917
+ canvas.height = Math.min(contentHeight, CANVAS_MAX_HEIGHT$1);
5874
5918
  canvas.style.top = '0px';
5875
5919
  let iframeEl = document.getElementById(this.heatmapIframeId);
5876
5920
  if (!iframeEl) {
@@ -5886,9 +5930,9 @@ class AttentionMapRenderer {
5886
5930
  for (const point of this.attentionMapData) {
5887
5931
  const timePercent = Math.floor((100 * point.cumulativeTime) / maxCumulativeTime);
5888
5932
  const colorIndex = timePercent <= 5 ? 0 : Math.min(Math.ceil(timePercent / 10), 8);
5889
- if (point.scrollPos / 100 <= 1) {
5933
+ if (point.scrollReachY / 100 <= 1) {
5890
5934
  point.colorIndex = colorIndex;
5891
- colorStops[point.scrollPos] = colorIndex;
5935
+ colorStops[point.scrollReachY] = colorIndex;
5892
5936
  }
5893
5937
  }
5894
5938
  // Smooth the top portion based on the fold/viewport ratio
@@ -5919,7 +5963,7 @@ class AttentionMapRenderer {
5919
5963
  buildAggregatedData = (doc, contentHeight, iframeEl) => {
5920
5964
  const elementCache = new Map();
5921
5965
  this.attentionMapData = Array.from({ length: 100 }, (_, i) => ({
5922
- scrollPos: i,
5966
+ scrollReachY: i,
5923
5967
  cumulativeTime: 0,
5924
5968
  colorIndex: 0,
5925
5969
  }));
@@ -5989,6 +6033,128 @@ class AttentionMapRenderer {
5989
6033
  };
5990
6034
  }
5991
6035
 
6036
+ const CANVAS_MAX_HEIGHT = 65535;
6037
+ const REDRAW_INTERVAL = 30;
6038
+ class ScrollBucketRenderer {
6039
+ heatmap;
6040
+ lastData = null;
6041
+ dimensionsListener = null;
6042
+ redrawTimeout = null;
6043
+ constructor(heatmap) {
6044
+ this.heatmap = heatmap;
6045
+ this.attachDimensionsListener();
6046
+ }
6047
+ redraw = () => {
6048
+ if (!this.lastData)
6049
+ return;
6050
+ if (this.redrawTimeout)
6051
+ clearTimeout(this.redrawTimeout);
6052
+ this.redrawTimeout = setTimeout(() => this.draw(this.lastData), REDRAW_INTERVAL);
6053
+ };
6054
+ attachDimensionsListener = () => {
6055
+ this.dimensionsListener = () => this.redraw();
6056
+ window.addEventListener('iframe-dimensions-applied', this.dimensionsListener);
6057
+ };
6058
+ detachDimensionsListener = () => {
6059
+ if (this.dimensionsListener) {
6060
+ window.removeEventListener('iframe-dimensions-applied', this.dimensionsListener);
6061
+ this.dimensionsListener = null;
6062
+ }
6063
+ if (this.redrawTimeout) {
6064
+ clearTimeout(this.redrawTimeout);
6065
+ this.redrawTimeout = null;
6066
+ }
6067
+ };
6068
+ reset = () => {
6069
+ // no-op: canvas state is managed by the shared heatmap instance
6070
+ };
6071
+ clear = () => {
6072
+ this.lastData = null;
6073
+ this.detachDimensionsListener();
6074
+ };
6075
+ /**
6076
+ * Render discrete color bands for scrollRevenue / scrollAttention data.
6077
+ * Uses the shared heatmap canvas (same as HeatmapHelper.scroll()) to avoid
6078
+ * duplicate portal canvas management.
6079
+ * Stores data so the canvas can be redrawn when iframe height changes.
6080
+ */
6081
+ renderBucket = async (data) => {
6082
+ if (!this.heatmap)
6083
+ return;
6084
+ this.attachDimensionsListener();
6085
+ this.lastData = data;
6086
+ await this.heatmap.waitForDialogs();
6087
+ this.draw(data);
6088
+ };
6089
+ draw = (data) => {
6090
+ if (!this.heatmap)
6091
+ return;
6092
+ const canvas = this.heatmap.overlay();
6093
+ if (!canvas)
6094
+ return;
6095
+ const context = canvas.getContext('2d');
6096
+ const doc = this.heatmap.state.window.document;
6097
+ const body = doc.body;
6098
+ const de = doc.documentElement;
6099
+ const height = Math.max(body.scrollHeight, body.offsetHeight, de.clientHeight, de.scrollHeight, de.offsetHeight);
6100
+ // Same canvas setup as HeatmapHelper.scroll(): full content height, top=0
6101
+ canvas.height = Math.min(height, CANVAS_MAX_HEIGHT);
6102
+ canvas.style.top = '0px';
6103
+ if (canvas.width <= 0 || canvas.height <= 0)
6104
+ return;
6105
+ const buckets = this.mapToBuckets(data);
6106
+ if (buckets.length === 0)
6107
+ return;
6108
+ const maxPercent = Math.max(...buckets.map((b) => b.percent));
6109
+ if (maxPercent <= 0)
6110
+ return;
6111
+ const gradient = context.createLinearGradient(0, 0, 0, canvas.height);
6112
+ for (const bucket of buckets) {
6113
+ const stopMid = (bucket.startY + bucket.endY) / 2 / 100;
6114
+ // Same hue direction as HeatmapHelper.scroll() (MaxHue = 240):
6115
+ // high percent → hue 0 (red/hot), low percent → hue 240 (blue/cold)
6116
+ const hue = (1 - bucket.percent / maxPercent) * 240;
6117
+ const color = `hsla(${Math.round(hue)}, 100%, 50%, 0.6)`;
6118
+ gradient.addColorStop(stopMid, color);
6119
+ }
6120
+ context.fillStyle = gradient;
6121
+ context.fillRect(0, 0, canvas.width, canvas.height);
6122
+ };
6123
+ /**
6124
+ * Convert flat position markers → structured buckets with startY/endY.
6125
+ *
6126
+ * Input positions: [0, 5, 10, 15, ..., 95]
6127
+ * position=0 → { startY: 0, endY: 5 } (special start marker)
6128
+ * position=10 → { startY: 5, endY: 10 }
6129
+ * position=15 → { startY: 10, endY: 15 }
6130
+ */
6131
+ mapToBuckets = (data) => {
6132
+ const sorted = [...data].sort((a, b) => a.position - b.position);
6133
+ const buckets = [];
6134
+ for (let i = 0; i < sorted.length; i++) {
6135
+ const current = sorted[i];
6136
+ let startY;
6137
+ let endY;
6138
+ if (current.position === 0) {
6139
+ // position=0 is the start marker → range 0 to next position
6140
+ const next = sorted[i + 1];
6141
+ if (!next)
6142
+ continue;
6143
+ startY = 0;
6144
+ endY = next.position;
6145
+ }
6146
+ else {
6147
+ // Each non-zero position is the END of its bucket; start = previous position
6148
+ const prev = sorted[i - 1];
6149
+ startY = prev ? prev.position : 0;
6150
+ endY = current.position;
6151
+ }
6152
+ buckets.push({ startY, endY, value: current.value, percent: current.percent });
6153
+ }
6154
+ return buckets;
6155
+ };
6156
+ }
6157
+
5992
6158
  var Event;
5993
6159
  (function (Event) {
5994
6160
  /* Data */
@@ -6301,15 +6467,23 @@ const renderLoop = async (ctx, options) => {
6301
6467
 
6302
6468
  class GXVisualizer extends Visualizer {
6303
6469
  attentionMap;
6470
+ scrollBucketMap;
6304
6471
  originalClearmap;
6305
6472
  originalSetup;
6473
+ originalScrollmap;
6474
+ originalClickmap;
6306
6475
  constructor() {
6307
6476
  super();
6308
6477
  this.attentionMap = new AttentionMapRenderer(null);
6478
+ this.scrollBucketMap = new ScrollBucketRenderer(null);
6309
6479
  this.originalSetup = this.setup;
6310
6480
  this.originalClearmap = this.clearmap;
6481
+ this.originalScrollmap = this.scrollmap;
6482
+ this.originalClickmap = this.clickmap;
6311
6483
  this.clearmap = this.clearmapOverride;
6312
6484
  this.setup = this.setupOverride;
6485
+ this.scrollmap = this.scrollmapOverride;
6486
+ this.clickmap = this.clickmapOverride;
6313
6487
  }
6314
6488
  htmlRender = async (props) => {
6315
6489
  const { decoded, target, portalCanvasId, useproxy, logerror } = props;
@@ -6354,12 +6528,27 @@ class GXVisualizer extends Visualizer {
6354
6528
  this.clearmapOverride();
6355
6529
  this.attentionMap.attention(attentionData, avgFold, this, isSecondary);
6356
6530
  };
6531
+ /**
6532
+ * Render discrete color bands for scrollRevenue / scrollAttention data.
6533
+ * @param data - Array of bucket inputs with position markers (0, 5, 10, ..., 95) and percent values
6534
+ */
6535
+ scrollBucket = async (data) => {
6536
+ this.clearmapOverride();
6537
+ await this.scrollBucketMap.renderBucket(data);
6538
+ };
6539
+ scrollmapOverride = (data, averageFold, addMarkers) => {
6540
+ this.clearmapOverride();
6541
+ this.originalScrollmap(data, averageFold, addMarkers);
6542
+ };
6543
+ clickmapOverride = (activity) => {
6544
+ this.clearmapOverride();
6545
+ this.originalClickmap(activity);
6546
+ };
6357
6547
  buildHtmlByCached = async (cached, options) => {
6358
6548
  const { target, useproxy, portalCanvasId } = options;
6359
6549
  if (!cached || !target)
6360
6550
  throw new Error('Failed to render HTML cached');
6361
6551
  const doc = target.document;
6362
- target.window;
6363
6552
  try {
6364
6553
  await this.setup(target, { version: cached.version, useproxy, portalCanvasId });
6365
6554
  doc.open();
@@ -6408,14 +6597,19 @@ class GXVisualizer extends Visualizer {
6408
6597
  await renderLoop(ctx, options);
6409
6598
  };
6410
6599
  setupOverride = async (target, options) => {
6411
- this.attentionMap?.clear();
6600
+ // Mirror original Visualizer.setup(): only reset (remove listeners/state), not clear canvas.
6601
+ // The canvas lives in the old window's DOM which will be replaced by originalSetup's reset().
6602
+ this.attentionMap?.reset();
6603
+ this.scrollBucketMap?.reset();
6412
6604
  await this.originalSetup(target, options);
6413
6605
  this.attentionMap = new AttentionMapRenderer(this.state);
6606
+ this.scrollBucketMap = new ScrollBucketRenderer(this.heatmap);
6414
6607
  return this;
6415
6608
  };
6416
6609
  clearmapOverride = () => {
6417
6610
  this.originalClearmap();
6418
6611
  this.attentionMap?.clear();
6612
+ this.scrollBucketMap?.clear();
6419
6613
  };
6420
6614
  }
6421
6615
 
@@ -6739,136 +6933,6 @@ const useContainerDimensions = (props) => {
6739
6933
  return { containerWidth, containerHeight };
6740
6934
  };
6741
6935
 
6742
- const useObserveIframeHeight = (props) => {
6743
- const iframeHeight = useHeatmapVizRectContext((s) => s.iframeHeight);
6744
- const setIframeHeight = useHeatmapVizRectContext((s) => s.setIframeHeight);
6745
- const isDomLoaded = useHeatmapVizContext((s) => s.isDomLoaded);
6746
- const wrapperHeight = useHeatmapVizRectContext((s) => s.wrapperHeight);
6747
- const { iframeRef } = props;
6748
- const resizeObserverRef = useRef(null);
6749
- const mutationObserverRef = useRef(null);
6750
- const debounceTimerRef = useRef(null);
6751
- const lastHeightRef = useRef(0);
6752
- const animationFrameRef = useRef(null);
6753
- const updateIframeHeight = useCallback(() => {
6754
- const iframe = iframeRef.current;
6755
- if (!iframe)
6756
- return;
6757
- try {
6758
- const iframeDocument = iframe.contentDocument;
6759
- const iframeBody = iframeDocument?.body;
6760
- const iframeDocumentElement = iframeDocument?.documentElement;
6761
- if (!iframeBody || !iframeDocumentElement)
6762
- return;
6763
- // iframe.style.height = 'auto'; // TODO: check if this is needed
6764
- requestAnimationFrame(() => {
6765
- iframe.style.height = `${wrapperHeight}px`;
6766
- const bodyHeight = Math.max(iframeBody.scrollHeight, iframeBody.offsetHeight, iframeBody.clientHeight);
6767
- const documentHeight = Math.max(iframeDocumentElement.scrollHeight, iframeDocumentElement.offsetHeight, iframeDocumentElement.clientHeight);
6768
- const actualHeight = Math.max(bodyHeight, documentHeight);
6769
- if (actualHeight > 0) {
6770
- lastHeightRef.current = actualHeight;
6771
- // iframe.height = `${actualHeight}px`;
6772
- iframe.style.height = `${actualHeight}px`;
6773
- setIframeHeight(actualHeight);
6774
- }
6775
- });
6776
- }
6777
- catch (error) {
6778
- console.warn('Cannot measure iframe content:', error);
6779
- }
6780
- }, [iframeRef, wrapperHeight]);
6781
- useCallback(() => {
6782
- // Cancel pending updates
6783
- if (debounceTimerRef.current) {
6784
- clearTimeout(debounceTimerRef.current);
6785
- }
6786
- if (animationFrameRef.current) {
6787
- cancelAnimationFrame(animationFrameRef.current);
6788
- }
6789
- debounceTimerRef.current = setTimeout(() => {
6790
- animationFrameRef.current = requestAnimationFrame(() => {
6791
- updateIframeHeight();
6792
- });
6793
- }, 50);
6794
- }, [updateIframeHeight]);
6795
- // Immediate update không debounce (cho ResizeObserver)
6796
- const immediateUpdate = useCallback(() => {
6797
- if (animationFrameRef.current) {
6798
- cancelAnimationFrame(animationFrameRef.current);
6799
- }
6800
- animationFrameRef.current = requestAnimationFrame(() => {
6801
- updateIframeHeight();
6802
- });
6803
- }, [updateIframeHeight]);
6804
- useEffect(() => {
6805
- const iframe = iframeRef.current;
6806
- if (!iframe || !iframeHeight || !isDomLoaded)
6807
- return;
6808
- const setupObservers = () => {
6809
- try {
6810
- const iframeDocument = iframe.contentDocument;
6811
- const iframeBody = iframeDocument?.body;
6812
- if (!iframeBody)
6813
- return;
6814
- // Cleanup existing observers
6815
- if (resizeObserverRef.current) {
6816
- resizeObserverRef.current.disconnect();
6817
- }
6818
- if (mutationObserverRef.current) {
6819
- mutationObserverRef.current.disconnect();
6820
- }
6821
- if (typeof window.ResizeObserver !== 'undefined') {
6822
- resizeObserverRef.current = new ResizeObserver(immediateUpdate);
6823
- resizeObserverRef.current.observe(iframeBody);
6824
- const iframeDocumentElement = iframeDocument?.documentElement;
6825
- if (iframeDocumentElement) {
6826
- resizeObserverRef.current.observe(iframeDocumentElement);
6827
- }
6828
- }
6829
- if (typeof window.MutationObserver !== 'undefined') {
6830
- mutationObserverRef.current = new MutationObserver(immediateUpdate);
6831
- mutationObserverRef.current.observe(iframeBody, {
6832
- childList: true,
6833
- subtree: true,
6834
- attributes: true,
6835
- attributeFilter: ['style', 'class'],
6836
- characterData: false,
6837
- });
6838
- }
6839
- updateIframeHeight();
6840
- }
6841
- catch (error) {
6842
- console.warn('Cannot access iframe content:', error);
6843
- }
6844
- };
6845
- if (iframe.contentDocument?.readyState === 'complete') {
6846
- setupObservers();
6847
- }
6848
- else {
6849
- iframe.addEventListener('load', setupObservers, { once: true });
6850
- }
6851
- return () => {
6852
- // Cleanup observers
6853
- if (resizeObserverRef.current) {
6854
- resizeObserverRef.current.disconnect();
6855
- }
6856
- if (mutationObserverRef.current) {
6857
- mutationObserverRef.current.disconnect();
6858
- }
6859
- // Cleanup timers
6860
- if (debounceTimerRef.current) {
6861
- clearTimeout(debounceTimerRef.current);
6862
- }
6863
- if (animationFrameRef.current) {
6864
- cancelAnimationFrame(animationFrameRef.current);
6865
- }
6866
- iframe.removeEventListener('load', setupObservers);
6867
- };
6868
- }, [iframeRef, iframeHeight, isDomLoaded]);
6869
- return {};
6870
- };
6871
-
6872
6936
  const useScaleCalculation = (props) => {
6873
6937
  const widthScale = useHeatmapVizContext((s) => s.widthScale);
6874
6938
  const zoomRatio = useHeatmapVizContext((s) => s.zoomRatio);
@@ -6948,7 +7012,7 @@ const useHeatmapScale = (props) => {
6948
7012
  // 2. Get content dimensions from config
6949
7013
  const viewport = useHeatmapViewportByDevice();
6950
7014
  // 3. Observe iframe height (now reacts to width changes)
6951
- useObserveIframeHeight({ iframeRef });
7015
+ // useObserveIframeHeight({ iframeRef });
6952
7016
  // 4. Calculate scale
6953
7017
  const { widthScale } = useScaleCalculation({ containerWidth, containerHeight, iframeHeight });
6954
7018
  // 5. Setup scroll sync