Bubble Chart

Bubble charts represent three-dimensional data on a two-dimensional plane. In addition to X and Y axis coordinates, the size of bubbles visualizes the value of a third dimension, effectively showing complex relationships.

Interface

BubbleChart Props

interface BubbleChartProps {
  // Required properties
  data: BubbleChartData;
  
  // Optional properties
  custom?: Partial<BubbleChartCustom>;
  title?: string;
  getScale?: (data: BubbleChartData) => BubbleChartScale;
}

Data Structure

type BubbleChartData = {
  datasets: {
    legend: string;     // Dataset name
    data: {
      x: number;       // X-axis value
      y: number;       // Y-axis value
      value: number;   // Bubble size value
      label: string;   // Data point label
    }[];
  }[];
};

type BubbleChartScale = {
  x: {
    min: number;
    max: number;
    step: number;
  };
  y: {
    min: number;
    max: number;
    step: number;
  };
  value: {
    min: number;
    max: number;
    step: number;
  };
};

Basic Usage

import Widget from '@meursyphus/flitter-react';
import { BubbleChart } from '@meursyphus/headless-chart';

const data = {
  datasets: [{
    legend: 'Product Performance',
    data: [
      { x: 10, y: 20, value: 30, label: 'Product A' },
      { x: 15, y: 35, value: 45, label: 'Product B' },
      { x: 25, y: 30, value: 20, label: 'Product C' },
      { x: 30, y: 45, value: 60, label: 'Product D' }
    ]
  }]
};

function BasicBubbleChart() {
  return (
    <Widget 
      width="600px" 
      height="400px" 
      widget={BubbleChart({ data })} 
    />
  );
}

Chart Components

Bubble Chart has a similar structure to Scatter Chart, but uses bubble elements instead of scatter:

BubbleChart
└── Layout (overall layout)
    ├── Title (chart title)
    ├── Legend (legend)
    └── Plot (plot area)
        ├── XAxis (X-axis)
        ├── YAxis (Y-axis)
        ├── Grid (grid)
        └── Series (data series)
            └── Bubble (individual bubble)

Customizable Elements

You can customize 18 components through the custom prop:

1. Layout Elements

layout, title, legend

Can be customized in the same way as other charts.

2. Data Visualization Elements

series

Container for all bubbles.

custom: {
  series: ({ points, scale }) => {
    return Stack({
      children: points.map(point => 
        Positioned({
          left: calculateX(point.x, scale.x),
          top: calculateY(point.y, scale.y),
          child: createBubble(point)
        })
      )
    });
  }
}

bubble

Individual bubble element. This is the most commonly customized element.

custom: {
  bubble: ({ value, label, legend, index }) => {
    const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444'];
    const color = colors[index % colors.length];
    
    // Calculate size so area is proportional to value
    const size = Math.sqrt(value / Math.PI) * 2;
    
    return Container({
      width: size,
      height: size,
      decoration: new BoxDecoration({
        color: `${color}60`,
        shape: 'circle',
        border: Border.all({ color: color, width: 2 })
      })
    });
  }
}

3. Axis and Grid Elements

xAxis, yAxis, grid, etc.

Uses the same components as Scatter Chart.

Real-World Usage Examples

1. Gradient Bubbles

import { Container, BoxDecoration } from '@meursyphus/flitter';

const gradientBubbleChart = BubbleChart({
  data,
  custom: {
    bubble: ({ value, index }) => {
      const gradients = [
        { inner: '#60a5fa', outer: '#3b82f6' },
        { inner: '#86efac', outer: '#10b981' },
        { inner: '#fbbf24', outer: '#f59e0b' }
      ];
      
      const { inner, outer } = gradients[index % gradients.length];
      const size = Math.sqrt(value / Math.PI) * 3;
      
      return Container({
        width: size,
        height: size,
        decoration: new BoxDecoration({
          shape: 'circle',
          gradient: {
            type: 'radial',
            colors: [inner, outer],
            stops: [0, 1],
            center: { x: 0.3, y: 0.3 },
            radius: 1
          },
          boxShadow: [{
            color: `${outer}40`,
            blurRadius: size / 4,
            offset: { x: 0, y: size / 8 }
          }]
        })
      });
    }
  }
});

2. Bubbles with Data Labels

import { Stack, Center, Text, TextStyle } from '@meursyphus/flitter';

const labeledBubbleChart = BubbleChart({
  data,
  custom: {
    bubble: ({ value, label, index }) => {
      const colors = ['#3b82f6', '#10b981', '#f59e0b'];
      const color = colors[index % colors.length];
      const size = Math.sqrt(value / Math.PI) * 3;
      
      return Stack({
        alignment: Alignment.center,
        children: [
          // Bubble
          Container({
            width: size,
            height: size,
            decoration: new BoxDecoration({
              shape: 'circle',
              color: `${color}40`,
              border: Border.all({ color, width: 2 })
            })
          }),
          // Label (only display on sufficiently large bubbles)
          if (size > 30)
            Column({
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(label, {
                  style: new TextStyle({
                    fontSize: Math.min(14, size / 4),
                    fontWeight: 'bold',
                    color: color
                  })
                }),
                Text(value.toString(), {
                  style: new TextStyle({
                    fontSize: Math.min(10, size / 5),
                    color: '#6b7280'
                  })
                })
              ]
            })
        ]
      });
    }
  }
});

