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

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 +225 -166
  5. package/dist/esm/index.mjs +225 -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;;CAmDxB,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,47 @@ 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
+ vizRef?.clearmap?.();
3573
+ if (!vizRef || !scrollmap || scrollmap.length === 0 || !isDomLoaded)
3574
+ return;
3575
+ switch (scrollType) {
3576
+ case EScrollType.Attention:
3577
+ case EScrollType.Revenue: {
3578
+ renderScrollBucket();
3579
+ break;
3580
+ }
3581
+ case EScrollType.Depth:
3582
+ default:
3583
+ renderScrollmap();
3584
+ break;
3585
+ }
3586
+ }, [vizRef, scrollmap, isDomLoaded, scrollType, renderScrollmap, renderScrollBucket]);
3573
3587
  return { start };
3574
3588
  };
3575
3589
 
@@ -4299,14 +4313,25 @@ function handleHeightChange(s) {
4299
4313
  s.throttleTimeout = setTimeout(() => {
4300
4314
  s.throttleTimeout = null;
4301
4315
  const currentHeight = getActualHeight(s);
4302
- if (currentHeight === s.lastHeight)
4316
+ if (currentHeight === s.lastHeight) {
4317
+ // Height returned to known state — cancel any stale pending debounce
4318
+ // to prevent it from firing with an outdated height and corrupting lastHeight.
4319
+ if (s.debounceTimeout) {
4320
+ clearTimeout(s.debounceTimeout);
4321
+ s.debounceTimeout = null;
4322
+ }
4303
4323
  return;
4324
+ }
4304
4325
  s.logger.log(`Height changed: ${s.lastHeight}px -> ${currentHeight}px`);
4305
4326
  if (s.debounceTimeout)
4306
4327
  clearTimeout(s.debounceTimeout);
4307
4328
  s.debounceTimeout = setTimeout(() => {
4308
4329
  s.debounceTimeout = null;
4309
- processHeightChange(s, currentHeight);
4330
+ // Re-read height at dispatch time to avoid using a stale closure value.
4331
+ const finalHeight = getActualHeight(s);
4332
+ if (finalHeight !== s.lastHeight) {
4333
+ processHeightChange(s, finalHeight);
4334
+ }
4310
4335
  }, s.debounceMs);
4311
4336
  }, s.throttleMs);
4312
4337
  }
@@ -4316,16 +4341,16 @@ function observe(s) {
4316
4341
  return;
4317
4342
  }
4318
4343
  s.observerCleanup?.();
4319
- s.lastHeight = s.iframe.contentDocument.documentElement.scrollHeight;
4344
+ s.lastHeight = getActualHeight(s);
4320
4345
  s.logger.log('Initial height:', s.lastHeight);
4321
4346
  s.observerCleanup = setup(s.iframe.contentDocument, () => handleHeightChange(s));
4322
4347
  }
