@gemx-dev/heatmap-react 3.5.56 → 3.5.57

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 (99) 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/configs/backdrop.d.ts +23 -0
  20. package/dist/esm/configs/backdrop.d.ts.map +1 -0
  21. package/dist/esm/configs/index.d.ts +1 -0
  22. package/dist/esm/configs/index.d.ts.map +1 -1
  23. package/dist/esm/constants/viz-elm-callout.d.ts +6 -1
  24. package/dist/esm/constants/viz-elm-callout.d.ts.map +1 -1
  25. package/dist/esm/helpers/canvas-backdrop.d.ts +28 -0
  26. package/dist/esm/helpers/canvas-backdrop.d.ts.map +1 -0
  27. package/dist/esm/helpers/index.d.ts +1 -0
  28. package/dist/esm/helpers/index.d.ts.map +1 -1
  29. package/dist/esm/helpers/viz-elm-callout/dimensions.d.ts +9 -8
  30. package/dist/esm/helpers/viz-elm-callout/dimensions.d.ts.map +1 -1
  31. package/dist/esm/helpers/viz-elm-callout/position-calculator.d.ts +4 -12
  32. package/dist/esm/helpers/viz-elm-callout/position-calculator.d.ts.map +1 -1
  33. package/dist/esm/helpers/viz-elm-callout/position-candidates.d.ts +4 -10
  34. package/dist/esm/helpers/viz-elm-callout/position-candidates.d.ts.map +1 -1
  35. package/dist/esm/helpers/viz-elm-callout/position-selector.d.ts +2 -8
  36. package/dist/esm/helpers/viz-elm-callout/position-selector.d.ts.map +1 -1
  37. package/dist/esm/helpers/viz-elm-callout/position-validator.d.ts +5 -3
  38. package/dist/esm/helpers/viz-elm-callout/position-validator.d.ts.map +1 -1
  39. package/dist/esm/helpers/viz-elm-callout/viz-elm.d.ts +2 -20
  40. package/dist/esm/helpers/viz-elm-callout/viz-elm.d.ts.map +1 -1
  41. package/dist/esm/hooks/viz-elm/useHoveredElement.d.ts.map +1 -1
  42. package/dist/esm/index.js +505 -222
  43. package/dist/esm/index.mjs +505 -222
  44. package/dist/esm/stores/viz-click.d.ts +5 -4
  45. package/dist/esm/stores/viz-click.d.ts.map +1 -1
  46. package/dist/esm/types/viz-elm-callout.d.ts +60 -9
  47. package/dist/esm/types/viz-elm-callout.d.ts.map +1 -1
  48. package/dist/style.css +3 -1
  49. package/dist/umd/components/VizElement/BackdropCanvas.d.ts +38 -0
  50. package/dist/umd/components/VizElement/BackdropCanvas.d.ts.map +1 -0
  51. package/dist/umd/components/VizElement/DefaultRankBadges.d.ts.map +1 -1
  52. package/dist/umd/components/VizElement/ElementCallout.d.ts.map +1 -1
  53. package/dist/umd/components/VizElement/ElementCalloutClicked.d.ts +12 -0
  54. package/dist/umd/components/VizElement/ElementCalloutClicked.d.ts.map +1 -0
  55. package/dist/umd/components/VizElement/ElementCalloutHovered.d.ts +10 -0
  56. package/dist/umd/components/VizElement/ElementCalloutHovered.d.ts.map +1 -0
  57. package/dist/umd/components/VizElement/ElementCalloutOverlay.d.ts +3 -0
  58. package/dist/umd/components/VizElement/ElementCalloutOverlay.d.ts.map +1 -0
  59. package/dist/umd/components/VizElement/ElementMissing.d.ts +1 -0
  60. package/dist/umd/components/VizElement/ElementMissing.d.ts.map +1 -1
  61. package/dist/umd/components/VizElement/ElementOverlay.d.ts +3 -3
  62. package/dist/umd/components/VizElement/ElementOverlay.d.ts.map +1 -1
  63. package/dist/umd/components/VizElement/HeatmapElements.d.ts +1 -1
  64. package/dist/umd/components/VizElement/HeatmapElements.d.ts.map +1 -1
  65. package/dist/umd/components/VizElement/RankBadge.d.ts +2 -0
  66. package/dist/umd/components/VizElement/RankBadge.d.ts.map +1 -1
  67. package/dist/umd/configs/backdrop.d.ts +23 -0
  68. package/dist/umd/configs/backdrop.d.ts.map +1 -0
  69. package/dist/umd/configs/index.d.ts +1 -0
  70. package/dist/umd/configs/index.d.ts.map +1 -1
  71. package/dist/umd/constants/viz-elm-callout.d.ts +6 -1
  72. package/dist/umd/constants/viz-elm-callout.d.ts.map +1 -1
  73. package/dist/umd/helpers/canvas-backdrop.d.ts +28 -0
  74. package/dist/umd/helpers/canvas-backdrop.d.ts.map +1 -0
  75. package/dist/umd/helpers/index.d.ts +1 -0
  76. package/dist/umd/helpers/index.d.ts.map +1 -1
  77. package/dist/umd/helpers/viz-elm-callout/dimensions.d.ts +9 -8
  78. package/dist/umd/helpers/viz-elm-callout/dimensions.d.ts.map +1 -1
  79. package/dist/umd/helpers/viz-elm-callout/position-calculator.d.ts +4 -12
  80. package/dist/umd/helpers/viz-elm-callout/position-calculator.d.ts.map +1 -1
  81. package/dist/umd/helpers/viz-elm-callout/position-candidates.d.ts +4 -10
  82. package/dist/umd/helpers/viz-elm-callout/position-candidates.d.ts.map +1 -1
  83. package/dist/umd/helpers/viz-elm-callout/position-selector.d.ts +2 -8
  84. package/dist/umd/helpers/viz-elm-callout/position-selector.d.ts.map +1 -1
  85. package/dist/umd/helpers/viz-elm-callout/position-validator.d.ts +5 -3
  86. package/dist/umd/helpers/viz-elm-callout/position-validator.d.ts.map +1 -1
  87. package/dist/umd/helpers/viz-elm-callout/viz-elm.d.ts +2 -20
  88. package/dist/umd/helpers/viz-elm-callout/viz-elm.d.ts.map +1 -1
  89. package/dist/umd/hooks/viz-elm/useHoveredElement.d.ts.map +1 -1
  90. package/dist/umd/index.js +2 -2
  91. package/dist/umd/stores/viz-click.d.ts +5 -4
  92. package/dist/umd/stores/viz-click.d.ts.map +1 -1
  93. package/dist/umd/types/viz-elm-callout.d.ts +60 -9
  94. package/dist/umd/types/viz-elm-callout.d.ts.map +1 -1
  95. package/package.json +4 -4
  96. package/dist/esm/components/VizElement/HoveredElementCallout.d.ts +0 -4
  97. package/dist/esm/components/VizElement/HoveredElementCallout.d.ts.map +0 -1
  98. package/dist/umd/components/VizElement/HoveredElementCallout.d.ts +0 -4
  99. 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;
