Component Architecture

The power of Headless Chart comes from its architecture that allows every component to be replaced. This chapter explores the chart structure and customization methods.

Basic Chart Structure

All charts follow this hierarchical structure:

Chart
└── Layout
    ├── Title
    ├── Legends
    └── Plot
        ├── Grid
        ├── Axes (X/Y)
        └── Series (Data Visualization)

Key Component Descriptions

  • Layout: Determines the overall chart layout
  • Title: Chart title
  • Legends: Legend components
  • Plot: Area where actual data is rendered
  • Grid: Grid lines
  • Axes: X and Y axes
  • Series: Actual data representation like bars, lines, points

Understanding the Custom Prop

Basic Concept

The custom prop is the core feature that allows you to replace any component in the chart:

BarChart({
  data: myData,
  custom: {
    bar: MyCustomBar,        // Replace bar component
    xAxis: MyCustomXAxis,    // Replace X-axis component
    layout: MyCustomLayout   // Replace layout component
  }
});

Component Signatures

Each custom component receives specific parameters:

// Parameters received by Bar component
type BarArgs = {
  value: number;      // Data value
  label: string;      // X-axis label
  legend: string;     // Dataset name
  index: number;      // Index
};

// Custom Bar component
const MyCustomBar = ({ value, label, legend, index }) => {
  return Container({
    width: Infinity,
    height: Infinity,
    color: getColorByIndex(index)
  });
};

Bar Chart Component Structure

List of Replaceable Components

BarChart({
  custom: {
    // Layout related
    layout: ({ title, legends, plot }) => Widget,
    
    // Data visualization
    bar: ({ value, label, legend, index }) => Widget,
    barGroup: ({ bars, index, label, values }) => Widget,
    series: ({ barGroups }) => Widget,
    
    // Axis related
    xAxis: ({ line, labels, tick }) => Widget,
    yAxis: ({ line, labels, tick }) => Widget,
    xAxisLabel: ({ name, index }) => Widget,
    yAxisLabel: ({ name, index }) => Widget,
    xAxisLine: () => Widget,
    yAxisLine: () => Widget,
    xAxisTick: () => Widget,
    yAxisTick: () => Widget,
    
    // Others
    grid: ({ xLine, yLine }) => Widget,
    gridXLine: () => Widget,
    gridYLine: () => Widget,
    legend: ({ name, index }) => Widget,
    title: ({ name }) => Widget,
    dataLabel: ({ value, label, legend }) => Widget,
    plot: ({ xAxis, yAxis, series, grid }) => Widget
  }
});

Practical Customization Examples

1. Creating Custom Bars

const GradientBar = ({ value, index, datasetIndex }) => {
  const colors = [
    ['#3b82f6', '#1d4ed8'],
    ['#10b981', '#059669'],
    ['#f59e0b', '#d97706']
  ];
  
  const [startColor, endColor] = colors[datasetIndex] || colors[0];
  
  return Container({
    width: Infinity,
    height: Infinity,
    margin: EdgeInsets.symmetric({ horizontal: 2 }),
    decoration: new BoxDecoration({
      gradient: {
        type: 'linear',
        colors: [startColor, endColor],
        begin: { x: 0, y: 0 },
        end: { x: 0, y: 1 }
      },
      borderRadius: BorderRadius.only({
        topLeft: 4,
        topRight: 4
      })
    })
  });
};

2. Creating Custom Layout

const DarkThemeLayout = ({ title, legends, plot }) => {
  return Container({
    decoration: new BoxDecoration({
      color: '#1f2937',
      borderRadius: BorderRadius.circular(8)
    }),
    padding: EdgeInsets.all(20),
    child: Column({
      children: [
        // Title and legends on same line
        Row({
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            title,
            Row({ children: legends })
          ]
        }),
        SizedBox({ height: 20 }),
        // Chart area
        Expanded({ child: plot })
      ]
    })
  });
};

3. Custom Axis Labels

const RotatedXAxisLabel = ({ name, index }) => {
  return Transform.rotate({
    angle: -45 * Math.PI / 180,  // 45 degree rotation
    child: Container({
      padding: EdgeInsets.all(4),
      child: Text(name, {
        style: new TextStyle({
          fontSize: 11,
          color: '#6b7280'
        })
      })
    })
  });
};

4. Interactive Legend

const InteractiveLegend = ({ name, index }) => {
  const [isHovered, setIsHovered] = useState(false);
  const [isActive, setIsActive] = useState(true);
  
  return MouseRegion({
    onEnter: () => setIsHovered(true),
    onExit: () => setIsHovered(false),
    child: GestureDetector({
      onTap: () => setIsActive(!isActive),
      child: Container({
        padding: EdgeInsets.symmetric({ horizontal: 12, vertical: 6 }),
        decoration: new BoxDecoration({
          color: isHovered ? '#f3f4f6' : 'transparent',
          borderRadius: BorderRadius.circular(4),
          opacity: isActive ? 1 : 0.5
        }),
        child: Row({
          children: [
            Container({
              width: 12,
              height: 12,
              color: getColorByIndex(index),
              margin: EdgeInsets.only({ right: 8 })
            }),
            Text(name, {
              style: new TextStyle({
                fontSize: 14,
                color: '#374151',
                textDecoration: isActive ? 'none' : 'line-through'
              })
            })
          ]
        })
      })
    })
  });
};

Component Composition Strategies

1. Partial Customization

Replace only the necessary parts and use defaults for the rest:

BarChart({
  data: myData,
  custom: {
    // Customize only bars
    bar: GradientBar,
    // Use defaults for everything else
  }
});

2. Theme-based Customization

Create component sets by theme for reuse:

const darkTheme = {
  layout: DarkThemeLayout,
  bar: DarkThemeBar,
  xAxisLabel: DarkThemeAxisLabel,
  yAxisLabel: DarkThemeAxisLabel,
  gridXLine: () => Container({ height: 1, color: '#374151' }),
  gridYLine: () => Container({ width: 1, color: '#374151' })
};

const lightTheme = {
  layout: LightThemeLayout,
  bar: LightThemeBar,
  // ...
};

// Usage
BarChart({
  data: myData,
  custom: isDarkMode ? darkTheme : lightTheme
});

3. Composition Pattern

Extend by wrapping existing components:

const EnhancedBar = (defaultBar) => {
  return (args) => {
    const bar = defaultBar(args);
    
    // Add tooltip
    return Tooltip({
      message: `${args.label}: ${args.value}`,
      child: bar
    });
  };
};

// Usage
BarChart({
  custom: {
    bar: EnhancedBar(DefaultBar)
  }
});

Structure of Other Chart Types

Line Chart

LineChart({
  custom: {
    line: ({ points }) => Widget,
    point: ({ x, y, value }) => Widget,
    area: ({ points }) => Widget,  // For Area chart
    // ... other common components
  }
});

Scatter Chart

ScatterChart({
  custom: {
    point: ({ x, y, size, color }) => Widget,
    // ... other common components
  }
});

Next Steps

Now that you understand component architecture, learn how to handle chart data in Data Structure.

You can find practical customization examples for each chart type in the Chart Guide.