Area Chart

Area charts express data changes over time while visually emphasizing size and volume. As a variation of Line Chart, they effectively represent cumulative values or overall scale by filling the area below the line.

Interface

AreaChart Props

interface AreaChartProps {
  // Required properties
  data: AreaChartData;
  
  // Optional properties
  custom?: Partial<AreaChartCustom>;
  title?: string;
  getScale?: (data: AreaChartData) => AreaChartScale;
}

Data Structure

type AreaChartData = {
  labels: string[];     // X-axis labels (time/categories)
  datasets: {
    legend: string;     // Dataset name
    values: number[];   // Y-axis values
  }[];
};

type AreaChartScale = {
  min: number;
  max: number;
  step: number;
};

Basic Usage

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

const data = {
  labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
  datasets: [{
    legend: 'Revenue',
    values: [1200, 1900, 3000, 2500, 4200, 3800]
  }]
};

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

Chart Components

Area Chart has the same hierarchical structure as Line Chart, but uses area elements instead of line:

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

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 Line Chart.

2. Data Visualization Elements

series

Container for all areas.

custom: {
  series: ({ areas }) => {
    return Stack({
      children: areas
    });
  }
}

area

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

custom: {
  area: ({ values, legend, index }) => {
    return CustomPaint({
      painter: {
        svg: {
          createDefaultSvgEl: (context) => ({
            line: context.createSvgEl("path"),
            area: context.createSvgEl("path")
          }),
          paint: ({ line, area }, { width, height }) => {
            // Create line path
            const linePath = createLinePath({ values, width, height });
            
            // Line styling
            line.setAttribute("fill", "none");
            line.setAttribute("stroke", getColor(index));
            line.setAttribute("stroke-width", "2");
            line.setAttribute("d", linePath.getD());
            
            // Create area path (copy line path and extend to bottom)
            const areaPath = linePath.clone();
            areaPath
              .lineTo({ x: width, y: height })
              .lineTo({ x: 0, y: height })
              .close();
            
            // Area styling
            area.setAttribute("fill", getColor(index));
            area.setAttribute("opacity", "0.3");
            area.setAttribute("d", areaPath.getD());
          }
        }
      }
    });
  }
}

3. Axis and Grid Elements

xAxis, yAxis, grid, etc.

Uses the same components as Line Chart.

Real-World Usage Examples

1. Gradient Area Chart

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

const gradientAreaChart = AreaChart({
  data,
  custom: {
    area: ({ values, index }) => {
      const colors = [
        { stroke: '#3b82f6', fill: 'url(#gradient-0)' },
        { stroke: '#10b981', fill: 'url(#gradient-1)' },
        { stroke: '#f59e0b', fill: 'url(#gradient-2)' }
      ];
      
      const { stroke, fill } = colors[index % colors.length];
      
      return CustomPaint({
        painter: {
          svg: {
            createDefaultSvgEl: (context) => {
              const defs = context.createSvgEl("defs");
              const gradient = context.createSvgEl("linearGradient");
              gradient.setAttribute("id", `gradient-${index}`);
              gradient.setAttribute("x1", "0%");
              gradient.setAttribute("y1", "0%");
              gradient.setAttribute("x2", "0%");
              gradient.setAttribute("y2", "100%");
              
              const stop1 = context.createSvgEl("stop");
              stop1.setAttribute("offset", "0%");
              stop1.setAttribute("stop-color", stroke);
              stop1.setAttribute("stop-opacity", "0.3");
              
              const stop2 = context.createSvgEl("stop");
              stop2.setAttribute("offset", "100%");
              stop2.setAttribute("stop-color", stroke);
              stop2.setAttribute("stop-opacity", "0");
              
              gradient.appendChild(stop1);
              gradient.appendChild(stop2);
              defs.appendChild(gradient);
              
              return {
                defs,
                line: context.createSvgEl("path"),
                area: context.createSvgEl("path")
              };
            },
            paint: ({ line, area }, { width, height }) => {
              const path = createLinePath({ values, width, height });
              
              // Line
              line.setAttribute("fill", "none");
              line.setAttribute("stroke", stroke);
              line.setAttribute("stroke-width", "2");
              line.setAttribute("d", path.getD());
              
              // Area
              const areaPath = path.clone();
              areaPath
                .lineTo({ x: width, y: height })
                .lineTo({ x: 0, y: height })
                .close();
              
              area.setAttribute("fill", fill);
              area.setAttribute("d", areaPath.getD());
            }
          }
        }
      });
    }
  }
});

2. Stacked Area Chart

const stackedAreaChart = AreaChart({
  data,
  custom: {
    series: ({ areas }) => {
      // Calculate cumulative values
      const stackedAreas = areas.map((area, index) => {
        if (index === 0) return area;
        
        // Accumulate values from previous areas
        const accumulatedValues = calculateAccumulatedValues(data.datasets, index);
        return createStackedArea(area, accumulatedValues);
      });
      
      return Stack({
        children: stackedAreas.reverse() // Draw from back to front for proper layering
      });
    }
  }
});

function calculateAccumulatedValues(datasets, upToIndex) {
  const result = [];
  for (let i = 0; i < datasets[0].values.length; i++) {
    let sum = 0;
    for (let j = 0; j < upToIndex; j++) {
      sum += datasets[j].values[i];
    }
    result.push(sum);
  }
  return result;
}