@@ -7582,7 +7732,11 @@ const VizAreaClick = ({ iframeRef, visualRef, shadowRoot, autoCreateTopN = 10, e
7582
7732
  };
7583
7733
  VizAreaClick.displayName = 'VizAreaClick';
7584
7734
 
7585
- const RankBadgeComponent = ({ index, elementRect, widthScale, clickOnElement }) => {
7735
+ const RankBadgeComponent = ({ index, hash, elementRect, widthScale, show = true, clickOnElement, }) => {
7736
+ const clickedHash = useHeatmapClick((s) => s.selectedElement?.hash);
7737
+ const isShow = !!show && clickedHash !== hash;
7738
+ if (!isShow)
7739
+ return null;
7586
7740
  const style = calculateRankPosition(elementRect, widthScale);
7587
7741
  return (jsx("div", { className: "gx-hm-rank-badge", style: style, onClick: clickOnElement, children: index }));
7588
7742
  };
@@ -7600,7 +7754,7 @@ const DefaultRankBadgesComponent = ({ getRect, hidden }) => {
7600
7754
  const rect = getRect(element);
7601
7755
  if (!rect)
7602
7756
  return null;
7603
- return jsx(RankBadge, { index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash);
7757
+ return (jsx(RankBadge, { hash: element.hash, index: index + 1, elementRect: rect, widthScale: widthScale }, element.hash));
7604
7758
  }) }));
