Menu

QuadTree.percent-range.html.svelte component

Creates an interaction layer (in HTML) using d3-quadtree to find the nearest datapoint to the mouse. This component creates a snippet that exposes variables x, y, found (the found datapoint), visible (a Boolean whether any data was found) and e (the event object). This component works with a percent range so the x and y values coming back will be percentages.

The quadtree searches across both the x and y dimensions at the same time. But if you want to only search across one, set the x and y props to the same value. For example, the shared tooltip component sets y='x' since it's nicer behavior to only pick up on the nearest x-value.

Param Default Required Description
x string 'x'
no
The dimension to search across when moving the mouse left and right.
y string 'y'
no
The dimension to search across when moving the mouse up and down.
searchRadius (number | undefined) None
no
The number of pixels to search around the mouse's location. This is the third argument passed to quadtree.find and by default a value of undefined means an unlimited range.
dataset (Array<Object> | undefined) None
no
The dataset to work off of—defaults to $data if left unset. You can pass something custom in here in case you don't want to use the main data or it's in a strange format.
<!--
  @component
  Creates an interaction layer (in HTML) using [d3-quadtree](https://github.com/d3/d3-quadtree) to find the nearest datapoint to the mouse. This component creates a snippet that exposes variables `x`, `y`, `found` (the found datapoint), `visible` (a Boolean whether any data was found) and `e` (the event object). This component works with a percent range so the `x` and `y` values coming back will be percentages.

  The quadtree searches across both the x and y dimensions at the same time. But if you want to only search across one, set the `x` and `y` props to the same value. For example, the [shared tooltip component](https://layercake.graphics/components/SharedTooltip.html.svelte) sets `y='x'` since it's nicer behavior to only pick up on the nearest x-value.
 -->
<script>
  import { getContext } from 'svelte';
  import { quadtree } from 'd3-quadtree';

  const { data, xGet, yGet, width, height } = getContext('LayerCake');

  let visible = $state(false);
  let found = $state({});
  let e = $state({});

  /**
   * @typedef {Object} Props
   * @property {string} [x='x'] - The dimension to search across when moving the mouse left and right.
   * @property {string} [y='y'] - The dimension to search across when moving the mouse up and down.
   * @property {number|undefined} [searchRadius] - The number of pixels to search around the mouse's location. This is the third argument passed to [`quadtree.find`](https://github.com/d3/d3-quadtree#quadtree_find) and by default a value of `undefined` means an unlimited range.
   * @property {Array<Object>|undefined} [dataset] - The dataset to work off of—defaults to $data if left unset. You can pass something custom in here in case you don't want to use the main data or it's in a strange format.
   * @property {import('svelte').Snippet<[any]>} [children]
   */

  /** @type {Props} */
  let { x = 'x', y = 'y', searchRadius, dataset, children } = $props();

  let xGetter = $derived(x === 'x' ? $xGet : $yGet);
  let yGetter = $derived(y === 'y' ? $yGet : $xGet);

  /** @param {MouseEvent} evt*/
  function findItem(evt) {
    e = evt;

    const xLayerKey = /** @type {'layerX'|'layerY'} */ (`layer${x.toUpperCase()}`);
    const yLayerKey = /** @type {'layerX'|'layerY'}*/ (`layer${y.toUpperCase()}`);

    const xLayerVal = (evt[xLayerKey] / (x === 'x' ? $width : $height)) * 100;
    const yLayerVal = (evt[yLayerKey] / (y === 'y' ? $height : $width)) * 100;

    found = finder.find(xLayerVal, yLayerVal, searchRadius) || {};

    visible = Object.keys(found).length > 0;
  }

  let finder = $derived(
    quadtree()
      .extent([
        [-1, -1],
        [$width + 1, $height + 1]
      ])
      .x(xGetter)
      .y(yGetter)
      .addAll(dataset || $data)
  );
</script>

<div
  class="bg"
  onmousemove={findItem}
  onmouseout={() => (visible = false)}
  onblur={() => (visible = false)}
  role="tooltip"
></div>
{@render children?.({ x: xGetter(found) || 0, y: yGetter(found) || 0, found, visible, e })}

<style>
  .bg {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
  }
</style>