3. Spline Curve Area

const smoothAreaChart = AreaChart({
  data,
  custom: {
    area: ({ values, index }) => {
      const colors = ['#3b82f6', '#10b981', '#f59e0b'];
      const color = colors[index % colors.length];
      
      return CustomPaint({
        painter: {
          svg: {
            createDefaultSvgEl: (context) => ({
              line: context.createSvgEl("path"),
              area: context.createSvgEl("path")
            }),
            paint: ({ line, area }, { width, height }) => {
              // Create spline curve path
              const path = createSplinePath({ values, width, height });
              
              // Line
              line.setAttribute("fill", "none");
              line.setAttribute("stroke", color);
              line.setAttribute("stroke-width", "2");
              line.setAttribute("d", path.getD());
              
              // Area
              const areaPath = path.clone();
              areaPath
                .lineTo({ x: width, y: height })
                .lineTo({ x: 0, y: height })
                .close();
              
              area.setAttribute("fill", color);
              area.setAttribute("opacity", "0.2");
              area.setAttribute("d", areaPath.getD());
            }
          }
        }
      });
    }
  }
});

function createSplinePath({ values, width, height }) {
  const path = new Path();
  const points = values.map((value, index) => ({
    x: (index / (values.length - 1)) * width,
    y: height - (value / Math.max(...values)) * height
  }));
  
  // Calculate Bezier control points
  const controlPoints = calculateBezierControlPoints(points);
  
  path.moveTo(points[0]);
  
  for (let i = 1; i < points.length; i++) {
    const cp1 = controlPoints[i - 1].next;
    const cp2 = controlPoints[i].prev;
    
    path.cubicTo(cp1, cp2, points[i]);
  }
  
  return path;
}

4. Range Area Chart

const rangeAreaChart = AreaChart({
  data: {
    labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
    datasets: [
      { legend: 'Min Value', values: [10, 15, 12, 18, 14] },
      { legend: 'Max Value', values: [25, 30, 28, 35, 32] }
    ]
  },
  custom: {
    series: ({ areas }) => {
      // Display range between two datasets
      const [minArea, maxArea] = areas;
      const minValues = data.datasets[0].values;
      const maxValues = data.datasets[1].values;
      
      return CustomPaint({
        painter: {
          svg: {
            createDefaultSvgEl: (context) => ({
              range: context.createSvgEl("path"),
              minLine: context.createSvgEl("path"),
              maxLine: context.createSvgEl("path")
            }),
            paint: ({ range, minLine, maxLine }, { width, height }) => {
              // Range area path
              const rangePath = new Path();
              
              // Draw minimum value line
              const minPath = createLinePath({ values: minValues, width, height });
              
              // Draw maximum value line (in reverse)
              const maxPoints = maxValues.map((value, index) => ({
                x: (index / (maxValues.length - 1)) * width,
                y: height - (value / Math.max(...maxValues)) * height
              })).reverse();
              
              // Create range area
              rangePath.moveTo(minPath.getFirstPoint());
              minPath.getPoints().forEach(point => rangePath.lineTo(point));
              maxPoints.forEach(point => rangePath.lineTo(point));
              rangePath.close();
              
              // Apply styling
              range.setAttribute("fill", "rgba(59, 130, 246, 0.2)");
              range.setAttribute("d", rangePath.getD());
              
              minLine.setAttribute("stroke", "#3b82f6");
              minLine.setAttribute("stroke-width", "1");
              minLine.setAttribute("stroke-dasharray", "3 3");
              minLine.setAttribute("fill", "none");
              minLine.setAttribute("d", minPath.getD());
              
              maxLine.setAttribute("stroke", "#3b82f6");
              maxLine.setAttribute("stroke-width", "1");
              maxLine.setAttribute("stroke-dasharray", "3 3");
              maxLine.setAttribute("fill", "none");
              maxLine.setAttribute("d", createLinePath({ values: maxValues, width, height }).getD());
            }
          }
        }
      });
    }
  }
});

Adding Animation

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

class AnimatedArea extends StatefulWidget {
  constructor({ values, color }) {
    super();
    this.values = values;
    this.color = color;
  }

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

class AnimatedAreaState extends State {
  initState() {
    super.initState();
    this.controller = new AnimationController({
      duration: { milliseconds: 1000 }
    });
    this.animation = new Animation({
      controller: this.controller,
      value: Tween({ begin: 0, end: 1 })
    });
    this.controller.forward();
  }

  build() {
    return AnimatedBuilder({
      animation: this.animation,
      builder: () => {
        // Progressive horizontal reveal effect
        return ClipRect({
          clipper: ({ width, height }) => Rect.fromLTWH({
            left: 0,
            top: 0,
            width: width * this.animation.value,
            height: height
          }),
          child: CustomPaint({
            painter: createAreaPainter(this.widget.values, this.widget.color)
          })
        });
      }
    });
  }

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

Performance Optimization Tips

  1. Opacity Optimization: Adjust transparency when multiple areas overlap for better readability
  2. Data Aggregation: Properly aggregate many data points for display
  3. SVG vs Canvas: Consider SVG for static charts, Canvas for dynamic charts
  4. Minimize Layers: Be careful of performance degradation with many overlapping areas

Next Steps