Menu
📚 ARCHIVED DOCUMENTATION

This documentation is for pre-Svelte 5 runes and is no longer maintained. For the latest Layer Cake documentation, visit layercake.graphics

Beeswarm.html.svelte component

Generates an HTML Beeswarm chart.

Param Default Required Description
r Number 3
no
The circle radius size in pixels.
strokeWidth Number 0
no
The circle's stroke width in pixels.
stroke String '#fff'
no
The circle's stroke color.
spacing Number 1.5
no
Spacing, in pixels, between each circle.
getTitle (Function | undefined) None
no
An accessor function to get the field on the data element to display as a hover label. Mostly useful for debugging, needs better styling for production.
<!--
  @component
  Generates an HTML Beeswarm chart.
 -->
<script>
  import { getContext } from 'svelte';

  const { data, xGet, zGet, height, config } = getContext('LayerCake');

  /** @type {Number} [r=3] - The circle radius size in pixels. */
  export let r = 3;

  /** @type {Number} [strokeWidth=0] - The circle's stroke width in pixels. */
  export let strokeWidth = 0;

  /** @type {String} [stroke='#fff'] - The circle's stroke color. */
  export let stroke = '#fff';

  /** @type {Number} [spacing=1.5] - Spacing, in pixels, between each circle. */
  export let spacing = 1.5;

  /** @type {Function|undefined} [getTitle] - An accessor function to get the field on the data element to display as a hover label. Mostly useful for debugging, needs better styling for production. */
  export let getTitle = undefined;

  $: circles = dodge($data, { rds: r * 2 + spacing + strokeWidth, x: $xGet });

  function dodge(data, { rds = 1, x = d => d } = {}) {
    const radius2 = rds ** 2;
    const circles = data
      .map(d => ({ x: x(d), [$config.z]: d[$config.z], data: d }))
      .sort((a, b) => a.x - b.x);
    const epsilon = 1e-3;
    let head = null,
      tail = null;

    // Returns true if circle ⟨x,y⟩ intersects with any circle in the queue.
    function intersects(x, y) {
      let a = head;
      while (a) {
        if (radius2 - epsilon > (a.x - x) ** 2 + (a.y - y) ** 2) {
          return true;
        }
        a = a.next;
      }
      return false;
    }

    // Place each circle sequentially.
    for (const b of circles) {
      // Remove circles from the queue that can’t intersect the new circle b.
      while (head && head.x < b.x - radius2) head = head.next;

      // Choose the minimum non-intersecting tangent.
      if (intersects(b.x, (b.y = 0))) {
        let a = head;
        b.y = Infinity;
        do {
          let y = a.y + Math.sqrt(radius2 - (a.x - b.x) ** 2);
          if (y < b.y && !intersects(b.x, y)) b.y = y;
          a = a.next;
        } while (a);
      }

      // Add b to the queue.
      b.next = null;
      if (head === null) head = tail = b;
      else tail = tail.next = b;
    }

    return circles;
  }
</script>

<div class="bee-group">
  {#each circles as d}
    <div
      class="bee"
      style="
        background:{$zGet(d)};
        border-color:{stroke};
        border-width:{strokeWidth};
        left:{d.x}px;
        top:{$height - r - spacing - strokeWidth / 2 - d.y}px;
        width:{r * 2}px;
        height:{r * 2}px;
      "
    >
      {#if getTitle}
        <div class="title">{getTitle(d)}</div>
      {/if}
    </div>
  {/each}
</div>

<style>
  .bee {
    position: absolute;
    border-style: solid;
    border-radius: 50%;
    transform: translate(-50%, -50%);
  }
  .title {
    display: none;
    white-space: nowrap;
    padding: 0 3px;
    border-radius: 3px;
    font-size: 12px;
    pointer-events: none;
    position: absolute;
    top: -15px;
    left: 5px;
    z-index: 9999;
  }
  .bee:hover .title {
    display: block;
  }
</style>