@gemx-dev/heatmap-react 3.5.56 → 3.5.58

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 (107) hide show
  1. package/dist/esm/components/VizElement/BackdropCanvas.d.ts +38 -0
  2. package/dist/esm/components/VizElement/BackdropCanvas.d.ts.map +1 -0
  3. package/dist/esm/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
  4. package/dist/esm/components/VizElement/ElementCallout.d.ts.map +1 -1
  5. package/dist/esm/components/VizElement/ElementCalloutClicked.d.ts +12 -0
  6. package/dist/esm/components/VizElement/ElementCalloutClicked.d.ts.map +1 -0
  7. package/dist/esm/components/VizElement/ElementCalloutHovered.d.ts +10 -0
  8. package/dist/esm/components/VizElement/ElementCalloutHovered.d.ts.map +1 -0
  9. package/dist/esm/components/VizElement/ElementCalloutOverlay.d.ts +3 -0
  10. package/dist/esm/components/VizElement/ElementCalloutOverlay.d.ts.map +1 -0
  11. package/dist/esm/components/VizElement/ElementMissing.d.ts +1 -0
  12. package/dist/esm/components/VizElement/ElementMissing.d.ts.map +1 -1
  13. package/dist/esm/components/VizElement/ElementOverlay.d.ts +3 -3
  14. package/dist/esm/components/VizElement/ElementOverlay.d.ts.map +1 -1
  15. package/dist/esm/components/VizElement/HeatmapElements.d.ts +1 -1
  16. package/dist/esm/components/VizElement/HeatmapElements.d.ts.map +1 -1
  17. package/dist/esm/components/VizElement/RankBadge.d.ts +2 -0
  18. package/dist/esm/components/VizElement/RankBadge.d.ts.map +1 -1
  19. package/dist/esm/components/VizScrollmap/VizScrollMap.d.ts.map +1 -1
  20. package/dist/esm/configs/backdrop.d.ts +23 -0
  21. package/dist/esm/configs/backdrop.d.ts.map +1 -0
  22. package/dist/esm/configs/index.d.ts +1 -0
  23. package/dist/esm/configs/index.d.ts.map +1 -1
  24. package/dist/esm/constants/viz-elm-callout.d.ts +6 -1
  25. package/dist/esm/constants/viz-elm-callout.d.ts.map +1 -1
  26. package/dist/esm/helpers/canvas-backdrop.d.ts +28 -0
  27. package/dist/esm/helpers/canvas-backdrop.d.ts.map +1 -0
  28. package/dist/esm/helpers/index.d.ts +1 -0
  29. package/dist/esm/helpers/index.d.ts.map +1 -1
  30. package/dist/esm/helpers/viz-elm-callout/dimensions.d.ts +9 -8
  31. package/dist/esm/helpers/viz-elm-callout/dimensions.d.ts.map +1 -1
  32. package/dist/esm/helpers/viz-elm-callout/position-calculator.d.ts +4 -12
  33. package/dist/esm/helpers/viz-elm-callout/position-calculator.d.ts.map +1 -1
  34. package/dist/esm/helpers/viz-elm-callout/position-candidates.d.ts +4 -10
  35. package/dist/esm/helpers/viz-elm-callout/position-candidates.d.ts.map +1 -1
  36. package/dist/esm/helpers/viz-elm-callout/position-selector.d.ts +2 -8
  37. package/dist/esm/helpers/viz-elm-callout/position-selector.d.ts.map +1 -1
  38. package/dist/esm/helpers/viz-elm-callout/position-validator.d.ts +5 -3
  39. package/dist/esm/helpers/viz-elm-callout/position-validator.d.ts.map +1 -1
  40. package/dist/esm/helpers/viz-elm-callout/viz-elm.d.ts +2 -20
  41. package/dist/esm/helpers/viz-elm-callout/viz-elm.d.ts.map +1 -1
  42. package/dist/esm/hooks/view-context/useHeatmapViz.d.ts +2 -26
  43. package/dist/esm/hooks/view-context/useHeatmapViz.d.ts.map +1 -1
  44. package/dist/esm/hooks/viz-elm/useHoveredElement.d.ts.map +1 -1
  45. package/dist/esm/hooks/viz-scale/useScaleCalculation.d.ts.map +1 -1
  46. package/dist/esm/index.js +527 -233
  47. package/dist/esm/index.mjs +527 -233
  48. package/dist/esm/stores/viz-click.d.ts +5 -4
  49. package/dist/esm/stores/viz-click.d.ts.map +1 -1
  50. package/dist/esm/types/viz-elm-callout.d.ts +60 -9
  51. package/dist/esm/types/viz-elm-callout.d.ts.map +1 -1
  52. package/dist/style.css +3 -1
  53. package/dist/umd/components/VizElement/BackdropCanvas.d.ts +38 -0
  54. package/dist/umd/components/VizElement/BackdropCanvas.d.ts.map +1 -0
  55. package/dist/umd/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
  56. package/dist/umd/components/VizElement/ElementCallout.d.ts.map +1 -1
  57. package/dist/umd/components/VizElement/ElementCalloutClicked.d.ts +12 -0
  58. package/dist/umd/components/VizElement/ElementCalloutClicked.d.ts.map +1 -0
  59. package/dist/umd/components/VizElement/ElementCalloutHovered.d.ts +10 -0
  60. package/dist/umd/components/VizElement/ElementCalloutHovered.d.ts.map +1 -0
  61. package/dist/umd/components/VizElement/ElementCalloutOverlay.d.ts +3 -0
  62. package/dist/umd/components/VizElement/ElementCalloutOverlay.d.ts.map +1 -0
  63. package/dist/umd/components/VizElement/ElementMissing.d.ts +1 -0
  64. package/dist/umd/components/VizElement/ElementMissing.d.ts.map +1 -1
  65. package/dist/umd/components/VizElement/ElementOverlay.d.ts +3 -3
  66. package/dist/umd/components/VizElement/ElementOverlay.d.ts.map +1 -1
  67. package/dist/umd/components/VizElement/HeatmapElements.d.ts +1 -1
  68. package/dist/umd/components/VizElement/HeatmapElements.d.ts.map +1 -1
  69. package/dist/umd/components/VizElement/RankBadge.d.ts +2 -0
  70. package/dist/umd/components/VizElement/RankBadge.d.ts.map +1 -1
  71. package/dist/umd/components/VizScrollmap/VizScrollMap.d.ts.map +1 -1
  72. package/dist/umd/configs/backdrop.d.ts +23 -0
  73. package/dist/umd/configs/backdrop.d.ts.map +1 -0
  74. package/dist/umd/configs/index.d.ts +1 -0
  75. package/dist/umd/configs/index.d.ts.map +1 -1
  76. package/dist/umd/constants/viz-elm-callout.d.ts +6 -1
  77. package/dist/umd/constants/viz-elm-callout.d.ts.map +1 -1
  78. package/dist/umd/helpers/canvas-backdrop.d.ts +28 -0
  79. package/dist/umd/helpers/canvas-backdrop.d.ts.map +1 -0
  80. package/dist/umd/helpers/index.d.ts +1 -0
  81. package/dist/umd/helpers/index.d.ts.map +1 -1
  82. package/dist/umd/helpers/viz-elm-callout/dimensions.d.ts +9 -8
  83. package/dist/umd/helpers/viz-elm-callout/dimensions.d.ts.map +1 -1
  84. package/dist/umd/helpers/viz-elm-callout/position-calculator.d.ts +4 -12
  85. package/dist/umd/helpers/viz-elm-callout/position-calculator.d.ts.map +1 -1
  86. package/dist/umd/helpers/viz-elm-callout/position-candidates.d.ts +4 -10
  87. package/dist/umd/helpers/viz-elm-callout/position-candidates.d.ts.map +1 -1
  88. package/dist/umd/helpers/viz-elm-callout/position-selector.d.ts +2 -8
  89. package/dist/umd/helpers/viz-elm-callout/position-selector.d.ts.map +1 -1
  90. package/dist/umd/helpers/viz-elm-callout/position-validator.d.ts +5 -3
  91. package/dist/umd/helpers/viz-elm-callout/position-validator.d.ts.map +1 -1
  92. package/dist/umd/helpers/viz-elm-callout/viz-elm.d.ts +2 -20
  93. package/dist/umd/helpers/viz-elm-callout/viz-elm.d.ts.map +1 -1
  94. package/dist/umd/hooks/view-context/useHeatmapViz.d.ts +2 -26
  95. package/dist/umd/hooks/view-context/useHeatmapViz.d.ts.map +1 -1
  96. package/dist/umd/hooks/viz-elm/useHoveredElement.d.ts.map +1 -1
  97. package/dist/umd/hooks/viz-scale/useScaleCalculation.d.ts.map +1 -1
  98. package/dist/umd/index.js +2 -2
  99. package/dist/umd/stores/viz-click.d.ts +5 -4
  100. package/dist/umd/stores/viz-click.d.ts.map +1 -1
  101. package/dist/umd/types/viz-elm-callout.d.ts +60 -9
  102. package/dist/umd/types/viz-elm-callout.d.ts.map +1 -1
  103. package/package.json +4 -4
  104. package/dist/esm/components/VizElement/HoveredElementCallout.d.ts +0 -4
  105. package/dist/esm/components/VizElement/HoveredElementCallout.d.ts.map +0 -1
  106. package/dist/umd/components/VizElement/HoveredElementCallout.d.ts +0 -4
  107. package/dist/umd/components/VizElement/HoveredElementCallout.d.ts.map +0 -1
