Skip to content

ExamplesTrends & comparison

Bubble

A scatter plot with a third, quantitative dimension: r maps to marker area (never radius) through sizeRange — the min/max marker diameter in px. Use a bubble chart when all three values are quantitative and the third is a magnitude (reach, population, spend). Don't use it when the third dimension is categorical (use color/series instead) or when precise comparison of the third value matters — area is read approximately; put the exact value in the tooltip, where ChartCraft shows x, y, and r.

ts
import { createChart } from '@chartcraft/core';

const chart = createChart(document.querySelector<HTMLElement>('#chart')!, {
  type: 'bubble',
  title: 'Campaign efficiency',
  subtitle: 'Monthly spend vs. qualified leads — bubble area = audience reach (thousands)',
  data: {
    series: [
      {
        id: 'paid',
        name: 'Paid channels',
        sizeRange: [10, 44], // min/max marker DIAMETER px; default [8, 40]
        data: [
          { x: 18, y: 240, r: 320, label: 'Search ads' },
          { x: 26, y: 310, r: 480, label: 'Social ads' },
          { x: 9, y: 90, r: 150, label: 'Display' },
          { x: 14, y: 150, r: 610, label: 'Video' },
        ],
      },
      {
        id: 'owned',
        name: 'Owned channels',
        sizeRange: [10, 44],
        data: [
          { x: 6, y: 180, r: 260, label: 'Newsletter' },
          { x: 11, y: 260, r: 140, label: 'Webinars' },
          { x: 4, y: 120, r: 90, label: 'Community' },
          { x: 8, y: 95, r: 400, label: 'Blog / SEO' },
        ],
      },
    ],
  },
  xAxis: { label: 'Monthly spend ($k)', min: 0 },
  yAxis: { label: 'Qualified leads', min: 0 },
  a11y: {
    description:
      'Owned channels produce leads at lower spend than paid channels; social ads have the highest spend and lead count, while video reaches the largest audience.',
  },
});
vue
<script setup lang="ts">
import { BubbleChart } from '@chartcraft/vue';
import type { ChartSpec } from '@chartcraft/vue';

const options: ChartSpec = {
  title: 'Campaign efficiency',
  subtitle: 'Monthly spend vs. qualified leads — bubble area = audience reach (thousands)',
  data: {
    series: [
      {
        id: 'paid',
        name: 'Paid channels',
        sizeRange: [10, 44], // min/max marker DIAMETER px; default [8, 40]
        data: [
          { x: 18, y: 240, r: 320, label: 'Search ads' },
          { x: 26, y: 310, r: 480, label: 'Social ads' },
          { x: 9, y: 90, r: 150, label: 'Display' },
          { x: 14, y: 150, r: 610, label: 'Video' },
        ],
      },
      {
        id: 'owned',
        name: 'Owned channels',
        sizeRange: [10, 44],
        data: [
          { x: 6, y: 180, r: 260, label: 'Newsletter' },
          { x: 11, y: 260, r: 140, label: 'Webinars' },
          { x: 4, y: 120, r: 90, label: 'Community' },
          { x: 8, y: 95, r: 400, label: 'Blog / SEO' },
        ],
      },
    ],
  },
  xAxis: { label: 'Monthly spend ($k)', min: 0 },
  yAxis: { label: 'Qualified leads', min: 0 },
  a11y: {
    description:
      'Owned channels produce leads at lower spend than paid channels; social ads have the highest spend and lead count, while video reaches the largest audience.',
  },
};
</script>

<template>
  <BubbleChart :options="options" style="height: 380px" />
</template>

Points also accept [x, y, r] tuples when you don't need per-point labels. Because r maps to area, a value twice as large reads as a bubble of twice the area — the perceptually honest encoding.