3. Pattern Bubbles

import { CustomPaint, Path } from '@meursyphus/flitter';

const patternBubbleChart = BubbleChart({
  data,
  custom: {
    bubble: ({ value, index }) => {
      const patterns = ['dots', 'lines', 'crosses'];
      const pattern = patterns[index % patterns.length];
      const size = Math.sqrt(value / Math.PI) * 3;
      
      return CustomPaint({
        size: { width: size, height: size },
        painter: {
          svg: {
            createDefaultSvgEl: (context) => {
              const g = context.createSvgEl("g");
              const circle = context.createSvgEl("circle");
              const pattern = context.createSvgEl("pattern");
              
              pattern.setAttribute("id", `pattern-${index}`);
              pattern.setAttribute("patternUnits", "userSpaceOnUse");
              pattern.setAttribute("width", "10");
              pattern.setAttribute("height", "10");
              
              return { g, circle, pattern };
            },
            paint: ({ g, circle, pattern: patternEl }, { width, height }) => {
              // Define pattern
              createPattern(patternEl, pattern);
              
              // Draw circle
              circle.setAttribute("cx", (width / 2).toString());
              circle.setAttribute("cy", (height / 2).toString());
              circle.setAttribute("r", (width / 2 - 1).toString());
              circle.setAttribute("fill", `url(#pattern-${index})`);
              circle.setAttribute("stroke", "#3b82f6");
              circle.setAttribute("stroke-width", "2");
              
              g.appendChild(patternEl);
              g.appendChild(circle);
            }
          }
        }
      });
    }
  }
});

4. 3D Effect Bubbles

const threeDimensionalBubbleChart = BubbleChart({
  data,
  custom: {
    bubble: ({ value, index }) => {
      const colors = ['#3b82f6', '#10b981', '#f59e0b'];
      const color = colors[index % colors.length];
      const size = Math.sqrt(value / Math.PI) * 3;
      
      return Stack({
        children: [
          // Shadow
          Container({
            width: size,
            height: size,
            margin: EdgeInsets.only({ top: size * 0.1 }),
            decoration: new BoxDecoration({
              shape: 'circle',
              color: 'rgba(0, 0, 0, 0.2)',
              filter: 'blur(8px)'
            })
          }),
          // Main bubble
          Container({
            width: size,
            height: size,
            decoration: new BoxDecoration({
              shape: 'circle',
              gradient: {
                type: 'radial',
                colors: [
                  `${color}FF`,
                  `${color}CC`,
                  `${color}99`
                ],
                center: { x: 0.3, y: 0.3 },
                radius: 1
              },
              border: Border.all({ 
                color: color, 
                width: 1 
              })
            })
          }),
          // Highlight
          Positioned({
            top: size * 0.15,
            left: size * 0.15,
            child: Container({
              width: size * 0.3,
              height: size * 0.3,
              decoration: new BoxDecoration({
                shape: 'circle',
                gradient: {
                  type: 'radial',
                  colors: [
                    'rgba(255, 255, 255, 0.8)',
                    'rgba(255, 255, 255, 0)'
                  ],
                  radius: 1
                }
              })
            })
          })
        ]
      });
    }
  }
});

Adding Animation

import { StatefulWidget, State, AnimationController, Animation } from '@meursyphus/flitter';

class AnimatedBubble extends StatefulWidget {
  constructor({ value, color }) {
    super();
    this.value = value;
    this.color = color;
  }

  createState() {
    return new AnimatedBubbleState();
  }
}

class AnimatedBubbleState extends State {
  initState() {
    super.initState();
    this.controller = new AnimationController({
      duration: { milliseconds: 1000 }
    });
    
    // Size animation
    this.sizeAnimation = new Animation({
      controller: this.controller,
      value: Tween({ 
        begin: 0, 
        end: Math.sqrt(this.widget.value / Math.PI) * 3 
      }).chain(
        CurveTween({ curve: Curves.elasticOut })
      )
    });
    
    // Opacity animation
    this.opacityAnimation = new Animation({
      controller: this.controller,
      value: Tween({ begin: 0, end: 0.6 })
    });
    
    this.controller.forward();
  }

  build() {
    return AnimatedBuilder({
      animation: this.controller,
      builder: () => {
        const size = this.sizeAnimation.value;
        
        return Container({
          width: size,
          height: size,
          decoration: new BoxDecoration({
            shape: 'circle',
            color: this.widget.color.replace(')', `, ${this.opacityAnimation.value})`),
            border: Border.all({ 
              color: this.widget.color, 
              width: 2 
            })
          })
        });
      }
    });
  }

  dispose() {
    this.controller.dispose();
    super.dispose();
  }
}

Performance Optimization Tips

  1. Size Calculation: Use Math.sqrt(value) so area is proportional to value
  2. Overlap Handling: Prevent bubble collisions with force-directed layout
  3. LOD Application: Keep small bubbles simple, add detail only to large bubbles
  4. Virtualization: Don’t render bubbles outside the screen

Next Steps