package/dist/esm/index.js CHANGED
@@ -41,6 +41,29 @@ const GraphView = ({ children, width, height }) => {
41
41
  return (jsxs(ReactFlow, { nodes: nodes, nodeTypes: nodeTypes, onNodesChange: onNodesChange, debug: true, minZoom: 0.5, maxZoom: 2, fitView: true, children: [jsx(Controls, {}), jsx(Background, {})] }));
42
42
  };
43
43
 
44
+ /**
45
+ * Default backdrop configuration
46
+ */
47
+ const BACKDROP_CONFIG = {
48
+ /**
49
+ * Default backdrop color
50
+ */
51
+ COLOR: '#000000',
52
+ /**
53
+ * Default backdrop opacity (0-1)
54
+ */
55
+ OPACITY: 0.5,
56
+ /**
57
+ * Default cutout expansion (pixels)
58
+ * Adds padding around the active element cutout
59
+ */
60
+ CUTOUT_EXPANSION: 2,
61
+ /**
62
+ * Z-index for backdrop canvas
63
+ */
64
+ Z_INDEX: 999,
65
+ };
66
+
44
67
  // Portal mode: Full permissions for proper functionality
45
68
  // Need allow-forms for add to cart, allow-popups for some features
46
69
  const HEATMAP_IFRAME = {
@@ -1422,7 +1445,8 @@ const AREA_RENDERER_SELECTORS = {
1422
1445
 
1423
1446
  const CALLOUT_PADDING = 0;
1424
1447
  const CALLOUT_ARROW_SIZE = 8;
1425
- const CALLOUT_HORIZONTAL_OFFSET = 0;
1448
+ const CALLOUT_OFFSET = { x: -8, y: 0 };
1449
+ const CALLOUT_ALIGNMENT = 'left';
1426
1450
  const CLICKED_ELEMENT_ID_BASE = 'gx-hm-clicked-element';
1427
1451
  const SECONDARY_CLICKED_ELEMENT_ID_BASE = 'gx-hm-secondary-clicked-element';
1428
1452
  const HOVERED_ELEMENT_ID_BASE = 'gx-hm-hovered-element';
@@ -2198,10 +2222,9 @@ function calculateRankPosition(rect, widthScale) {
2198
2222
  };
2199
2223
  }
2200
2224
 
2201
- const getViewportDimensions = (containerElm, scale, scrollOffset) => {
2225
+ const getViewportDimensions = (containerElm, scale) => {
2202
2226
  if (containerElm) {
2203
2227
  const containerRect = containerElm.getBoundingClientRect();
2204
- // If scale provided, adjust dimensions
2205
2228
  const width = scale ? containerRect.width / scale : containerRect.width;
2206
2229
  const height = scale ? containerRect.height / scale : containerRect.height;
2207
2230
  return { width, height };
@@ -2211,48 +2234,12 @@ const getViewportDimensions = (containerElm, scale, scrollOffset) => {
2211
2234
  height: window.innerHeight,
2212
2235
  };
2213
2236
  };
2214
- const getElementDimensions = (targetElm, calloutElm, scale, containerRect, mousePosition) => {
2215
- let targetRect;
2216
- if (mousePosition && containerRect && scale) {
2217
- // Create virtual target rect from mouse position
2218
- // Convert viewport coordinates to container-relative coordinates
2219
- const relativeX = (mousePosition.x - containerRect.left) / scale;
2220
- const relativeY = (mousePosition.y - containerRect.top) / scale;
2221
- // Create a small rect (1x1) at mouse position
2222
- targetRect = {
2223
- top: relativeY,
2224
- left: relativeX,
2225
- right: relativeX + 1,
2226
- bottom: relativeY + 1,
2227
- width: 1,
2228
- height: 1,
2229
- x: relativeX,
2230
- y: relativeY,
2231
- toJSON: () => ({}),
2232
- };
2233
- }
2234
- else if (mousePosition) {
2235
- // Fixed mode: use viewport coordinates directly
2236
- targetRect = {
2237
- top: mousePosition.y,
2238
- left: mousePosition.x,
2239
- right: mousePosition.x + 1,
2240
- bottom: mousePosition.y + 1,
2241
- width: 1,
2242
- height: 1,
2243
- x: mousePosition.x,
2244
- y: mousePosition.y,
2245
- toJSON: () => ({}),
2246
- };
2247
- }
2248
- else {
2249
- // No mouse position, use actual target element
2250
- targetRect = targetElm.getBoundingClientRect();
2251
- }
2237
+ const getElementDimensions = (options) => {
2238
+ const { targetElm, calloutElm, scale, containerElm } = options;
2239
+ const targetRect = targetElm.getBoundingClientRect();
2252
2240
  const calloutRect = calloutElm.getBoundingClientRect();
2253
- // If scale provided and no mousePosition, adjust dimensions for absolute positioning
2254
- if (scale && containerRect && !mousePosition) {
2255
- // Convert viewport coordinates to container-relative coordinates, then scale
2241
+ const containerRect = containerElm.getBoundingClientRect();
2242
+ if (scale && containerRect) {
2256
2243
  const relativeTop = (targetRect.top - containerRect.top) / scale;
2257
2244
  const relativeLeft = (targetRect.left - containerRect.left) / scale;
2258
2245
  const scaledWidth = targetRect.width / scale;
@@ -2274,7 +2261,6 @@ const getElementDimensions = (targetElm, calloutElm, scale, containerRect, mouse
2274
2261
  },
2275
2262
  };
2276
2263
  }
2277
- // Return with scaled callout rect if scale provided
2278
2264
  if (scale) {
2279
2265
  return {
2280
2266
  targetRect,
@@ -2298,23 +2284,42 @@ const getAlignmentOrder = (alignment) => {
2298
2284
  return ['right', 'center', 'left'];
2299
2285
  }
2300
2286
  };
2301
- const calculateLeftPosition = ({ targetRect, calloutRect, hozOffset, align, }) => {
2287
+ const calculateLeftPosition = (align, options) => {
2288
+ const { rectDimensions, offset } = options;
2289
+ const { targetRect, calloutRect } = rectDimensions;
2290
+ const { x: hozOffset } = offset;
2291
+ const relLeft = targetRect.left;
2292
+ const relRight = targetRect.right;
2293
+ const relWidth = targetRect.width;
2294
+ const calloutWidth = calloutRect.width;
2295
+ let left;
2302
2296
  switch (align) {
2303
2297
  case 'left':
2304
- return targetRect.left + hozOffset;
2298
+ left = relLeft + hozOffset;
2299
+ break;
2305
2300
  case 'right':
2306
- return targetRect.right - calloutRect.width - hozOffset;
2301
+ left = relRight - calloutWidth - hozOffset;
2302
+ break;
2307
2303
  case 'center':
2308
2304
  default:
2309
- return targetRect.left + targetRect.width / 2 - calloutRect.width / 2;
2305
+ left = relLeft + relWidth / 2 - calloutWidth / 2;
2306
+ break;
2310
2307
  }
2308
+ // No clamping - let validation determine if position is valid
2309
+ // If position would overflow, valid = false and system chooses different placement
2310
+ return left;
2311
2311
  };
2312
- const calculateVerticalPosition = (targetRect, calloutRect, placement, padding, arrowSize, offsetY = 0) => {
2312
+ const calculateVerticalPosition = (placement, options) => {
2313
+ const { rectDimensions, padding, arrowSize, offset } = options;
2314
+ const { targetRect, calloutRect } = rectDimensions;
2315
+ const { y: offsetY } = offset;
2313
2316
  return placement === 'top'
2314
2317
  ? targetRect.top - calloutRect.height - padding - arrowSize + offsetY
2315
2318
  : targetRect.bottom + padding + arrowSize + offsetY;
2316
2319
  };
2317
- const calculateHorizontalPlacementPosition = (targetRect, calloutRect, placement, padding, arrowSize) => {
2320
+ const calculateHorizontalPosition = (placement, options) => {
2321
+ const { rectDimensions, padding, arrowSize } = options;
2322
+ const { targetRect, calloutRect } = rectDimensions;
2318
2323
  const top = targetRect.top + targetRect.height / 2 - calloutRect.height / 2;
2319
2324
  const left = placement === 'right'
2320
2325
  ? targetRect.right + padding + arrowSize
@@ -2322,98 +2327,115 @@ const calculateHorizontalPlacementPosition = (targetRect, calloutRect, placement
2322
2327
  return { top, left };
2323
2328
  };
2324
2329
 
2325
- const isLeftPositionValid = (leftPos, calloutWidth, viewportWidth, padding, containerRect) => {
2326
- if (containerRect) {
2327
- return leftPos >= containerRect.left + padding && leftPos + calloutWidth <= containerRect.right - padding;
2328
- }
2329
- return leftPos >= padding && leftPos + calloutWidth <= viewportWidth - padding;
2330
+ const EPSILON = 0.1; // Tolerance for floating point errors
2331
+ const isLeftPositionValid = (leftPos, options) => {
2332
+ const { rectDimensions, viewport, padding, offset } = options;
2333
+ const { width: calloutWidth } = rectDimensions.calloutRect;
2334
+ const { width: viewportWidth } = viewport;
2335
+ const absLeft = rectDimensions.targetAbsoluteRect?.left ?? 0;
2336
+ const relLeftPos = absLeft + leftPos - offset.x;
2337
+ const maxViewportWidth = viewportWidth - padding + EPSILON;
2338
+ const isValidLeft = relLeftPos >= padding - EPSILON;
2339
+ const isRectCalloutShowValid = relLeftPos + calloutWidth <= maxViewportWidth;
2340
+ return isValidLeft && isRectCalloutShowValid;
2330
2341
  };
2331
- const isVerticalPositionValid = (targetRect, calloutRect, placement, viewportHeight, padding, arrowSize, containerRect) => {
2332
- if (containerRect) {
2333
- return placement === 'top'
2334
- ? targetRect.top - calloutRect.height - padding - arrowSize >= containerRect.top
2335
- : targetRect.bottom + calloutRect.height + padding + arrowSize <= containerRect.bottom;
2336
- }
2342
+ const isRightPositionValid = (leftPos, options) => {
2343
+ const { rectDimensions, viewport, padding } = options;
2344
+ const { width: calloutWidth } = rectDimensions.calloutRect;
2345
+ const { width: viewportWidth } = viewport;
2346
+ const maxViewportWidth = viewportWidth - padding + EPSILON;
2347
+ const isValidRight = leftPos - calloutWidth - padding - EPSILON <= maxViewportWidth;
2348
+ return isValidRight;
2349
+ };
2350
+ const isVerticalPositionValid = (placement, options) => {
2351
+ const { rectDimensions, viewport, padding, arrowSize } = options;
2352
+ const { targetRect, calloutRect } = rectDimensions;
2353
+ const { height: viewportHeight } = viewport;
2354
+ const { height: calloutHeight } = calloutRect;
2337
2355
  return placement === 'top'
2338
- ? targetRect.top - calloutRect.height - padding - arrowSize > 0
2339
- : targetRect.bottom + calloutRect.height + padding + arrowSize < viewportHeight;
2356
+ ? targetRect.top - calloutHeight - padding - arrowSize > -EPSILON
2357
+ : targetRect.bottom + calloutHeight + padding + arrowSize < viewportHeight + EPSILON;
2340
2358
  };
2341
- const isHorizontalPlacementValid = (targetRect, calloutRect, placement, viewportWidth, padding, arrowSize, containerRect) => {
2342
- if (containerRect) {
2343
- return placement === 'right'
2344
- ? targetRect.right + calloutRect.width + padding + arrowSize <= containerRect.right
2345
- : targetRect.left - calloutRect.width - padding - arrowSize >= containerRect.left;
2346
- }
2359
+ const isHorizontalPositionValid = (placement, options) => {
2360
+ const { rectDimensions, viewport, padding, arrowSize } = options;
2361
+ const { targetRect, calloutRect } = rectDimensions;
2362
+ const { width: viewportWidth } = viewport;
2363
+ const { width: calloutWidth } = calloutRect;
2347
2364
  return placement === 'right'
2348
- ? targetRect.right + calloutRect.width + padding + arrowSize < viewportWidth
2349
- : targetRect.left - calloutRect.width - padding - arrowSize > 0;
2365
+ ? targetRect.right + calloutWidth + padding + arrowSize < viewportWidth + EPSILON
2366
+ : targetRect.left - calloutWidth - padding - arrowSize > -EPSILON;
2350
2367
  };
2351
2368
 
2352
- const generateVerticalPositionCandidates = (targetRect, calloutRect, viewportHeight, viewportWidth, alignment, offset, padding, arrowSize, containerRect) => {
2369
+ const generateVerticalPositionCandidates = (options) => {
2370
+ const { alignment } = options;
2353
2371
  const candidates = [];
2354
2372
  const placements = ['top', 'bottom'];
2355
2373
  placements.forEach((placement) => {
2356
- const verticalPos = calculateVerticalPosition(targetRect, calloutRect, placement, padding, arrowSize, offset.y);
2357
- const verticalValid = isVerticalPositionValid(targetRect, calloutRect, placement, viewportHeight, padding, arrowSize, containerRect);
2374
+ const verticalPos = calculateVerticalPosition(placement, options);
2375
+ const verticalValid = isVerticalPositionValid(placement, options);
2358
2376
  const alignmentOrder = getAlignmentOrder(alignment);
2359
2377
  alignmentOrder.forEach((align) => {
2360
- const horizontalPos = calculateLeftPosition({
2361
- targetRect,
2362
- calloutRect,
2363
- hozOffset: offset.x,
2364
- align,
2365
- });
2366
- candidates.push({
2367
- placement,
2368
- top: verticalPos,
2369
- left: horizontalPos,
2370
- horizontalAlign: align,
2371
- valid: verticalValid && isLeftPositionValid(horizontalPos, calloutRect.width, viewportWidth, padding, containerRect),
2372
- });
2378
+ const leftPos = calculateLeftPosition(align, options);
2379
+ const isValidLeft = isLeftPositionValid(leftPos, options);
2380
+ const isValidRight = isRightPositionValid(leftPos, options);
2381
+ const candidate = { placement, top: verticalPos, left: leftPos, horizontalAlign: align, valid: false };
2382
+ switch (align) {
2383
+ case 'left':
2384
+ candidate.valid = verticalValid && isValidLeft;
2385
+ break;
2386
+ case 'right':
2387
+ candidate.valid = verticalValid && isValidRight;
2388
+ break;
2389
+ }
2390
+ candidates.push(candidate);
2373
2391
  });
2374
2392
  });
2375
2393
  return candidates;
2376
2394
  };
2377
- const generateHorizontalPositionCandidates = (targetRect, calloutRect, viewportWidth, padding, arrowSize, containerRect) => {
2395
+ const generateHorizontalPositionCandidates = (options) => {
2378
2396
  const placements = ['left', 'right'];
2379
2397
  return placements.map((placement) => {
2380
- const { top, left } = calculateHorizontalPlacementPosition(targetRect, calloutRect, placement, padding, arrowSize);
2381
- return {
2382
- placement,
2383
- top,
2384
- left,
2385
- horizontalAlign: 'center',
2386
- valid: isHorizontalPlacementValid(targetRect, calloutRect, placement, viewportWidth, padding, arrowSize, containerRect),
2387
- };
2398
+ const { top, left } = calculateHorizontalPosition(placement, options);
2399
+ const isValidHorizontal = isHorizontalPositionValid(placement, options);
2400
+ const candidate = { placement, top, left, horizontalAlign: 'center', valid: false };
2401
+ candidate.valid = isValidHorizontal;
2402
+ return candidate;
2388
2403
  });
2389
2404
  };
2390
- const generateAllPositionCandidates = (rectDimensions, viewport, alignment, offset, padding, arrowSize, containerRect) => {
2391
- const { targetRect, calloutRect } = rectDimensions;
2392
- const verticalCandidates = generateVerticalPositionCandidates(targetRect, calloutRect, viewport.height, viewport.width, alignment, offset, padding, arrowSize, containerRect);
2393
- const horizontalCandidates = generateHorizontalPositionCandidates(targetRect, calloutRect, viewport.width, padding, arrowSize, containerRect);
2405
+ const generateAllCandidates = (options) => {
2406
+ const verticalCandidates = generateVerticalPositionCandidates(options);
2407
+ const horizontalCandidates = generateHorizontalPositionCandidates(options);
2394
2408
  return [...verticalCandidates, ...horizontalCandidates];
2395
2409
  };
2396
2410
 
2397
2411
  const selectBestPosition = (candidates) => {
2398
2412
  return candidates.find((p) => p.valid) || candidates[0];
2399
2413
  };
2400
- const constrainToViewport = (position, calloutRect, viewport, padding, containerRect) => {
2414
+ const constrainToViewport = (candidate, options) => {
2415
+ const { containerRect, padding, rectDimensions, viewport } = options;
2416
+ const { calloutRect } = rectDimensions;
2417
+ const { left: leftPos, top: topPos } = candidate;
2401
2418
  if (containerRect) {
2402
- const left = Math.max(containerRect.left + padding, Math.min(position.left, containerRect.right - calloutRect.width - padding));
2403
- const top = Math.max(containerRect.top + padding, Math.min(position.top, containerRect.bottom - calloutRect.height - padding));
2419
+ const containerTop = containerRect.top + padding;
2420
+ const containerLeft = containerRect.left + padding;
2421
+ const containerRight = containerRect.right - calloutRect.width - padding;
2422
+ const containerBottom = containerRect.bottom - calloutRect.height - padding;
2423
+ const left = Math.max(containerLeft, Math.min(leftPos, containerRight));
2424
+ const top = Math.max(containerTop, Math.min(topPos, containerBottom));
2404
2425
  return { top, left };
2405
2426
  }
2406
- const left = Math.max(padding, Math.min(position.left, viewport.width - calloutRect.width - padding));
2407
- const top = Math.max(padding, Math.min(position.top, viewport.height - calloutRect.height - padding));
2427
+ const viewportLeft = padding;
2428
+ const viewportTop = padding;
2429
+ const viewportRight = viewport.width - calloutRect.width - padding;
2430
+ const viewportBottom = viewport.height - calloutRect.height - padding;
2431
+ const left = Math.max(viewportLeft, Math.min(leftPos, viewportRight));
2432
+ const top = Math.max(viewportTop, Math.min(topPos, viewportBottom));
2408
2433
  return { top, left };
2409
2434
  };
2410
2435
 
2411
- /**
2412
- * Get scroll offset from visualRef for absolute positioning
2413
- */
2414
- const getScrollOffset = (isAbsolute, visualRef) => {
2415
- if (!isAbsolute || !visualRef?.current)
2416
- return undefined;
2436
+ const getScrollOffset = (visualRef) => {
2437
+ if (!visualRef?.current)
2438
+ return;
2417
2439
  return {
2418
2440
  top: visualRef.current.scrollTop,
2419
2441
  left: visualRef.current.scrollLeft,
@@ -2424,17 +2446,19 @@ const getScrollOffset = (isAbsolute, visualRef) => {
2424
2446
  * - With scroll: represents visible area in container coordinates
2425
2447
  * - Without scroll: represents full container in container coordinates
2426
2448
  */
2427
- const createAdjustedContainerRect = (rawContainerRect, scale, scrollOffset) => {
2449
+ const createAdjustedContainerRect = (options) => {
2450
+ const { containerElm, scale, isAbsolute, visualRef } = options;
2451
+ const containerRect = containerElm.getBoundingClientRect();
2452
+ const scrollOffset = getScrollOffset(visualRef);
2428
2453
  // No scale = fixed positioning, use raw rect
2429
- if (!scale) {
2430
- return rawContainerRect;
2431
- }
2432
- const scaledWidth = rawContainerRect.width / scale;
2433
- const scaledHeight = rawContainerRect.height / scale;
2454
+ if (!scale)
2455
+ return containerRect;
2456
+ const scaledWidth = containerRect.width / scale;
2457
+ const scaledHeight = containerRect.height / scale;
2434
2458
  // Absolute positioning with scroll offset
2435
- if (scrollOffset) {
2459
+ if (isAbsolute && scrollOffset) {
2436
2460
  return {
2437
- ...rawContainerRect,
2461
+ ...containerRect,
2438
2462
  top: scrollOffset.top,
2439
2463
  left: scrollOffset.left,
2440
2464
  right: scrollOffset.left + scaledWidth,
@@ -2445,17 +2469,21 @@ const createAdjustedContainerRect = (rawContainerRect, scale, scrollOffset) => {
2445
2469
  }
2446
2470
  // Absolute positioning without scroll
2447
2471
  return {
2448
- ...rawContainerRect,
2472
+ ...containerRect,
2449
2473
  top: 0,
2450
2474
  left: 0,
2451
2475
  right: scaledWidth,
2452
- bottom: scaledHeight,
2453
2476
  width: scaledWidth,
2477
+ bottom: scaledHeight,
2454
2478
  height: scaledHeight,
2455
2479
  };
2456
2480
  };
2457
2481
  const calcCalloutPosition = (options) => {
2458
- const { targetElm, calloutElm, setPosition, offset = { x: CALLOUT_HORIZONTAL_OFFSET, y: 0 }, alignment = 'center', positionMode, widthScale, visualRef, mousePosition, } = options;
2482
+ const { targetElm, calloutElm, setPosition, positionMode, widthScale, visualRef } = options;
2483
+ const offset = options.offset ?? CALLOUT_OFFSET;
2484
+ const alignment = options.alignment ?? CALLOUT_ALIGNMENT;
2485
+ const padding = CALLOUT_PADDING;
2486
+ const arrowSize = CALLOUT_ARROW_SIZE;
2459
2487
  return () => {
2460
2488
  const isAbsolute = positionMode === 'absolute';
2461
2489
  const scale = isAbsolute ? widthScale : 1;
@@ -2465,35 +2493,94 @@ const calcCalloutPosition = (options) => {
2465
2493
  const containerElm = isAbsolute ? calloutElm.parentElement : visualRef?.current;
2466
2494
  if (!containerElm)
2467
2495
  return;
2468
- const rawContainerRect = containerElm.getBoundingClientRect();
2469
- const scrollOffset = getScrollOffset(isAbsolute, visualRef);
2470
- // Step 1: Get element dimensions
2471
- // For mousePosition: creates virtual 1x1 rect at mouse coordinates
2472
- // For targetElm: uses actual element rect
2473
- const rectDimensions = getElementDimensions(targetElm, calloutElm, scale, rawContainerRect, mousePosition);
2474
- // Step 2: Get viewport dimensions
2475
2496
  const viewport = getViewportDimensions(visualRef?.current, scale);
2476
- // Step 3: Adjust container rect for absolute positioning
2477
- // This rect represents the valid bounds for positioning
2478
- const adjustedContainerRect = createAdjustedContainerRect(rawContainerRect, scale, scrollOffset);
2479
- const padding = CALLOUT_PADDING;
2480
- const arrowSize = CALLOUT_ARROW_SIZE;
2481
- // Step 4: Generate all position candidates
2482
- const candidates = generateAllPositionCandidates(rectDimensions, viewport, alignment, offset, padding, arrowSize, adjustedContainerRect);
2483
- // Step 5: Select best valid position
2484
- const bestPosition = selectBestPosition(candidates);
2485
- // Step 6: Constrain to viewport/container bounds
2486
- const constrainedPosition = constrainToViewport({ top: bestPosition.top, left: bestPosition.left }, rectDimensions.calloutRect, viewport, padding, adjustedContainerRect);
2487
- // Step 7: Set final position
2497
+ const rectDimensions = getElementDimensions({ targetElm, calloutElm, scale, containerElm });
2498
+ const containerRect = createAdjustedContainerRect({ containerElm, scale, isAbsolute, visualRef });
2499
+ const options = {
2500
+ rectDimensions,
2501
+ viewport,
2502
+ alignment,
2503
+ offset,
2504
+ padding,
2505
+ arrowSize,
2506
+ containerRect,
2507
+ };
2508
+ const candidates = generateAllCandidates(options);
2509
+ const candidate = selectBestPosition(candidates);
2510
+ // Constrain to viewport/container bounds
2511
+ const constrainedCandidate = constrainToViewport(candidate, options);
2512
+ // Final callout position
2488
2513
  const finalPosition = {
2489
- top: constrainedPosition.top,
2490
- left: constrainedPosition.left,
2491
- placement: bestPosition.placement,
2492
- horizontalAlign: bestPosition.horizontalAlign,
2514
+ top: constrainedCandidate.top,
2515
+ left: constrainedCandidate.left,
2516
+ placement: candidate.placement,
2517
+ horizontalAlign: candidate.horizontalAlign,
2493
2518
  };
2494
2519
  setPosition(finalPosition);
2495
2520
  };
2496
2521
  };
2522
+ const calcCalloutPositionAbsolute = (props) => {
2523
+ const { widthScale, calloutElm, containerElm, element, onChange } = props;
2524
+ const mousePosition = element?.mousePosition;
2525
+ if (!mousePosition)
2526
+ return;
2527
+ const padding = props.padding ?? CALLOUT_PADDING;
2528
+ const arrowSize = props.arrowSize ?? CALLOUT_ARROW_SIZE;
2529
+ const rawCalloutRect = calloutElm.getBoundingClientRect();
2530
+ if (rawCalloutRect.width === 0 || rawCalloutRect.height === 0)
2531
+ return;
2532
+ const calloutRect = {
2533
+ ...rawCalloutRect,
2534
+ width: rawCalloutRect.width / widthScale,
2535
+ height: rawCalloutRect.height / widthScale,
2536
+ };
2537
+ const containerRect = containerElm.getBoundingClientRect();
2538
+ const containerWidth = containerRect.width / widthScale;
2539
+ const containerHeight = containerRect.height / widthScale;
2540
+ const mouseX = mousePosition.x;
2541
+ const mouseY = mousePosition.y;
2542
+ const targetRect = {
2543
+ top: mouseY,
2544
+ left: mouseX,
2545
+ right: mouseX + 1,
2546
+ bottom: mouseY + 1,
2547
+ width: 1,
2548
+ height: 1,
2549
+ x: mouseX,
2550
+ y: mouseY,
2551
+ toJSON: () => ({}),
2552
+ };
2553
+ const rectDimensions = {
2554
+ targetRect,
2555
+ calloutRect,
2556
+ targetAbsoluteRect: {
2557
+ top: element.top,
2558
+ left: element.left,
2559
+ },
2560
+ };
2561
+ const viewport = {
2562
+ width: containerWidth,
2563
+ height: containerHeight,
2564
+ };
2565
+ const options = {
2566
+ rectDimensions,
2567
+ viewport,
2568
+ alignment: CALLOUT_ALIGNMENT,
2569
+ offset: CALLOUT_OFFSET,
2570
+ padding,
2571
+ arrowSize,
2572
+ containerRect,
2573
+ };
2574
+ const candidates = generateAllCandidates(options);
2575
+ const bestPosition = selectBestPosition(candidates);
2576
+ const style = {
2577
+ position: 'absolute',
2578
+ top: bestPosition.top,
2579
+ left: bestPosition.left,
2580
+ zIndex: 1000,
2581
+ };
2582
+ onChange(style);
2583
+ };
2497
2584
 
2498
2585
  /**
2499
2586
  * Throttle a function using requestAnimationFrame
@@ -3639,6 +3726,59 @@ class IframeHeightProcessor {
3639
3726
  }
3640
3727
  }
3641
3728
 
3729
+ /**
3730
+ * Draw a backdrop overlay on canvas with a cutout for the active element
3731
+ * This creates a "spotlight" effect highlighting the active element
3732
+ */
3733
+ const drawBackdropWithCutout = (options) => {
3734
+ const { canvas, activeRect, backdropColor = '#000000', backdropOpacity = 0.5, cutoutExpansion = 0 } = options;
3735
+ const ctx = canvas.getContext('2d');
3736
+ if (!ctx)
3737
+ return;
3738
+ const { width: canvasWidth, height: canvasHeight } = canvas;
3739
+ // Apply expansion to the cutout rect
3740
+ const top = Math.max(0, activeRect.top - cutoutExpansion);
3741
+ const left = Math.max(0, activeRect.left - cutoutExpansion);
3742
+ const width = Math.min(canvasWidth - left, activeRect.width + cutoutExpansion * 2);
3743
+ const height = Math.min(canvasHeight - top, activeRect.height + cutoutExpansion * 2);
3744
+ // Clear previous drawing
3745
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight);
3746
+ // Set backdrop style
3747
+ ctx.fillStyle = backdropColor;
3748
+ ctx.globalAlpha = backdropOpacity;
3749
+ // Draw backdrop in 4 rectangles around the active element
3750
+ // This creates a cutout effect
3751
+ // Top rectangle (above active element)
3752
+ if (top > 0) {
3753
+ ctx.fillRect(0, 0, canvasWidth, top);
3754
+ }
3755
+ // Bottom rectangle (below active element)
3756
+ const bottomY = top + height;
3757
+ if (bottomY < canvasHeight) {
3758
+ ctx.fillRect(0, bottomY, canvasWidth, canvasHeight - bottomY);
3759
+ }
3760
+ // Left rectangle (left of active element)
3761
+ if (left > 0) {
3762
+ ctx.fillRect(0, top, left, height);
3763
+ }
3764
+ // Right rectangle (right of active element)
3765
+ const rightX = left + width;
3766
+ if (rightX < canvasWidth) {
3767
+ ctx.fillRect(rightX, top, canvasWidth - rightX, height);
3768
+ }
3769
+ // Reset alpha
3770
+ ctx.globalAlpha = 1.0;
3771
+ };
3772
+ /**
3773
+ * Clear the entire canvas
3774
+ */
3775
+ const clearCanvas = (canvas) => {
3776
+ const ctx = canvas.getContext('2d');
3777
+ if (!ctx)
3778
+ return;
3779
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
3780
+ };
3781
+
3642
3782
  function validateAreaCreation(dataInfo, hash, areas) {
3643
3783
  if (!dataInfo?.elementMapInfo || !dataInfo?.totalClicks) {
3644
3784
  logger$4.warn('Cannot create area: missing heatmap data');
@@ -4451,8 +4591,9 @@ const useHoveredElement = ({ iframeRef, getRect }) => {
4451
4591
  reset();
4452
4592
  return;
4453
4593
  }
4454
- setHoveredElement({ ...elementInfo, mousePosition: { x: event.clientX, y: event.clientY } });
4455
- }, [dataInfo, getRect, reset, getHashFromEvent, setHoveredElement]);
4594
+ const mousePosition = getElementMousePosition(event, widthScale);
4595
+ setHoveredElement({ ...elementInfo, mousePosition });
4596
+ }, [dataInfo, getRect, reset, widthScale, getHashFromEvent, setHoveredElement]);
4456
4597
  const handleMouseMove = useMemo(() => throttleRAF(onHandleMouseMove), [onHandleMouseMove]);
4457
4598
  const handleClick = useCallback((event, hash) => {
4458
4599
  if (!hash)
@@ -4461,12 +4602,12 @@ const useHoveredElement = ({ iframeRef, getRect }) => {
4461
4602
  setSelectedElement({ hash });
4462
4603
  return;
4463
4604
  }
4464
- const mousePosition = { x: event.clientX, y: event.clientY };
4605
+ const mousePosition = getElementMousePosition(event, widthScale);
4465
4606
  setSelectedElement({
4466
4607
  hash,
4467
4608
  mousePosition,
4468
4609
  });
4469
- }, [setSelectedElement]);
4610
+ }, [setSelectedElement, widthScale]);
4470
4611
  // Cleanup throttled function on unmount
4471
4612
  useEffect(() => {
4472
4613
  return () => {
@@ -4479,6 +4620,15 @@ const useHoveredElement = ({ iframeRef, getRect }) => {
4479
4620
  handleClick,
4480
4621
  };
4481
4622
  };
4623
+ const getElementMousePosition = (event, widthScale) => {
4624
+ const containerElm = event.target;
4625
+ if (!containerElm)
4626
+ return;
4627
+ const containerRect = containerElm.getBoundingClientRect();
4628
+ const elementRelativeX = (event.clientX - containerRect.left) / widthScale;
4629
+ const elementRelativeY = (event.clientY - containerRect.top) / widthScale;
4630
+ return { x: elementRelativeX, y: elementRelativeY };
4631
+ };
4482
4632
  const convertViewportToIframeCoords = (clientX, clientY, iframeRect, scale) => {
4483
4633
  let x = clientX - iframeRect.left;
4484
4634
  let y = clientY - iframeRect.top;
@@ -6416,6 +6566,8 @@ const useObserveIframeHeight = (props) => {
6416
6566
  return {};
6417
6567
  };
6418
6568
 
6569
+ // Max zoom ratio constant: 100% = fit to width
6570
+ const MAX_ZOOM_RATIO = 100;
6419
6571
  const useScaleCalculation = (props) => {
6420
6572
  const widthScale = useHeatmapViz((s) => s.widthScale);
6421
6573
  const zoomRatio = useHeatmapViz((s) => s.zoomRatio);
@@ -6427,23 +6579,31 @@ const useScaleCalculation = (props) => {
6427
6579
  const { containerWidth, containerHeight, contentWidth, contentHeight } = props;
6428
6580
  const calculateScaleResult = useCallback(() => {
6429
6581
  if (containerWidth > 0 && contentWidth > 0 && containerHeight > 0 && contentHeight > 0) {
6430
- // 1. Calculate widthScale (base scale from width)
6582
+ // 1. Calculate available dimensions
6431
6583
  const availableWidth = containerWidth - HEATMAP_CONFIG['padding'] * 2;
6432
- const widthScale = Math.min(availableWidth / contentWidth, 1);
6433
- // 2. Calculate available height
6434
6584
  const toolbarHeight = HEATMAP_CONFIG['heightToolbar'] || 0;
6435
6585
  const paddingTotal = HEATMAP_CONFIG['padding'] * 2;
6436
- const availableHeight = containerHeight - toolbarHeight - paddingTotal; // 10px buffer to avoid scroll bar
6437
- // 3. Calculate minZoomRatio (zoom ratio minimum to fit iframe in container)
6438
- const roundedMinZoomRatio = (availableHeight / (contentHeight * widthScale)) * 100;
6439
- // Limit minZoomRatio to a reasonable range (10-100)
6440
- const finalMinZoomRatio = Math.max(10, Math.min(roundedMinZoomRatio, 100));
6441
- // 4. Apply zoom ratio (cannot be less than minZoomRatio)
6442
- const clampedZoomRatio = Math.max(zoomRatio, finalMinZoomRatio);
6586
+ const availableHeight = containerHeight - toolbarHeight - paddingTotal;
6587
+ // 2. Calculate widthScale (base scale to fit content width into container width)
6588
+ // This represents 100% zoom (fit to width)
6589
+ const widthScale = Math.min(availableWidth / contentWidth, 1);
6590
+ // 3. Calculate minZoomRatio (zoom ratio to fit height)
6591
+ // At minZoomRatio, the content should fit entirely within the container height
6592
+ // Formula: contentHeight * widthScale * (minZoomRatio / 100) = availableHeight
6593
+ // => minZoomRatio = (availableHeight / (contentHeight * widthScale)) * 100
6594
+ const calculatedMinZoomRatio = (availableHeight / (contentHeight * widthScale)) * 100;
6595
+ // Limit minZoomRatio: cannot exceed MAX_ZOOM_RATIO (100%)
6596
+ // and should have a reasonable minimum (e.g., 1%)
6597
+ const finalMinZoomRatio = Math.max(1, Math.min(calculatedMinZoomRatio, MAX_ZOOM_RATIO));
6598
+ // 4. Apply zoom ratio (clamp between min and max)
6599
+ const clampedZoomRatio = Math.max(finalMinZoomRatio, Math.min(zoomRatio, MAX_ZOOM_RATIO));
6443
6600
  const zoomMultiplier = clampedZoomRatio / 100;
6444
6601
  // 5. Calculate finalScale
6602
+ // finalScale = widthScale * zoomMultiplier
6603
+ // At 100% zoom: finalScale = widthScale (content fits container width)
6604
+ // At minZoomRatio: finalScale fits content entirely in container
6445
6605
  const finalScale = widthScale * zoomMultiplier;
6446
- // 6. Check if it is currently fitted
6606
+ // 6. Check if it is currently fitted (at minimum zoom)
6447
6607
  const isCurrentlyFitted = zoomRatio <= finalMinZoomRatio;
6448
6608
  // 7. Update store
6449
6609
  setScale(finalScale);
@@ -7582,7 +7742,11 @@ const VizAreaClick = ({ iframeRef, visualRef, shadowRoot, autoCreateTopN = 10, e
7582
7742
  };
7583
7743
  VizAreaClick.displayName = 'VizAreaClick';
7584
7744
 
7585
- const RankBadgeComponent = ({ index, elementRect, widthScale, clickOnElement }) => {
7745
+ const RankBadgeComponent = ({ index, hash, elementRect, widthScale, show = true, clickOnElement, }) => {
7746
+ const clickedHash = useHeatmapClick((s) => s.selectedElement?.hash);
7747
+ const isShow = !!show && clickedHash !== hash;
7748
+ if (!isShow)
7749
+ return null;
7586
7750
  const style = calculateRankPosition(elementRect, widthScale);
7587
7751
  return (jsx("div", { className: "gx-hm-rank-badge", style: style, onClick: clickOnElement, children: index }));
7588
7752
  };
@@ -7600,7 +7764,7 @@ const DefaultRankBadgesComponent = ({ getRect, hidden }) => {
7600
7764
  const rect = getRect(element);
7601
7765
  if (!rect)
7602
7766
  return null;
7603
- return jsx(RankBadge, { index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash);
7767
+ return (jsx(RankBadge, { hash: element.hash, index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash));
7604
7768
  }) }));
7605
7769
  };
7606
7770
  DefaultRankBadgesComponent.displayName = 'DefaultRankBadges';
@@ -7612,10 +7776,40 @@ const DEFAULT_POSITION = {
7612
7776
  placement: 'top',
7613
7777
  horizontalAlign: 'center',
7614
7778
  };
7615
- const DEFAULT_MOUSE_OFFSET = { x: -8, y: 12 };
7779
+ const ElementCallout = (props) => {
7780
+ const viewId = useViewIdContext();
7781
+ const CompElementCallout = useHeatmapControlStore((state) => state.controls.ElementCallout);
7782
+ const calloutRef = useRef(null);
7783
+ const element = props.element;
7784
+ const positionMode = props.positionMode ?? DEFAULT_POSITION_MODE;
7785
+ const isAbsolute = positionMode === 'absolute';
7786
+ const position = useAnchorPosition(calloutRef, props);
7787
+ const className = `clarity-callout clarity-callout--${position.placement} clarity-callout--align-${position.horizontalAlign}`;
7788
+ const portalContainerId = getPortalElmId();
7789
+ if (!portalContainerId)
7790
+ return null;
7791
+ const calloutContent = (jsx("div", { ref: calloutRef, className: className, style: {
7792
+ position: positionMode,
7793
+ top: position.top,
7794
+ left: position.left,
7795
+ zIndex: Z_INDEX.CALLOUT,
7796
+ }, "aria-live": "assertive", role: "tooltip", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
7797
+ if (!document.getElementById(portalContainerId))
7798
+ return null;
7799
+ return createPortal(calloutContent, document.getElementById(portalContainerId));
7800
+ function getPortalElmId() {
7801
+ const containerElmId = `gx-hm-elements-${viewId}`;
7802
+ const vizContainerElmId = `gx-hm-viz-container-${viewId}`;
7803
+ if (!isAbsolute)
7804
+ return vizContainerElmId;
7805
+ if (!element.mousePosition)
7806
+ return containerElmId;
7807
+ return '';
7808
+ }
7809
+ };
7616
7810
  const useAnchorPosition = (calloutRef, props) => {
7617
7811
  const widthScale = useHeatmapViz((s) => s.widthScale);
7618
- const { target, visualRef, offset = DEFAULT_MOUSE_OFFSET, alignment, element, positionMode } = props;
7812
+ const { target, visualRef, alignment, element, positionMode } = props;
7619
7813
  const [position, setPosition] = useState(DEFAULT_POSITION);
7620
7814
  const isAbsolute = positionMode === 'absolute';
7621
7815
  const mousePosition = element.mousePosition;
@@ -7630,15 +7824,11 @@ const useAnchorPosition = (calloutRef, props) => {
7630
7824
  targetElm,
7631
7825
  calloutElm,
7632
7826
  setPosition,
7633
- offset,
7634
7827
  alignment,
7635
7828
  positionMode,
7636
7829
  visualRef,
7637
7830
  widthScale,
7638
- mousePosition,
7639
7831
  });
7640
- // Delay initial calculation to allow scroll animations to complete
7641
- // Use multiple frames to ensure layout is stable
7642
7832
  const rafId1 = requestAnimationFrame(() => {
7643
7833
  requestAnimationFrame(() => {
7644
7834
  positionFn();
@@ -7647,13 +7837,9 @@ const useAnchorPosition = (calloutRef, props) => {
7647
7837
  const handleUpdate = () => {
7648
7838
  requestAnimationFrame(positionFn);
7649
7839
  };
7650
- // Listen to events based on positioning mode
7651
- // Absolute mode: position is in container coordinates, doesn't change with scroll
7652
- // Fixed mode: position is in viewport coordinates, needs scroll updates
7653
7840
  const visualContainer = visualRef?.current;
7654
7841
  window.addEventListener('resize', handleUpdate);
7655
7842
  if (!isAbsolute) {
7656
- // Fixed mode: listen to scroll events for viewport position updates
7657
7843
  window.addEventListener('scroll', handleUpdate, true);
7658
7844
  visualContainer?.addEventListener('scroll', handleUpdate);
7659
7845
  }
@@ -7665,81 +7851,190 @@ const useAnchorPosition = (calloutRef, props) => {
7665
7851
  visualContainer?.removeEventListener('scroll', handleUpdate);
7666
7852
  }
7667
7853
  };
7668
- }, [element, target, visualRef, offset, alignment, isAbsolute, widthScale, calloutRef, mousePosition, positionMode]);
7854
+ }, [element, target, visualRef, alignment, isAbsolute, widthScale, calloutRef, mousePosition, positionMode]);
7669
7855
  return position;
7670
7856
  };
7671
- const ElementCallout = (props) => {
7672
- const viewId = useViewIdContext();
7673
- const CompElementCallout = useHeatmapControlStore((state) => state.controls.ElementCallout);
7674
- const calloutRef = useRef(null);
7675
- const element = props.element;
7676
- const positionMode = props.positionMode ?? DEFAULT_POSITION_MODE;
7677
- const isAbsolute = positionMode === 'absolute';
7678
- const position = useAnchorPosition(calloutRef, props);
7679
- const className = `clarity-callout clarity-callout--${position.placement} clarity-callout--align-${position.horizontalAlign}`;
7680
- // Determine portal container based on position mode
7681
- const containerElmId = `gx-hm-elements-${viewId}`;
7682
- const vizContainerElmId = `gx-hm-viz-container-${viewId}`;
7683
- const portalContainerId = isAbsolute ? containerElmId : vizContainerElmId;
7684
- const calloutContent = (jsx("div", { ref: calloutRef, className: className, style: {
7685
- position: positionMode,
7686
- top: position.top,
7687
- left: position.left,
7688
- zIndex: Z_INDEX.CALLOUT,
7689
- }, "aria-live": "assertive", role: "tooltip", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
7690
- return createPortal(calloutContent, document.getElementById(portalContainerId));
7691
- };
7692
7857
 
7693
- const ElementMissing = ({ show = true }) => {
7858
+ const ElementMissing = ({ show = true, visualRef }) => {
7694
7859
  const widthScale = useHeatmapViz((s) => s.widthScale);
7860
+ const missingElementRef = useRef(null);
7861
+ const wrapperWidth = useHeatmapConfigStore((s) => s.width);
7862
+ const [scrollPosition, setScrollPosition] = useState({ scrollTop: 0, scrollLeft: 0 });
7863
+ useEffect(() => {
7864
+ const container = visualRef.current;
7865
+ if (!container)
7866
+ return;
7867
+ const updateScrollPosition = () => {
7868
+ setScrollPosition({
7869
+ scrollTop: container.scrollTop,
7870
+ scrollLeft: container.scrollLeft,
7871
+ });
7872
+ };
7873
+ // Initial position
7874
+ updateScrollPosition();
7875
+ // Listen to scroll events
7876
+ container.addEventListener('scroll', updateScrollPosition);
7877
+ return () => {
7878
+ container.removeEventListener('scroll', updateScrollPosition);
7879
+ };
7880
+ }, [visualRef]);
7695
7881
  if (!show)
7696
7882
  return null;
7697
- return (jsx("div", { className: "missingElement", style: {
7698
- position: 'fixed',
7699
- top: '50%',
7700
- left: '50%',
7701
- transform: `translate(-50%, -50%) scale(${1 / widthScale})`,
7702
- background: 'rgba(0, 0, 0, 0.8)',
7703
- color: 'white',
7704
- padding: '12px 20px',
7705
- borderRadius: '8px',
7706
- fontSize: '14px',
7707
- fontWeight: '500',
7708
- zIndex: 9999,
7709
- pointerEvents: 'none',
7710
- whiteSpace: 'nowrap',
7711
- }, "aria-live": "assertive", children: "Element not visible on current screen" }));
7883
+ const container = visualRef.current;
7884
+ const containerRect = container?.getBoundingClientRect();
7885
+ const elementRect = missingElementRef.current?.getBoundingClientRect();
7886
+ const elementHeightCenter = elementRect?.height ?? 0;
7887
+ const scrollTop = scrollPosition.scrollTop ?? 0;
7888
+ const containerHeight = containerRect?.height ?? 0;
7889
+ const topPosition = scrollTop + (containerHeight + elementHeightCenter) / 2;
7890
+ const topPositionScaled = topPosition / widthScale;
7891
+ const leftPosition = wrapperWidth / 2;
7892
+ return (jsxs(Fragment, { children: [jsx("div", { className: "missingElement-backdrop", style: {
7893
+ position: 'absolute',
7894
+ top: 0,
7895
+ left: 0,
7896
+ right: 0,
7897
+ bottom: 0,
7898
+ background: 'rgba(0, 0, 0, 0.5)',
7899
+ zIndex: 9998,
7900
+ pointerEvents: 'none',
7901
+ } }), jsx("div", { ref: missingElementRef, className: "missingElement", style: {
7902
+ position: 'absolute',
7903
+ top: topPositionScaled,
7904
+ left: leftPosition,
7905
+ transform: `translate(-50%, -50%) scale(${1 / widthScale})`,
7906
+ background: 'rgba(0, 0, 0, 0.8)',
7907
+ color: 'white',
7908
+ padding: '12px 20px',
7909
+ borderRadius: '8px',
7910
+ fontSize: '14px',
7911
+ fontWeight: '500',
7912
+ zIndex: 9999,
7913
+ pointerEvents: 'none',
7914
+ whiteSpace: 'nowrap',
7915
+ }, "aria-live": "assertive", children: "Element not visible on current screen" })] }));
7712
7916
  };
7713
7917
 
7714
- const ElementOverlayComponent = ({ type, element, onClick, elementId }) => {
7918
+ /**
7919
+ * Example component showing how to use canvas backdrop
7920
+ * Renders a dark overlay with cutout for active element
7921
+ */
7922
+ const BackdropCanvas = ({ activeElement, viewportWidth, viewportHeight, borderWidth = 0, show = true, cutoutExpansion = BACKDROP_CONFIG.CUTOUT_EXPANSION, backdropColor = BACKDROP_CONFIG.COLOR, backdropOpacity = BACKDROP_CONFIG.OPACITY, }) => {
7923
+ const canvasRef = useRef(null);
7924
+ useEffect(() => {
7925
+ const canvas = canvasRef.current;
7926
+ if (!canvas || !show)
7927
+ return;
7928
+ // Set canvas dimensions
7929
+ canvas.width = viewportWidth;
7930
+ canvas.height = viewportHeight;
7931
+ if (!activeElement || (activeElement.width === 0 && activeElement.height === 0)) {
7932
+ // No active element - clear canvas
7933
+ clearCanvas(canvas);
7934
+ return;
7935
+ }
7936
+ // Draw backdrop with cutout for active element
7937
+ drawBackdropWithCutout({
7938
+ canvas,
7939
+ activeRect: {
7940
+ top: activeElement.top + borderWidth,
7941
+ left: activeElement.left + borderWidth,
7942
+ width: activeElement.width,
7943
+ height: activeElement.height,
7944
+ },
7945
+ backdropColor,
7946
+ backdropOpacity,
7947
+ cutoutExpansion,
7948
+ });
7949
+ }, [
7950
+ activeElement,
7951
+ viewportWidth,
7952
+ viewportHeight,
7953
+ borderWidth,
7954
+ show,
7955
+ cutoutExpansion,
7956
+ backdropColor,
7957
+ backdropOpacity,
7958
+ ]);
7959
+ if (!show)
7960
+ return null;
7961
+ return (jsx("canvas", { ref: canvasRef, style: {
7962
+ position: 'absolute',
7963
+ top: 0,
7964
+ left: 0,
7965
+ width: viewportWidth,
7966
+ height: viewportHeight,
7967
+ pointerEvents: 'none', // Allow clicks to pass through
7968
+ zIndex: BACKDROP_CONFIG.Z_INDEX, // Below callout but above content
7969
+ } }));
7970
+ };
7971
+
7972
+ const ElementCalloutOverlay = (props) => {
7973
+ const { element, containerRef } = props;
7974
+ const widthScale = useHeatmapViz((s) => s.widthScale);
7975
+ const CompElementCallout = useHeatmapControlStore((state) => state.controls.ElementCallout);
7976
+ const calloutRef = useRef(null);
7977
+ const [calloutStyle, setCalloutStyle] = useState(undefined);
7978
+ useEffect(() => {
7979
+ const calloutElm = calloutRef.current;
7980
+ const containerElm = containerRef?.current;
7981
+ if (!element || !calloutElm || !containerElm)
7982
+ return;
7983
+ calcCalloutPositionAbsolute({
7984
+ widthScale,
7985
+ calloutElm,
7986
+ containerElm,
7987
+ element,
7988
+ onChange: setCalloutStyle,
7989
+ });
7990
+ }, [element, widthScale, containerRef]);
7991
+ if (!element)
7992
+ return null;
7993
+ return (jsx("div", { ref: calloutRef, style: calloutStyle, className: "clarity-callout", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
7994
+ };
7995
+ ElementCalloutOverlay.displayName = 'ElementCalloutOverlay';
7996
+
7997
+ const ElementOverlayComponent = (props) => {
7998
+ const { type, element, onClick, elementId } = props;
7715
7999
  const widthScale = useHeatmapViz((s) => s.widthScale);
8000
+ const viewportHeight = useHeatmapVizRect((s) => s.iframeHeight);
8001
+ const viewportWidth = useHeatmapConfigStore((s) => s.width);
7716
8002
  const overlayStyle = useMemo(() => {
7717
- if (!element || (element.width === 0 && element.height === 0))
8003
+ const isInvalid = !element || (element.width === 0 && element.height === 0);
8004
+ if (isInvalid)
7718
8005
  return null;
7719
8006
  return {
7720
8007
  top: element.top + HEATMAP_CONFIG['borderWidthIframe'],
7721
8008
  left: element.left + HEATMAP_CONFIG['borderWidthIframe'],
7722
8009
  width: element.width,
7723
8010
  height: element.height,
7724
- cursor: 'pointer',
7725
8011
  };
7726
8012
  }, [element]);
7727
8013
  if (!overlayStyle)
7728
8014
  return null;
7729
8015
  const isHovered = type === 'hovered';
7730
8016
  const badgeWidthScale = isHovered ? 1 : widthScale;
7731
- return (jsxs(Fragment$1, { children: [jsx("div", { onClick: onClick, className: `heatmapElement heatmapElement--${type}`, id: elementId, style: overlayStyle }), jsx(RankBadge, { index: element.rank, elementRect: element, widthScale: badgeWidthScale, clickOnElement: onClick })] }));
8017
+ const showCallout = !!element?.mousePosition && !isHovered;
8018
+ return (jsxs(Fragment$1, { children: [jsx("div", { onClick: onClick, className: `heatmapElement heatmapElement--${type}`, id: elementId, style: overlayStyle, children: showCallout && jsx(ElementCalloutOverlay, { ...props }) }), jsx(BackdropCanvas, { activeElement: overlayStyle, viewportWidth: viewportWidth, viewportHeight: viewportHeight, show: !isHovered }), jsx(RankBadge, { hash: element.hash, show: isHovered, index: element.rank, elementRect: element, widthScale: badgeWidthScale, clickOnElement: onClick })] }));
7732
8019
  };
7733
8020
  ElementOverlayComponent.displayName = 'ElementOverlay';
7734
8021
  const ElementOverlay = memo(ElementOverlayComponent);
7735
8022
 
7736
- const HoveredElementCalloutComponent = ({ target }) => {
7737
- const hoveredElement = useHeatmapHover((s) => s.hoveredElement);
7738
- if (!hoveredElement)
8023
+ const ElementCalloutClickedComponent = (props) => {
8024
+ const viewId = useViewIdContext();
8025
+ const { clickedElement, showMissingElement, shouldShowCallout } = useClickedElement({
8026
+ visualRef: props.visualRef,
8027
+ getRect: props.getRect,
8028
+ });
8029
+ const elementId = getClickedElementId(viewId, props.isSecondary);
8030
+ if (!clickedElement && showMissingElement)
8031
+ return jsx(ElementMissing, { visualRef: props.visualRef });
8032
+ if (!clickedElement)
7739
8033
  return null;
7740
- return jsx(ElementCallout, { element: hoveredElement, target: target, visualRef: { current: null } });
8034
+ const isShowClickedElement = shouldShowCallout && !clickedElement?.mousePosition;
8035
+ return (jsxs(Fragment, { children: [jsx(ElementOverlay, { type: "clicked", element: clickedElement, elementId: elementId, containerRef: props.containerRef }), isShowClickedElement && (jsx(ElementCallout, { element: clickedElement, target: `#${elementId}`, visualRef: props.visualRef, positionMode: props.positionMode }))] }));
7741
8036
  };
7742
- memo(HoveredElementCalloutComponent);
8037
+ const ElementCalloutClicked = memo(ElementCalloutClickedComponent);
7743
8038
 
7744
8039
  const HoveredElementOverlayComponent = ({ onClick }) => {
7745
8040
  const viewId = useViewIdContext();
@@ -7756,16 +8051,19 @@ const HoveredElementOverlayComponent = ({ onClick }) => {
7756
8051
  };
7757
8052
  const HoveredElementOverlay = memo(HoveredElementOverlayComponent);
7758
8053
 
7759
- const ELEMENT_CALLOUT = {
7760
- offset: { x: -8, y: 0 },
7761
- alignment: 'left',
8054
+ const IS_SHOW_CALLOUT = false;
8055
+ const ElementCalloutHoveredComponent = (props) => {
8056
+ const viewId = useViewIdContext();
8057
+ useHeatmapHover((s) => s.hoveredElement);
8058
+ getHoveredElementId(viewId, props.isSecondary);
8059
+ const isShowCallout = IS_SHOW_CALLOUT ;
8060
+ return (jsxs(Fragment, { children: [jsx(HoveredElementOverlay, { onClick: props.onClick }), isShowCallout ] }));
7762
8061
  };
7763
- const IS_SHOW_HOVERED_ELEMENT = false;
8062
+ const ElementCalloutHovered = memo(ElementCalloutHoveredComponent);
8063
+
7764
8064
  const HeatmapElements = (props) => {
7765
8065
  const viewId = useViewIdContext();
7766
8066
  const iframeHeight = useHeatmapVizRect((s) => s.iframeHeight);
7767
- const clickedElementId = getClickedElementId(viewId, props.isSecondary);
7768
- getHoveredElementId(viewId, props.isSecondary);
7769
8067
  const elementCalloutRef = useRef(null);
7770
8068
  const { iframeDimensions, isVisible = true, areDefaultRanksHidden, positionMode } = props;
7771
8069
  const { getRect } = useHeatmapElementPosition({
@@ -7773,10 +8071,6 @@ const HeatmapElements = (props) => {
7773
8071
  wrapperRef: props.wrapperRef,
7774
8072
  visualizer: props.visualizer,
7775
8073
  });
7776
- const { clickedElement, showMissingElement, shouldShowCallout } = useClickedElement({
7777
- visualRef: props.visualRef,
7778
- getRect,
7779
- });
7780
8074
  const { handleMouseMove, handleMouseLeave, handleClick } = useHoveredElement({
7781
8075
  iframeRef: props.iframeRef,
7782
8076
  getRect,
@@ -7786,8 +8080,7 @@ const HeatmapElements = (props) => {
7786
8080
  useRenderCount('HeatmapElements');
7787
8081
  if (!isVisible)
7788
8082
  return null;
7789
- const isShowClickedElement = shouldShowCallout && clickedElement;
7790
- return (jsxs("div", { id: `gx-hm-elements-${viewId}`, ref: elementCalloutRef, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, className: "gx-hm-elements", style: { ...iframeDimensions, height: `${iframeHeight}px` }, children: [jsx(ElementMissing, { show: showMissingElement }), jsx(DefaultRankBadges, { getRect: getRect, hidden: areDefaultRanksHidden }), jsx(ElementOverlay, { type: "clicked", element: clickedElement, elementId: clickedElementId }), jsx(HoveredElementOverlay, { onClick: handleClick }), IS_SHOW_HOVERED_ELEMENT , isShowClickedElement && (jsx(ElementCallout, { element: clickedElement, target: `#${clickedElementId}`, visualRef: props.visualRef, positionMode: props.positionMode, ...ELEMENT_CALLOUT }))] }));
8083
+ return (jsxs("div", { id: `gx-hm-elements-${viewId}`, ref: elementCalloutRef, onMouseMove: handleMouseMove, onMouseLeave: handleMouseLeave, className: "gx-hm-elements", style: { ...iframeDimensions, height: `${iframeHeight}px` }, children: [jsx(DefaultRankBadges, { getRect: getRect, hidden: areDefaultRanksHidden }), jsx(ElementCalloutClicked, { visualRef: props.visualRef, positionMode: props.positionMode, getRect: getRect, isSecondary: props.isSecondary, containerRef: elementCalloutRef }), jsx(ElementCalloutHovered, { visualRef: props.visualRef, onClick: handleClick, isSecondary: props.isSecondary, positionMode: props.positionMode })] }));
7791
8084
  };
7792
8085
 
7793
8086
  const VizElements = ({ iframeRef, visualRef, wrapperRef }) => {
@@ -8070,6 +8363,7 @@ const VizScrollMap = ({ iframeRef, wrapperRef }) => {
8070
8363
  width: `calc(100% - 4px)`,
8071
8364
  height: '100%',
8072
8365
  transform: 'translateZ(0)',
8366
+ zIndex: 2,
8073
8367
  }, children: [jsx(ScrollmapMarker, { iframeRef: iframeRef, wrapperRef: wrapperRef }), jsx(AverageFoldLine, { iframeRef: iframeRef, wrapperRef: wrapperRef }), jsx(ScrollMapOverlay, { wrapperRef: wrapperRef, iframeRef: iframeRef })] }));
8074
8368
  };
8075
8369
 
@@ -8248,4 +8542,4 @@ const HeatmapLayout = ({ data, clickmap, clickAreas, scrollmap, controls, dataIn
8248
8542
  }
8249
8543
  };
8250
8544
 
8251
- export { DEFAULT_SIDEBAR_WIDTH, DEFAULT_VIEW_ID, GraphView, HEATMAP_CONFIG, HEATMAP_IFRAME, HEATMAP_STYLE, HeatmapLayout, IClickMode, IClickType, IHeatmapType, IScrollType, ViewIdContext, Z_INDEX, compareViewPerformance, convertViewportToIframeCoords, createStorePerformanceTracker, downloadPerformanceReport, getCompareViewId, getMetricsByViewId, getPerformanceReportJSON, getScrollGradientColor, performanceLogger, printPerformanceSummary, scrollToElementIfNeeded, sendPerformanceReport, serializeAreas, trackStoreAction, useAreaCreation, useAreaEditMode, useAreaFilterVisible, useAreaHydration, useAreaInteraction, useAreaPositionsUpdater, useAreaRectSync, useAreaRendererContainer, useAreaTopAutoDetect, useClickedElement, useDebounceCallback, useElementCalloutVisible, useHeatmapAreaClick, useHeatmapCanvas, useHeatmapClick, useHeatmapCompareStore, useHeatmapConfigStore, useHeatmapCopyView, useHeatmapData, useHeatmapEffects, useHeatmapElementPosition, useHeatmapHover, useHeatmapLiveStore, useHeatmapRenderByMode, useHeatmapScale, useHeatmapScroll, useHeatmapViz, useHeatmapVizRect, useHoveredElement, useIframeHeight, useIframeHeightProcessor, useMeasureFunction, useRegisterConfig, useRegisterControl, useRegisterData, useRegisterHeatmap, useRenderCount, useScrollmapZones, useTrackHookCall, useViewIdContext, useVizLiveRender, useWhyDidYouUpdate, useWrapperRefHeight, useZonePositions, withPerformanceTracking };
8545
+ export { BACKDROP_CONFIG, DEFAULT_SIDEBAR_WIDTH, DEFAULT_VIEW_ID, GraphView, HEATMAP_CONFIG, HEATMAP_IFRAME, HEATMAP_STYLE, HeatmapLayout, IClickMode, IClickType, IHeatmapType, IScrollType, ViewIdContext, Z_INDEX, compareViewPerformance, convertViewportToIframeCoords, createStorePerformanceTracker, downloadPerformanceReport, getCompareViewId, getMetricsByViewId, getPerformanceReportJSON, getScrollGradientColor, performanceLogger, printPerformanceSummary, scrollToElementIfNeeded, sendPerformanceReport, serializeAreas, trackStoreAction, useAreaCreation, useAreaEditMode, useAreaFilterVisible, useAreaHydration, useAreaInteraction, useAreaPositionsUpdater, useAreaRectSync, useAreaRendererContainer, useAreaTopAutoDetect, useClickedElement, useDebounceCallback, useElementCalloutVisible, useHeatmapAreaClick, useHeatmapCanvas, useHeatmapClick, useHeatmapCompareStore, useHeatmapConfigStore, useHeatmapCopyView, useHeatmapData, useHeatmapEffects, useHeatmapElementPosition, useHeatmapHover, useHeatmapLiveStore, useHeatmapRenderByMode, useHeatmapScale, useHeatmapScroll, useHeatmapViz, useHeatmapVizRect, useHoveredElement, useIframeHeight, useIframeHeightProcessor, useMeasureFunction, useRegisterConfig, useRegisterControl, useRegisterData, useRegisterHeatmap, useRenderCount, useScrollmapZones, useTrackHookCall, useViewIdContext, useVizLiveRender, useWhyDidYouUpdate, useWrapperRefHeight, useZonePositions, withPerformanceTracking };