smooth_line_chart 0.0.4
smooth_line_chart: ^0.0.4 copied to clipboard
The simplest way to add stunning line charts to your Flutter app. Smooth bezier curves, tap/hover tooltips, dark mode, gradient fill, async data loading — all with zero dependencies.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:smooth_line_chart/smooth_line_chart.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'smooth_line_chart demo',
theme: ThemeData(useMaterial3: true),
home: const DemoPage(),
);
}
}
class DemoPage extends StatefulWidget {
const DemoPage({super.key});
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
static const _x = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'];
static const _y = [120000.0, 340000.0, 210000.0, 480000.0, 390000.0, 520000.0];
static const _qty = [12, 34, 21, 48, 39, 52];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('smooth_line_chart demo')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_section('Basic usage'),
SmoothLineChart(
xValues: _x,
yValues: _y,
qtyValues: _qty,
title: 'Monthly Revenue',
labelFormatter: (v) => 'Rp ${shortNumber(v)}',
qtyFormatter: (q) => 'Orders: $q',
),
_section('onPointTap — bottom sheet'),
SmoothLineChart(
xValues: _x,
yValues: _y,
qtyValues: _qty,
title: 'Tap a point to see detail',
labelFormatter: (v) => 'Rp ${shortNumber(v)}',
// onPointTap gives you: index, x label, y value, qty, date range
// Use context from the StatefulWidget to show dialogs/sheets
onPointTap: (i, x, y, qty, range) {
showModalBottomSheet(
context: context,
builder: (_) => _PointDetailSheet(
x: x,
y: y,
qty: qty,
),
);
},
),
_section('Gradient fill + dots + value labels'),
SmoothLineChart(
xValues: _x,
yValues: _y,
title: 'All visual extras',
labelFormatter: (v) => shortNumber(v),
theme: const ChartTheme(
lineColor: Color(0xFF6366F1),
fillGradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0x556366F1), Colors.transparent],
),
showDots: true,
dotRadius: 4,
showValueLabels: true,
valueLabelStyle: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
color: Color(0xFF6366F1),
),
),
),
_section('yMin — high baseline data'),
SmoothLineChart(
xValues: _x,
yValues: const [400000, 420000, 390000, 450000, 430000, 470000],
title: 'Without yMin (chart looks flat)',
labelFormatter: (v) => shortNumber(v),
),
const SizedBox(height: 16),
SmoothLineChart(
xValues: _x,
yValues: const [400000, 420000, 390000, 450000, 430000, 470000],
title: 'With yMin: 350000 (variations visible)',
yMin: 350000,
labelFormatter: (v) => shortNumber(v),
),
_section('Empty state'),
SmoothLineChart(
xValues: const [],
yValues: const [],
title: 'Default empty state',
labelFormatter: (v) => shortNumber(v),
),
const SizedBox(height: 16),
SmoothLineChart(
xValues: const [],
yValues: const [],
title: 'Custom empty state',
labelFormatter: (v) => shortNumber(v),
emptyWidget: Container(
height: 120,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.bar_chart_outlined, size: 32, color: Colors.grey),
SizedBox(height: 8),
Text('Belum ada data bulan ini',
style: TextStyle(color: Colors.grey)),
],
),
),
),
),
_section('Card styling — background + border radius'),
SmoothLineChart(
xValues: _x,
yValues: _y,
title: 'Revenue',
labelFormatter: (v) => shortNumber(v),
backgroundColor: const Color(0xFFF0F0FF),
borderRadius: 16,
),
_section('Dark theme + padding'),
SmoothLineChart(
xValues: _x,
yValues: _y,
title: 'Revenue',
theme: ChartTheme.dark,
labelFormatter: (v) => shortNumber(v),
backgroundColor: const Color(0xFF111827),
borderRadius: 16,
padding: const EdgeInsets.all(16),
height: 200,
),
_section('Tooltip with arrow'),
SmoothLineChart(
xValues: _x,
yValues: _y,
title: 'Revenue',
labelFormatter: (v) => 'Rp ${shortNumber(v)}',
tooltipArrow: true,
),
_section('Async + range selector'),
SmoothLineChartAsync(
title: 'Sales Analysis',
initialRange: '1M',
rangeOptions: const [
RangeOption(label: '1W', value: '1W'),
RangeOption(label: '1M', value: '1M'),
RangeOption(label: '3M', value: '3M'),
RangeOption(label: '6M', value: '6M'),
// RangeOption(label: '1Y', value: '1Y'),
],
dataLoader: (range) async {
await Future.delayed(const Duration(milliseconds: 600));
return _mockData(range);
},
onRangeChanged: (range) => debugPrint('Range changed: $range'),
labelFormatter: (v) => 'Rp ${shortNumber(v)}',
),
const SizedBox(height: 32),
],
),
),
);
}
Widget _section(String title) => Padding(
padding: const EdgeInsets.only(top: 32, bottom: 8),
child: Text(title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
);
static ChartData _mockData(String range) {
return switch (range) {
'1W' => ChartData(
xValues: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
yValues: [50000, 75000, 62000, 90000, 88000, 110000, 95000],
),
'1M' => ChartData(
xValues: ['W1', 'W2', 'W3', 'W4'],
yValues: [350000, 420000, 390000, 510000],
),
'3M' => ChartData(
xValues: ['Jan', 'Feb', 'Mar'],
yValues: [1200000, 1500000, 1350000],
),
'6M' => ChartData(
xValues: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
yValues: [1200000, 1500000, 1350000, 1700000, 1600000, 1900000],
),
_ => ChartData(
xValues: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
yValues: [1200000, 1500000, 1350000, 1700000, 1600000, 1900000,
2100000, 1950000, 2200000, 2400000, 2300000, 2600000],
),
};
}
}
class _PointDetailSheet extends StatelessWidget {
const _PointDetailSheet({required this.x, required this.y, this.qty});
final String x;
final double y;
final int? qty;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(x, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('Revenue: Rp ${shortNumber(y)}',
style: const TextStyle(fontSize: 16)),
if (qty != null) ...[
const SizedBox(height: 4),
Text('Orders: $qty', style: const TextStyle(fontSize: 16)),
],
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
),
],
),
);
}
}