drafter 0.2.0
drafter: ^0.2.0 copied to clipboard
A premium, dependency-free Flutter charting library with ~27 chart types, smooth Catmull-Rom curves, soft gradient fills and a left-to-right reveal.
Drafter
๐ A powerful, flexible charting library for Flutter โ a native Dart port of Drafter for Compose and DrafterCharts for SwiftUI.
Features #
- ๐ 27 chart types out of the box:
- Bars โ Bar, Grouped Bar, Stacked Bar, Histogram, Waterfall
- Lines โ Line, Grouped Line, Stacked Line, Step Line, Area
- Distribution โ Scatter, Bubble, Box Plot, Candlestick
- Part-to-whole โ Pie, Donut, Funnel, Treemap, Polar Area, Sunburst
- Specialized โ Radar, Gantt, Gauge, Bullet, Sankey, Stream Graph, Contribution Heatmap
- ๐จ Highly customizable appearance with a shared
DrafterThemeColors(light/dark, custom palettes) - โจ Smooth, premium rendering โ Catmull-Rom curves, soft gradient fills, rounded shapes
- ๐ฌ Built-in left-to-right reveal animation with a one-line
replayhook - ๐ Pure Flutter
CustomPaint/Canvas, zero dependencies beyond the SDK - ๐ฑ Immutable value-type data models and an
InheritedWidget-based theme - โฟ๏ธ Semantics built in โ every chart announces its kind and a data summary, so a
Canvasis never silently invisible to screen readers - ๐งฉ One consistent, type-safe API โ every chart takes its bound elements directly (
points:,series:,bars:,slices:,nodes:โฆ), so a label can't desync from its value and there's nodata:wrapper to learn
Installation #
Add Drafter to your pubspec.yaml:
dependencies:
drafter: ^0.2.0
Or from the command line:
flutter pub add drafter
And import it:
import 'package:drafter/drafter.dart';
Anatomy of a chart #
Every chart is a Flutter Widget that takes an immutable data model. Two optional
knobs are shared by all charts:
| Parameter | Default | Meaning |
|---|---|---|
animate |
true |
Play the left-to-right reveal on first build. Pass false to draw fully revealed. |
replay |
0 |
Change this value (e.g. from a button) to replay the entrance animation. |
AreaChart(points: areaPoints) // animates on build
AreaChart(points: areaPoints, animate: false) // static
AreaChart(points: areaPoints, replay: replayKey) // bump replayKey to re-run
Size charts like any Flutter widget (SizedBox, AspectRatio, Expandedโฆ), and
set the palette / light-dark with a DrafterTheme ancestor.
For the simplest single-series charts there are values-first convenience constructors, so trivial cases can skip building labeled elements:
LineChart.values(values: [40, 65, 50, 80, 70, 95])
AreaChart.values(values: [12, 18, 9, 24, 20, 30], color: DrafterColors.teal)
SimpleBarChart.values(values: [24, 38, 30, 46])
StepLineChart.values(values: [10, 25, 18, 32])
ScatterPlot.values(values: [(1, 2), (3, 5), (4, 3)]) // raw (x, y) pairs
The full point/series form (points:, series:, bars:) is the primary API for
labels, multi-series, and per-element colors.
Table of Contents #
- Bar Charts โ Simple ยท Grouped ยท Stacked
- Line Charts โ Simple ยท Grouped ยท Stacked
- Histogram Chart
- Waterfall Chart
- Area Chart
- Step Line Chart
- Pie & Donut Chart
- Scatter Plot Chart
- Bubble Chart
- Candlestick Chart
- Box Plot Chart
- Radar Chart
- Gauge Chart
- Bullet Chart
- Funnel Chart
- Treemap Chart
- Polar Area Chart
- Sunburst Chart
- Sankey Chart
- Stream Graph Chart
- Gantt Chart
- Heatmap Chart
Every snippet below renders a single chart. Wrap it in a
SizedBox(or any sizing widget) to give it bounds, as shown in the first example.
Bar Charts #
Simple Bar Chart #
SizedBox(
height: 300,
child: SimpleBarChart(
bars: const [BarItem('Q1', 24), BarItem('Q2', 38), BarItem('Q3', 30), BarItem('Q4', 46)],
),
)
Grouped Bar Chart #
GroupedBarChart(
series: [
ChartSeries(name: '2023', color: DrafterColors.blue, values: [20, 34, 26, 40]),
ChartSeries(name: '2024', color: DrafterColors.teal, values: [28, 30, 38, 44]),
],
categories: const ['Q1', 'Q2', 'Q3', 'Q4'],
)
Stacked Bar Chart #
StackedBarChart(
series: [
ChartSeries(color: DrafterColors.blue, values: [12, 16, 14, 20]),
ChartSeries(color: DrafterColors.teal, values: [8, 10, 12, 14]),
ChartSeries(color: DrafterColors.violet, values: [6, 8, 10, 9]),
],
categories: const ['Q1', 'Q2', 'Q3', 'Q4'],
)
Line Charts #
Simple Line Chart #
LineChart(
points: const [ChartPoint('Jan', 40), ChartPoint('Feb', 65), ChartPoint('Mar', 50), ChartPoint('Apr', 80)],
color: DrafterColors.blue,
)
Grouped Line Chart #
GroupedLineChart(
series: [
ChartSeries(name: 'A', color: DrafterColors.blue, values: [30, 45, 40, 70]),
ChartSeries(name: 'B', color: DrafterColors.teal, values: [20, 35, 50, 45]),
],
categories: const ['Jan', 'Feb', 'Mar', 'Apr'],
)
Stacked Line Chart #
StackedLineChart(
series: [
ChartSeries(color: DrafterColors.violet, values: [10, 14, 12, 20]),
ChartSeries(color: DrafterColors.green, values: [8, 10, 14, 12]),
],
categories: const ['Jan', 'Feb', 'Mar', 'Apr'],
)
Histogram Chart #
Histogram(
values: const [2, 3, 3, 4, 5, 5, 6, 7, 8, 9, 10, 12, 13, 15],
binCount: 5,
)
Waterfall Chart #
Each WaterfallStep is an incremental change applied to initialValue; the
number of bars is driven by steps (one step per delta), so the counts always
line up.
WaterfallChart(
steps: const [WaterfallStep('Revenue', 50), WaterfallStep('Cost', -20), WaterfallStep('Profit', 30)],
initialValue: 100,
)
Opt into a leading Start bar (the initial value) and a trailing Total bar (the final running total) โ the classic Start โฆ Total waterfall:
WaterfallChart(
steps: const [WaterfallStep('Sales', 60), WaterfallStep('Costs', -25), WaterfallStep('Tax', -10)],
initialValue: 50,
startLabel: 'Start', // draws a leading bar at the initial value
totalLabel: 'Net', // draws a trailing bar at the final running total
)
Counts don't have to be perfect: every chart drives its element count from the value arrays, and mismatched
categories/colorsare handled gracefully (missing entries fall back, extras are ignored) โ no ghost columns or crashes.
Area Chart #
AreaChart(
points: const [
ChartPoint('Jan', 12), ChartPoint('Feb', 28), ChartPoint('Mar', 18),
ChartPoint('Apr', 34), ChartPoint('May', 24), ChartPoint('Jun', 40),
],
color: DrafterColors.blue,
)
Step Line Chart #
StepLineChart(
points: const [
ChartPoint('Jan', 10), ChartPoint('Feb', 25),
ChartPoint('Mar', 18), ChartPoint('Apr', 32),
],
)
Pie & Donut Chart #
final slices = [
PieSlice(value: 40, color: DrafterColors.blue, label: 'Blue'),
PieSlice(value: 30, color: DrafterColors.teal, label: 'Teal'),
PieSlice(value: 20, color: DrafterColors.violet, label: 'Violet'),
PieSlice(value: 10, color: DrafterColors.amber, label: 'Amber'),
];
PieChart(slices: slices);
DonutChart(slices: slices);
Scatter Plot Chart #
ScatterPlot(
points: [
const ScatterPoint(x: 1, y: 2),
const ScatterPoint(x: 2, y: 5),
ScatterPoint(x: 3, y: 3, color: DrafterColors.coral),
],
)
Bubble Chart #
BubbleChart(
series: [
[ BubbleData(x: 10, y: 26, size: 30, color: DrafterColors.blue),
BubbleData(x: 26, y: 30, size: 60, color: DrafterColors.blue) ],
[ BubbleData(x: 14, y: 15, size: 30, color: DrafterColors.teal),
BubbleData(x: 22, y: 36, size: 45, color: DrafterColors.teal) ],
],
)
Candlestick Chart #
CandlestickChart(
candles: const [
Candle(label: '1', open: 20, high: 30, low: 16, close: 26),
Candle(label: '2', open: 26, high: 32, low: 22, close: 23),
Candle(label: '3', open: 23, high: 28, low: 18, close: 27),
Candle(label: '4', open: 27, high: 38, low: 25, close: 35),
],
movingAverages: [MovingAverage(period: 3, color: DrafterColors.amber)],
)
Box Plot Chart #
BoxPlotChart(
groups: [
BoxGroup(label: 'A', min: 5, q1: 18, median: 28, q3: 38, max: 52, color: DrafterColors.violet),
BoxGroup(label: 'B', min: 10, q1: 22, median: 30, q3: 41, max: 48, color: DrafterColors.blue),
BoxGroup(label: 'C', min: 8, q1: 15, median: 24, q3: 33, max: 44, color: DrafterColors.teal),
],
)
Radar Chart #
RadarChart(
series: [
RadarSeries(color: DrafterColors.blue, values: const {'Speed': 0.8, 'Power': 0.6, 'Range': 0.9}),
RadarSeries(color: DrafterColors.teal, values: const {'Speed': 0.5, 'Power': 0.9, 'Range': 0.6}),
],
)
Gauge Chart #
GaugeChart(value: 72, min: 0, max: 100, label: 'Score', color: DrafterColors.teal)
Bullet Chart #
BulletChart(
metrics: [
BulletMetric(label: 'Revenue', value: 72, target: 80, ranges: const [40, 65, 100], color: DrafterColors.blue),
BulletMetric(label: 'Profit', value: 55, target: 50, ranges: const [30, 60, 90], color: DrafterColors.teal),
],
)
Funnel Chart #
FunnelChart(
stages: [
FunnelStage(label: 'Visits', value: 100, color: DrafterColors.blue),
FunnelStage(label: 'Signups', value: 64, color: DrafterColors.teal),
FunnelStage(label: 'Trials', value: 38, color: DrafterColors.violet),
FunnelStage(label: 'Paid', value: 18, color: DrafterColors.amber),
],
)
Treemap Chart #
TreemapChart(
items: [
TreemapItem(label: 'Mobile', value: 45, color: DrafterColors.blue),
TreemapItem(label: 'Desktop', value: 30, color: DrafterColors.teal),
TreemapItem(label: 'Tablet', value: 15, color: DrafterColors.violet),
TreemapItem(label: 'Watch', value: 8, color: DrafterColors.amber),
],
)
Polar Area Chart #
PolarAreaChart(
slices: [
PolarSlice(label: 'N', value: 40, color: DrafterColors.blue),
PolarSlice(label: 'E', value: 35, color: DrafterColors.violet),
PolarSlice(label: 'S', value: 30, color: DrafterColors.green),
PolarSlice(label: 'W', value: 22, color: DrafterColors.amber),
],
)
Sunburst Chart #
SunburstChart(
roots: [
SunburstNode(label: 'Web', value: 50, color: DrafterColors.blue, children: [
SunburstNode(label: 'HTML', value: 20, color: DrafterColors.blue),
SunburstNode(label: 'CSS', value: 15, color: DrafterColors.blue),
SunburstNode(label: 'JS', value: 15, color: DrafterColors.blue),
]),
SunburstNode(label: 'Mobile', value: 35, color: DrafterColors.teal, children: [
SunburstNode(label: 'iOS', value: 20, color: DrafterColors.teal),
SunburstNode(label: 'Android', value: 15, color: DrafterColors.teal),
]),
],
)
Sankey Chart #
SankeyChart(
nodes: [
SankeyNode(id: 'a', label: 'Source A', column: 0, color: DrafterColors.blue),
SankeyNode(id: 'b', label: 'Source B', column: 0, color: DrafterColors.teal),
SankeyNode(id: 'm', label: 'Hub', column: 1, color: DrafterColors.violet),
SankeyNode(id: 'x', label: 'Out X', column: 2, color: DrafterColors.amber),
SankeyNode(id: 'y', label: 'Out Y', column: 2, color: DrafterColors.green),
],
links: const [
SankeyLink(from: 'a', to: 'm', value: 30),
SankeyLink(from: 'b', to: 'm', value: 20),
SankeyLink(from: 'm', to: 'x', value: 28),
SankeyLink(from: 'm', to: 'y', value: 22),
],
)
Stream Graph Chart #
StreamGraphChart(
series: [
ChartSeries(name: 'A', color: DrafterColors.blue, values: [4, 6, 8, 7, 9, 6]),
ChartSeries(name: 'B', color: DrafterColors.teal, values: [3, 4, 6, 8, 7, 9]),
],
categories: const ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
)
Gantt Chart #
GanttChart(
tasks: const [
GanttTask(name: 'Design', startMonth: 0, duration: 2, color: DrafterColors.blue),
GanttTask(name: 'Build', startMonth: 2, duration: 3, color: DrafterColors.teal),
],
)
Heatmap Chart #
final start = DateTime(2026, 1, 1);
final contributions = [
for (var day = 0; day < 365; day++)
ContributionData(
date: start.add(Duration(days: day)),
count: (day * 13 + day % 7 * 5) % 16 - 4,
),
];
Heatmap(contributions: contributions);
Theming #
All charts read their palette and light/dark colors from the nearest
DrafterTheme ancestor. Set it once for a subtree:
DrafterTheme(
colors: DrafterThemeColors.dark, // or .light, or a custom set
child: Column(
children: [
AreaChart(points: areaPoints),
PieChart(slices: pieSlices),
],
),
)
A custom palette is just a DrafterThemeColors:
DrafterTheme(
colors: DrafterThemeColors(
palette: [DrafterColors.blue, DrafterColors.teal, DrafterColors.indigo],
grid: const Color(0xFFEDF0F5),
label: const Color(0xFF9AA3B2),
surface: const Color(0xFFFFFFFF),
isDark: false,
),
child: chart,
)
DrafterTheme.brightness(dark: true, child: โฆ) is a convenience for picking the
built-in light/dark set by a boolean. Each chart's geometry lives in a pure
ChartRenderer hosted by ChartCanvas, so the drawing is testable and the
theming + reveal animation are centralized in one place.
Writing a custom chart #
The everyday package:drafter/drafter.dart import gives you the chart widgets,
data models and theming. The lower-level building blocks for authoring your own
chart live in a separate entrypoint so they stay out of your way:
import 'package:drafter/drafter.dart'; // DrafterThemeColors, data models
import 'package:drafter/painting.dart'; // ChartRenderer, ChartCanvas, helpers
class MyRenderer extends ChartRenderer {
const MyRenderer();
@override
void draw(Canvas canvas, Size size, DrafterThemeColors theme, double progress) {
final bounds = ChartBounds(size); // shared layout math
canvas.drawRect(bounds.rect, Paint()..color = theme.colorAt(0));
drawChartText(canvas, 'hi', bounds.rect.center, color: theme.label);
}
@override
String get accessibilityLabel => 'My chart';
@override
String get accessibilityValue => 'a summary of the data';
}
// Host it in the shared animating canvas:
const ChartCanvas(renderer: MyRenderer());
painting.dart exposes the renderer base (ChartRenderer/ChartCanvas), the
layout helpers (ChartBounds, RadialLayout, ChartAxis, HAlign/VAlign),
the smooth-graphics helpers (smoothPath, drawSmoothLine, drawChartText,
areaGradientShader), and the shared formatters.
Accessibility #
A Canvas is a single opaque drawing โ by default screen readers skip right over
it. Drafter fixes this for you: ChartCanvas wraps each chart in one Semantics
node and pulls its description from the renderer, so every chart announces what
it is and a summary of its data with no extra work at the call site.
AreaChart(points: const [ChartPoint('Jan', 40), ChartPoint('Feb', 65), ChartPoint('Mar', 30)])
// TalkBack/VoiceOver: "Area chart, 3 points, Jan 40, Feb 65, Mar 30"
GaugeChart(value: 72, min: 0, max: 100, label: 'Score')
// TalkBack/VoiceOver: "Gauge, Score 72 of 0 to 100"
The label/value come from each ChartRenderer's accessibilityLabel and
accessibilityValue, so if you write a custom renderer you can describe it the
same way.
Demo #
A runnable gallery of every chart โ wrapped in light-themed cards โ lives in
example/. Run it on any platform:
cd example
flutter run # or: flutter run -d macos / -d chrome
Contributing #
Contributions are welcome! Found a bug, have an improvement, or want a new chart? Open an issue or a pull request โ see CONTRIBUTING.md.
Find this repository useful? โค๏ธ #
Support it by joining stargazers for this repository. โญ
Also, follow me on GitHub for my next creations! ๐คฉ
License #
Designed and developed by AndroidPoet (Ranbir Singh)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