7605
7759
  };
7606
7760
  DefaultRankBadgesComponent.displayName = 'DefaultRankBadges';
@@ -7612,10 +7766,40 @@ const DEFAULT_POSITION = {
7612
7766
  placement: 'top',
7613
7767
  horizontalAlign: 'center',
7614
7768
  };
7615
- const DEFAULT_MOUSE_OFFSET = { x: -8, y: 12 };
7769
+ const ElementCallout = (props) => {
7770
+ const viewId = useViewIdContext();
7771
+ const CompElementCallout = useHeatmapControlStore((state) => state.controls.ElementCallout);
7772
+ const calloutRef = useRef(null);
7773
+ const element = props.element;
7774
+ const positionMode = props.positionMode ?? DEFAULT_POSITION_MODE;
7775
+ const isAbsolute = positionMode === 'absolute';
7776
+ const position = useAnchorPosition(calloutRef, props);
7777
+ const className = `clarity-callout clarity-callout--${position.placement} clarity-callout--align-${position.horizontalAlign}`;
7778
+ const portalContainerId = getPortalElmId();
7779
+ if (!portalContainerId)
7780
+ return null;
7781
+ const calloutContent = (jsx("div", { ref: calloutRef, className: className, style: {
7782
+ position: positionMode,
7783
+ top: position.top,
7784
+ left: position.left,
7785
+ zIndex: Z_INDEX.CALLOUT,
7786
+ }, "aria-live": "assertive", role: "tooltip", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
7787
+ if (!document.getElementById(portalContainerId))
7788
+ return null;
7789
+ return createPortal(calloutContent, document.getElementById(portalContainerId));
7790
+ function getPortalElmId() {
7791
+ const containerElmId = `gx-hm-elements-${viewId}`;
7792
+ const vizContainerElmId = `gx-hm-viz-container-${viewId}`;
7793
+ if (!isAbsolute)
7794
+ return vizContainerElmId;
7795
+ if (!element.mousePosition)
7796
+ return containerElmId;
7797
+ return '';
7798
+ }
7799
+ };
7616
7800
  const useAnchorPosition = (calloutRef, props) => {
7617
7801
  const widthScale = useHeatmapViz((s) => s.widthScale);
7618
- const { target, visualRef, offset = DEFAULT_MOUSE_OFFSET, alignment, element, positionMode } = props;
7802
+ const { target, visualRef, alignment, element, positionMode } = props;
7619
7803
  const [position, setPosition] = useState(DEFAULT_POSITION);
7620
7804
  const isAbsolute = positionMode === 'absolute';
7621
7805
  const mousePosition = element.mousePosition;
@@ -7630,15 +7814,11 @@ const useAnchorPosition = (calloutRef, props) => {
7630
7814
  targetElm,
7631
7815
  calloutElm,
7632
7816
  setPosition,
7633
- offset,
7634
7817
  alignment,
7635
7818
  positionMode,
7636
7819
  visualRef,
7637
7820
  widthScale,
7638
- mousePosition,
7639
7821
  });
7640
- // Delay initial calculation to allow scroll animations to complete
7641
- // Use multiple frames to ensure layout is stable
7642
7822
  const rafId1 = requestAnimationFrame(() => {
7643
7823
  requestAnimationFrame(() => {
7644
7824
  positionFn();
@@ -7647,13 +7827,9 @@ const useAnchorPosition = (calloutRef, props) => {
7647
7827
  const handleUpdate = () => {
7648
7828
  requestAnimationFrame(positionFn);
7649
7829
  };
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
7830
  const visualContainer = visualRef?.current;
7654
7831
  window.addEventListener('resize', handleUpdate);
7655
7832
  if (!isAbsolute) {
7656
- // Fixed mode: listen to scroll events for viewport position updates
7657
7833
  window.addEventListener('scroll', handleUpdate, true);
7658
7834
  visualContainer?.addEventListener('scroll', handleUpdate);
7659
7835
  }
@@ -7665,81 +7841,190 @@ const useAnchorPosition = (calloutRef, props) => {
7665
7841
  visualContainer?.removeEventListener('scroll', handleUpdate);
7666
7842
  }
7667
7843
  };
7668
- }, [element, target, visualRef, offset, alignment, isAbsolute, widthScale, calloutRef, mousePosition, positionMode]);
7844
+ }, [element, target, visualRef, alignment, isAbsolute, widthScale, calloutRef, mousePosition, positionMode]);
7669
7845
  return position;
7670
7846
  };
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
7847
 
7693
- const ElementMissing = ({ show = true }) => {
7848
+ const ElementMissing = ({ show = true, visualRef }) => {
7694
7849
  const widthScale = useHeatmapViz((s) => s.widthScale);
7850
+ const missingElementRef = useRef(null);
7851
+ const wrapperWidth = useHeatmapConfigStore((s) => s.width);
7852
+ const [scrollPosition, setScrollPosition] = useState({ scrollTop: 0, scrollLeft: 0 });
7853
+ useEffect(() => {
7854
+ const container = visualRef.current;
7855
+ if (!container)
7856
+ return;
7857
+ const updateScrollPosition = () => {
7858
+ setScrollPosition({
7859
+ scrollTop: container.scrollTop,
7860
+ scrollLeft: container.scrollLeft,
7861
+ });
7862
+ };
7863
+ // Initial position
7864
+ updateScrollPosition();
7865
+ // Listen to scroll events
7866
+ container.addEventListener('scroll', updateScrollPosition);
7867
+ return () => {
7868
+ container.removeEventListener('scroll', updateScrollPosition);
7869
+ };
7870
+ }, [visualRef]);
7695
7871
  if (!show)
7696
7872
  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" }));
7873
+ const container = visualRef.current;
7874
+ const containerRect = container?.getBoundingClientRect();
7875
+ const elementRect = missingElementRef.current?.getBoundingClientRect();
7876
+ const elementHeightCenter = elementRect?.height ?? 0;
7877
+ const scrollTop = scrollPosition.scrollTop ?? 0;
7878
+ const containerHeight = containerRect?.height ?? 0;
7879
+ const topPosition = scrollTop + (containerHeight + elementHeightCenter) / 2;
7880
+ const topPositionScaled = topPosition / widthScale;
7881
+ const leftPosition = wrapperWidth / 2;
7882
+ return (jsxs(Fragment, { children: [jsx("div", { className: "missingElement-backdrop", style: {
7883
+ position: 'absolute',
7884
+ top: 0,
7885
+ left: 0,
7886
+ right: 0,
7887
+ bottom: 0,
7888
+ background: 'rgba(0, 0, 0, 0.5)',
7889
+ zIndex: 9998,
7890
+ pointerEvents: 'none',
7891
+ } }), jsx("div", { ref: missingElementRef, className: "missingElement", style: {
7892
+ position: 'absolute',
7893
+ top: topPositionScaled,
7894
+ left: leftPosition,
7895
+ transform: `translate(-50%, -50%) scale(${1 / widthScale})`,
7896
+ background: 'rgba(0, 0, 0, 0.8)',
7897
+ color: 'white',
7898
+ padding: '12px 20px',
7899
+ borderRadius: '8px',
7900
+ fontSize: '14px',
7901
+ fontWeight: '500',
7902
+ zIndex: 9999,
7903
+ pointerEvents: 'none',
7904
+ whiteSpace: 'nowrap',
7905
+ }, "aria-live": "assertive", children: "Element not visible on current screen" })] }));
7712
7906
  };
7713
7907
 
7714
- const ElementOverlayComponent = ({ type, element, onClick, elementId }) => {
7908
+ /**
7909
+ * Example component showing how to use canvas backdrop
7910
+ * Renders a dark overlay with cutout for active element
7911
+ */
7912
+ const BackdropCanvas = ({ activeElement, viewportWidth, viewportHeight, borderWidth = 0, show = true, cutoutExpansion = BACKDROP_CONFIG.CUTOUT_EXPANSION, backdropColor = BACKDROP_CONFIG.COLOR, backdropOpacity = BACKDROP_CONFIG.OPACITY, }) => {
7913
+ const canvasRef = useRef(null);
7914
+ useEffect(() => {
7915
+ const canvas = canvasRef.current;
7916
+ if (!canvas || !show)
7917
+ return;
7918
+ // Set canvas dimensions
7919
+ canvas.width = viewportWidth;
7920
+ canvas.height = viewportHeight;
7921
+ if (!activeElement || (activeElement.width === 0 && activeElement.height === 0)) {
7922
+ // No active element - clear canvas
7923
+ clearCanvas(canvas);
7924
+ return;
7925
+ }
7926
+ // Draw backdrop with cutout for active element
7927
+ drawBackdropWithCutout({
7928
+ canvas,
7929
+ activeRect: {
7930
+ top: activeElement.top + borderWidth,
7931
+ left: activeElement.left + borderWidth,
7932
+ width: activeElement.width,
7933
+ height: activeElement.height,
7934
+ },
7935
+ backdropColor,
7936
+ backdropOpacity,
7937
+ cutoutExpansion,
7938
+ });
7939
+ }, [
7940
+ activeElement,
7941
+ viewportWidth,
7942
+ viewportHeight,
7943
+ borderWidth,
7944
+ show,
7945
+ cutoutExpansion,
7946
+ backdropColor,
7947
+ backdropOpacity,
7948
+ ]);
7949
+ if (!show)
7950
+ return null;
7951
+ return (jsx("canvas", { ref: canvasRef, style: {
7952
+ position: 'absolute',
7953
+ top: 0,
7954
+ left: 0,
7955
+ width: viewportWidth,
7956
+ height: viewportHeight,
7957
+ pointerEvents: 'none', // Allow clicks to pass through
7958
+ zIndex: BACKDROP_CONFIG.Z_INDEX, // Below callout but above content
7959
+ } }));
7960
+ };
7961
+
7962
+ const ElementCalloutOverlay = (props) => {
7963
+ const { element, containerRef } = props;
7715
7964
  const widthScale = useHeatmapViz((s) => s.widthScale);
7965
+ const CompElementCallout = useHeatmapControlStore((state) => state.controls.ElementCallout);
7966
+ const calloutRef = useRef(null);
7967
+ const [calloutStyle, setCalloutStyle] = useState(undefined);
7968
+ useEffect(() => {
7969
+ const calloutElm = calloutRef.current;
7970
+ const containerElm = containerRef?.current;
7971
+ if (!element || !calloutElm || !containerElm)
7972
+ return;
7973
+ calcCalloutPositionAbsolute({
7974
+ widthScale,
7975
+ calloutElm,
7976
+ containerElm,
7977
+ element,
7978
+ onChange: setCalloutStyle,
7979
+ });
7980
+ }, [element, widthScale, containerRef]);
7981
+ if (!element)
7982
+ return null;
7983
+ return (jsx("div", { ref: calloutRef, style: calloutStyle, className: "clarity-callout", children: CompElementCallout && jsx(CompElementCallout, { elementHash: element.hash }) }));
7984
+ };
7985
+ ElementCalloutOverlay.displayName = 'ElementCalloutOverlay';
7986
+
7987
+ const ElementOverlayComponent = (props) => {
7988
+ const { type, element, onClick, elementId } = props;
7989
+ const widthScale = useHeatmapViz((s) => s.widthScale);
7990
+ const viewportHeight = useHeatmapVizRect((s) => s.iframeHeight);
7991
+ const viewportWidth = useHeatmapConfigStore((s) => s.width);
7716
7992
  const overlayStyle = useMemo(() => {
7717
- if (!element || (element.width === 0 && element.height === 0))
7993
+ const isInvalid = !element || (element.width === 0 && element.height === 0);
7994
+ if (isInvalid)
7718
7995
  return null;
7719
7996
  return {
7720
7997
  top: element.top + HEATMAP_CONFIG['borderWidthIframe'],
7721
7998
  left: element.left + HEATMAP_CONFIG['borderWidthIframe'],
7722
7999
  width: element.width,
7723
8000
  height: element.height,
7724
- cursor: 'pointer',
7725
8001
  };
7726
8002
  }, [element]);
7727
8003
  if (!overlayStyle)
7728
8004
  return null;
7729
8005
  const isHovered = type === 'hovered';
7730
8006
  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 })] }));
