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.