Menu

Beeswarm.svelte component

Generates an SVG 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
Whitespace padding between each circle, in pixels
getTitle Function None
no
An accessor function to get the field on the data element to display as a hover label using a <title> tag.

Used in these examples:

<!--
  @component
  Generates an SVG 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] - Whitespace padding between each circle, in pixels */
  export let spacing = 1.5;

  /** @type {Function} [getTitle] — An accessor function to get the field on the data element to display as a hover label using a `<title>` tag. */
  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>

<g class='bee-group'>
  {#each circles as d}
    <circle
      fill={$zGet(d)}
      stroke='{stroke}'
      stroke-width='{strokeWidth}'
      cx='{d.x}'
      cy='{$height - r - spacing - strokeWidth / 2 - d.y}'
      r='{r}'
    >
      {#if getTitle}
        <title>{getTitle(d)}</title>
      {/if}
    </circle>
  {/each}
</g>