8007
+ const showCallout = !!element?.mousePosition && !isHovered;
8008
+ 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
8009
  };
7733
8010
  ElementOverlayComponent.displayName = 'ElementOverlay';
7734
8011
  const ElementOverlay = memo(ElementOverlayComponent);
7735
8012
 
7736
- const HoveredElementCalloutComponent = ({ target }) => {
7737
- const hoveredElement = useHeatmapHover((s) => s.hoveredElement);
7738
- if (!hoveredElement)
8013
+ const ElementCalloutClickedComponent = (props) => {
8014
+ const viewId = useViewIdContext();
8015
+ const { clickedElement, showMissingElement, shouldShowCallout } = useClickedElement({
8016
+ visualRef: props.visualRef,
8017
+ getRect: props.getRect,
8018
+ });
8019
+ const elementId = getClickedElementId(viewId, props.isSecondary);
8020
+ if (!clickedElement && showMissingElement)
8021
+ return jsx(ElementMissing, { visualRef: props.visualRef });
8022
+ if (!clickedElement)
7739
8023
  return null;
7740
- return jsx(ElementCallout, { element: hoveredElement, target: target, visualRef: { current: null } });
8024
+ const isShowClickedElement = shouldShowCallout && !clickedElement?.mousePosition;
8025
+ 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
8026
  };
7742
- memo(HoveredElementCalloutComponent);
8027
+ const ElementCalloutClicked = memo(ElementCalloutClickedComponent);
7743
8028
 
7744
8029
  const HoveredElementOverlayComponent = ({ onClick }) => {
7745
8030
  const viewId = useViewIdContext();
@@ -7756,16 +8041,19 @@ const HoveredElementOverlayComponent = ({ onClick }) => {
7756
8041
  };
7757
8042
  const HoveredElementOverlay = memo(HoveredElementOverlayComponent);
7758
8043
 
7759
- const ELEMENT_CALLOUT = {
7760
- offset: { x: -8, y: 0 },
7761
- alignment: 'left',
8044
+ const IS_SHOW_CALLOUT = false;
8045
+ const ElementCalloutHoveredComponent = (props) => {
8046
+ const viewId = useViewIdContext();
8047
+ useHeatmapHover((s) => s.hoveredElement);
8048
+ getHoveredElementId(viewId, props.isSecondary);
8049
+ const isShowCallout = IS_SHOW_CALLOUT ;
8050
+ return (jsxs(Fragment, { children: [jsx(HoveredElementOverlay, { onClick: props.onClick }), isShowCallout ] }));
7762
8051
  };
7763
- const IS_SHOW_HOVERED_ELEMENT = false;
8052
+ const ElementCalloutHovered = memo(ElementCalloutHoveredComponent);
8053
+
7764
8054
  const HeatmapElements = (props) => {
7765
8055
  const viewId = useViewIdContext();
7766
8056
  const iframeHeight = useHeatmapVizRect((s) => s.iframeHeight);
7767
- const clickedElementId = getClickedElementId(viewId, props.isSecondary);
7768
- getHoveredElementId(viewId, props.isSecondary);
7769
8057
  const elementCalloutRef = useRef(null);
7770
8058
  const { iframeDimensions, isVisible = true, areDefaultRanksHidden, positionMode } = props;
7771
8059
  const { getRect } = useHeatmapElementPosition({
@@ -7773,10 +8061,6 @@ const HeatmapElements = (props) => {
7773
8061
  wrapperRef: props.wrapperRef,
7774
8062
  visualizer: props.visualizer,
7775
8063
  });
7776
- const { clickedElement, showMissingElement, shouldShowCallout } = useClickedElement({
7777
- visualRef: props.visualRef,
7778
- getRect,
7779
- });
7780
8064
  const { handleMouseMove, handleMouseLeave, handleClick } = useHoveredElement({
7781
8065
  iframeRef: props.iframeRef,
7782
8066
  getRect,
@@ -7786,8 +8070,7 @@ const HeatmapElements = (props) => {
7786
8070
  useRenderCount('HeatmapElements');
7787
8071
  if (!isVisible)
7788
8072
  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 }))] }));
8073
+ 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
8074
  };
7792
8075
 
7793
8076
  const VizElements = ({ iframeRef, visualRef, wrapperRef }) => {
@@ -8248,4 +8531,4 @@ const HeatmapLayout = ({ data, clickmap, clickAreas, scrollmap, controls, dataIn
8248
8531
  }
8249
8532
  };
8250
8533
 
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 };
8534
+ 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 };