4323
- function start$5(s, cfg) {
4348
+ function start$5(s, iframe, cfg) {
4324
4349
  if (s.running) {
4325
4350
  s.logger.warn('Observer is already running. Call stop() first.');
4326
4351
  return;
4327
4352
  }
4328
- s.iframe = cfg.iframe;
4353
+ s.iframe = iframe;
4329
4354
  s.config = cfg;
4330
4355
  s.throttleMs = cfg.throttleMs ?? 25;
4331
4356
  s.debounceMs = cfg.debounceMs ?? 500;
@@ -4382,7 +4407,7 @@ function createHeightObserver() {
4382
4407
  running: false,
4383
4408
  };
4384
4409
  return {
4385
- start: (cfg) => start$5(s, cfg),
4410
+ start: (iframe, cfg) => start$5(s, iframe, cfg),
4386
4411
  stop: () => stop$5(s),
4387
4412
  observe: () => observe(s),
4388
4413
  clear: () => clear(s),
@@ -5426,6 +5451,20 @@ async function process(s) {
5426
5451
  perf$3.endSession();
5427
5452
  s.logger.groupEnd();
5428
5453
  s.logger.log('Process completed:', result);
5454
+ s.heightObserver.stop();
5455
+ s.heightObserver.start(s.iframe, {
5456
+ iframe: s.iframe,
5457
+ debug: s.config.debug,
5458
+ throttleMs: 25,
5459
+ debounceMs: 500,
5460
+ onHeightChange: (result) => {
5461
+ s.config?.onSuccess?.(result);
5462
+ dispatchDimensionsEvent(result);
5463
+ },
5464
+ onError: (error) => {
5465
+ s.config?.onError?.(error);
5466
+ },
5467
+ });
5429
5468
  s.config.onSuccess?.(result);
5430
5469
  dispatchDimensionsEvent(result);
5431
5470
  }
@@ -5783,7 +5822,7 @@ const htmlCache = {
5783
5822
 
5784
5823
  const CANVAS_ID = 'clarity-heatmap-canvas';
5785
5824
  const ATTENTION_HUES = [240, 210, 180, 150, 120, 90, 60, 30, 0];
5786
- const CANVAS_MAX_HEIGHT = 65535;
5825
+ const CANVAS_MAX_HEIGHT$1 = 65535;
5787
5826
  const Z_INDEX = 2147483647;
5788
5827
  const DEFAULT_IFRAME_ID = 'clarity-snapshot-heatmap-iframe';
5789
5828
  const SECONDARY_IFRAME_ID = 'clarity-snapshot-heatmap-iframe-secondary';
@@ -5848,7 +5887,7 @@ class AttentionMapRenderer {
5848
5887
  const body = doc.body;
5849
5888
  const de = doc.documentElement;
5850
5889
  const contentHeight = Math.max(body.scrollHeight, body.offsetHeight, de.clientHeight, de.scrollHeight, de.offsetHeight);
5851
- canvas.height = Math.min(contentHeight, CANVAS_MAX_HEIGHT);
5890
+ canvas.height = Math.min(contentHeight, CANVAS_MAX_HEIGHT$1);
5852
5891
  canvas.style.top = '0px';
5853
5892
  if (canvas.width <= 0 || canvas.height <= 0 || !this.attentionMapData)
5854
5893
  return;
@@ -5870,7 +5909,7 @@ class AttentionMapRenderer {
5870
5909
  const body = doc.body;
5871
5910
  const de = doc.documentElement;
5872
5911
  const contentHeight = Math.max(body.scrollHeight, body.offsetHeight, de.clientHeight, de.scrollHeight, de.offsetHeight);
5873
- canvas.height = Math.min(contentHeight, CANVAS_MAX_HEIGHT);
5912
+ canvas.height = Math.min(contentHeight, CANVAS_MAX_HEIGHT$1);
5874
5913
  canvas.style.top = '0px';
5875
5914
  let iframeEl = document.getElementById(this.heatmapIframeId);
5876
5915
  if (!iframeEl) {
@@ -5886,9 +5925,9 @@ class AttentionMapRenderer {
5886
5925
  for (const point of this.attentionMapData) {
5887
5926
  const timePercent = Math.floor((100 * point.cumulativeTime) / maxCumulativeTime);
5888
5927
  const colorIndex = timePercent <= 5 ? 0 : Math.min(Math.ceil(timePercent / 10), 8);
5889
- if (point.scrollPos / 100 <= 1) {
5928
+ if (point.scrollReachY / 100 <= 1) {
5890
5929
  point.colorIndex = colorIndex;
5891
- colorStops[point.scrollPos] = colorIndex;
5930
+ colorStops[point.scrollReachY] = colorIndex;
5892
5931
  }
5893
5932
  }
5894
5933
  // Smooth the top portion based on the fold/viewport ratio
@@ -5919,7 +5958,7 @@ class AttentionMapRenderer {
5919
5958
  buildAggregatedData = (doc, contentHeight, iframeEl) => {
5920
5959
  const elementCache = new Map();
5921
5960
  this.attentionMapData = Array.from({ length: 100 }, (_, i) => ({
5922
- scrollPos: i,
5961
+ scrollReachY: i,
5923
5962
  cumulativeTime: 0,
5924
5963
  colorIndex: 0,
5925
5964
  }));
@@ -5989,6 +6028,128 @@ class AttentionMapRenderer {
5989
6028
  };
5990
6029
  }
5991
6030
 
6031
+ const CANVAS_MAX_HEIGHT = 65535;
6032
+ const REDRAW_INTERVAL = 30;
6033
+ class ScrollBucketRenderer {
6034
+ heatmap;
6035
+ lastData = null;
6036
+ dimensionsListener = null;
6037
+ redrawTimeout = null;
6038
+ constructor(heatmap) {
6039
+ this.heatmap = heatmap;
6040
+ this.attachDimensionsListener();
6041
+ }
6042
+ redraw = () => {
6043
+ if (!this.lastData)
6044
+ return;
6045
+ if (this.redrawTimeout)
6046
+ clearTimeout(this.redrawTimeout);
6047
+ this.redrawTimeout = setTimeout(() => this.draw(this.lastData), REDRAW_INTERVAL);
6048
+ };
6049
+ attachDimensionsListener = () => {
6050
+ this.dimensionsListener = () => this.redraw();
6051
+ window.addEventListener('iframe-dimensions-applied', this.dimensionsListener);
6052
+ };
6053
+ detachDimensionsListener = () => {
6054
+ if (this.dimensionsListener) {
6055
+ window.removeEventListener('iframe-dimensions-applied', this.dimensionsListener);
6056
+ this.dimensionsListener = null;
6057
+ }
6058
+ if (this.redrawTimeout) {
6059
+ clearTimeout(this.redrawTimeout);
6060
+ this.redrawTimeout = null;
6061
+ }
6062
+ };
6063
+ reset = () => {
6064
+ // no-op: canvas state is managed by the shared heatmap instance
6065
+ };
6066
+ clear = () => {
6067
+ this.lastData = null;
6068
+ this.detachDimensionsListener();
6069
+ };
6070
+ /**
6071
+ * Render discrete color bands for scrollRevenue / scrollAttention data.
6072
+ * Uses the shared heatmap canvas (same as HeatmapHelper.scroll()) to avoid
6073
+ * duplicate portal canvas management.
6074
+ * Stores data so the canvas can be redrawn when iframe height changes.
6075
+ */
6076
+ renderBucket = async (data) => {
6077
+ if (!this.heatmap)
6078
+ return;
6079
+ this.attachDimensionsListener();
6080
+ this.lastData = data;
6081
+ await this.heatmap.waitForDialogs();
6082
+ this.draw(data);
6083
+ };
6084
+ draw = (data) => {
6085
+ if (!this.heatmap)
6086
+ return;
6087
+ const canvas = this.heatmap.overlay();
6088
+ if (!canvas)
6089
+ return;
6090
+ const context = canvas.getContext('2d');
6091
+ const doc = this.heatmap.state.window.document;
6092
+ const body = doc.body;
6093
+ const de = doc.documentElement;
6094
+ const height = Math.max(body.scrollHeight, body.offsetHeight, de.clientHeight, de.scrollHeight, de.offsetHeight);
6095
+ // Same canvas setup as HeatmapHelper.scroll(): full content height, top=0
6096
+ canvas.height = Math.min(height, CANVAS_MAX_HEIGHT);
6097
+ canvas.style.top = '0px';
6098
+ if (canvas.width <= 0 || canvas.height <= 0)
6099
+ return;
6100
+ const buckets = this.mapToBuckets(data);
6101
+ if (buckets.length === 0)
6102
+ return;
6103
+ const maxPercent = Math.max(...buckets.map((b) => b.percent));
6104
+ if (maxPercent <= 0)
6105
+ return;
6106
+ const gradient = context.createLinearGradient(0, 0, 0, canvas.height);
6107
+ for (const bucket of buckets) {
6108
+ const stopMid = (bucket.startY + bucket.endY) / 2 / 100;
6109
+ // Same hue direction as HeatmapHelper.scroll() (MaxHue = 240):
6110
+ // high percent → hue 0 (red/hot), low percent → hue 240 (blue/cold)
6111
+ const hue = (1 - bucket.percent / maxPercent) * 240;
6112
+ const color = `hsla(${Math.round(hue)}, 100%, 50%, 0.6)`;
6113
+ gradient.addColorStop(stopMid, color);
6114
+ }
6115
+ context.fillStyle = gradient;
6116
+ context.fillRect(0, 0, canvas.width, canvas.height);
6117
+ };
6118
+ /**
6119
+ * Convert flat position markers → structured buckets with startY/endY.
6120
+ *
6121
+ * Input positions: [0, 5, 10, 15, ..., 95]
6122
+ * position=0 → { startY: 0, endY: 5 } (special start marker)
6123
+ * position=10 → { startY: 5, endY: 10 }
6124
+ * position=15 → { startY: 10, endY: 15 }
6125
+ */
6126
+ mapToBuckets = (data) => {
6127
+ const sorted = [...data].sort((a, b) => a.position - b.position);
6128
+ const buckets = [];
6129
+ for (let i = 0; i < sorted.length; i++) {
6130
+ const current = sorted[i];
6131
+ let startY;
6132
+ let endY;
6133
+ if (current.position === 0) {
6134
+ // position=0 is the start marker → range 0 to next position
6135
+ const next = sorted[i + 1];
6136
+ if (!next)
6137
+ continue;
6138
+ startY = 0;
6139
+ endY = next.position;
6140
+ }
6141
+ else {
6142
+ // Each non-zero position is the END of its bucket; start = previous position
6143
+ const prev = sorted[i - 1];
6144
+ startY = prev ? prev.position : 0;
6145
+ endY = current.position;
6146
+ }
6147
+ buckets.push({ startY, endY, value: current.value, percent: current.percent });
6148
+ }
6149
+ return buckets;
6150
+ };
6151
+ }
6152
+
5992
6153
  var Event;
5993
6154
  (function (Event) {
5994
6155
  /* Data */
@@ -6301,15 +6462,23 @@ const renderLoop = async (ctx, options) => {
6301
6462
 
6302
6463
  class GXVisualizer extends Visualizer {
6303
6464
  attentionMap;
6465
+ scrollBucketMap;
6304
6466
  originalClearmap;
6305
6467
  originalSetup;
6468
+ originalScrollmap;
6469
+ originalClickmap;
6306
6470
  constructor() {
6307
6471
  super();
6308
6472
  this.attentionMap = new AttentionMapRenderer(null);
6473
+ this.scrollBucketMap = new ScrollBucketRenderer(null);
6309
6474
  this.originalSetup = this.setup;
6310
6475
  this.originalClearmap = this.clearmap;
6476
+ this.originalScrollmap = this.scrollmap;
6477
+ this.originalClickmap = this.clickmap;
6311
6478
  this.clearmap = this.clearmapOverride;
6312
6479
  this.setup = this.setupOverride;
6480
+ this.scrollmap = this.scrollmapOverride;
6481
+ this.clickmap = this.clickmapOverride;
6313
6482
  }
6314
6483
  htmlRender = async (props) => {
6315
6484
  const { decoded, target, portalCanvasId, useproxy, logerror } = props;
@@ -6354,12 +6523,27 @@ class GXVisualizer extends Visualizer {
6354
6523
  this.clearmapOverride();
6355
6524
  this.attentionMap.attention(attentionData, avgFold, this, isSecondary);
6356
6525
  };
6526
+ /**
6527
+ * Render discrete color bands for scrollRevenue / scrollAttention data.
6528
+ * @param data - Array of bucket inputs with position markers (0, 5, 10, ..., 95) and percent values
6529
+ */
6530
+ scrollBucket = async (data) => {
6531
+ this.clearmapOverride();
6532
+ await this.scrollBucketMap.renderBucket(data);
6533
+ };
6534
+ scrollmapOverride = (data, averageFold, addMarkers) => {
6535
+ this.clearmapOverride();
6536
+ this.originalScrollmap(data, averageFold, addMarkers);
6537
+ };
6538
+ clickmapOverride = (activity) => {
6539
+ this.clearmapOverride();
6540
+ this.originalClickmap(activity);
6541
+ };
6357
6542
  buildHtmlByCached = async (cached, options) => {
6358
6543
  const { target, useproxy, portalCanvasId } = options;
6359
6544
  if (!cached || !target)
6360
6545
  throw new Error('Failed to render HTML cached');
6361
6546
  const doc = target.document;
6362
- target.window;
6363
6547
  try {
6364
6548
  await this.setup(target, { version: cached.version, useproxy, portalCanvasId });
6365
6549
  doc.open();
@@ -6408,14 +6592,19 @@ class GXVisualizer extends Visualizer {
6408
6592
  await renderLoop(ctx, options);
6409
6593
  };
6410
6594
  setupOverride = async (target, options) => {
6411
- this.attentionMap?.clear();
6595
+ // Mirror original Visualizer.setup(): only reset (remove listeners/state), not clear canvas.
6596
+ // The canvas lives in the old window's DOM which will be replaced by originalSetup's reset().
6597
+ this.attentionMap?.reset();
6598
+ this.scrollBucketMap?.reset();
6412
6599
  await this.originalSetup(target, options);
6413
6600
  this.attentionMap = new AttentionMapRenderer(this.state);
6601
+ this.scrollBucketMap = new ScrollBucketRenderer(this.heatmap);
6414
6602
  return this;
6415
6603
  };
6416
6604
  clearmapOverride = () => {
6417
6605
  this.originalClearmap();
6418
6606
  this.attentionMap?.clear();
6607
+ this.scrollBucketMap?.clear();
6419
6608
  };
6420
6609
  }
6421
6610
 
@@ -6739,136 +6928,6 @@ const useContainerDimensions = (props) => {
6739
6928
  return { containerWidth, containerHeight };
6740
6929
  };
6741
6930
 
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
6931
  const useScaleCalculation = (props) => {
6873
6932
  const widthScale = useHeatmapVizContext((s) => s.widthScale);
6874
6933
  const zoomRatio = useHeatmapVizContext((s) => s.zoomRatio);
@@ -6948,7 +7007,7 @@ const useHeatmapScale = (props) => {
6948
7007
  // 2. Get content dimensions from config
6949
7008
  const viewport = useHeatmapViewportByDevice();
6950
7009
  // 3. Observe iframe height (now reacts to width changes)
6951
- useObserveIframeHeight({ iframeRef });
7010
+ // useObserveIframeHeight({ iframeRef });
6952
7011
  // 4. Calculate scale
6953
7012
  const { widthScale } = useScaleCalculation({ containerWidth, containerHeight, iframeHeight });
6954
7013
  // 5. Setup scroll sync