1.0.0
Some checks failed
Code Coverage / upload (push) Has been cancelled
Gh-Pages / build (push) Has been cancelled
Code Verification / verify (push) Has been cancelled

This commit is contained in:
zypherift
2025-08-09 18:17:34 +02:00
commit c7e3f36b06
438 changed files with 79192 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
import 'package:fl_chart_app/urls.dart';
import 'package:fl_chart_app/util/app_utils.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:equatable/equatable.dart';
part 'app_state.dart';
class AppCubit extends Cubit<AppState> {
AppCubit() : super(const AppState()) {
initialize();
}
void initialize() async {
PackageInfo packageInfo = await PackageInfo.fromPlatform();
emit(state.copyWith(
currentPackageInfo: packageInfo,
availableVersionToUpdate: '',
usingFlChartVersion: BuildConstants.usingFlChartVersion,
showDownloadNativeAppButton: kIsWeb || kIsWasm,
));
}
void onVersionClicked() {
AppUtils().tryToLaunchUrl(
Urls.getVersionReleaseUrl(state.usingFlChartVersion),
);
}
void hideDownloadNativeAppButton() {
emit(state.copyWith(
showDownloadNativeAppButton: false,
));
}
}
class BuildConstants {
static const String usingFlChartVersion = String.fromEnvironment(
'USING_FL_CHART_VERSION',
defaultValue: '',
);
}

View File

@@ -0,0 +1,39 @@
part of 'app_cubit.dart';
class AppState extends Equatable {
final PackageInfo? currentPackageInfo;
final String availableVersionToUpdate;
final String usingFlChartVersion;
final bool showDownloadNativeAppButton;
String? get appVersion => currentPackageInfo?.version;
const AppState([
this.currentPackageInfo,
this.availableVersionToUpdate = '',
this.usingFlChartVersion = '',
this.showDownloadNativeAppButton = false,
]);
AppState copyWith({
PackageInfo? currentPackageInfo,
String? availableVersionToUpdate,
String? usingFlChartVersion,
bool? showDownloadNativeAppButton,
}) {
return AppState(
currentPackageInfo ?? this.currentPackageInfo,
availableVersionToUpdate ?? this.availableVersionToUpdate,
usingFlChartVersion ?? this.usingFlChartVersion,
showDownloadNativeAppButton ?? this.showDownloadNativeAppButton,
);
}
@override
List<Object?> get props => [
currentPackageInfo,
availableVersionToUpdate,
usingFlChartVersion,
showDownloadNativeAppButton,
];
}

38
example/lib/main.dart Normal file
View File

@@ -0,0 +1,38 @@
import 'package:fl_chart_app/cubits/app/app_cubit.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
import 'presentation/router/app_router.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AppCubit>(create: (BuildContext context) => AppCubit()),
],
child: MaterialApp.router(
title: AppTexts.appName,
theme: ThemeData(
brightness: Brightness.dark,
useMaterial3: true,
textTheme: GoogleFonts.assistantTextTheme(
Theme.of(context).textTheme.apply(
bodyColor: AppColors.mainTextColor3,
),
),
scaffoldBackgroundColor: AppColors.pageBackground,
),
routerConfig: appRouterConfig,
),
);
}
}

View File

@@ -0,0 +1,166 @@
import 'package:dartx/dartx.dart';
import 'package:fl_chart_app/cubits/app/app_cubit.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/urls.dart';
import 'package:fl_chart_app/util/app_helper.dart';
import 'package:fl_chart_app/util/app_utils.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
import 'fl_chart_banner.dart';
import 'menu_row.dart';
class AppMenu extends StatefulWidget {
final List<ChartMenuItem> menuItems;
final int currentSelectedIndex;
final Function(int, ChartMenuItem) onItemSelected;
final VoidCallback? onBannerClicked;
const AppMenu({
super.key,
required this.menuItems,
required this.currentSelectedIndex,
required this.onItemSelected,
required this.onBannerClicked,
});
@override
AppMenuState createState() => AppMenuState();
}
class AppMenuState extends State<AppMenu> {
@override
Widget build(BuildContext context) {
return Container(
color: AppColors.itemsBackground,
child: Column(
children: [
SafeArea(
child: AspectRatio(
aspectRatio: 3,
child: Center(
child: InkWell(
onTap: widget.onBannerClicked,
child: const FlChartBanner(),
),
),
),
),
Expanded(
child: ListView.builder(
itemBuilder: (context, position) {
final menuItem = widget.menuItems[position];
return MenuRow(
text: menuItem.text,
svgPath: menuItem.iconPath,
isSelected: widget.currentSelectedIndex == position,
onTap: () {
widget.onItemSelected(position, menuItem);
},
onDocumentsTap: () async {
final url = Uri.parse(menuItem.chartType.documentationUrl);
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
},
);
},
itemCount: widget.menuItems.length,
),
),
const _AppVersionRow(),
],
),
);
}
}
class _AppVersionRow extends StatelessWidget {
const _AppVersionRow();
@override
Widget build(BuildContext context) {
return BlocBuilder<AppCubit, AppState>(builder: (context, state) {
if (state.appVersion.isNullOrBlank) {
return Container();
}
return Container(
margin: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: RichText(
text: TextSpan(
text: '',
style: DefaultTextStyle.of(context).style,
children: <TextSpan>[
const TextSpan(text: 'App version: '),
TextSpan(
text: 'v${state.appVersion!}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
if (state.usingFlChartVersion.isNotBlank) ...[
TextSpan(
text: '\nfl_chart: ',
recognizer: TapGestureRecognizer()
..onTap = BlocProvider.of<AppCubit>(context)
.onVersionClicked,
),
TextSpan(
text: 'v${state.usingFlChartVersion}',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
recognizer: TapGestureRecognizer()
..onTap = BlocProvider.of<AppCubit>(context)
.onVersionClicked,
),
]
],
),
),
),
),
state.availableVersionToUpdate.isNotBlank
? TextButton(
onPressed: () {},
child: Text(
'Update to ${state.availableVersionToUpdate}',
style: const TextStyle(
color: AppColors.primary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
)
: TextButton(
onPressed: () => AppUtils().tryToLaunchUrl(Urls.aboutUrl),
child: const Text(
'About',
style: TextStyle(
color: AppColors.primary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
});
}
}
class ChartMenuItem {
final ChartType chartType;
final String text;
final String iconPath;
const ChartMenuItem(this.chartType, this.text, this.iconPath);
}

View File

@@ -0,0 +1,38 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class FlChartBanner extends StatelessWidget {
const FlChartBanner({super.key});
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final maxWidth = constraints.maxWidth;
final imageSize = maxWidth / 5.14;
final space = maxWidth / 16.0;
final textWidth = maxWidth / 2.8;
return Row(
children: [
SizedBox(
width: imageSize,
),
Image.asset(
AppAssets.flChartLogoIcon,
width: imageSize,
height: imageSize,
),
SizedBox(
width: space,
),
SvgPicture.asset(
AppAssets.flChartLogoText,
width: textWidth,
),
],
);
});
}
}

View File

@@ -0,0 +1,99 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class MenuRow extends StatefulWidget {
final String text;
final String svgPath;
final bool isSelected;
final VoidCallback onTap;
final VoidCallback onDocumentsTap;
const MenuRow({
super.key,
required this.text,
required this.svgPath,
required this.isSelected,
required this.onTap,
required this.onDocumentsTap,
});
@override
State<MenuRow> createState() => _MenuRowState();
}
class _MenuRowState extends State<MenuRow> {
bool get _showSelectedState => widget.isSelected;
bool isHovered = false;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: InkWell(
onHover: (bool hovered) {
setState(() {
isHovered = hovered;
});
},
onTap: widget.onTap,
child: SizedBox(
height: AppDimens.menuRowHeight,
child: Row(
children: [
const SizedBox(
width: 36,
),
SvgPicture.asset(
widget.svgPath,
width: AppDimens.menuIconSize,
height: AppDimens.menuIconSize,
colorFilter:
const ColorFilter.mode(AppColors.primary, BlendMode.srcIn),
),
const SizedBox(
width: 18,
),
Text(
widget.text,
style: TextStyle(
color: _showSelectedState ? AppColors.primary : Colors.white,
fontSize: AppDimens.menuTextSize,
),
),
Expanded(child: Container()),
_DocumentationIcon(onTap: widget.onDocumentsTap),
const SizedBox(
width: 18,
),
],
),
),
),
);
}
}
class _DocumentationIcon extends StatelessWidget {
const _DocumentationIcon({
required this.onTap,
});
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return SizedBox(
width: AppDimens.menuDocumentationIconSize,
height: AppDimens.menuDocumentationIconSize,
child: IconButton(
onPressed: onTap,
icon: const Icon(
Icons.article,
color: AppColors.contentColorWhite,
),
tooltip: 'Documentation',
),
);
}
}

View File

@@ -0,0 +1,42 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/presentation/samples/chart_samples.dart';
import 'package:fl_chart_app/presentation/widgets/chart_holder.dart';
import 'package:fl_chart_app/util/app_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
class ChartSamplesPage extends StatelessWidget {
final ChartType chartType;
final samples = ChartSamples.samples;
ChartSamplesPage({
super.key,
required this.chartType,
});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
body: MasonryGridView.builder(
itemCount: samples[chartType]!.length,
key: ValueKey(chartType),
padding: const EdgeInsets.only(
left: AppDimens.chartSamplesSpace,
right: AppDimens.chartSamplesSpace,
top: AppDimens.chartSamplesSpace,
bottom: AppDimens.chartSamplesSpace + 68,
),
crossAxisSpacing: AppDimens.chartSamplesSpace,
mainAxisSpacing: AppDimens.chartSamplesSpace,
itemBuilder: (BuildContext context, int index) {
return ChartHolder(chartSample: samples[chartType]![index]);
},
gridDelegate: const SliverSimpleGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 600,
),
),
);
}
}

View File

@@ -0,0 +1,116 @@
import 'package:dartx/dartx.dart';
import 'package:fl_chart_app/cubits/app/app_cubit.dart';
import 'package:fl_chart_app/presentation/menu/app_menu.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/presentation/widgets/download_native_app_button.dart';
import 'package:fl_chart_app/urls.dart';
import 'package:fl_chart_app/util/app_helper.dart';
import 'package:fl_chart_app/util/app_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'chart_samples_page.dart';
class HomePage extends StatelessWidget {
HomePage({
super.key,
required this.showingChartType,
}) {
_initMenuItems();
}
void _initMenuItems() {
_menuItemsIndices = {};
_menuItems = ChartType.values.mapIndexed(
(int index, ChartType type) {
_menuItemsIndices[type] = index;
return ChartMenuItem(
type,
type.displayName,
type.assetIcon,
);
},
).toList();
}
final ChartType showingChartType;
late final Map<ChartType, int> _menuItemsIndices;
late final List<ChartMenuItem> _menuItems;
@override
Widget build(BuildContext context) {
final selectedMenuIndex = _menuItemsIndices[showingChartType]!;
return LayoutBuilder(
builder: (context, constraints) {
final needsDrawer = constraints.maxWidth <=
AppDimens.menuMaxNeededWidth + AppDimens.chartBoxMinWidth;
final appMenuWidget = AppMenu(
menuItems: _menuItems,
currentSelectedIndex: selectedMenuIndex,
onItemSelected: (newIndex, chartMenuItem) {
context.go('/${chartMenuItem.chartType.name}');
if (needsDrawer) {
/// to close the drawer
Navigator.of(context).pop();
}
},
onBannerClicked: () => AppUtils().tryToLaunchUrl(Urls.flChartUrl),
);
final samplesSectionWidget =
ChartSamplesPage(chartType: showingChartType);
final body = needsDrawer
? samplesSectionWidget
: Row(
children: [
SizedBox(
width: AppDimens.menuMaxNeededWidth,
child: appMenuWidget,
),
Expanded(
child: samplesSectionWidget,
)
],
);
return BlocBuilder<AppCubit, AppState>(
builder: (context, state) {
return Scaffold(
body: Stack(
children: [
body,
if (state.showDownloadNativeAppButton)
Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: DownloadNativeAppButton(
onClose: () => context
.read<AppCubit>()
.hideDownloadNativeAppButton(),
onDownload: () =>
AppUtils().tryToLaunchUrl(Urls.downloadUrl),
),
),
),
],
),
drawer: needsDrawer
? Drawer(
child: appMenuWidget,
)
: null,
appBar: needsDrawer
? AppBar(
elevation: 0,
backgroundColor: Colors.transparent,
title: Text(showingChartType.displayName),
)
: null,
);
},
);
},
);
}
}

View File

@@ -0,0 +1,16 @@
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
class AppUtils {
static String getFormattedCurrency(
BuildContext context,
double value, {
bool noDecimals = true,
}) {
final germanFormat = NumberFormat.currency(
symbol: '',
decimalDigits: noDecimals && value % 1 == 0 ? 0 : 2,
);
return germanFormat.format(value);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:fl_chart_app/util/app_helper.dart';
class AppAssets {
static String getChartIcon(ChartType type) {
switch (type) {
case ChartType.line:
return 'assets/icons/ic_line_chart.svg';
case ChartType.bar:
return 'assets/icons/ic_bar_chart.svg';
case ChartType.pie:
return 'assets/icons/ic_pie_chart.svg';
case ChartType.scatter:
return 'assets/icons/ic_scatter_chart.svg';
case ChartType.radar:
return 'assets/icons/ic_radar_chart.svg';
case ChartType.candlestick:
return 'assets/icons/ic_candle_chart.svg';
}
}
static const flChartLogoIcon = 'assets/icons/fl_chart_logo_icon.png';
static const flChartLogoText = 'assets/icons/fl_chart_logo_text.svg';
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class AppColors {
static const Color primary = contentColorCyan;
static const Color menuBackground = Color(0xFF090912);
static const Color itemsBackground = Color(0xFF1B2339);
static const Color pageBackground = Color(0xFF282E45);
static const Color mainTextColor1 = Colors.white;
static const Color mainTextColor2 = Colors.white70;
static const Color mainTextColor3 = Colors.white38;
static const Color mainGridLineColor = Colors.white10;
static const Color borderColor = Colors.white54;
static const Color gridLinesColor = Color(0x11FFFFFF);
static const Color contentColorBlack = Colors.black;
static const Color contentColorWhite = Colors.white;
static const Color contentColorBlue = Color(0xFF2196F3);
static const Color contentColorYellow = Color(0xFFFFC300);
static const Color contentColorOrange = Color(0xFFFF683B);
static const Color contentColorGreen = Color(0xFF3BFF49);
static const Color contentColorPurple = Color(0xFF6E1BFF);
static const Color contentColorPink = Color(0xFFFF3AF2);
static const Color contentColorRed = Color(0xFFE80054);
static const Color contentColorCyan = Color(0xFF50E4FF);
}

View File

@@ -0,0 +1,13 @@
class AppDimens {
static const double menuMaxNeededWidth = 304;
static const double menuRowHeight = 74;
static const double menuIconSize = 32;
static const double menuDocumentationIconSize = 44;
static const double menuTextSize = 20;
static const double chartBoxMinWidth = 350;
static const double defaultRadius = 8;
static const double chartSamplesSpace = 32.0;
static const double chartSamplesMinWidth = 350;
}

View File

@@ -0,0 +1,4 @@
export 'app_colors.dart';
export 'app_assets.dart';
export 'app_dimens.dart';
export 'app_texts.dart';

View File

@@ -0,0 +1,3 @@
class AppTexts {
static const appName = 'FL Chart App';
}

View File

@@ -0,0 +1,37 @@
import 'package:fl_chart_app/presentation/pages/home_page.dart';
import 'package:fl_chart_app/presentation/resources/app_colors.dart';
import 'package:fl_chart_app/util/app_helper.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final appRouterConfig = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => Container(color: AppColors.pageBackground),
redirect: (context, state) {
return '/${ChartType.values.first.name}';
},
),
...ChartType.values.map(
(ChartType chartType) => GoRoute(
path: '/${chartType.name}',
pageBuilder: (BuildContext context, GoRouterState state) =>
MaterialPage<void>(
/// We set a key for HomePage to prevent recreate it
/// when user choose a new chart type to show
key: const ValueKey('home_page'),
child: HomePage(showingChartType: chartType),
),
),
),
GoRoute(
path: '/:any',
builder: (context, state) => Container(color: AppColors.pageBackground),
redirect: (context, state) {
// Unsupported path, we redirect it to /, which redirects it to /line
return '/';
},
),
],
);

View File

@@ -0,0 +1,397 @@
import 'dart:async';
import 'dart:math';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/util/extensions/color_extensions.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class BarChartSample1 extends StatefulWidget {
BarChartSample1({super.key});
List<Color> get availableColors => const <Color>[
AppColors.contentColorPurple,
AppColors.contentColorYellow,
AppColors.contentColorBlue,
AppColors.contentColorOrange,
AppColors.contentColorPink,
AppColors.contentColorRed,
];
final Color barBackgroundColor =
AppColors.contentColorWhite.darken().withValues(alpha: 0.3);
final Color barColor = AppColors.contentColorWhite;
final Color touchedBarColor = AppColors.contentColorGreen;
@override
State<StatefulWidget> createState() => BarChartSample1State();
}
class BarChartSample1State extends State<BarChartSample1> {
final Duration animDuration = const Duration(milliseconds: 250);
int touchedIndex = -1;
bool isPlaying = false;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1,
child: Stack(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const Text(
'Mingguan',
style: TextStyle(
color: AppColors.contentColorGreen,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
height: 4,
),
Text(
'Grafik konsumsi kalori',
style: TextStyle(
color: AppColors.contentColorGreen.darken(),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
height: 38,
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: BarChart(
isPlaying ? randomData() : mainBarData(),
duration: animDuration,
),
),
),
const SizedBox(
height: 12,
),
],
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Align(
alignment: Alignment.topRight,
child: IconButton(
icon: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
color: AppColors.contentColorGreen,
),
onPressed: () {
setState(() {
isPlaying = !isPlaying;
if (isPlaying) {
refreshState();
}
});
},
),
),
)
],
),
);
}
BarChartGroupData makeGroupData(
int x,
double y, {
bool isTouched = false,
Color? barColor,
double width = 22,
List<int> showTooltips = const [],
}) {
barColor ??= widget.barColor;
return BarChartGroupData(
x: x,
barRods: [
BarChartRodData(
toY: isTouched ? y + 1 : y,
color: isTouched ? widget.touchedBarColor : barColor,
width: width,
borderSide: isTouched
? BorderSide(color: widget.touchedBarColor.darken(80))
: const BorderSide(color: Colors.white, width: 0),
backDrawRodData: BackgroundBarChartRodData(
show: true,
toY: 20,
color: widget.barBackgroundColor,
),
),
],
showingTooltipIndicators: showTooltips,
);
}
List<BarChartGroupData> showingGroups() => List.generate(7, (i) {
switch (i) {
case 0:
return makeGroupData(0, 5, isTouched: i == touchedIndex);
case 1:
return makeGroupData(1, 6.5, isTouched: i == touchedIndex);
case 2:
return makeGroupData(2, 5, isTouched: i == touchedIndex);
case 3:
return makeGroupData(3, 7.5, isTouched: i == touchedIndex);
case 4:
return makeGroupData(4, 9, isTouched: i == touchedIndex);
case 5:
return makeGroupData(5, 11.5, isTouched: i == touchedIndex);
case 6:
return makeGroupData(6, 6.5, isTouched: i == touchedIndex);
default:
return throw Error();
}
});
BarChartData mainBarData() {
return BarChartData(
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (_) => Colors.blueGrey,
tooltipHorizontalAlignment: FLHorizontalAlignment.right,
tooltipMargin: -10,
getTooltipItem: (group, groupIndex, rod, rodIndex) {
String weekDay;
switch (group.x) {
case 0:
weekDay = 'Monday';
break;
case 1:
weekDay = 'Tuesday';
break;
case 2:
weekDay = 'Wednesday';
break;
case 3:
weekDay = 'Thursday';
break;
case 4:
weekDay = 'Friday';
break;
case 5:
weekDay = 'Saturday';
break;
case 6:
weekDay = 'Sunday';
break;
default:
throw Error();
}
return BarTooltipItem(
'$weekDay\n',
const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 18,
),
children: <TextSpan>[
TextSpan(
text: (rod.toY - 1).toString(),
style: const TextStyle(
color: Colors.white, //widget.touchedBarColor,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
],
);
},
),
touchCallback: (FlTouchEvent event, barTouchResponse) {
setState(() {
if (!event.isInterestedForInteractions ||
barTouchResponse == null ||
barTouchResponse.spot == null) {
touchedIndex = -1;
return;
}
touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
});
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: getTitles,
reservedSize: 38,
),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
),
borderData: FlBorderData(
show: false,
),
barGroups: showingGroups(),
gridData: const FlGridData(show: false),
);
}
Widget getTitles(double value, TitleMeta meta) {
const style = TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
);
Widget text;
switch (value.toInt()) {
case 0:
text = const Text('M', style: style);
break;
case 1:
text = const Text('T', style: style);
break;
case 2:
text = const Text('W', style: style);
break;
case 3:
text = const Text('T', style: style);
break;
case 4:
text = const Text('F', style: style);
break;
case 5:
text = const Text('S', style: style);
break;
case 6:
text = const Text('S', style: style);
break;
default:
text = const Text('', style: style);
break;
}
return SideTitleWidget(
meta: meta,
space: 16,
child: text,
);
}
BarChartData randomData() {
return BarChartData(
barTouchData: const BarTouchData(
enabled: false,
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: getTitles,
reservedSize: 38,
),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
),
borderData: FlBorderData(
show: false,
),
barGroups: List.generate(7, (i) {
switch (i) {
case 0:
return makeGroupData(
0,
Random().nextInt(15).toDouble() + 6,
barColor: widget.availableColors[
Random().nextInt(widget.availableColors.length)],
);
case 1:
return makeGroupData(
1,
Random().nextInt(15).toDouble() + 6,
barColor: widget.availableColors[
Random().nextInt(widget.availableColors.length)],
);
case 2:
return makeGroupData(
2,
Random().nextInt(15).toDouble() + 6,
barColor: widget.availableColors[
Random().nextInt(widget.availableColors.length)],
);
case 3:
return makeGroupData(
3,
Random().nextInt(15).toDouble() + 6,
barColor: widget.availableColors[
Random().nextInt(widget.availableColors.length)],
);
case 4:
return makeGroupData(
4,
Random().nextInt(15).toDouble() + 6,
barColor: widget.availableColors[
Random().nextInt(widget.availableColors.length)],
);
case 5:
return makeGroupData(
5,
Random().nextInt(15).toDouble() + 6,
barColor: widget.availableColors[
Random().nextInt(widget.availableColors.length)],
);
case 6:
return makeGroupData(
6,
Random().nextInt(15).toDouble() + 6,
barColor: widget.availableColors[
Random().nextInt(widget.availableColors.length)],
);
default:
return throw Error();
}
}),
gridData: const FlGridData(show: false),
);
}
Future<dynamic> refreshState() async {
setState(() {});
await Future<dynamic>.delayed(
animDuration + const Duration(milliseconds: 50),
);
if (isPlaying) {
await refreshState();
}
}
}

View File

@@ -0,0 +1,283 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/util/extensions/color_extensions.dart';
import 'package:flutter/material.dart';
class BarChartSample2 extends StatefulWidget {
BarChartSample2({super.key});
final Color leftBarColor = AppColors.contentColorYellow;
final Color rightBarColor = AppColors.contentColorRed;
final Color avgColor =
AppColors.contentColorOrange.avg(AppColors.contentColorRed);
@override
State<StatefulWidget> createState() => BarChartSample2State();
}
class BarChartSample2State extends State<BarChartSample2> {
final double width = 7;
late List<BarChartGroupData> rawBarGroups;
late List<BarChartGroupData> showingBarGroups;
int touchedGroupIndex = -1;
@override
void initState() {
super.initState();
final barGroup1 = makeGroupData(0, 5, 12);
final barGroup2 = makeGroupData(1, 16, 12);
final barGroup3 = makeGroupData(2, 18, 5);
final barGroup4 = makeGroupData(3, 20, 16);
final barGroup5 = makeGroupData(4, 17, 6);
final barGroup6 = makeGroupData(5, 19, 1.5);
final barGroup7 = makeGroupData(6, 10, 1.5);
final items = [
barGroup1,
barGroup2,
barGroup3,
barGroup4,
barGroup5,
barGroup6,
barGroup7,
];
rawBarGroups = items;
showingBarGroups = rawBarGroups;
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
makeTransactionsIcon(),
const SizedBox(
width: 38,
),
const Text(
'Transactions',
style: TextStyle(color: Colors.white, fontSize: 22),
),
const SizedBox(
width: 4,
),
const Text(
'state',
style: TextStyle(color: Color(0xff77839a), fontSize: 16),
),
],
),
const SizedBox(
height: 38,
),
Expanded(
child: BarChart(
BarChartData(
maxY: 20,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipColor: ((group) {
return Colors.grey;
}),
getTooltipItem: (a, b, c, d) => null,
),
touchCallback: (FlTouchEvent event, response) {
if (response == null || response.spot == null) {
setState(() {
touchedGroupIndex = -1;
showingBarGroups = List.of(rawBarGroups);
});
return;
}
touchedGroupIndex = response.spot!.touchedBarGroupIndex;
setState(() {
if (!event.isInterestedForInteractions) {
touchedGroupIndex = -1;
showingBarGroups = List.of(rawBarGroups);
return;
}
showingBarGroups = List.of(rawBarGroups);
if (touchedGroupIndex != -1) {
var sum = 0.0;
for (final rod
in showingBarGroups[touchedGroupIndex].barRods) {
sum += rod.toY;
}
final avg = sum /
showingBarGroups[touchedGroupIndex]
.barRods
.length;
showingBarGroups[touchedGroupIndex] =
showingBarGroups[touchedGroupIndex].copyWith(
barRods: showingBarGroups[touchedGroupIndex]
.barRods
.map((rod) {
return rod.copyWith(
toY: avg, color: widget.avgColor);
}).toList(),
);
}
});
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: bottomTitles,
reservedSize: 42,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
interval: 1,
getTitlesWidget: leftTitles,
),
),
),
borderData: FlBorderData(
show: false,
),
barGroups: showingBarGroups,
gridData: const FlGridData(show: false),
),
),
),
const SizedBox(
height: 12,
),
],
),
),
);
}
Widget leftTitles(double value, TitleMeta meta) {
const style = TextStyle(
color: Color(0xff7589a2),
fontWeight: FontWeight.bold,
fontSize: 14,
);
String text;
if (value == 0) {
text = '1K';
} else if (value == 10) {
text = '5K';
} else if (value == 19) {
text = '10K';
} else {
return Container();
}
return SideTitleWidget(
meta: meta,
space: 0,
child: Text(text, style: style),
);
}
Widget bottomTitles(double value, TitleMeta meta) {
final titles = <String>['Mn', 'Te', 'Wd', 'Tu', 'Fr', 'St', 'Su'];
final Widget text = Text(
titles[value.toInt()],
style: const TextStyle(
color: Color(0xff7589a2),
fontWeight: FontWeight.bold,
fontSize: 14,
),
);
return SideTitleWidget(
meta: meta,
space: 16, //margin top
child: text,
);
}
BarChartGroupData makeGroupData(int x, double y1, double y2) {
return BarChartGroupData(
barsSpace: 4,
x: x,
barRods: [
BarChartRodData(
toY: y1,
color: widget.leftBarColor,
width: width,
),
BarChartRodData(
toY: y2,
color: widget.rightBarColor,
width: width,
),
],
);
}
Widget makeTransactionsIcon() {
const width = 4.5;
const space = 3.5;
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
width: width,
height: 10,
color: Colors.white.withValues(alpha: 0.4),
),
const SizedBox(
width: space,
),
Container(
width: width,
height: 28,
color: Colors.white.withValues(alpha: 0.8),
),
const SizedBox(
width: space,
),
Container(
width: width,
height: 42,
color: Colors.white.withValues(alpha: 1),
),
const SizedBox(
width: space,
),
Container(
width: width,
height: 28,
color: Colors.white.withValues(alpha: 0.8),
),
const SizedBox(
width: space,
),
Container(
width: width,
height: 10,
color: Colors.white.withValues(alpha: 0.4),
),
],
);
}
}

View File

@@ -0,0 +1,209 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/util/extensions/color_extensions.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class _BarChart extends StatelessWidget {
const _BarChart();
@override
Widget build(BuildContext context) {
return BarChart(
BarChartData(
barTouchData: barTouchData,
titlesData: titlesData,
borderData: borderData,
barGroups: barGroups,
gridData: const FlGridData(show: false),
alignment: BarChartAlignment.spaceAround,
maxY: 20,
),
);
}
BarTouchData get barTouchData => BarTouchData(
enabled: false,
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (group) => Colors.transparent,
tooltipPadding: EdgeInsets.zero,
tooltipMargin: 8,
getTooltipItem: (
BarChartGroupData group,
int groupIndex,
BarChartRodData rod,
int rodIndex,
) {
return BarTooltipItem(
rod.toY.round().toString(),
const TextStyle(
color: AppColors.contentColorCyan,
fontWeight: FontWeight.bold,
),
);
},
),
);
Widget getTitles(double value, TitleMeta meta) {
final style = TextStyle(
color: AppColors.contentColorBlue.darken(20),
fontWeight: FontWeight.bold,
fontSize: 14,
);
String text;
switch (value.toInt()) {
case 0:
text = 'Mn';
break;
case 1:
text = 'Te';
break;
case 2:
text = 'Wd';
break;
case 3:
text = 'Tu';
break;
case 4:
text = 'Fr';
break;
case 5:
text = 'St';
break;
case 6:
text = 'Sn';
break;
default:
text = '';
break;
}
return SideTitleWidget(
meta: meta,
space: 4,
child: Text(text, style: style),
);
}
FlTitlesData get titlesData => FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: getTitles,
),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
);
FlBorderData get borderData => FlBorderData(
show: false,
);
LinearGradient get _barsGradient => LinearGradient(
colors: [
AppColors.contentColorBlue.darken(20),
AppColors.contentColorCyan,
],
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
);
List<BarChartGroupData> get barGroups => [
BarChartGroupData(
x: 0,
barRods: [
BarChartRodData(
toY: 8,
gradient: _barsGradient,
)
],
showingTooltipIndicators: [0],
),
BarChartGroupData(
x: 1,
barRods: [
BarChartRodData(
toY: 10,
gradient: _barsGradient,
)
],
showingTooltipIndicators: [0],
),
BarChartGroupData(
x: 2,
barRods: [
BarChartRodData(
toY: 14,
gradient: _barsGradient,
)
],
showingTooltipIndicators: [0],
),
BarChartGroupData(
x: 3,
barRods: [
BarChartRodData(
toY: 15,
gradient: _barsGradient,
)
],
showingTooltipIndicators: [0],
),
BarChartGroupData(
x: 4,
barRods: [
BarChartRodData(
toY: 13,
gradient: _barsGradient,
)
],
showingTooltipIndicators: [0],
),
BarChartGroupData(
x: 5,
barRods: [
BarChartRodData(
toY: 10,
gradient: _barsGradient,
)
],
showingTooltipIndicators: [0],
),
BarChartGroupData(
x: 6,
barRods: [
BarChartRodData(
toY: 16,
gradient: _barsGradient,
)
],
showingTooltipIndicators: [0],
),
];
}
class BarChartSample3 extends StatefulWidget {
const BarChartSample3({super.key});
@override
State<StatefulWidget> createState() => BarChartSample3State();
}
class BarChartSample3State extends State<BarChartSample3> {
@override
Widget build(BuildContext context) {
return const AspectRatio(
aspectRatio: 1.6,
child: _BarChart(),
);
}
}

View File

@@ -0,0 +1,352 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/util/extensions/color_extensions.dart';
import 'package:flutter/material.dart';
class BarChartSample4 extends StatefulWidget {
BarChartSample4({super.key});
final Color dark = AppColors.contentColorCyan.darken(60);
final Color normal = AppColors.contentColorCyan.darken(30);
final Color light = AppColors.contentColorCyan;
@override
State<StatefulWidget> createState() => BarChartSample4State();
}
class BarChartSample4State extends State<BarChartSample4> {
Widget bottomTitles(double value, TitleMeta meta) {
const style = TextStyle(fontSize: 10);
String text;
switch (value.toInt()) {
case 0:
text = 'Apr';
break;
case 1:
text = 'May';
break;
case 2:
text = 'Jun';
break;
case 3:
text = 'Jul';
break;
case 4:
text = 'Aug';
break;
default:
text = '';
break;
}
return SideTitleWidget(
meta: meta,
child: Text(text, style: style),
);
}
Widget leftTitles(double value, TitleMeta meta) {
if (value == meta.max) {
return Container();
}
const style = TextStyle(
fontSize: 10,
);
return SideTitleWidget(
meta: meta,
child: Text(
meta.formattedValue,
style: style,
),
);
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.66,
child: Padding(
padding: const EdgeInsets.only(top: 16),
child: LayoutBuilder(
builder: (context, constraints) {
final barsSpace = 4.0 * constraints.maxWidth / 400;
final barsWidth = 8.0 * constraints.maxWidth / 400;
return BarChart(
BarChartData(
alignment: BarChartAlignment.center,
barTouchData: const BarTouchData(
enabled: false,
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 28,
getTitlesWidget: bottomTitles,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: leftTitles,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
show: true,
checkToShowHorizontalLine: (value) => value % 10 == 0,
getDrawingHorizontalLine: (value) => FlLine(
color: AppColors.borderColor.withValues(alpha: 0.1),
strokeWidth: 1,
),
drawVerticalLine: false,
),
borderData: FlBorderData(
show: false,
),
groupsSpace: barsSpace,
barGroups: getData(barsWidth, barsSpace),
),
);
},
),
),
);
}
List<BarChartGroupData> getData(double barsWidth, double barsSpace) {
return [
BarChartGroupData(
x: 0,
barsSpace: barsSpace,
barRods: [
BarChartRodData(
toY: 17000000000,
rodStackItems: [
BarChartRodStackItem(0, 2000000000, widget.dark),
BarChartRodStackItem(2000000000, 12000000000, widget.normal),
BarChartRodStackItem(12000000000, 17000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 24000000000,
rodStackItems: [
BarChartRodStackItem(0, 13000000000, widget.dark),
BarChartRodStackItem(13000000000, 14000000000, widget.normal),
BarChartRodStackItem(14000000000, 24000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 23000000000.5,
rodStackItems: [
BarChartRodStackItem(0, 6000000000.5, widget.dark),
BarChartRodStackItem(6000000000.5, 18000000000, widget.normal),
BarChartRodStackItem(18000000000, 23000000000.5, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 29000000000,
rodStackItems: [
BarChartRodStackItem(0, 9000000000, widget.dark),
BarChartRodStackItem(9000000000, 15000000000, widget.normal),
BarChartRodStackItem(15000000000, 29000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 32000000000,
rodStackItems: [
BarChartRodStackItem(0, 2000000000.5, widget.dark),
BarChartRodStackItem(2000000000.5, 17000000000.5, widget.normal),
BarChartRodStackItem(17000000000.5, 32000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
],
),
BarChartGroupData(
x: 1,
barsSpace: barsSpace,
barRods: [
BarChartRodData(
toY: 31000000000,
rodStackItems: [
BarChartRodStackItem(0, 11000000000, widget.dark),
BarChartRodStackItem(11000000000, 18000000000, widget.normal),
BarChartRodStackItem(18000000000, 31000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 35000000000,
rodStackItems: [
BarChartRodStackItem(0, 14000000000, widget.dark),
BarChartRodStackItem(14000000000, 27000000000, widget.normal),
BarChartRodStackItem(27000000000, 35000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 31000000000,
rodStackItems: [
BarChartRodStackItem(0, 8000000000, widget.dark),
BarChartRodStackItem(8000000000, 24000000000, widget.normal),
BarChartRodStackItem(24000000000, 31000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 15000000000,
rodStackItems: [
BarChartRodStackItem(0, 6000000000.5, widget.dark),
BarChartRodStackItem(6000000000.5, 12000000000.5, widget.normal),
BarChartRodStackItem(12000000000.5, 15000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 17000000000,
rodStackItems: [
BarChartRodStackItem(0, 9000000000, widget.dark),
BarChartRodStackItem(9000000000, 15000000000, widget.normal),
BarChartRodStackItem(15000000000, 17000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
],
),
BarChartGroupData(
x: 2,
barsSpace: barsSpace,
barRods: [
BarChartRodData(
toY: 34000000000,
rodStackItems: [
BarChartRodStackItem(0, 6000000000, widget.dark),
BarChartRodStackItem(6000000000, 23000000000, widget.normal),
BarChartRodStackItem(23000000000, 34000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 32000000000,
rodStackItems: [
BarChartRodStackItem(0, 7000000000, widget.dark),
BarChartRodStackItem(7000000000, 24000000000, widget.normal),
BarChartRodStackItem(24000000000, 32000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 14000000000.5,
rodStackItems: [
BarChartRodStackItem(0, 1000000000.5, widget.dark),
BarChartRodStackItem(1000000000.5, 12000000000, widget.normal),
BarChartRodStackItem(12000000000, 14000000000.5, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 20000000000,
rodStackItems: [
BarChartRodStackItem(0, 4000000000, widget.dark),
BarChartRodStackItem(4000000000, 15000000000, widget.normal),
BarChartRodStackItem(15000000000, 20000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 24000000000,
rodStackItems: [
BarChartRodStackItem(0, 4000000000, widget.dark),
BarChartRodStackItem(4000000000, 15000000000, widget.normal),
BarChartRodStackItem(15000000000, 24000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
],
),
BarChartGroupData(
x: 3,
barsSpace: barsSpace,
barRods: [
BarChartRodData(
toY: 14000000000,
rodStackItems: [
BarChartRodStackItem(0, 1000000000.5, widget.dark),
BarChartRodStackItem(1000000000.5, 12000000000, widget.normal),
BarChartRodStackItem(12000000000, 14000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 27000000000,
rodStackItems: [
BarChartRodStackItem(0, 7000000000, widget.dark),
BarChartRodStackItem(7000000000, 25000000000, widget.normal),
BarChartRodStackItem(25000000000, 27000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 29000000000,
rodStackItems: [
BarChartRodStackItem(0, 6000000000, widget.dark),
BarChartRodStackItem(6000000000, 23000000000, widget.normal),
BarChartRodStackItem(23000000000, 29000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 16000000000.5,
rodStackItems: [
BarChartRodStackItem(0, 9000000000, widget.dark),
BarChartRodStackItem(9000000000, 15000000000, widget.normal),
BarChartRodStackItem(15000000000, 16000000000.5, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
BarChartRodData(
toY: 15000000000,
rodStackItems: [
BarChartRodStackItem(0, 7000000000, widget.dark),
BarChartRodStackItem(7000000000, 12000000000.5, widget.normal),
BarChartRodStackItem(12000000000.5, 15000000000, widget.light),
],
borderRadius: BorderRadius.zero,
width: barsWidth,
),
],
),
];
}
}

View File

@@ -0,0 +1,360 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/util/app_utils.dart';
import 'package:flutter/material.dart';
class BarChartSample5 extends StatefulWidget {
const BarChartSample5({super.key});
@override
State<StatefulWidget> createState() => BarChartSample5State();
}
class BarChartSample5State extends State<BarChartSample5> {
static const double barWidth = 22;
static const shadowOpacity = 0.2;
static const mainItems = <int, List<double>>{
0: [2, 3, 2.5, 8],
1: [-1.8, -2.7, -3, -6.5],
2: [1.5, 2, 3.5, 6],
3: [1.5, 1.5, 4, 6.5],
4: [-2, -2, -5, -9],
5: [-1.2, -1.5, -4.3, -10],
6: [1.2, 4.8, 5, 5],
};
int touchedIndex = -1;
@override
void initState() {
super.initState();
}
Widget bottomTitles(double value, TitleMeta meta) {
const style = TextStyle(color: Colors.white, fontSize: 10);
String text;
switch (value.toInt()) {
case 0:
text = 'Mon';
break;
case 1:
text = 'Tue';
break;
case 2:
text = 'Wed';
break;
case 3:
text = 'Thu';
break;
case 4:
text = 'Fri';
break;
case 5:
text = 'Sat';
break;
case 6:
text = 'Sun';
break;
default:
text = '';
break;
}
return SideTitleWidget(
meta: meta,
child: Text(text, style: style),
);
}
Widget topTitles(double value, TitleMeta meta) {
const style = TextStyle(color: Colors.white, fontSize: 10);
String text;
switch (value.toInt()) {
case 0:
text = 'Mon';
break;
case 1:
text = 'Tue';
break;
case 2:
text = 'Wed';
break;
case 3:
text = 'Thu';
break;
case 4:
text = 'Fri';
break;
case 5:
text = 'Sat';
break;
case 6:
text = 'Sun';
break;
default:
return Container();
}
return SideTitleWidget(
meta: meta,
child: Text(text, style: style),
);
}
Widget leftTitles(double value, TitleMeta meta) {
const style = TextStyle(color: Colors.white, fontSize: 10);
String text;
if (value == 0) {
text = '0';
} else {
text = '${value.toInt()}0k';
}
return SideTitleWidget(
angle: AppUtils().degreeToRadian(value < 0 ? -45 : 45),
meta: meta,
space: 4,
child: Text(
text,
style: style,
textAlign: TextAlign.center,
),
);
}
Widget rightTitles(double value, TitleMeta meta) {
const style = TextStyle(color: Colors.white, fontSize: 10);
String text;
if (value == 0) {
text = '0';
} else {
text = '${value.toInt()}0k';
}
return SideTitleWidget(
angle: AppUtils().degreeToRadian(90),
meta: meta,
space: 0,
child: Text(
text,
style: style,
textAlign: TextAlign.center,
),
);
}
BarChartGroupData generateGroup(
int x,
double value1,
double value2,
double value3,
double value4,
) {
final isTop = value1 > 0;
final sum = value1 + value2 + value3 + value4;
final isTouched = touchedIndex == x;
return BarChartGroupData(
x: x,
groupVertically: true,
showingTooltipIndicators: isTouched ? [0] : [],
barRods: [
BarChartRodData(
toY: sum,
width: barWidth,
borderRadius: isTop
? const BorderRadius.only(
topLeft: Radius.circular(6),
topRight: Radius.circular(6),
)
: const BorderRadius.only(
bottomLeft: Radius.circular(6),
bottomRight: Radius.circular(6),
),
rodStackItems: [
BarChartRodStackItem(
0,
value1,
AppColors.contentColorGreen,
BorderSide(
color: Colors.white,
width: isTouched ? 2 : 0,
),
),
BarChartRodStackItem(
value1,
value1 + value2,
AppColors.contentColorYellow,
BorderSide(
color: Colors.white,
width: isTouched ? 2 : 0,
),
),
BarChartRodStackItem(
value1 + value2,
value1 + value2 + value3,
AppColors.contentColorPink,
BorderSide(
color: Colors.white,
width: isTouched ? 2 : 0,
),
),
BarChartRodStackItem(
value1 + value2 + value3,
value1 + value2 + value3 + value4,
AppColors.contentColorBlue,
BorderSide(
color: Colors.white,
width: isTouched ? 2 : 0,
),
),
],
),
BarChartRodData(
toY: -sum,
width: barWidth,
color: Colors.transparent,
borderRadius: isTop
? const BorderRadius.only(
bottomLeft: Radius.circular(6),
bottomRight: Radius.circular(6),
)
: const BorderRadius.only(
topLeft: Radius.circular(6),
topRight: Radius.circular(6),
),
rodStackItems: [
BarChartRodStackItem(
0,
-value1,
AppColors.contentColorGreen.withValues(
alpha: isTouched ? shadowOpacity * 2 : shadowOpacity),
const BorderSide(color: Colors.transparent),
),
BarChartRodStackItem(
-value1,
-(value1 + value2),
AppColors.contentColorYellow.withValues(
alpha: isTouched ? shadowOpacity * 2 : shadowOpacity),
const BorderSide(color: Colors.transparent),
),
BarChartRodStackItem(
-(value1 + value2),
-(value1 + value2 + value3),
AppColors.contentColorPink.withValues(
alpha: isTouched ? shadowOpacity * 2 : shadowOpacity),
const BorderSide(color: Colors.transparent),
),
BarChartRodStackItem(
-(value1 + value2 + value3),
-(value1 + value2 + value3 + value4),
AppColors.contentColorBlue.withValues(
alpha: isTouched ? shadowOpacity * 2 : shadowOpacity),
const BorderSide(color: Colors.transparent),
),
],
),
],
);
}
bool isShadowBar(int rodIndex) => rodIndex == 1;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 0.8,
child: Padding(
padding: const EdgeInsets.only(top: 16),
child: BarChart(
BarChartData(
alignment: BarChartAlignment.center,
maxY: 20,
minY: -20,
groupsSpace: 12,
barTouchData: BarTouchData(
handleBuiltInTouches: false,
touchCallback: (FlTouchEvent event, barTouchResponse) {
if (!event.isInterestedForInteractions ||
barTouchResponse == null ||
barTouchResponse.spot == null) {
setState(() {
touchedIndex = -1;
});
return;
}
final rodIndex = barTouchResponse.spot!.touchedRodDataIndex;
if (isShadowBar(rodIndex)) {
setState(() {
touchedIndex = -1;
});
return;
}
setState(() {
touchedIndex = barTouchResponse.spot!.touchedBarGroupIndex;
});
},
),
titlesData: FlTitlesData(
show: true,
topTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 32,
getTitlesWidget: topTitles,
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 32,
getTitlesWidget: bottomTitles,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: leftTitles,
interval: 5,
reservedSize: 42,
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: rightTitles,
interval: 5,
reservedSize: 42,
),
),
),
gridData: FlGridData(
show: true,
checkToShowHorizontalLine: (value) => value % 5 == 0,
getDrawingHorizontalLine: (value) {
if (value == 0) {
return FlLine(
color: AppColors.borderColor.withValues(alpha: 0.1),
strokeWidth: 3,
);
}
return FlLine(
color: AppColors.borderColor.withValues(alpha: 0.05),
strokeWidth: 0.8,
);
},
),
borderData: FlBorderData(
show: false,
),
barGroups: mainItems.entries
.map(
(e) => generateGroup(
e.key,
e.value[0],
e.value[1],
e.value[2],
e.value[3],
),
)
.toList(),
),
),
),
);
}
}

View File

@@ -0,0 +1,184 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/presentation/widgets/legend_widget.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class BarChartSample6 extends StatelessWidget {
const BarChartSample6({super.key});
final pilateColor = AppColors.contentColorPurple;
final cyclingColor = AppColors.contentColorCyan;
final quickWorkoutColor = AppColors.contentColorBlue;
final betweenSpace = 0.2;
BarChartGroupData generateGroupData(
int x,
double pilates,
double quickWorkout,
double cycling,
) {
return BarChartGroupData(
x: x,
groupVertically: true,
barRods: [
BarChartRodData(
fromY: 0,
toY: pilates,
color: pilateColor,
width: 5,
),
BarChartRodData(
fromY: pilates + betweenSpace,
toY: pilates + betweenSpace + quickWorkout,
color: quickWorkoutColor,
width: 5,
),
BarChartRodData(
fromY: pilates + betweenSpace + quickWorkout + betweenSpace,
toY: pilates + betweenSpace + quickWorkout + betweenSpace + cycling,
color: cyclingColor,
width: 5,
),
],
);
}
Widget bottomTitles(double value, TitleMeta meta) {
const style = TextStyle(fontSize: 10);
String text;
switch (value.toInt()) {
case 0:
text = 'JAN';
break;
case 1:
text = 'FEB';
break;
case 2:
text = 'MAR';
break;
case 3:
text = 'APR';
break;
case 4:
text = 'MAY';
break;
case 5:
text = 'JUN';
break;
case 6:
text = 'JUL';
break;
case 7:
text = 'AUG';
break;
case 8:
text = 'SEP';
break;
case 9:
text = 'OCT';
break;
case 10:
text = 'NOV';
break;
case 11:
text = 'DEC';
break;
default:
text = '';
}
return SideTitleWidget(
meta: meta,
child: Text(text, style: style),
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Activity',
style: TextStyle(
color: AppColors.contentColorBlue,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
LegendsListWidget(
legends: [
Legend('Pilates', pilateColor),
Legend('Quick workouts', quickWorkoutColor),
Legend('Cycling', cyclingColor),
],
),
const SizedBox(height: 14),
AspectRatio(
aspectRatio: 2,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceBetween,
titlesData: FlTitlesData(
leftTitles: const AxisTitles(),
rightTitles: const AxisTitles(),
topTitles: const AxisTitles(),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: bottomTitles,
reservedSize: 20,
),
),
),
barTouchData: const BarTouchData(enabled: false),
borderData: FlBorderData(show: false),
gridData: const FlGridData(show: false),
barGroups: [
generateGroupData(0, 2, 3, 2),
generateGroupData(1, 2, 5, 1.7),
generateGroupData(2, 1.3, 3.1, 2.8),
generateGroupData(3, 3.1, 4, 3.1),
generateGroupData(4, 0.8, 3.3, 3.4),
generateGroupData(5, 2, 5.6, 1.8),
generateGroupData(6, 1.3, 3.2, 2),
generateGroupData(7, 2.3, 3.2, 3),
generateGroupData(8, 2, 4.8, 2.5),
generateGroupData(9, 1.2, 3.2, 2.5),
generateGroupData(10, 1, 4.8, 3),
generateGroupData(11, 2, 4.4, 2.8),
],
maxY: 11 + (betweenSpace * 3),
extraLinesData: ExtraLinesData(
horizontalLines: [
HorizontalLine(
y: 3.3,
color: pilateColor,
strokeWidth: 1,
dashArray: [20, 4],
),
HorizontalLine(
y: 8,
color: quickWorkoutColor,
strokeWidth: 1,
dashArray: [20, 4],
),
HorizontalLine(
y: 11,
color: cyclingColor,
strokeWidth: 1,
dashArray: [20, 4],
),
],
),
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,254 @@
import 'dart:math' as math;
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
class BarChartSample7 extends StatefulWidget {
BarChartSample7({super.key});
final shadowColor = const Color(0xFFCCCCCC);
final dataList = [
const _BarData(AppColors.contentColorYellow, 18, 18),
const _BarData(AppColors.contentColorGreen, 17, 8),
const _BarData(AppColors.contentColorOrange, 10, 15),
const _BarData(AppColors.contentColorPink, 2.5, 5),
const _BarData(AppColors.contentColorBlue, 2, 2.5),
const _BarData(AppColors.contentColorRed, 2, 2),
];
@override
State<BarChartSample7> createState() => _BarChartSample7State();
}
class _BarChartSample7State extends State<BarChartSample7> {
BarChartGroupData generateBarGroup(
int x,
Color color,
double value,
double shadowValue,
) {
return BarChartGroupData(
x: x,
barRods: [
BarChartRodData(
toY: value,
color: color,
width: 6,
),
BarChartRodData(
toY: shadowValue,
color: widget.shadowColor,
width: 6,
),
],
showingTooltipIndicators: touchedGroupIndex == x ? [0] : [],
);
}
int touchedGroupIndex = -1;
int rotationTurns = 1;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Row(
children: [
Expanded(child: Container()),
const Text(
'Horizontal Bar Chart',
style: TextStyle(
color: AppColors.mainTextColor1,
fontSize: 20,
),
),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: Tooltip(
message: 'Rotate the chart 90 degrees (cw)',
child: IconButton(
onPressed: () {
setState(() {
rotationTurns += 1;
});
},
icon: RotatedBox(
quarterTurns: rotationTurns - 1,
child: const Icon(
Icons.rotate_90_degrees_cw,
),
),
),
),
)),
],
),
const SizedBox(height: 18),
AspectRatio(
aspectRatio: 1.4,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceBetween,
rotationQuarterTurns: rotationTurns,
borderData: FlBorderData(
show: true,
border: Border.symmetric(
horizontal: BorderSide(
color: AppColors.borderColor.withValues(alpha: 0.2),
),
),
),
titlesData: FlTitlesData(
show: true,
leftTitles: const AxisTitles(
drawBelowEverything: true,
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 36,
getTitlesWidget: (value, meta) {
final index = value.toInt();
return SideTitleWidget(
meta: meta,
child: _IconWidget(
color: widget.dataList[index].color,
isSelected: touchedGroupIndex == index,
),
);
},
),
),
rightTitles: const AxisTitles(),
topTitles: const AxisTitles(),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (value) => FlLine(
color: AppColors.borderColor.withValues(alpha: 0.2),
strokeWidth: 1,
),
),
barGroups: widget.dataList.asMap().entries.map((e) {
final index = e.key;
final data = e.value;
return generateBarGroup(
index,
data.color,
data.value,
data.shadowValue,
);
}).toList(),
maxY: 20,
barTouchData: BarTouchData(
enabled: true,
handleBuiltInTouches: false,
touchTooltipData: BarTouchTooltipData(
getTooltipColor: (group) => Colors.transparent,
tooltipMargin: 0,
getTooltipItem: (
BarChartGroupData group,
int groupIndex,
BarChartRodData rod,
int rodIndex,
) {
return BarTooltipItem(
rod.toY.toString(),
TextStyle(
fontWeight: FontWeight.bold,
color: rod.color,
fontSize: 18,
shadows: const [
Shadow(
color: Colors.black26,
blurRadius: 12,
)
],
),
);
},
),
touchCallback: (event, response) {
if (event.isInterestedForInteractions &&
response != null &&
response.spot != null) {
setState(() {
touchedGroupIndex = response.spot!.touchedBarGroupIndex;
});
} else {
setState(() {
touchedGroupIndex = -1;
});
}
},
),
),
),
),
],
),
);
}
}
class _BarData {
const _BarData(this.color, this.value, this.shadowValue);
final Color color;
final double value;
final double shadowValue;
}
class _IconWidget extends ImplicitlyAnimatedWidget {
const _IconWidget({
required this.color,
required this.isSelected,
}) : super(duration: const Duration(milliseconds: 300));
final Color color;
final bool isSelected;
@override
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() =>
_IconWidgetState();
}
class _IconWidgetState extends AnimatedWidgetBaseState<_IconWidget> {
Tween<double>? _rotationTween;
@override
Widget build(BuildContext context) {
final rotation = math.pi * 4 * _rotationTween!.evaluate(animation);
final scale = 1 + _rotationTween!.evaluate(animation) * 0.5;
return Transform(
transform: Matrix4.rotationZ(rotation).scaled(scale, scale),
origin: const Offset(14, 14),
child: Icon(
widget.isSelected ? Icons.face_retouching_natural : Icons.face,
color: widget.color,
size: 28,
),
);
}
@override
void forEachTween(TweenVisitor<dynamic> visitor) {
_rotationTween = visitor(
_rotationTween,
widget.isSelected ? 1.0 : 0.0,
(dynamic value) => Tween<double>(
begin: value as double,
end: widget.isSelected ? 1.0 : 0.0,
),
) as Tween<double>?;
}
}

View File

@@ -0,0 +1,174 @@
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/util/extensions/color_extensions.dart';
import 'package:flutter/material.dart';
class BarChartSample8 extends StatefulWidget {
BarChartSample8({super.key});
final Color barBackgroundColor =
AppColors.contentColorWhite.darken().withValues(alpha: 0.3);
final Color barColor = AppColors.contentColorWhite;
@override
State<StatefulWidget> createState() => BarChartSample1State();
}
class BarChartSample1State extends State<BarChartSample8> {
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.graphic_eq),
const SizedBox(
width: 32,
),
Text(
'Sales forecasting chart',
style: TextStyle(
color: widget.barColor,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(
height: 32,
),
Expanded(
child: BarChart(
randomData(),
),
),
],
),
),
);
}
BarChartGroupData makeGroupData(
int x,
double y,
FlErrorRange errorRange,
) {
return BarChartGroupData(
x: x,
barRods: [
BarChartRodData(
toY: y,
toYErrorRange: errorRange,
color: x >= 4 ? Colors.transparent : widget.barColor,
borderRadius: BorderRadius.zero,
borderDashArray: x >= 4 ? [4, 4] : null,
width: 22,
borderSide: BorderSide(color: widget.barColor, width: 2.0),
),
],
);
}
Widget getTitles(double value, TitleMeta meta) {
const style = TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14,
);
List<String> days = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
Widget text = Text(
days[value.toInt()],
style: style,
);
return SideTitleWidget(
meta: meta,
space: 16,
child: text,
);
}
BarChartData randomData() {
return BarChartData(
maxY: 300.0,
barTouchData: const BarTouchData(
enabled: false,
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: getTitles,
reservedSize: 38,
),
),
leftTitles: const AxisTitles(
sideTitles: SideTitles(
reservedSize: 30,
showTitles: true,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(
showTitles: false,
),
),
),
borderData: FlBorderData(
show: false,
),
barGroups: List.generate(
7,
(i) {
final y = Random().nextInt(290).toDouble() + 10;
final lowerBy = y < 50
? Random().nextDouble() * 10
: Random().nextDouble() * 30 + 5;
final upperBy = y > 290
? Random().nextDouble() * 10
: Random().nextDouble() * 30 + 5;
return makeGroupData(
i,
y,
FlErrorRange(
lowerBy: lowerBy,
upperBy: upperBy,
),
);
},
),
gridData: const FlGridData(show: false),
errorIndicatorData: FlErrorIndicatorData(
painter: _errorPainter,
),
);
}
FlSpotErrorRangePainter _errorPainter(
BarChartSpotErrorRangeCallbackInput input,
) =>
FlSimpleErrorPainter(
lineWidth: 2.0,
capLength: 14,
lineColor: input.groupIndex < 4
? AppColors.contentColorOrange
: AppColors.primary.withValues(alpha: 0.5),
);
}

View File

@@ -0,0 +1,348 @@
import 'package:equatable/equatable.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_colors.dart';
import 'package:fl_chart_app/util/csv_parser.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CandlestickChartSample1 extends StatefulWidget {
const CandlestickChartSample1({super.key});
@override
State<StatefulWidget> createState() => CandlestickChartSample1State();
}
class CandlestickChartSample1State extends State<CandlestickChartSample1> {
List<List<_BtcCandlestickData>>? _btcMonthlyData;
int _currentMonthIndex = 0;
late final List<String> monthsNames;
final int minDays = 1;
final int maxDays = 31;
late final FlLine _gridLine;
@override
void initState() {
monthsNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
_loadData();
_gridLine = FlLine(
color: Colors.blueGrey.withValues(alpha: 0.4),
strokeWidth: 0.4,
dashArray: [8, 4],
);
super.initState();
}
void _loadData() async {
final data = await rootBundle
.loadString('assets/data/bitcoin_2023-01-01_2023-12-31.csv');
final rows = CsvParser.parse(data);
if (!mounted) {
return;
}
setState(() {
final allData = rows.skip(1).map((row) {
// 2023-12-31,2024-01-01
return _BtcCandlestickData(
datetime: DateTime.parse(row[0]),
open: double.parse(row[2]),
high: double.parse(row[3]),
low: double.parse(row[4]),
close: double.parse(row[5]),
volume: double.parse(row[6]),
marketCap: double.parse(row[7]),
);
}).toList();
_btcMonthlyData = List.generate(12, (index) {
final month = index + 1;
final monthData = allData
.where((element) => element.datetime.month == month)
.toList();
monthData.sort((a, b) => a.datetime.compareTo(b.datetime));
return monthData;
});
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 18),
const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'BTC Price 2024',
style: TextStyle(
color: AppColors.contentColorYellow,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 18),
Row(
children: [
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: _canGoPrevious ? _previousMonth : null,
icon: const Icon(Icons.navigate_before_rounded),
),
),
),
SizedBox(
width: 92,
child: Text(
monthsNames[_currentMonthIndex],
textAlign: TextAlign.center,
style: const TextStyle(
color: AppColors.contentColorWhite,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: IconButton(
onPressed: _canGoNext ? _nextMonth : null,
icon: const Icon(Icons.navigate_next_rounded),
),
),
),
],
),
const SizedBox(height: 18),
AspectRatio(
aspectRatio: 1.5,
child: Stack(
children: [
if (_btcMonthlyData != null)
Padding(
padding: const EdgeInsets.only(
top: 0.0,
right: 18.0,
),
child: CandlestickChart(
CandlestickChartData(
candlestickSpots: _btcMonthlyData![_currentMonthIndex]
.asMap()
.entries
.map((entry) {
final index = entry.key;
final data = entry.value;
return CandlestickSpot(
x: index.toDouble(),
open: data.open,
high: data.high,
low: data.low,
close: data.close,
);
}).toList(),
minX: 0,
maxX: 31,
gridData: FlGridData(
show: true,
getDrawingHorizontalLine: (_) => _gridLine,
getDrawingVerticalLine: (_) => _gridLine,
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
drawBelowEverything: true,
sideTitles: SideTitles(
showTitles: true,
maxIncluded: false,
minIncluded: false,
reservedSize: 60,
getTitlesWidget: _leftTitles,
),
),
bottomTitles: AxisTitles(
axisNameWidget: Container(
margin: const EdgeInsets.only(bottom: 20),
child: const Text(
'Day of month',
style: TextStyle(
color: AppColors.contentColorGreen,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
axisNameSize: 40,
sideTitles: SideTitles(
showTitles: true,
reservedSize: 38,
maxIncluded: false,
interval: 1,
getTitlesWidget: _bottomTitles,
),
),
),
touchedPointIndicator: AxisSpotIndicator(
painter: AxisLinesIndicatorPainter(
verticalLineProvider: (x) {
final data =
_btcMonthlyData![_currentMonthIndex][x.toInt()];
return VerticalLine(
x: x,
color: (data.isUp
? AppColors.contentColorGreen
: AppColors.contentColorRed)
.withValues(alpha: 0.5),
strokeWidth: 1,
);
},
horizontalLineProvider: (y) => HorizontalLine(
y: y,
label: HorizontalLineLabel(
show: true,
style: const TextStyle(
color: AppColors.contentColorYellow,
fontSize: 12,
fontWeight: FontWeight.bold,
),
labelResolver: (hLine) =>
hLine.y.toInt().toString(),
alignment: Alignment.topLeft),
color: AppColors.contentColorYellow.withValues(
alpha: 0.8,
),
strokeWidth: 1,
),
),
),
),
),
),
if (_btcMonthlyData == null)
const Center(
child: CircularProgressIndicator(),
)
],
),
),
],
);
}
bool get _canGoNext => _currentMonthIndex < 11;
bool get _canGoPrevious => _currentMonthIndex > 0;
void _previousMonth() {
if (!_canGoPrevious) {
return;
}
setState(() {
_currentMonthIndex--;
});
}
void _nextMonth() {
if (!_canGoNext) {
return;
}
setState(() {
_currentMonthIndex++;
});
}
Widget _bottomTitles(double value, TitleMeta meta) {
final day = value.toInt() + 1;
final isImportantToShow = day % 5 == 0 || day == 1;
if (!isImportantToShow) {
return const SizedBox();
}
return SideTitleWidget(
meta: meta,
child: Text(
day.toString(),
style: const TextStyle(
color: AppColors.contentColorGreen,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _leftTitles(double value, TitleMeta meta) {
return SideTitleWidget(
meta: meta,
child: Text(
meta.formattedValue,
style: const TextStyle(
color: AppColors.contentColorYellow,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
);
}
}
class _BtcCandlestickData with EquatableMixin {
_BtcCandlestickData({
required this.datetime,
required this.open,
required this.high,
required this.low,
required this.close,
required this.volume,
required this.marketCap,
});
final DateTime datetime;
final double open;
final double high;
final double low;
final double close;
final double volume;
final double marketCap;
bool get isUp => open < close;
@override
List<Object?> get props => [
datetime,
open,
high,
low,
close,
volume,
marketCap,
];
}

View File

@@ -0,0 +1,48 @@
import 'package:fl_chart_app/urls.dart';
import 'package:fl_chart_app/util/app_helper.dart';
import 'package:flutter/cupertino.dart';
abstract class ChartSample {
final int number;
final WidgetBuilder builder;
ChartType get type;
String get name => '${type.displayName} Sample $number';
String get url => Urls.getChartSourceCodeUrl(type, number);
ChartSample(this.number, this.builder);
}
class LineChartSample extends ChartSample {
LineChartSample(super.number, super.builder);
@override
ChartType get type => ChartType.line;
}
class BarChartSample extends ChartSample {
BarChartSample(super.number, super.builder);
@override
ChartType get type => ChartType.bar;
}
class PieChartSample extends ChartSample {
PieChartSample(super.number, super.builder);
@override
ChartType get type => ChartType.pie;
}
class ScatterChartSample extends ChartSample {
ScatterChartSample(super.number, super.builder);
@override
ChartType get type => ChartType.scatter;
}
class RadarChartSample extends ChartSample {
RadarChartSample(super.number, super.builder);
@override
ChartType get type => ChartType.radar;
}
class CandlestickChartSample extends ChartSample {
CandlestickChartSample(super.number, super.builder);
@override
ChartType get type => ChartType.candlestick;
}

View File

@@ -0,0 +1,76 @@
import 'package:fl_chart_app/presentation/samples/candlestick/candlestick_chart_sample1.dart';
import 'package:fl_chart_app/util/app_helper.dart';
import 'bar/bar_chart_sample1.dart';
import 'bar/bar_chart_sample2.dart';
import 'bar/bar_chart_sample3.dart';
import 'bar/bar_chart_sample4.dart';
import 'bar/bar_chart_sample5.dart';
import 'bar/bar_chart_sample6.dart';
import 'bar/bar_chart_sample7.dart';
import 'bar/bar_chart_sample8.dart';
import 'chart_sample.dart';
import 'line/line_chart_sample1.dart';
import 'line/line_chart_sample10.dart';
import 'line/line_chart_sample11.dart';
import 'line/line_chart_sample12.dart';
import 'line/line_chart_sample13.dart';
import 'line/line_chart_sample2.dart';
import 'line/line_chart_sample3.dart';
import 'line/line_chart_sample4.dart';
import 'line/line_chart_sample5.dart';
import 'line/line_chart_sample6.dart';
import 'line/line_chart_sample7.dart';
import 'line/line_chart_sample8.dart';
import 'line/line_chart_sample9.dart';
import 'pie/pie_chart_sample1.dart';
import 'pie/pie_chart_sample2.dart';
import 'pie/pie_chart_sample3.dart';
import 'radar/radar_chart_sample1.dart';
import 'scatter/scatter_chart_sample1.dart';
import 'scatter/scatter_chart_sample2.dart';
class ChartSamples {
static final Map<ChartType, List<ChartSample>> samples = {
ChartType.line: [
LineChartSample(1, (context) => const LineChartSample1()),
LineChartSample(2, (context) => const LineChartSample2()),
LineChartSample(3, (context) => LineChartSample3()),
LineChartSample(4, (context) => LineChartSample4()),
LineChartSample(5, (context) => const LineChartSample5()),
LineChartSample(6, (context) => LineChartSample6()),
LineChartSample(7, (context) => LineChartSample7()),
LineChartSample(8, (context) => const LineChartSample8()),
LineChartSample(9, (context) => LineChartSample9()),
LineChartSample(10, (context) => const LineChartSample10()),
LineChartSample(11, (context) => const LineChartSample11()),
LineChartSample(12, (context) => const LineChartSample12()),
LineChartSample(13, (context) => const LineChartSample13()),
],
ChartType.bar: [
BarChartSample(1, (context) => BarChartSample1()),
BarChartSample(2, (context) => BarChartSample2()),
BarChartSample(3, (context) => const BarChartSample3()),
BarChartSample(4, (context) => BarChartSample4()),
BarChartSample(5, (context) => const BarChartSample5()),
BarChartSample(6, (context) => const BarChartSample6()),
BarChartSample(7, (context) => BarChartSample7()),
BarChartSample(8, (context) => BarChartSample8()),
],
ChartType.pie: [
PieChartSample(1, (context) => const PieChartSample1()),
PieChartSample(2, (context) => const PieChartSample2()),
PieChartSample(3, (context) => const PieChartSample3()),
],
ChartType.scatter: [
ScatterChartSample(1, (context) => ScatterChartSample1()),
ScatterChartSample(2, (context) => const ScatterChartSample2()),
],
ChartType.radar: [
RadarChartSample(1, (context) => RadarChartSample1()),
],
ChartType.candlestick: [
CandlestickChartSample(1, (context) => const CandlestickChartSample1()),
]
};
}

View File

@@ -0,0 +1,366 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
class _LineChart extends StatelessWidget {
const _LineChart({required this.isShowingMainData});
final bool isShowingMainData;
@override
Widget build(BuildContext context) {
return LineChart(
isShowingMainData ? sampleData1 : sampleData2,
duration: const Duration(milliseconds: 250),
);
}
LineChartData get sampleData1 => LineChartData(
lineTouchData: lineTouchData1,
gridData: gridData,
titlesData: titlesData1,
borderData: borderData,
lineBarsData: lineBarsData1,
minX: 0,
maxX: 14,
maxY: 4,
minY: 0,
);
LineChartData get sampleData2 => LineChartData(
lineTouchData: lineTouchData2,
gridData: gridData,
titlesData: titlesData2,
borderData: borderData,
lineBarsData: lineBarsData2,
minX: 0,
maxX: 14,
maxY: 6,
minY: 0,
);
LineTouchData get lineTouchData1 => LineTouchData(
handleBuiltInTouches: true,
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (touchedSpot) =>
Colors.blueGrey.withValues(alpha: 0.8),
),
);
FlTitlesData get titlesData1 => FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: bottomTitles,
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: leftTitles(),
),
);
List<LineChartBarData> get lineBarsData1 => [
lineChartBarData1_1,
lineChartBarData1_2,
lineChartBarData1_3,
];
LineTouchData get lineTouchData2 => const LineTouchData(
enabled: false,
);
FlTitlesData get titlesData2 => FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: bottomTitles,
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: leftTitles(),
),
);
List<LineChartBarData> get lineBarsData2 => [
lineChartBarData2_1,
lineChartBarData2_2,
lineChartBarData2_3,
];
Widget leftTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
);
String text;
switch (value.toInt()) {
case 1:
text = '1m';
break;
case 2:
text = '2m';
break;
case 3:
text = '3m';
break;
case 4:
text = '5m';
break;
case 5:
text = '6m';
break;
default:
return Container();
}
return SideTitleWidget(
meta: meta,
child: Text(
text,
style: style,
textAlign: TextAlign.center,
),
);
}
SideTitles leftTitles() => SideTitles(
getTitlesWidget: leftTitleWidgets,
showTitles: true,
interval: 1,
reservedSize: 40,
);
Widget bottomTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
);
Widget text;
switch (value.toInt()) {
case 2:
text = const Text('SEPT', style: style);
break;
case 7:
text = const Text('OCT', style: style);
break;
case 12:
text = const Text('DEC', style: style);
break;
default:
text = const Text('');
break;
}
return SideTitleWidget(
meta: meta,
space: 10,
child: text,
);
}
SideTitles get bottomTitles => SideTitles(
showTitles: true,
reservedSize: 32,
interval: 1,
getTitlesWidget: bottomTitleWidgets,
);
FlGridData get gridData => const FlGridData(show: false);
FlBorderData get borderData => FlBorderData(
show: true,
border: Border(
bottom: BorderSide(
color: AppColors.primary.withValues(alpha: 0.2), width: 4),
left: const BorderSide(color: Colors.transparent),
right: const BorderSide(color: Colors.transparent),
top: const BorderSide(color: Colors.transparent),
),
);
LineChartBarData get lineChartBarData1_1 => LineChartBarData(
isCurved: true,
color: AppColors.contentColorGreen,
barWidth: 8,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
spots: const [
FlSpot(1, 1),
FlSpot(3, 1.5),
FlSpot(5, 1.4),
FlSpot(7, 3.4),
FlSpot(10, 2),
FlSpot(12, 2.2),
FlSpot(13, 1.8),
],
);
LineChartBarData get lineChartBarData1_2 => LineChartBarData(
isCurved: true,
color: AppColors.contentColorPink,
barWidth: 8,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: false,
color: AppColors.contentColorPink.withValues(alpha: 0),
),
spots: const [
FlSpot(1, 1),
FlSpot(3, 2.8),
FlSpot(7, 1.2),
FlSpot(10, 2.8),
FlSpot(12, 2.6),
FlSpot(13, 3.9),
],
);
LineChartBarData get lineChartBarData1_3 => LineChartBarData(
isCurved: true,
color: AppColors.contentColorCyan,
barWidth: 8,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
spots: const [
FlSpot(1, 2.8),
FlSpot(3, 1.9),
FlSpot(6, 3),
FlSpot(10, 1.3),
FlSpot(13, 2.5),
],
);
LineChartBarData get lineChartBarData2_1 => LineChartBarData(
isCurved: true,
curveSmoothness: 0,
color: AppColors.contentColorGreen.withValues(alpha: 0.5),
barWidth: 4,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(show: false),
spots: const [
FlSpot(1, 1),
FlSpot(3, 4),
FlSpot(5, 1.8),
FlSpot(7, 5),
FlSpot(10, 2),
FlSpot(12, 2.2),
FlSpot(13, 1.8),
],
);
LineChartBarData get lineChartBarData2_2 => LineChartBarData(
isCurved: true,
color: AppColors.contentColorPink.withValues(alpha: 0.5),
barWidth: 4,
isStrokeCapRound: true,
dotData: const FlDotData(show: false),
belowBarData: BarAreaData(
show: true,
color: AppColors.contentColorPink.withValues(alpha: 0.2),
),
spots: const [
FlSpot(1, 1),
FlSpot(3, 2.8),
FlSpot(7, 1.2),
FlSpot(10, 2.8),
FlSpot(12, 2.6),
FlSpot(13, 3.9),
],
);
LineChartBarData get lineChartBarData2_3 => LineChartBarData(
isCurved: true,
curveSmoothness: 0,
color: AppColors.contentColorCyan.withValues(alpha: 0.5),
barWidth: 2,
isStrokeCapRound: true,
dotData: const FlDotData(show: true),
belowBarData: BarAreaData(show: false),
spots: const [
FlSpot(1, 3.8),
FlSpot(3, 1.9),
FlSpot(6, 5),
FlSpot(10, 3.3),
FlSpot(13, 4.5),
],
);
}
class LineChartSample1 extends StatefulWidget {
const LineChartSample1({super.key});
@override
State<StatefulWidget> createState() => LineChartSample1State();
}
class LineChartSample1State extends State<LineChartSample1> {
late bool isShowingMainData;
@override
void initState() {
super.initState();
isShowingMainData = true;
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.23,
child: Stack(
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const SizedBox(
height: 37,
),
const Text(
'Monthly Sales',
style: TextStyle(
color: AppColors.primary,
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
textAlign: TextAlign.center,
),
const SizedBox(
height: 37,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(right: 16, left: 6),
child: _LineChart(isShowingMainData: isShowingMainData),
),
),
const SizedBox(
height: 10,
),
],
),
IconButton(
icon: Icon(
Icons.refresh,
color:
Colors.white.withValues(alpha: isShowingMainData ? 1.0 : 0.5),
),
onPressed: () {
setState(() {
isShowingMainData = !isShowingMainData;
});
},
)
],
),
);
}
}

View File

@@ -0,0 +1,146 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
class LineChartSample10 extends StatefulWidget {
const LineChartSample10({super.key});
final Color sinColor = AppColors.contentColorBlue;
final Color cosColor = AppColors.contentColorPink;
@override
State<LineChartSample10> createState() => _LineChartSample10State();
}
class _LineChartSample10State extends State<LineChartSample10> {
final limitCount = 100;
final sinPoints = <FlSpot>[];
final cosPoints = <FlSpot>[];
double xValue = 0;
double step = 0.05;
late Timer timer;
@override
void initState() {
super.initState();
timer = Timer.periodic(const Duration(milliseconds: 40), (timer) {
while (sinPoints.length > limitCount) {
sinPoints.removeAt(0);
cosPoints.removeAt(0);
}
setState(() {
sinPoints.add(FlSpot(xValue, math.sin(xValue)));
cosPoints.add(FlSpot(xValue, math.cos(xValue)));
});
xValue += step;
});
}
@override
Widget build(BuildContext context) {
return cosPoints.isNotEmpty
? Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 12),
Text(
'x: ${xValue.toStringAsFixed(1)}',
style: const TextStyle(
color: AppColors.mainTextColor2,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'sin: ${sinPoints.last.y.toStringAsFixed(1)}',
style: TextStyle(
color: widget.sinColor,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'cos: ${cosPoints.last.y.toStringAsFixed(1)}',
style: TextStyle(
color: widget.cosColor,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(
height: 12,
),
AspectRatio(
aspectRatio: 1.5,
child: Padding(
padding: const EdgeInsets.only(bottom: 24.0),
child: LineChart(
LineChartData(
minY: -1,
maxY: 1,
minX: sinPoints.first.x,
maxX: sinPoints.last.x,
lineTouchData: const LineTouchData(enabled: false),
clipData: const FlClipData.all(),
gridData: const FlGridData(
show: true,
drawVerticalLine: false,
),
borderData: FlBorderData(show: false),
lineBarsData: [
sinLine(sinPoints),
cosLine(cosPoints),
],
titlesData: const FlTitlesData(
show: false,
),
),
),
),
)
],
)
: Container();
}
LineChartBarData sinLine(List<FlSpot> points) {
return LineChartBarData(
spots: points,
dotData: const FlDotData(
show: false,
),
gradient: LinearGradient(
colors: [widget.sinColor.withValues(alpha: 0), widget.sinColor],
stops: const [0.1, 1.0],
),
barWidth: 4,
isCurved: false,
);
}
LineChartBarData cosLine(List<FlSpot> points) {
return LineChartBarData(
spots: points,
dotData: const FlDotData(
show: false,
),
gradient: LinearGradient(
colors: [widget.cosColor.withValues(alpha: 0), widget.cosColor],
stops: const [0.1, 1.0],
),
barWidth: 4,
isCurved: false,
);
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
}

View File

@@ -0,0 +1,202 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class LineChartSample11 extends StatefulWidget {
const LineChartSample11({super.key});
@override
State<LineChartSample11> createState() => _LineChartSample11State();
}
class _LineChartSample11State extends State<LineChartSample11> {
var baselineX = 0.0;
var baselineY = 0.0;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.5,
child: Padding(
padding: const EdgeInsets.only(
top: 18.0,
right: 18.0,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Row(
children: [
RotatedBox(
quarterTurns: 1,
child: Slider(
value: baselineY,
onChanged: (newValue) {
setState(() {
baselineY = newValue;
});
},
min: -10,
max: 10,
),
),
Expanded(
child: _Chart(
baselineX,
(20 - (baselineY + 10)) - 10,
),
)
],
),
),
Slider(
value: baselineX,
onChanged: (newValue) {
setState(() {
baselineX = newValue;
});
},
min: -10,
max: 10,
),
],
),
),
);
}
}
class _Chart extends StatelessWidget {
final double baselineX;
final double baselineY;
const _Chart(this.baselineX, this.baselineY) : super();
Widget getHorizontalTitles(value, TitleMeta meta) {
TextStyle style;
if ((value - baselineX).abs() <= 0.1) {
style = const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
);
} else {
style = const TextStyle(
color: Colors.white60,
fontSize: 14,
);
}
return Padding(
padding: const EdgeInsets.all(4.0),
child: Text(meta.formattedValue, style: style),
);
}
Widget getVerticalTitles(value, TitleMeta meta) {
TextStyle style;
if ((value - baselineY).abs() <= 0.1) {
style = const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
);
} else {
style = const TextStyle(
color: Colors.white60,
fontSize: 14,
);
}
return Padding(
padding: const EdgeInsets.all(4.0),
child: Text(meta.formattedValue, style: style),
);
}
FlLine getHorizontalVerticalLine(double value) {
if ((value - baselineY).abs() <= 0.1) {
return const FlLine(
color: Colors.white70,
strokeWidth: 1,
dashArray: [8, 4],
);
} else {
return const FlLine(
color: Colors.blueGrey,
strokeWidth: 0.4,
dashArray: [8, 4],
);
}
}
FlLine getVerticalVerticalLine(double value) {
if ((value - baselineX).abs() <= 0.1) {
return const FlLine(
color: Colors.white70,
strokeWidth: 1,
dashArray: [8, 4],
);
} else {
return const FlLine(
color: Colors.blueGrey,
strokeWidth: 0.4,
dashArray: [8, 4],
);
}
}
@override
Widget build(BuildContext context) {
return LineChart(
LineChartData(
lineBarsData: [
LineChartBarData(
spots: [],
),
],
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: getVerticalTitles,
reservedSize: 36,
),
),
topTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: getHorizontalTitles,
reservedSize: 32),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: getVerticalTitles,
reservedSize: 36,
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: getHorizontalTitles,
reservedSize: 32),
),
),
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: true,
getDrawingHorizontalLine: getHorizontalVerticalLine,
getDrawingVerticalLine: getVerticalVerticalLine,
),
minY: -10,
maxY: 10,
baselineY: baselineY,
minX: -10,
maxX: 10,
baselineX: baselineX,
),
duration: Duration.zero,
);
}
}

View File

@@ -0,0 +1,416 @@
import 'dart:convert';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/presentation_utils.dart';
import 'package:fl_chart_app/presentation/resources/app_colors.dart';
import 'package:fl_chart_app/util/extensions/color_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class LineChartSample12 extends StatefulWidget {
const LineChartSample12({super.key});
@override
State<LineChartSample12> createState() => _LineChartSample12State();
}
class _LineChartSample12State extends State<LineChartSample12> {
List<(DateTime, double)>? _bitcoinPriceHistory;
late TransformationController _transformationController;
bool _isPanEnabled = true;
bool _isScaleEnabled = true;
@override
void initState() {
_reloadData();
_transformationController = TransformationController();
super.initState();
}
void _reloadData() async {
final dataStr = await rootBundle.loadString(
'assets/data/btc_last_year_price.json',
);
if (!mounted) {
return;
}
final json = jsonDecode(dataStr) as Map<String, dynamic>;
setState(() {
_bitcoinPriceHistory = (json['prices'] as List).map((item) {
final timestamp = item[0] as int;
final price = item[1] as double;
return (DateTime.fromMillisecondsSinceEpoch(timestamp), price);
}).toList();
});
}
@override
Widget build(BuildContext context) {
const leftReservedSize = 52.0;
return Column(
spacing: 16,
children: [
LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
return width >= 380
? Row(
children: [
const SizedBox(width: leftReservedSize),
const _ChartTitle(),
const Spacer(),
Center(
child: _TransformationButtons(
controller: _transformationController,
),
),
],
)
: Column(
children: [
const _ChartTitle(),
const SizedBox(height: 16),
_TransformationButtons(
controller: _transformationController,
),
],
);
},
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
spacing: 16,
children: [
const Text('Pan'),
Switch(
value: _isPanEnabled,
onChanged: (value) {
setState(() {
_isPanEnabled = value;
});
},
),
const Text('Scale'),
Switch(
value: _isScaleEnabled,
onChanged: (value) {
setState(() {
_isScaleEnabled = value;
});
},
),
],
),
),
AspectRatio(
aspectRatio: 1.4,
child: Padding(
padding: const EdgeInsets.only(
top: 0.0,
right: 18.0,
),
child: LineChart(
transformationConfig: FlTransformationConfig(
scaleAxis: FlScaleAxis.horizontal,
minScale: 1.0,
maxScale: 25.0,
panEnabled: _isPanEnabled,
scaleEnabled: _isScaleEnabled,
transformationController: _transformationController,
),
LineChartData(
lineBarsData: [
LineChartBarData(
spots: _bitcoinPriceHistory?.asMap().entries.map((e) {
final index = e.key;
final item = e.value;
final value = item.$2;
return FlSpot(index.toDouble(), value);
}).toList() ??
[],
dotData: const FlDotData(show: false),
color: AppColors.contentColorYellow,
barWidth: 1,
shadow: const Shadow(
color: AppColors.contentColorYellow,
blurRadius: 2,
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
AppColors.contentColorYellow.withValues(alpha: 0.2),
AppColors.contentColorYellow.withValues(alpha: 0.0),
],
stops: const [0.5, 1.0],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
),
],
lineTouchData: LineTouchData(
touchSpotThreshold: 5,
getTouchLineStart: (_, __) => -double.infinity,
getTouchLineEnd: (_, __) => double.infinity,
getTouchedSpotIndicator:
(LineChartBarData barData, List<int> spotIndexes) {
return spotIndexes.map((spotIndex) {
return TouchedSpotIndicatorData(
const FlLine(
color: AppColors.contentColorRed,
strokeWidth: 1.5,
dashArray: [8, 2],
),
FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 6,
color: AppColors.contentColorYellow,
strokeWidth: 0,
strokeColor: AppColors.contentColorYellow,
);
},
),
);
}).toList();
},
touchTooltipData: LineTouchTooltipData(
getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
return touchedBarSpots.map((barSpot) {
final price = barSpot.y;
final date =
_bitcoinPriceHistory![barSpot.x.toInt()].$1;
return LineTooltipItem(
'',
const TextStyle(
color: AppColors.contentColorBlack,
fontWeight: FontWeight.bold,
),
children: [
TextSpan(
text: '${date.year}/${date.month}/${date.day}',
style: TextStyle(
color: AppColors.contentColorGreen.darken(20),
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
TextSpan(
text: '\n${AppUtils.getFormattedCurrency(
context,
price,
noDecimals: true,
)}',
style: const TextStyle(
color: AppColors.contentColorYellow,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
);
}).toList();
},
getTooltipColor: (LineBarSpot barSpot) =>
AppColors.contentColorBlack,
),
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: const AxisTitles(
drawBelowEverything: true,
sideTitles: SideTitles(
showTitles: true,
reservedSize: leftReservedSize,
maxIncluded: false,
minIncluded: false,
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 38,
maxIncluded: false,
getTitlesWidget: (double value, TitleMeta meta) {
final date = _bitcoinPriceHistory![value.toInt()].$1;
return SideTitleWidget(
meta: meta,
child: Transform.rotate(
angle: -45 * 3.14 / 180,
child: Text(
'${date.month}/${date.day}',
style: const TextStyle(
color: AppColors.contentColorGreen,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
);
},
),
),
),
),
duration: Duration.zero,
),
),
),
],
);
}
@override
void dispose() {
_transformationController.dispose();
super.dispose();
}
}
class _ChartTitle extends StatelessWidget {
const _ChartTitle();
@override
Widget build(BuildContext context) {
return const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 14),
Text(
'Bitcoin Price History',
style: TextStyle(
color: AppColors.contentColorYellow,
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
Text(
'2023/12/19 - 2024/12/17',
style: TextStyle(
color: AppColors.contentColorGreen,
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
SizedBox(height: 14),
],
);
}
}
class _TransformationButtons extends StatelessWidget {
const _TransformationButtons({
required this.controller,
});
final TransformationController controller;
@override
Widget build(BuildContext context) {
return Column(
children: [
Tooltip(
message: 'Zoom in',
child: IconButton(
icon: const Icon(
Icons.add,
size: 16,
),
onPressed: _transformationZoomIn,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Tooltip(
message: 'Move left',
child: IconButton(
icon: const Icon(
Icons.arrow_back_ios,
size: 16,
),
onPressed: _transformationMoveLeft,
),
),
Tooltip(
message: 'Reset zoom',
child: IconButton(
icon: const Icon(
Icons.refresh,
size: 16,
),
onPressed: _transformationReset,
),
),
Tooltip(
message: 'Move right',
child: IconButton(
icon: const Icon(
Icons.arrow_forward_ios,
size: 16,
),
onPressed: _transformationMoveRight,
),
),
],
),
Tooltip(
message: 'Zoom out',
child: IconButton(
icon: const Icon(
Icons.minimize,
size: 16,
),
onPressed: _transformationZoomOut,
),
),
],
);
}
void _transformationReset() {
controller.value = Matrix4.identity();
}
void _transformationZoomIn() {
controller.value *= Matrix4.diagonal3Values(
1.1,
1.1,
1,
);
}
void _transformationMoveLeft() {
controller.value *= Matrix4.translationValues(
20,
0,
0,
);
}
void _transformationMoveRight() {
controller.value *= Matrix4.translationValues(
-20,
0,
0,
);
}
void _transformationZoomOut() {
controller.value *= Matrix4.diagonal3Values(
0.9,
0.9,
1,
);
}
}

View File

@@ -0,0 +1,494 @@
import 'package:equatable/equatable.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/util/app_utils.dart';
import 'package:fl_chart_app/util/csv_parser.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class LineChartSample13 extends StatefulWidget {
const LineChartSample13({super.key});
@override
State<LineChartSample13> createState() => _LineChartSample13State();
}
class _LineChartSample13State extends State<LineChartSample13> {
List<List<_WeatherData>>? monthlyWeatherData;
int _currentMonthIndex = 0;
late final List<String> monthsNames;
final int minDays = 1;
final int maxDays = 31;
late final double overallMinTemp;
late final double overallMaxTemp;
int _interactedSpotIndex = -1;
@override
void initState() {
monthsNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
_loadWeatherData();
super.initState();
}
void _loadWeatherData() async {
final data =
await rootBundle.loadString('assets/data/amsterdam_2024_weather.csv');
final rows = CsvParser.parse(data);
if (!mounted) {
return;
}
setState(() {
final allWeatherData =
rows.skip(1).map((row) => _WeatherData.fromCsv(row)).toList();
monthlyWeatherData = List.generate(12, (index) {
final month = index + 1;
return allWeatherData
.where((element) => element.datetime.month == month)
.toList();
});
overallMinTemp = allWeatherData
.map((e) => e.temp)
.reduce((value, element) => value < element ? value : element);
overallMaxTemp = allWeatherData
.map((e) => e.temp)
.reduce((value, element) => value > element ? value : element);
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
const SizedBox(height: 18),
Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Amsterdam Weather 2024',
style: TextStyle(
color: AppColors.contentColorOrange,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Tooltip(
message: 'Source: visualcrossing.com',
child: IconButton(
onPressed: () {
AppUtils().tryToLaunchUrl(
'https://www.visualcrossing.com/weather-history/Amsterdam,Netherlands/metric/2024-01-01/2024-12-31',
);
},
icon: const Icon(
Icons.info_outline_rounded,
color: AppColors.contentColorOrange,
size: 18,
)),
)
],
),
const SizedBox(height: 18),
Row(
children: [
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: IconButton(
onPressed: _canGoPrevious ? _previousMonth : null,
icon: const Icon(Icons.navigate_before_rounded),
),
),
),
SizedBox(
width: 92,
child: Text(
monthsNames[_currentMonthIndex],
textAlign: TextAlign.center,
style: const TextStyle(
color: AppColors.contentColorBlue,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: IconButton(
onPressed: _canGoNext ? _nextMonth : null,
icon: const Icon(Icons.navigate_next_rounded),
),
),
),
],
),
const SizedBox(height: 18),
AspectRatio(
aspectRatio: 1.4,
child: Stack(
children: [
if (monthlyWeatherData != null)
Padding(
padding: const EdgeInsets.only(
top: 0.0,
right: 18.0,
),
child: LineChart(
LineChartData(
minY: overallMinTemp - 5,
maxY: overallMaxTemp + 5,
minX: 0,
maxX: 31,
lineBarsData: [
LineChartBarData(
spots: monthlyWeatherData![_currentMonthIndex]
.asMap()
.entries
.map((e) {
final index = e.key;
final item = e.value;
final value = item.temp;
return FlSpot(
index.toDouble(),
value,
yError: FlErrorRange(
lowerBy: (item.tempmin - value).abs(),
upperBy: item.tempmax - value,
),
);
}).toList(),
isCurved: false,
dotData: const FlDotData(show: false),
color: AppColors.contentColorBlue,
barWidth: 1,
errorIndicatorData: FlErrorIndicatorData(
show: true,
painter: _errorPainter,
),
),
],
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
horizontalInterval: 5,
getDrawingHorizontalLine: _horizontalGridLines,
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
drawBelowEverything: true,
sideTitles: SideTitles(
showTitles: true,
maxIncluded: false,
minIncluded: false,
reservedSize: 40,
getTitlesWidget: (double value, TitleMeta meta) {
return SideTitleWidget(
meta: meta,
child: Text(
'${meta.formattedValue}°',
),
);
},
),
),
bottomTitles: AxisTitles(
axisNameWidget: Container(
margin: const EdgeInsets.only(bottom: 20),
child: const Text(
'Day of month',
style: TextStyle(
color: AppColors.contentColorGreen,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
axisNameSize: 40,
sideTitles: SideTitles(
showTitles: true,
reservedSize: 38,
maxIncluded: false,
interval: 1,
getTitlesWidget: _bottomTitles,
),
),
),
lineTouchData: LineTouchData(
enabled: true,
handleBuiltInTouches: false,
touchCallback: _touchCallback,
),
),
),
),
if (monthlyWeatherData == null)
const Center(
child: CircularProgressIndicator(),
)
],
),
),
],
);
}
bool get _canGoNext => _currentMonthIndex < 11;
bool get _canGoPrevious => _currentMonthIndex > 0;
void _previousMonth() {
if (!_canGoPrevious) {
return;
}
setState(() {
_currentMonthIndex--;
});
}
void _nextMonth() {
if (!_canGoNext) {
return;
}
setState(() {
_currentMonthIndex++;
});
}
FlSpotErrorRangePainter _errorPainter(
LineChartSpotErrorRangeCallbackInput input,
) =>
FlSimpleErrorPainter(
lineWidth: 1.0,
lineColor: _interactedSpotIndex == input.spotIndex
? Colors.white
: Colors.white38,
showErrorTexts: _interactedSpotIndex == input.spotIndex,
);
FlLine _horizontalGridLines(double value) {
final isZero = value == 0.0;
return FlLine(
color: isZero ? Colors.white38 : Colors.blueGrey,
strokeWidth: isZero ? 0.8 : 0.4,
dashArray: isZero ? null : [8, 4],
);
}
Widget _bottomTitles(double value, TitleMeta meta) {
final day = value.toInt() + 1;
final isDayHovered = _interactedSpotIndex == day - 1;
final isImportantToShow = day % 5 == 0 || day == 1;
if (!isImportantToShow && !isDayHovered) {
return const SizedBox();
}
return SideTitleWidget(
meta: meta,
child: Text(
day.toString(),
style: TextStyle(
color: isDayHovered
? AppColors.contentColorWhite
: AppColors.contentColorGreen,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
);
}
_touchCallback(FlTouchEvent event, LineTouchResponse? touchResponse) {
if (!event.isInterestedForInteractions ||
touchResponse?.lineBarSpots == null ||
touchResponse!.lineBarSpots!.isEmpty) {
setState(() {
_interactedSpotIndex = -1;
});
return;
}
setState(() {
_interactedSpotIndex = touchResponse.lineBarSpots!.first.spotIndex;
});
}
@override
void dispose() {
super.dispose();
}
}
class _WeatherData with EquatableMixin {
final String name;
final DateTime datetime;
final double tempmax;
final double tempmin;
final double temp;
final double feelslikemax;
final double feelslikemin;
final double feelslike;
final double dew;
final double humidity;
final double precip;
final double precipprob;
final double precipcover;
final String preciptype;
final double snow;
final double snowdepth;
final double windgust;
final double windspeed;
final double winddir;
final double sealevelpressure;
final double cloudcover;
final double visibility;
final double solarradiation;
final double solarenergy;
final double uvindex;
final double severerisk;
final DateTime sunrise;
final DateTime sunset;
final double moonphase;
final String conditions;
final String description;
final String icon;
final String stations;
const _WeatherData({
required this.name,
required this.datetime,
required this.tempmax,
required this.tempmin,
required this.temp,
required this.feelslikemax,
required this.feelslikemin,
required this.feelslike,
required this.dew,
required this.humidity,
required this.precip,
required this.precipprob,
required this.precipcover,
required this.preciptype,
required this.snow,
required this.snowdepth,
required this.windgust,
required this.windspeed,
required this.winddir,
required this.sealevelpressure,
required this.cloudcover,
required this.visibility,
required this.solarradiation,
required this.solarenergy,
required this.uvindex,
required this.severerisk,
required this.sunrise,
required this.sunset,
required this.moonphase,
required this.conditions,
required this.description,
required this.icon,
required this.stations,
});
// parse from csv row
// name,datetime,tempmax,tempmin,temp,feelslikemax,feelslikemin,feelslike,dew,humidity,precip,precipprob,precipcover,preciptype,snow,snowdepth,windgust,windspeed,winddir,sealevelpressure,cloudcover,visibility,solarradiation,solarenergy,uvindex,severerisk,sunrise,sunset,moonphase,conditions,description,icon,stations
// "Amsterdam,Netherlands",2024-01-01,9.1,6.4,8,5.3,2.5,4.1,5.1,82.4,14.26,100,37.5,rain,0,0,53.9,40.2,225.9,1000.1,88.7,20.5,20.6,1.8,2,,2024-01-01T08:50:34,2024-01-01T16:37:06,0.68,"Rain, Partially cloudy",Partly cloudy throughout the day with a chance of rain throughout the day.,rain,"06260099999,D3248,06348099999,06249099999,C0449,06240099999,06269099999,06257099999,06344099999"
factory _WeatherData.fromCsv(List<String> row) => _WeatherData(
name: row[0],
datetime: DateTime.parse(row[1]),
tempmax: double.parse(row[2]),
tempmin: double.parse(row[3]),
temp: double.parse(row[4]),
feelslikemax: double.parse(row[5]),
feelslikemin: double.parse(row[6]),
feelslike: double.parse(row[7]),
dew: double.parse(row[8]),
humidity: double.parse(row[9]),
precip: double.parse(row[10]),
precipprob: double.parse(row[11]),
precipcover: double.parse(row[12]),
preciptype: row[13],
snow: double.parse(row[14]),
snowdepth: double.parse(row[15]),
windgust: double.parse(row[16]),
windspeed: double.parse(row[17]),
winddir: double.parse(row[18]),
sealevelpressure: double.parse(row[19]),
cloudcover: double.parse(row[20]),
visibility: double.parse(row[21]),
solarradiation: double.parse(row[22]),
solarenergy: double.parse(row[23]),
uvindex: double.parse(row[24]),
severerisk: row[25].isEmpty ? 0 : double.parse(row[25]),
sunrise: DateTime.parse(row[26]),
sunset: DateTime.parse(row[27]),
moonphase: double.parse(row[28]),
conditions: row[29],
description: row[30],
icon: row[31],
stations: row[32],
);
@override
List<Object?> get props => [
name,
datetime,
tempmax,
tempmin,
temp,
feelslikemax,
feelslikemin,
feelslike,
dew,
humidity,
precip,
precipprob,
precipcover,
preciptype,
snow,
snowdepth,
windgust,
windspeed,
winddir,
sealevelpressure,
cloudcover,
visibility,
solarradiation,
solarenergy,
uvindex,
severerisk,
sunrise,
sunset,
moonphase,
conditions,
description,
icon,
stations,
];
}

View File

@@ -0,0 +1,294 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
class LineChartSample2 extends StatefulWidget {
const LineChartSample2({super.key});
@override
State<LineChartSample2> createState() => _LineChartSample2State();
}
class _LineChartSample2State extends State<LineChartSample2> {
List<Color> gradientColors = [
AppColors.contentColorCyan,
AppColors.contentColorBlue,
];
bool showAvg = false;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
AspectRatio(
aspectRatio: 1.70,
child: Padding(
padding: const EdgeInsets.only(
right: 18,
left: 12,
top: 24,
bottom: 12,
),
child: LineChart(
showAvg ? avgData() : mainData(),
),
),
),
SizedBox(
width: 60,
height: 34,
child: TextButton(
onPressed: () {
setState(() {
showAvg = !showAvg;
});
},
child: Text(
'avg',
style: TextStyle(
fontSize: 12,
color: showAvg
? Colors.white.withValues(alpha: 0.5)
: Colors.white,
),
),
),
),
],
);
}
Widget bottomTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
);
Widget text;
switch (value.toInt()) {
case 2:
text = const Text('MAR', style: style);
break;
case 5:
text = const Text('JUN', style: style);
break;
case 8:
text = const Text('SEP', style: style);
break;
default:
text = const Text('', style: style);
break;
}
return SideTitleWidget(
meta: meta,
child: text,
);
}
Widget leftTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
);
String text;
switch (value.toInt()) {
case 1:
text = '10K';
break;
case 3:
text = '30k';
break;
case 5:
text = '50k';
break;
default:
return Container();
}
return Text(text, style: style, textAlign: TextAlign.left);
}
LineChartData mainData() {
return LineChartData(
gridData: FlGridData(
show: true,
drawVerticalLine: true,
horizontalInterval: 1,
verticalInterval: 1,
getDrawingHorizontalLine: (value) {
return const FlLine(
color: AppColors.mainGridLineColor,
strokeWidth: 1,
);
},
getDrawingVerticalLine: (value) {
return const FlLine(
color: AppColors.mainGridLineColor,
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
interval: 1,
getTitlesWidget: bottomTitleWidgets,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: leftTitleWidgets,
reservedSize: 42,
),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: const Color(0xff37434d)),
),
minX: 0,
maxX: 11,
minY: 0,
maxY: 6,
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 3),
FlSpot(2.6, 2),
FlSpot(4.9, 5),
FlSpot(6.8, 3.1),
FlSpot(8, 4),
FlSpot(9.5, 3),
FlSpot(11, 4),
],
isCurved: true,
gradient: LinearGradient(
colors: gradientColors,
),
barWidth: 5,
isStrokeCapRound: true,
dotData: const FlDotData(
show: false,
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: gradientColors
.map((color) => color.withValues(alpha: 0.3))
.toList(),
),
),
),
],
);
}
LineChartData avgData() {
return LineChartData(
lineTouchData: const LineTouchData(enabled: false),
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
verticalInterval: 1,
horizontalInterval: 1,
getDrawingVerticalLine: (value) {
return const FlLine(
color: Color(0xff37434d),
strokeWidth: 1,
);
},
getDrawingHorizontalLine: (value) {
return const FlLine(
color: Color(0xff37434d),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: bottomTitleWidgets,
interval: 1,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: leftTitleWidgets,
reservedSize: 42,
interval: 1,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(color: const Color(0xff37434d)),
),
minX: 0,
maxX: 11,
minY: 0,
maxY: 6,
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 3.44),
FlSpot(2.6, 3.44),
FlSpot(4.9, 3.44),
FlSpot(6.8, 3.44),
FlSpot(8, 3.44),
FlSpot(9.5, 3.44),
FlSpot(11, 3.44),
],
isCurved: true,
gradient: LinearGradient(
colors: [
ColorTween(begin: gradientColors[0], end: gradientColors[1])
.lerp(0.2)!,
ColorTween(begin: gradientColors[0], end: gradientColors[1])
.lerp(0.2)!,
],
),
barWidth: 5,
isStrokeCapRound: true,
dotData: const FlDotData(
show: false,
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
ColorTween(begin: gradientColors[0], end: gradientColors[1])
.lerp(0.2)!
.withValues(alpha: 0.1),
ColorTween(begin: gradientColors[0], end: gradientColors[1])
.lerp(0.2)!
.withValues(alpha: 0.1),
],
),
),
),
],
);
}
}

View File

@@ -0,0 +1,451 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
class LineChartSample3 extends StatefulWidget {
LineChartSample3({
super.key,
Color? lineColor,
Color? indicatorLineColor,
Color? indicatorTouchedLineColor,
Color? indicatorSpotStrokeColor,
Color? indicatorTouchedSpotStrokeColor,
Color? bottomTextColor,
Color? bottomTouchedTextColor,
Color? averageLineColor,
Color? tooltipBgColor,
Color? tooltipTextColor,
}) : lineColor = lineColor ?? AppColors.contentColorRed,
indicatorLineColor = indicatorLineColor ??
AppColors.contentColorYellow.withValues(alpha: 0.2),
indicatorTouchedLineColor =
indicatorTouchedLineColor ?? AppColors.contentColorYellow,
indicatorSpotStrokeColor = indicatorSpotStrokeColor ??
AppColors.contentColorYellow.withValues(alpha: 0.5),
indicatorTouchedSpotStrokeColor =
indicatorTouchedSpotStrokeColor ?? AppColors.contentColorYellow,
bottomTextColor = bottomTextColor ??
AppColors.contentColorYellow.withValues(alpha: 0.2),
bottomTouchedTextColor =
bottomTouchedTextColor ?? AppColors.contentColorYellow,
averageLineColor = averageLineColor ??
AppColors.contentColorGreen.withValues(alpha: 0.8),
tooltipBgColor = tooltipBgColor ?? AppColors.contentColorGreen,
tooltipTextColor = tooltipTextColor ?? Colors.black;
final Color lineColor;
final Color indicatorLineColor;
final Color indicatorTouchedLineColor;
final Color indicatorSpotStrokeColor;
final Color indicatorTouchedSpotStrokeColor;
final Color bottomTextColor;
final Color bottomTouchedTextColor;
final Color averageLineColor;
final Color tooltipBgColor;
final Color tooltipTextColor;
List<String> get weekDays =>
const ['Sat', 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
List<double> get yValues => const [1.3, 1, 1.8, 1.5, 2.2, 1.8, 3];
@override
State createState() => _LineChartSample3State();
}
class _LineChartSample3State extends State<LineChartSample3> {
late double touchedValue;
bool fitInsideBottomTitle = true;
bool fitInsideLeftTitle = false;
@override
void initState() {
touchedValue = -1;
super.initState();
}
Widget leftTitleWidgets(double value, TitleMeta meta) {
if (value % 1 != 0) {
return Container();
}
final style = TextStyle(
color: AppColors.mainTextColor1.withValues(alpha: 0.5),
fontSize: 10,
);
String text;
switch (value.toInt()) {
case 0:
text = '';
break;
case 1:
text = '1k calories';
break;
case 2:
text = '2k calories';
break;
case 3:
text = '3k calories';
break;
default:
return Container();
}
return SideTitleWidget(
meta: meta,
space: 6,
fitInside: fitInsideLeftTitle
? SideTitleFitInsideData.fromTitleMeta(meta)
: SideTitleFitInsideData.disable(),
child: Text(text, style: style, textAlign: TextAlign.center),
);
}
Widget bottomTitleWidgets(double value, TitleMeta meta) {
final isTouched = value == touchedValue;
final style = TextStyle(
color: isTouched ? widget.bottomTouchedTextColor : widget.bottomTextColor,
fontWeight: FontWeight.bold,
);
if (value % 1 != 0) {
return Container();
}
return SideTitleWidget(
space: 4,
meta: meta,
fitInside: fitInsideBottomTitle
? SideTitleFitInsideData.fromTitleMeta(meta, distanceFromEdge: 0)
: SideTitleFitInsideData.disable(),
child: Text(
widget.weekDays[value.toInt()],
style: style,
),
);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const SizedBox(height: 10),
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Average Line',
style: TextStyle(
color: widget.averageLineColor.withValues(alpha: 1),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const Text(
' and ',
style: TextStyle(
color: AppColors.mainTextColor1,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
Text(
'Indicators',
style: TextStyle(
color: widget.indicatorLineColor.withValues(alpha: 1),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
),
const SizedBox(
height: 18,
),
AspectRatio(
aspectRatio: 2,
child: Padding(
padding: const EdgeInsets.only(right: 20.0, left: 12),
child: LineChart(
LineChartData(
lineTouchData: LineTouchData(
getTouchedSpotIndicator:
(LineChartBarData barData, List<int> spotIndexes) {
return spotIndexes.map((spotIndex) {
final spot = barData.spots[spotIndex];
if (spot.x == 0 || spot.x == 6) {
return null;
}
return TouchedSpotIndicatorData(
FlLine(
color: widget.indicatorTouchedLineColor,
strokeWidth: 4,
),
FlDotData(
getDotPainter: (spot, percent, barData, index) {
if (index.isEven) {
return FlDotCirclePainter(
radius: 8,
color: Colors.white,
strokeWidth: 5,
strokeColor:
widget.indicatorTouchedSpotStrokeColor,
);
} else {
return FlDotSquarePainter(
size: 16,
color: Colors.white,
strokeWidth: 5,
strokeColor:
widget.indicatorTouchedSpotStrokeColor,
);
}
},
),
);
}).toList();
},
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (touchedSpot) => widget.tooltipBgColor,
getTooltipItems: (List<LineBarSpot> touchedBarSpots) {
return touchedBarSpots.map((barSpot) {
final flSpot = barSpot;
if (flSpot.x == 0 || flSpot.x == 6) {
return null;
}
TextAlign textAlign;
switch (flSpot.x.toInt()) {
case 1:
textAlign = TextAlign.left;
break;
case 5:
textAlign = TextAlign.right;
break;
default:
textAlign = TextAlign.center;
}
return LineTooltipItem(
'${widget.weekDays[flSpot.x.toInt()]} \n',
TextStyle(
color: widget.tooltipTextColor,
fontWeight: FontWeight.bold,
),
children: [
TextSpan(
text: flSpot.y.toString(),
style: TextStyle(
color: widget.tooltipTextColor,
fontWeight: FontWeight.w900,
),
),
const TextSpan(
text: ' k ',
style: TextStyle(
fontStyle: FontStyle.italic,
fontWeight: FontWeight.w900,
),
),
const TextSpan(
text: 'calories',
style: TextStyle(
fontWeight: FontWeight.normal,
),
),
],
textAlign: textAlign,
);
}).toList();
},
),
touchCallback:
(FlTouchEvent event, LineTouchResponse? lineTouch) {
if (!event.isInterestedForInteractions ||
lineTouch == null ||
lineTouch.lineBarSpots == null) {
setState(() {
touchedValue = -1;
});
return;
}
final value = lineTouch.lineBarSpots![0].x;
if (value == 0 || value == 6) {
setState(() {
touchedValue = -1;
});
return;
}
setState(() {
touchedValue = value;
});
},
),
extraLinesData: ExtraLinesData(
horizontalLines: [
HorizontalLine(
y: 1.8,
color: widget.averageLineColor,
strokeWidth: 3,
dashArray: [20, 10],
),
],
),
lineBarsData: [
LineChartBarData(
isStepLineChart: true,
spots: widget.yValues.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value);
}).toList(),
isCurved: false,
barWidth: 4,
color: widget.lineColor,
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
widget.lineColor.withValues(alpha: 0.5),
widget.lineColor.withValues(alpha: 0),
],
stops: const [0.5, 1.0],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
spotsLine: BarAreaSpotsLine(
show: true,
flLineStyle: FlLine(
color: widget.indicatorLineColor,
strokeWidth: 2,
),
checkToShowSpotLine: (spot) {
if (spot.x == 0 || spot.x == 6) {
return false;
}
return true;
},
),
),
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
if (index.isEven) {
return FlDotCirclePainter(
radius: 6,
color: Colors.white,
strokeWidth: 3,
strokeColor: widget.indicatorSpotStrokeColor,
);
} else {
return FlDotSquarePainter(
size: 12,
color: Colors.white,
strokeWidth: 3,
strokeColor: widget.indicatorSpotStrokeColor,
);
}
},
checkToShowDot: (spot, barData) {
return spot.x != 0 && spot.x != 6;
},
),
),
],
minY: 0,
borderData: FlBorderData(
show: true,
border: Border.all(
color: AppColors.borderColor,
),
),
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: true,
checkToShowHorizontalLine: (value) => value % 1 == 0,
checkToShowVerticalLine: (value) => value % 1 == 0,
getDrawingHorizontalLine: (value) {
if (value == 0) {
return const FlLine(
color: AppColors.contentColorOrange,
strokeWidth: 2,
);
} else {
return const FlLine(
color: AppColors.mainGridLineColor,
strokeWidth: 0.5,
);
}
},
getDrawingVerticalLine: (value) {
if (value == 0) {
return const FlLine(
color: Colors.redAccent,
strokeWidth: 10,
);
} else {
return const FlLine(
color: AppColors.mainGridLineColor,
strokeWidth: 0.5,
);
}
},
),
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 46,
getTitlesWidget: leftTitleWidgets,
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
getTitlesWidget: bottomTitleWidgets,
),
),
),
),
),
),
),
Column(
children: [
const Text('Fit Inside Title Option'),
const Divider(),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Left Title'),
Switch(
value: fitInsideLeftTitle,
onChanged: (value) => setState(() {
fitInsideLeftTitle = value;
}),
),
const Text('Bottom Title'),
Switch(
value: fitInsideBottomTitle,
onChanged: (value) => setState(() {
fitInsideBottomTitle = value;
}),
)
],
),
],
),
],
);
}
}

View File

@@ -0,0 +1,202 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
class LineChartSample4 extends StatelessWidget {
LineChartSample4({
super.key,
Color? mainLineColor,
Color? belowLineColor,
Color? aboveLineColor,
}) : mainLineColor =
mainLineColor ?? AppColors.contentColorYellow.withValues(alpha: 1),
belowLineColor =
belowLineColor ?? AppColors.contentColorPink.withValues(alpha: 1),
aboveLineColor = aboveLineColor ??
AppColors.contentColorPurple.withValues(alpha: 0.7);
final Color mainLineColor;
final Color belowLineColor;
final Color aboveLineColor;
Widget bottomTitleWidgets(double value, TitleMeta meta) {
String text;
switch (value.toInt()) {
case 0:
text = 'Jan';
break;
case 1:
text = 'Feb';
break;
case 2:
text = 'Mar';
break;
case 3:
text = 'Apr';
break;
case 4:
text = 'May';
break;
case 5:
text = 'Jun';
break;
case 6:
text = 'Jul';
break;
case 7:
text = 'Aug';
break;
case 8:
text = 'Sep';
break;
case 9:
text = 'Oct';
break;
case 10:
text = 'Nov';
break;
case 11:
text = 'Dec';
break;
default:
return Container();
}
return SideTitleWidget(
meta: meta,
space: 4,
child: Text(
text,
style: TextStyle(
fontSize: 10,
color: mainLineColor,
fontWeight: FontWeight.bold,
),
),
);
}
Widget leftTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(
color: AppColors.mainTextColor3,
fontSize: 12,
);
return SideTitleWidget(
meta: meta,
child: Text('\$ ${value + 0.5}', style: style),
);
}
@override
Widget build(BuildContext context) {
const cutOffYValue = 5.0;
return AspectRatio(
aspectRatio: 2,
child: Padding(
padding: const EdgeInsets.only(
left: 12,
right: 28,
top: 22,
bottom: 12,
),
child: LineChart(
LineChartData(
lineTouchData: const LineTouchData(enabled: false),
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 4),
FlSpot(1, 3.5),
FlSpot(2, 4.5),
FlSpot(3, 1),
FlSpot(4, 4),
FlSpot(5, 6),
FlSpot(6, 6.5),
FlSpot(7, 6),
FlSpot(8, 4),
FlSpot(9, 6),
FlSpot(10, 6),
FlSpot(11, 7),
],
isCurved: true,
barWidth: 8,
color: mainLineColor,
belowBarData: BarAreaData(
show: true,
color: belowLineColor,
cutOffY: cutOffYValue,
applyCutOffY: true,
),
aboveBarData: BarAreaData(
show: true,
color: aboveLineColor,
cutOffY: cutOffYValue,
applyCutOffY: true,
),
dotData: const FlDotData(
show: false,
),
),
],
minY: 0,
titlesData: FlTitlesData(
show: true,
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
axisNameWidget: Text(
'2019',
style: TextStyle(
fontSize: 10,
color: mainLineColor,
fontWeight: FontWeight.bold,
),
),
sideTitles: SideTitles(
showTitles: true,
reservedSize: 18,
interval: 1,
getTitlesWidget: bottomTitleWidgets,
),
),
leftTitles: AxisTitles(
axisNameSize: 20,
axisNameWidget: const Text(
'Value',
style: TextStyle(
color: AppColors.mainTextColor2,
),
),
sideTitles: SideTitles(
showTitles: true,
interval: 1,
reservedSize: 40,
getTitlesWidget: leftTitleWidgets,
),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(
color: AppColors.borderColor,
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 1,
checkToShowHorizontalLine: (double value) {
return value == 1 || value == 6 || value == 4 || value == 5;
},
),
),
),
),
);
}
}

View File

@@ -0,0 +1,289 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
class LineChartSample5 extends StatefulWidget {
const LineChartSample5({
super.key,
Color? gradientColor1,
Color? gradientColor2,
Color? gradientColor3,
Color? indicatorStrokeColor,
}) : gradientColor1 = gradientColor1 ?? AppColors.contentColorBlue,
gradientColor2 = gradientColor2 ?? AppColors.contentColorPink,
gradientColor3 = gradientColor3 ?? AppColors.contentColorRed,
indicatorStrokeColor = indicatorStrokeColor ?? AppColors.mainTextColor1;
final Color gradientColor1;
final Color gradientColor2;
final Color gradientColor3;
final Color indicatorStrokeColor;
@override
State<LineChartSample5> createState() => _LineChartSample5State();
}
class _LineChartSample5State extends State<LineChartSample5> {
List<int> showingTooltipOnSpots = [1, 3, 5];
List<FlSpot> get allSpots => const [
FlSpot(0, 1),
FlSpot(1, 2),
FlSpot(2, 1.5),
FlSpot(3, 3),
FlSpot(4, 3.5),
FlSpot(5, 5),
FlSpot(6, 8),
];
Widget bottomTitleWidgets(double value, TitleMeta meta, double chartWidth) {
final style = TextStyle(
fontWeight: FontWeight.bold,
color: AppColors.contentColorPink,
fontFamily: 'Digital',
fontSize: 18 * chartWidth / 500,
);
String text;
switch (value.toInt()) {
case 0:
text = '00:00';
break;
case 1:
text = '04:00';
break;
case 2:
text = '08:00';
break;
case 3:
text = '12:00';
break;
case 4:
text = '16:00';
break;
case 5:
text = '20:00';
break;
case 6:
text = '23:59';
break;
default:
return Container();
}
return SideTitleWidget(
meta: meta,
child: Text(text, style: style),
);
}
@override
Widget build(BuildContext context) {
final lineBarsData = [
LineChartBarData(
showingIndicators: showingTooltipOnSpots,
spots: allSpots,
isCurved: true,
barWidth: 4,
shadow: const Shadow(
blurRadius: 8,
),
belowBarData: BarAreaData(
show: true,
gradient: LinearGradient(
colors: [
widget.gradientColor1.withValues(alpha: 0.4),
widget.gradientColor2.withValues(alpha: 0.4),
widget.gradientColor3.withValues(alpha: 0.4),
],
),
),
dotData: const FlDotData(show: false),
gradient: LinearGradient(
colors: [
widget.gradientColor1,
widget.gradientColor2,
widget.gradientColor3,
],
stops: const [0.1, 0.4, 0.9],
),
),
];
final tooltipsOnBar = lineBarsData[0];
return AspectRatio(
aspectRatio: 2.5,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24.0,
vertical: 10,
),
child: LayoutBuilder(builder: (context, constraints) {
return LineChart(
LineChartData(
showingTooltipIndicators: showingTooltipOnSpots.map((index) {
return ShowingTooltipIndicators([
LineBarSpot(
tooltipsOnBar,
lineBarsData.indexOf(tooltipsOnBar),
tooltipsOnBar.spots[index],
),
]);
}).toList(),
lineTouchData: LineTouchData(
enabled: true,
handleBuiltInTouches: false,
touchCallback:
(FlTouchEvent event, LineTouchResponse? response) {
if (response == null || response.lineBarSpots == null) {
return;
}
if (event is FlTapUpEvent) {
final spotIndex = response.lineBarSpots!.first.spotIndex;
setState(() {
if (showingTooltipOnSpots.contains(spotIndex)) {
showingTooltipOnSpots.remove(spotIndex);
} else {
showingTooltipOnSpots.add(spotIndex);
}
});
}
},
mouseCursorResolver:
(FlTouchEvent event, LineTouchResponse? response) {
if (response == null || response.lineBarSpots == null) {
return SystemMouseCursors.basic;
}
return SystemMouseCursors.click;
},
getTouchedSpotIndicator:
(LineChartBarData barData, List<int> spotIndexes) {
return spotIndexes.map((index) {
return TouchedSpotIndicatorData(
const FlLine(
color: Colors.pink,
),
FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) =>
FlDotCirclePainter(
radius: 8,
color: lerpGradient(
barData.gradient!.colors,
barData.gradient!.stops!,
percent / 100,
),
strokeWidth: 2,
strokeColor: widget.indicatorStrokeColor,
),
),
);
}).toList();
},
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (touchedSpot) => Colors.pink,
tooltipBorderRadius: BorderRadius.circular(8),
getTooltipItems: (List<LineBarSpot> lineBarsSpot) {
return lineBarsSpot.map((lineBarSpot) {
return LineTooltipItem(
lineBarSpot.y.toString(),
const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
);
}).toList();
},
),
),
lineBarsData: lineBarsData,
minY: 0,
titlesData: FlTitlesData(
leftTitles: const AxisTitles(
axisNameWidget: Text('count'),
axisNameSize: 24,
sideTitles: SideTitles(
showTitles: false,
reservedSize: 0,
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) {
return bottomTitleWidgets(
value,
meta,
constraints.maxWidth,
);
},
reservedSize: 30,
),
),
rightTitles: const AxisTitles(
axisNameWidget: Text('count'),
sideTitles: SideTitles(
showTitles: false,
reservedSize: 0,
),
),
topTitles: const AxisTitles(
axisNameWidget: Text(
'Wall clock',
textAlign: TextAlign.left,
),
axisNameSize: 24,
sideTitles: SideTitles(
showTitles: true,
reservedSize: 0,
),
),
),
gridData: const FlGridData(show: false),
borderData: FlBorderData(
show: true,
border: Border.all(
color: AppColors.borderColor,
),
),
),
);
}),
),
);
}
}
/// Lerps between a [LinearGradient] colors, based on [t]
Color lerpGradient(List<Color> colors, List<double> stops, double t) {
if (colors.isEmpty) {
throw ArgumentError('"colors" is empty.');
} else if (colors.length == 1) {
return colors[0];
}
if (stops.length != colors.length) {
stops = [];
/// provided gradientColorStops is invalid and we calculate it here
colors.asMap().forEach((index, color) {
final percent = 1.0 / (colors.length - 1);
stops.add(percent * index);
});
}
for (var s = 0; s < stops.length - 1; s++) {
final leftStop = stops[s];
final rightStop = stops[s + 1];
final leftColor = colors[s];
final rightColor = colors[s + 1];
if (t <= leftStop) {
return leftColor;
} else if (t < rightStop) {
final sectionT = (t - leftStop) / (rightStop - leftStop);
return Color.lerp(leftColor, rightColor, sectionT)!;
}
}
return colors.last;
}

View File

@@ -0,0 +1,286 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/util/extensions/color_extensions.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
// ignore: must_be_immutable
class LineChartSample6 extends StatelessWidget {
LineChartSample6({
super.key,
Color? line1Color1,
Color? line1Color2,
Color? line2Color1,
Color? line2Color2,
}) : line1Color1 = line1Color1 ?? AppColors.contentColorOrange,
line1Color2 = line1Color2 ?? AppColors.contentColorOrange.darken(60),
line2Color1 = line2Color1 ?? AppColors.contentColorBlue.darken(60),
line2Color2 = line2Color2 ?? AppColors.contentColorBlue {
minSpotX = spots.first.x;
maxSpotX = spots.first.x;
minSpotY = spots.first.y;
maxSpotY = spots.first.y;
for (final spot in spots) {
if (spot.x > maxSpotX) {
maxSpotX = spot.x;
}
if (spot.x < minSpotX) {
minSpotX = spot.x;
}
if (spot.y > maxSpotY) {
maxSpotY = spot.y;
}
if (spot.y < minSpotY) {
minSpotY = spot.y;
}
}
}
final Color line1Color1;
final Color line1Color2;
final Color line2Color1;
final Color line2Color2;
final spots = const [
FlSpot(0, 1),
FlSpot(2, 5),
FlSpot(4, 3),
FlSpot(6, 5),
];
final spots2 = const [
FlSpot(0, 3),
FlSpot(2, 1),
FlSpot(4, 2),
FlSpot(6, 1),
];
late double minSpotX;
late double maxSpotX;
late double minSpotY;
late double maxSpotY;
Widget leftTitleWidgets(double value, TitleMeta meta) {
final style = TextStyle(
color: line1Color1,
fontWeight: FontWeight.bold,
fontSize: 18,
);
final intValue = reverseY(value, minSpotY, maxSpotY).toInt();
if (intValue == (maxSpotY + minSpotY)) {
return Text('', style: style);
}
return Padding(
padding: const EdgeInsets.only(right: 6),
child: Text(
intValue.toString(),
style: style,
textAlign: TextAlign.center,
),
);
}
Widget rightTitleWidgets(double value, TitleMeta meta) {
final style = TextStyle(
color: line2Color2,
fontWeight: FontWeight.bold,
fontSize: 18,
);
final intValue = reverseY(value, minSpotY, maxSpotY).toInt();
if (intValue == (maxSpotY + minSpotY)) {
return Text('', style: style);
}
return Text(intValue.toString(), style: style, textAlign: TextAlign.right);
}
Widget topTitleWidgets(double value, TitleMeta meta) {
if (value % 1 != 0) {
return Container();
}
const style = TextStyle(
fontWeight: FontWeight.bold,
color: AppColors.mainTextColor2,
);
return SideTitleWidget(
meta: meta,
child: Text(value.toInt().toString(), style: style),
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 22, bottom: 20),
child: AspectRatio(
aspectRatio: 2,
child: LineChart(
LineChartData(
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
tooltipBorderRadius: BorderRadius.zero,
getTooltipColor: (spot) => Colors.white,
getTooltipItems: (List<LineBarSpot> touchedSpots) {
return touchedSpots.map((LineBarSpot touchedSpot) {
return LineTooltipItem(
touchedSpot.y.toString(),
TextStyle(
color: touchedSpot.bar.gradient!.colors.first,
fontWeight: FontWeight.bold,
fontSize: 20,
),
);
}).toList();
},
),
getTouchedSpotIndicator: (
_,
indicators,
) {
return indicators
.map((int index) => const TouchedSpotIndicatorData(
FlLine(color: Colors.transparent),
FlDotData(show: false),
))
.toList();
},
touchSpotThreshold: 12,
distanceCalculator:
(Offset touchPoint, Offset spotPixelCoordinates) =>
(touchPoint - spotPixelCoordinates).distance,
),
lineBarsData: [
LineChartBarData(
gradient: LinearGradient(
colors: [
line1Color1,
line1Color2,
],
),
spots: reverseSpots(spots, minSpotY, maxSpotY),
isCurved: true,
isStrokeCapRound: true,
barWidth: 10,
belowBarData: BarAreaData(
show: false,
),
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 12,
color: Color.lerp(
line1Color1,
line1Color2,
percent / 100,
)!,
strokeColor: Colors.white,
strokeWidth: 1,
);
},
),
),
LineChartBarData(
gradient: LinearGradient(
colors: [
line2Color1,
line2Color2,
],
),
spots: reverseSpots(spots2, minSpotY, maxSpotY),
isCurved: true,
isStrokeCapRound: true,
barWidth: 10,
belowBarData: BarAreaData(
show: false,
),
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 12,
color: Color.lerp(
line2Color1,
line2Color2,
percent / 100,
)!,
strokeColor: Colors.white,
strokeWidth: 1,
);
},
),
),
],
minY: 0,
maxY: maxSpotY + minSpotY,
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: leftTitleWidgets,
reservedSize: 38,
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: rightTitleWidgets,
reservedSize: 30,
),
),
bottomTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 32,
getTitlesWidget: topTitleWidgets,
),
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: true,
checkToShowHorizontalLine: (value) {
final intValue = reverseY(value, minSpotY, maxSpotY).toInt();
if (intValue == (maxSpotY + minSpotY).toInt()) {
return false;
}
return true;
},
),
borderData: FlBorderData(
show: true,
border: const Border(
left: BorderSide(color: AppColors.borderColor),
top: BorderSide(color: AppColors.borderColor),
bottom: BorderSide(color: Colors.transparent),
right: BorderSide(color: Colors.transparent),
),
),
),
),
),
);
}
double reverseY(double y, double minX, double maxX) {
return (maxX + minX) - y;
}
List<FlSpot> reverseSpots(List<FlSpot> inputSpots, double minY, double maxY) {
return inputSpots.map((spot) {
return spot.copyWith(y: (maxY + minY) - spot.y);
}).toList();
}
}

View File

@@ -0,0 +1,193 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
class LineChartSample7 extends StatelessWidget {
LineChartSample7({
super.key,
Color? line1Color,
Color? line2Color,
Color? betweenColor,
}) : line1Color = line1Color ?? AppColors.contentColorGreen,
line2Color = line2Color ?? AppColors.contentColorRed,
betweenColor =
betweenColor ?? AppColors.contentColorRed.withValues(alpha: 0.5);
final Color line1Color;
final Color line2Color;
final Color betweenColor;
Widget bottomTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
);
String text;
switch (value.toInt()) {
case 0:
text = 'Jan';
break;
case 1:
text = 'Feb';
break;
case 2:
text = 'Mar';
break;
case 3:
text = 'Apr';
break;
case 4:
text = 'May';
break;
case 5:
text = 'Jun';
break;
case 6:
text = 'Jul';
break;
case 7:
text = 'Aug';
break;
case 8:
text = 'Sep';
break;
case 9:
text = 'Oct';
break;
case 10:
text = 'Nov';
break;
case 11:
text = 'Dec';
break;
default:
return Container();
}
return SideTitleWidget(
meta: meta,
space: 4,
child: Text(text, style: style),
);
}
Widget leftTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(fontSize: 10);
return SideTitleWidget(
meta: meta,
child: Text(
'\$ ${value + 0.5}',
style: style,
),
);
}
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2,
child: Padding(
padding: const EdgeInsets.only(
left: 10,
right: 18,
top: 10,
bottom: 4,
),
child: LineChart(
LineChartData(
lineTouchData: const LineTouchData(enabled: false),
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 4),
FlSpot(1, 3.5),
FlSpot(2, 4.5),
FlSpot(3, 1),
FlSpot(4, 4),
FlSpot(5, 6),
FlSpot(6, 6.5),
FlSpot(7, 6),
FlSpot(8, 4),
FlSpot(9, 6),
FlSpot(10, 6),
FlSpot(11, 7),
],
isCurved: true,
barWidth: 2,
color: line1Color,
dotData: const FlDotData(
show: false,
),
),
LineChartBarData(
spots: const [
FlSpot(0, 7),
FlSpot(1, 3),
FlSpot(2, 4),
FlSpot(3, 2),
FlSpot(4, 3),
FlSpot(5, 4),
FlSpot(6, 5),
FlSpot(7, 3),
FlSpot(8, 1),
FlSpot(9, 8),
FlSpot(10, 1),
FlSpot(11, 3),
],
isCurved: false,
barWidth: 2,
color: line2Color,
dotData: const FlDotData(
show: false,
),
),
],
betweenBarsData: [
BetweenBarsData(
fromIndex: 0,
toIndex: 1,
color: betweenColor,
)
],
minY: 0,
borderData: FlBorderData(
show: false,
),
titlesData: FlTitlesData(
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: bottomTitleWidgets,
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: leftTitleWidgets,
interval: 1,
reservedSize: 36,
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: 1,
checkToShowHorizontalLine: (double value) {
return value == 1 || value == 6 || value == 4 || value == 5;
},
),
),
),
),
);
}
}

View File

@@ -0,0 +1,291 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_svg/flutter_svg.dart';
class LineChartSample8 extends StatefulWidget {
const LineChartSample8({super.key});
@override
State<LineChartSample8> createState() => _LineChartSample8State();
}
class _LineChartSample8State extends State<LineChartSample8> {
List<Color> gradientColors = const [
Color(0xffEEF3FE),
Color(0xffEEF3FE),
];
bool showAvg = false;
Future<ui.Image> loadImage(String asset) async {
final data = await rootBundle.load(asset);
final codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
final fi = await codec.getNextFrame();
return fi.image;
}
Future<SizedPicture> loadSvg() async {
const rawSvg =
'<svg height="14" width="14" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd" transform="translate(-.000014)"><circle cx="7" cy="7" fill="#495DFF" r="7"/><path d="m7 10.9999976c1.6562389 0 2.99998569-1.34374678 2.99998569-2.99999283s-1.34374679-4.99998808-2.99998569-4.99998808c-1.6562532 0-3 3.34374203-3 4.99998808s1.3437468 2.99999283 3 2.99999283z" fill="#fff" fill-rule="nonzero"/></g></svg>';
final pictureInfo =
await vg.loadPicture(const SvgStringLoader(rawSvg), null);
final sizedPicture = SizedPicture(pictureInfo.picture, 14, 14);
return sizedPicture;
}
@override
Widget build(BuildContext context) {
return FutureBuilder<SizedPicture>(
future: loadSvg(),
builder: (BuildContext context, imageSnapshot) {
if (imageSnapshot.connectionState == ConnectionState.done) {
return Stack(
children: <Widget>[
AspectRatio(
aspectRatio: 1.70,
child: Padding(
padding: const EdgeInsets.only(
right: 18,
left: 12,
top: 24,
bottom: 12,
),
child: LineChart(
mainData(imageSnapshot.data!),
),
),
),
],
);
} else {
return const CircularProgressIndicator();
}
},
);
}
Widget bottomTitleWidgets(double value, TitleMeta meta) {
const style = TextStyle(
fontSize: 10,
color: AppColors.mainTextColor1,
);
return SideTitleWidget(
meta: meta,
child: Text(meta.formattedValue, style: style),
);
}
Widget leftTitleWidgets(double value, TitleMeta meta) {
IconData icon;
Color color;
switch (value.toInt()) {
case 0:
icon = Icons.wb_sunny;
color = AppColors.contentColorYellow;
break;
case 2:
icon = Icons.wine_bar_sharp;
color = AppColors.contentColorRed;
break;
case 4:
icon = Icons.watch_later;
color = AppColors.contentColorGreen;
break;
case 6:
icon = Icons.whatshot;
color = AppColors.contentColorOrange;
break;
default:
throw StateError('Invalid');
}
return SideTitleWidget(
meta: meta,
child: Icon(
icon,
color: color,
size: 32,
),
);
}
LineChartData mainData(SizedPicture sizedPicture) {
return LineChartData(
rangeAnnotations: RangeAnnotations(
verticalRangeAnnotations: [
VerticalRangeAnnotation(
x1: 2,
x2: 5,
color: AppColors.contentColorBlue.withValues(alpha: 0.2),
),
VerticalRangeAnnotation(
x1: 8,
x2: 9,
color: AppColors.contentColorBlue.withValues(alpha: 0.2),
),
],
horizontalRangeAnnotations: [
HorizontalRangeAnnotation(
y1: 2,
y2: 3,
color: AppColors.contentColorGreen.withValues(alpha: 0.3),
),
],
),
// uncomment to see ExtraLines with RangeAnnotations
extraLinesData: ExtraLinesData(
// extraLinesOnTop: true,
horizontalLines: [
HorizontalLine(
y: 5,
color: AppColors.contentColorGreen,
strokeWidth: 2,
dashArray: [5, 10],
label: HorizontalLineLabel(
show: true,
alignment: Alignment.topRight,
padding: const EdgeInsets.only(right: 5, bottom: 5),
style: const TextStyle(
fontSize: 9,
fontWeight: FontWeight.bold,
),
labelResolver: (line) => 'H: ${line.y}',
),
),
],
verticalLines: [
VerticalLine(
x: 5.7,
color: AppColors.contentColorBlue,
strokeWidth: 2,
dashArray: [5, 10],
label: VerticalLineLabel(
show: true,
alignment: Alignment.bottomRight,
padding: const EdgeInsets.only(left: 5, bottom: 5),
style: const TextStyle(
fontSize: 9,
fontWeight: FontWeight.bold,
),
direction: LabelDirection.vertical,
labelResolver: (line) => 'V: ${line.x}',
),
),
VerticalLine(
x: 8.5,
color: Colors.transparent,
sizedPicture: sizedPicture,
),
VerticalLine(
x: 3.5,
color: Colors.transparent,
sizedPicture: sizedPicture,
)
],
),
gridData: const FlGridData(
show: true,
drawVerticalLine: false,
drawHorizontalLine: false,
verticalInterval: 1,
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 30,
getTitlesWidget: bottomTitleWidgets,
interval: 4,
),
),
leftTitles: AxisTitles(
drawBelowEverything: true,
sideTitles: SideTitles(
interval: 2,
showTitles: true,
getTitlesWidget: leftTitleWidgets,
reservedSize: 40,
),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
lineTouchData: LineTouchData(
getTouchLineEnd: (data, index) => double.infinity,
getTouchedSpotIndicator:
(LineChartBarData barData, List<int> spotIndexes) {
return spotIndexes.map((spotIndex) {
return TouchedSpotIndicatorData(
const FlLine(color: AppColors.contentColorOrange, strokeWidth: 3),
FlDotData(
getDotPainter: (spot, percent, barData, index) =>
FlDotCirclePainter(
radius: 8,
color: AppColors.contentColorOrange,
),
),
);
}).toList();
},
touchTooltipData: LineTouchTooltipData(
getTooltipColor: (touchedSpot) => AppColors.contentColorRed,
getTooltipItems: (List<LineBarSpot> touchedSpots) => touchedSpots
.map((LineBarSpot touchedSpot) => LineTooltipItem(
touchedSpot.y.toString(),
const TextStyle(
color: AppColors.contentColorWhite,
fontWeight: FontWeight.bold,
fontSize: 16,
),
))
.toList(),
),
),
borderData: FlBorderData(
show: true,
border: Border.all(
color: AppColors.borderColor,
),
),
minX: 0,
maxX: 11,
minY: 0,
maxY: 6,
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 1),
FlSpot(2, 1),
FlSpot(4.9, 5),
FlSpot(6.8, 5),
FlSpot(7.5, 3.5),
FlSpot.nullSpot,
FlSpot(7.5, 2),
FlSpot(8, 1),
FlSpot(10, 2),
FlSpot(11, 2.5),
],
dashArray: [10, 6],
isCurved: true,
color: AppColors.contentColorRed,
barWidth: 4,
isStrokeCapRound: true,
dotData: const FlDotData(
show: false,
),
),
],
);
}
}

View File

@@ -0,0 +1,154 @@
import 'dart:math';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:flutter/material.dart';
// ignore: must_be_immutable
class LineChartSample9 extends StatelessWidget {
LineChartSample9({super.key});
final spots = List.generate(101, (i) => (i - 50) / 10)
.map((x) => FlSpot(x, cos(x)))
.toList();
Widget bottomTitleWidgets(double value, TitleMeta meta, double chartWidth) {
if (value % 1 != 0) {
return Container();
}
final style = TextStyle(
color: AppColors.contentColorBlue,
fontWeight: FontWeight.bold,
fontSize: min(18, 18 * chartWidth / 300),
);
return SideTitleWidget(
meta: meta,
space: 16,
child: Text(meta.formattedValue, style: style),
);
}
Widget leftTitleWidgets(double value, TitleMeta meta, double chartWidth) {
final style = TextStyle(
color: AppColors.contentColorYellow,
fontWeight: FontWeight.bold,
fontSize: min(18, 18 * chartWidth / 300),
);
return SideTitleWidget(
meta: meta,
space: 16,
child: Text(meta.formattedValue, style: style),
);
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
left: 12,
bottom: 12,
right: 20,
top: 20,
),
child: AspectRatio(
aspectRatio: 1,
child: LayoutBuilder(
builder: (context, constraints) {
return LineChart(
LineChartData(
lineTouchData: LineTouchData(
touchTooltipData: LineTouchTooltipData(
maxContentWidth: 100,
getTooltipColor: (touchedSpot) => Colors.black,
getTooltipItems: (touchedSpots) {
return touchedSpots.map((LineBarSpot touchedSpot) {
final textStyle = TextStyle(
color: touchedSpot.bar.gradient?.colors[0] ??
touchedSpot.bar.color,
fontWeight: FontWeight.bold,
fontSize: 14,
);
return LineTooltipItem(
'${touchedSpot.x}, ${touchedSpot.y.toStringAsFixed(2)}',
textStyle,
);
}).toList();
},
),
handleBuiltInTouches: true,
getTouchLineStart: (data, index) => 0,
),
lineBarsData: [
LineChartBarData(
color: AppColors.contentColorPink,
spots: spots,
isCurved: true,
isStrokeCapRound: true,
barWidth: 3,
belowBarData: BarAreaData(
show: false,
),
dotData: const FlDotData(show: false),
),
],
minY: -1.5,
maxY: 1.5,
titlesData: FlTitlesData(
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) =>
leftTitleWidgets(value, meta, constraints.maxWidth),
reservedSize: 56,
),
drawBelowEverything: true,
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) =>
bottomTitleWidgets(value, meta, constraints.maxWidth),
reservedSize: 36,
interval: 1,
),
drawBelowEverything: true,
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: true,
horizontalInterval: 1.5,
verticalInterval: 5,
checkToShowHorizontalLine: (value) {
return value.toInt() == 0;
},
getDrawingHorizontalLine: (_) => FlLine(
color: AppColors.contentColorBlue.withValues(alpha: 1),
dashArray: [8, 2],
strokeWidth: 0.8,
),
getDrawingVerticalLine: (_) => FlLine(
color: AppColors.contentColorYellow.withValues(alpha: 1),
dashArray: [8, 2],
strokeWidth: 0.8,
),
checkToShowVerticalLine: (value) {
return value.toInt() == 0;
},
),
borderData: FlBorderData(show: false),
),
);
},
),
),
);
}
}

View File

@@ -0,0 +1,173 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/presentation/widgets/indicator.dart';
import 'package:flutter/material.dart';
class PieChartSample1 extends StatefulWidget {
const PieChartSample1({super.key});
@override
State<StatefulWidget> createState() => PieChartSample1State();
}
class PieChartSample1State extends State {
int touchedIndex = -1;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.3,
child: Column(
children: <Widget>[
const SizedBox(
height: 28,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Indicator(
color: AppColors.contentColorBlue,
text: 'One',
isSquare: false,
size: touchedIndex == 0 ? 18 : 16,
textColor: touchedIndex == 0
? AppColors.mainTextColor1
: AppColors.mainTextColor3,
),
Indicator(
color: AppColors.contentColorYellow,
text: 'Two',
isSquare: false,
size: touchedIndex == 1 ? 18 : 16,
textColor: touchedIndex == 1
? AppColors.mainTextColor1
: AppColors.mainTextColor3,
),
Indicator(
color: AppColors.contentColorPink,
text: 'Three',
isSquare: false,
size: touchedIndex == 2 ? 18 : 16,
textColor: touchedIndex == 2
? AppColors.mainTextColor1
: AppColors.mainTextColor3,
),
Indicator(
color: AppColors.contentColorGreen,
text: 'Four',
isSquare: false,
size: touchedIndex == 3 ? 18 : 16,
textColor: touchedIndex == 3
? AppColors.mainTextColor1
: AppColors.mainTextColor3,
),
],
),
const SizedBox(
height: 18,
),
Expanded(
child: AspectRatio(
aspectRatio: 1,
child: PieChart(
PieChartData(
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {
setState(() {
if (!event.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse.touchedSection == null) {
touchedIndex = -1;
return;
}
touchedIndex = pieTouchResponse
.touchedSection!.touchedSectionIndex;
});
},
),
startDegreeOffset: 180,
borderData: FlBorderData(
show: false,
),
sectionsSpace: 1,
centerSpaceRadius: 0,
sections: showingSections(),
),
),
),
),
],
),
);
}
List<PieChartSectionData> showingSections() {
return List.generate(
4,
(i) {
final isTouched = i == touchedIndex;
const color0 = AppColors.contentColorBlue;
const color1 = AppColors.contentColorYellow;
const color2 = AppColors.contentColorPink;
const color3 = AppColors.contentColorGreen;
switch (i) {
case 0:
return PieChartSectionData(
color: color0,
value: 25,
title: '',
radius: 80,
titlePositionPercentageOffset: 0.55,
borderSide: isTouched
? const BorderSide(
color: AppColors.contentColorWhite, width: 6)
: BorderSide(
color: AppColors.contentColorWhite.withValues(alpha: 0)),
);
case 1:
return PieChartSectionData(
color: color1,
value: 25,
title: '',
radius: 65,
titlePositionPercentageOffset: 0.55,
borderSide: isTouched
? const BorderSide(
color: AppColors.contentColorWhite, width: 6)
: BorderSide(
color: AppColors.contentColorWhite.withValues(alpha: 0)),
);
case 2:
return PieChartSectionData(
color: color2,
value: 25,
title: '',
radius: 60,
titlePositionPercentageOffset: 0.6,
borderSide: isTouched
? const BorderSide(
color: AppColors.contentColorWhite, width: 6)
: BorderSide(
color: AppColors.contentColorWhite.withValues(alpha: 0)),
);
case 3:
return PieChartSectionData(
color: color3,
value: 25,
title: '',
radius: 70,
titlePositionPercentageOffset: 0.55,
borderSide: isTouched
? const BorderSide(
color: AppColors.contentColorWhite, width: 6)
: BorderSide(
color: AppColors.contentColorWhite.withValues(alpha: 0)),
);
default:
throw Error();
}
},
);
}
}

View File

@@ -0,0 +1,164 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart_app/presentation/widgets/indicator.dart';
import 'package:flutter/material.dart';
class PieChartSample2 extends StatefulWidget {
const PieChartSample2({super.key});
@override
State<StatefulWidget> createState() => PieChart2State();
}
class PieChart2State extends State {
int touchedIndex = -1;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.3,
child: Row(
children: <Widget>[
const SizedBox(
height: 18,
),
Expanded(
child: AspectRatio(
aspectRatio: 1,
child: PieChart(
PieChartData(
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {
setState(() {
if (!event.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse.touchedSection == null) {
touchedIndex = -1;
return;
}
touchedIndex = pieTouchResponse
.touchedSection!.touchedSectionIndex;
});
},
),
borderData: FlBorderData(
show: false,
),
sectionsSpace: 0,
centerSpaceRadius: 40,
sections: showingSections(),
),
),
),
),
const Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Indicator(
color: AppColors.contentColorBlue,
text: 'First',
isSquare: true,
),
SizedBox(
height: 4,
),
Indicator(
color: AppColors.contentColorYellow,
text: 'Second',
isSquare: true,
),
SizedBox(
height: 4,
),
Indicator(
color: AppColors.contentColorPurple,
text: 'Third',
isSquare: true,
),
SizedBox(
height: 4,
),
Indicator(
color: AppColors.contentColorGreen,
text: 'Fourth',
isSquare: true,
),
SizedBox(
height: 18,
),
],
),
const SizedBox(
width: 28,
),
],
),
);
}
List<PieChartSectionData> showingSections() {
return List.generate(4, (i) {
final isTouched = i == touchedIndex;
final fontSize = isTouched ? 25.0 : 16.0;
final radius = isTouched ? 60.0 : 50.0;
const shadows = [Shadow(color: Colors.black, blurRadius: 2)];
switch (i) {
case 0:
return PieChartSectionData(
color: AppColors.contentColorBlue,
value: 40,
title: '40%',
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: AppColors.mainTextColor1,
shadows: shadows,
),
);
case 1:
return PieChartSectionData(
color: AppColors.contentColorYellow,
value: 30,
title: '30%',
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: AppColors.mainTextColor1,
shadows: shadows,
),
);
case 2:
return PieChartSectionData(
color: AppColors.contentColorPurple,
value: 15,
title: '15%',
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: AppColors.mainTextColor1,
shadows: shadows,
),
);
case 3:
return PieChartSectionData(
color: AppColors.contentColorGreen,
value: 15,
title: '15%',
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: AppColors.mainTextColor1,
shadows: shadows,
),
);
default:
throw Error();
}
});
}
}

View File

@@ -0,0 +1,181 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class PieChartSample3 extends StatefulWidget {
const PieChartSample3({super.key});
@override
State<StatefulWidget> createState() => PieChartSample3State();
}
class PieChartSample3State extends State {
int touchedIndex = 0;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.3,
child: AspectRatio(
aspectRatio: 1,
child: PieChart(
PieChartData(
pieTouchData: PieTouchData(
touchCallback: (FlTouchEvent event, pieTouchResponse) {
setState(() {
if (!event.isInterestedForInteractions ||
pieTouchResponse == null ||
pieTouchResponse.touchedSection == null) {
touchedIndex = -1;
return;
}
touchedIndex =
pieTouchResponse.touchedSection!.touchedSectionIndex;
});
},
),
borderData: FlBorderData(
show: false,
),
sectionsSpace: 0,
centerSpaceRadius: 0,
sections: showingSections(),
),
),
),
);
}
List<PieChartSectionData> showingSections() {
return List.generate(4, (i) {
final isTouched = i == touchedIndex;
final fontSize = isTouched ? 20.0 : 16.0;
final radius = isTouched ? 110.0 : 100.0;
final widgetSize = isTouched ? 55.0 : 40.0;
const shadows = [Shadow(color: Colors.black, blurRadius: 2)];
switch (i) {
case 0:
return PieChartSectionData(
color: AppColors.contentColorBlue,
value: 40,
title: '40%',
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: const Color(0xffffffff),
shadows: shadows,
),
badgeWidget: _Badge(
'assets/icons/ophthalmology-svgrepo-com.svg',
size: widgetSize,
borderColor: AppColors.contentColorBlack,
),
badgePositionPercentageOffset: .98,
);
case 1:
return PieChartSectionData(
color: AppColors.contentColorYellow,
value: 30,
title: '30%',
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: const Color(0xffffffff),
shadows: shadows,
),
badgeWidget: _Badge(
'assets/icons/librarian-svgrepo-com.svg',
size: widgetSize,
borderColor: AppColors.contentColorBlack,
),
badgePositionPercentageOffset: .98,
);
case 2:
return PieChartSectionData(
color: AppColors.contentColorPurple,
value: 16,
title: '16%',
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: const Color(0xffffffff),
shadows: shadows,
),
badgeWidget: _Badge(
'assets/icons/fitness-svgrepo-com.svg',
size: widgetSize,
borderColor: AppColors.contentColorBlack,
),
badgePositionPercentageOffset: .98,
);
case 3:
return PieChartSectionData(
color: AppColors.contentColorGreen,
value: 15,
title: '15%',
radius: radius,
titleStyle: TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
color: const Color(0xffffffff),
shadows: shadows,
),
badgeWidget: _Badge(
'assets/icons/worker-svgrepo-com.svg',
size: widgetSize,
borderColor: AppColors.contentColorBlack,
),
badgePositionPercentageOffset: .98,
);
default:
throw Exception('Oh no');
}
});
}
}
class _Badge extends StatelessWidget {
const _Badge(
this.svgAsset, {
required this.size,
required this.borderColor,
});
final String svgAsset;
final double size;
final Color borderColor;
@override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: PieChart.defaultDuration,
width: size,
height: size,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: borderColor,
width: 2,
),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withValues(alpha: .5),
offset: const Offset(3, 3),
blurRadius: 3,
),
],
),
padding: EdgeInsets.all(size * .15),
child: Center(
child: SvgPicture.asset(
svgAsset,
),
),
);
}
}

View File

@@ -0,0 +1,285 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/util/extensions/color_extensions.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class RadarChartSample1 extends StatefulWidget {
RadarChartSample1({super.key});
final gridColor = AppColors.contentColorPurple.lighten(80);
final titleColor = AppColors.contentColorPurple.lighten(80);
final fashionColor = AppColors.contentColorRed;
final artColor = AppColors.contentColorCyan;
final boxingColor = AppColors.contentColorGreen;
final entertainmentColor = AppColors.contentColorWhite;
final offRoadColor = AppColors.contentColorYellow;
@override
State<RadarChartSample1> createState() => _RadarChartSample1State();
}
class _RadarChartSample1State extends State<RadarChartSample1> {
int selectedDataSetIndex = -1;
double angleValue = 0;
bool relativeAngleMode = true;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Title configuration',
style: TextStyle(
color: AppColors.mainTextColor2,
),
),
Row(
children: [
const Text(
'Angle',
style: TextStyle(
color: AppColors.mainTextColor2,
),
),
Slider(
value: angleValue,
max: 360,
onChanged: (double value) => setState(() => angleValue = value),
),
],
),
Row(
children: [
Checkbox(
value: relativeAngleMode,
onChanged: (v) => setState(() => relativeAngleMode = v!),
),
const Text('Relative'),
],
),
GestureDetector(
onTap: () {
setState(() {
selectedDataSetIndex = -1;
});
},
child: Text(
'Categories'.toUpperCase(),
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.w300,
color: AppColors.mainTextColor1,
),
),
),
const SizedBox(height: 4),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: rawDataSets()
.asMap()
.map((index, value) {
final isSelected = index == selectedDataSetIndex;
return MapEntry(
index,
GestureDetector(
onTap: () {
setState(() {
selectedDataSetIndex = index;
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.symmetric(vertical: 2),
height: 26,
decoration: BoxDecoration(
color: isSelected
? AppColors.pageBackground
: Colors.transparent,
borderRadius: BorderRadius.circular(46),
),
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 6,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedContainer(
duration: const Duration(milliseconds: 400),
curve: Curves.easeInToLinear,
padding: EdgeInsets.all(isSelected ? 8 : 6),
decoration: BoxDecoration(
color: value.color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
AnimatedDefaultTextStyle(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInToLinear,
style: TextStyle(
color:
isSelected ? value.color : widget.gridColor,
),
child: Text(value.title),
),
],
),
),
),
);
})
.values
.toList(),
),
AspectRatio(
aspectRatio: 1.3,
child: RadarChart(
RadarChartData(
radarTouchData: RadarTouchData(
touchCallback: (FlTouchEvent event, response) {
if (!event.isInterestedForInteractions) {
setState(() {
selectedDataSetIndex = -1;
});
return;
}
setState(() {
selectedDataSetIndex =
response?.touchedSpot?.touchedDataSetIndex ?? -1;
});
},
),
dataSets: showingDataSets(),
radarBackgroundColor: Colors.transparent,
borderData: FlBorderData(show: false),
radarBorderData: const BorderSide(color: Colors.transparent),
titlePositionPercentageOffset: 0.2,
titleTextStyle:
TextStyle(color: widget.titleColor, fontSize: 14),
getTitle: (index, angle) {
final usedAngle =
relativeAngleMode ? angle + angleValue : angleValue;
switch (index) {
case 0:
return RadarChartTitle(
text: 'Mobile or Tablet',
angle: usedAngle,
);
case 2:
return RadarChartTitle(
text: 'Desktop',
angle: usedAngle,
);
case 1:
return RadarChartTitle(text: 'TV', angle: usedAngle);
default:
return const RadarChartTitle(text: '');
}
},
tickCount: 1,
ticksTextStyle:
const TextStyle(color: Colors.transparent, fontSize: 10),
tickBorderData: const BorderSide(color: Colors.transparent),
gridBorderData: BorderSide(color: widget.gridColor, width: 2),
),
duration: const Duration(milliseconds: 400),
),
),
],
),
);
}
List<RadarDataSet> showingDataSets() {
return rawDataSets().asMap().entries.map((entry) {
final index = entry.key;
final rawDataSet = entry.value;
final isSelected = index == selectedDataSetIndex
? true
: selectedDataSetIndex == -1
? true
: false;
return RadarDataSet(
fillColor: isSelected
? rawDataSet.color.withValues(alpha: 0.2)
: rawDataSet.color.withValues(alpha: 0.05),
borderColor: isSelected
? rawDataSet.color
: rawDataSet.color.withValues(alpha: 0.25),
entryRadius: isSelected ? 3 : 2,
dataEntries:
rawDataSet.values.map((e) => RadarEntry(value: e)).toList(),
borderWidth: isSelected ? 2.3 : 2,
);
}).toList();
}
List<RawDataSet> rawDataSets() {
return [
RawDataSet(
title: 'Fashion',
color: widget.fashionColor,
values: [
300,
50,
250,
],
),
RawDataSet(
title: 'Art & Tech',
color: widget.artColor,
values: [
250,
100,
200,
],
),
RawDataSet(
title: 'Entertainment',
color: widget.entertainmentColor,
values: [
200,
150,
50,
],
),
RawDataSet(
title: 'Off-road Vehicle',
color: widget.offRoadColor,
values: [
150,
200,
150,
],
),
RawDataSet(
title: 'Boxing',
color: widget.boxingColor,
values: [
100,
250,
100,
],
),
];
}
}
class RawDataSet {
RawDataSet({
required this.title,
required this.color,
required this.values,
});
final String title;
final Color color;
final List<double> values;
}

View File

@@ -0,0 +1,513 @@
import 'dart:math';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class ScatterChartSample1 extends StatefulWidget {
ScatterChartSample1({super.key});
final blue1 = AppColors.contentColorBlue.withValues(alpha: 0.5);
final blue2 = AppColors.contentColorBlue;
@override
State<StatefulWidget> createState() => ScatterChartSample1State();
}
class ScatterChartSample1State extends State<ScatterChartSample1> {
final maxX = 50.0;
final maxY = 50.0;
final radius = 8.0;
bool showFlutter = true;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
setState(() {
showFlutter = !showFlutter;
});
},
child: AspectRatio(
aspectRatio: 1,
child: ScatterChart(
ScatterChartData(
scatterSpots: showFlutter ? flutterLogoData() : randomData(),
minX: 0,
maxX: maxX,
minY: 0,
maxY: maxY,
borderData: FlBorderData(
show: false,
),
gridData: const FlGridData(
show: false,
),
titlesData: const FlTitlesData(
show: false,
),
scatterTouchData: ScatterTouchData(
enabled: false,
),
),
duration: const Duration(milliseconds: 600),
curve: Curves.fastOutSlowIn,
),
),
);
}
List<ScatterSpot> flutterLogoData() {
return [
/// section 1
ScatterSpot(
20,
14.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
20,
14.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
22,
16.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
24,
18.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
22,
12.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
24,
14.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
26,
16.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
24,
10.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
26,
12.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
28,
14.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
26,
8.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
28,
10.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
30,
12.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
28,
6.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
30,
8.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
32,
10.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
30,
4.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
32,
6.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
34,
8.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
34,
4.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
36,
6.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
ScatterSpot(
38,
4.5,
dotPainter: FlDotCirclePainter(color: widget.blue1, radius: radius),
),
/// section 2
ScatterSpot(
20,
14.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
22,
12.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
24,
10.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
22,
16.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
24,
14.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
26,
12.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
24,
18.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
26,
16.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
28,
14.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
26,
20.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
28,
18.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
30,
16.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
28,
22.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
30,
20.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
32,
18.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
30,
24.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
32,
22.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
34,
20.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
34,
24.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
36,
22.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
38,
24.5,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
/// section 3
ScatterSpot(
10,
25,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
12,
23,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
14,
21,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
12,
27,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
14,
25,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
16,
23,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
14,
29,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
16,
27,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
18,
25,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
16,
31,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
18,
29,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
20,
27,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
18,
33,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
20,
31,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
22,
29,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
20,
35,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
22,
33,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
24,
31,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
22,
37,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
24,
35,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
26,
33,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
24,
39,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
26,
37,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
28,
35,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
26,
41,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
28,
39,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
30,
37,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
28,
43,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
30,
41,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
32,
39,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
30,
45,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
32,
43,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
34,
41,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
34,
45,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
36,
43,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
ScatterSpot(
38,
45,
dotPainter: FlDotCirclePainter(color: widget.blue2, radius: radius),
),
];
}
List<ScatterSpot> randomData() {
const blue1Count = 21;
const blue2Count = 57;
return List.generate(blue1Count + blue2Count, (i) {
Color color;
if (i < blue1Count) {
color = widget.blue1;
} else {
color = widget.blue2;
}
return ScatterSpot(
(Random().nextDouble() * (maxX - 8)) + 4,
(Random().nextDouble() * (maxY - 8)) + 4,
dotPainter: FlDotCirclePainter(
color: color,
radius: (Random().nextDouble() * 16) + 4,
),
);
});
}
}

View File

@@ -0,0 +1,238 @@
import 'dart:math';
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class ScatterChartSample2 extends StatefulWidget {
const ScatterChartSample2({super.key});
@override
State<StatefulWidget> createState() => _ScatterChartSample2State();
}
class _ScatterChartSample2State extends State {
int touchedIndex = -1;
Color greyColor = Colors.grey;
final _availableColors = [
AppColors.contentColorGreen,
AppColors.contentColorYellow,
AppColors.contentColorPink,
AppColors.contentColorOrange,
AppColors.contentColorPurple,
AppColors.contentColorBlue,
AppColors.contentColorRed,
AppColors.contentColorCyan,
AppColors.contentColorBlue,
AppColors.contentColorGreen,
AppColors.contentColorPink,
];
List<int> selectedSpots = [];
PainterType _currentPaintType = PainterType.circle;
static FlDotPainter _getPaint(PainterType type, double size, Color color) {
switch (type) {
case PainterType.circle:
return FlDotCirclePainter(
color: color,
radius: size,
);
case PainterType.square:
return FlDotSquarePainter(
color: color,
size: size * 2,
strokeWidth: 0,
);
case PainterType.cross:
return FlDotCrossPainter(
color: color,
size: size * 2,
width: max(size / 5, 2),
);
}
}
@override
Widget build(BuildContext context) {
// (x, y, size)
final data = [
(4.0, 4.0, 4.0),
(2.0, 5.0, 12.0),
(4.0, 5.0, 8.0),
(8.0, 6.0, 20.0),
(5.0, 7.0, 14.0),
(7.0, 2.0, 18.0),
(3.0, 2.0, 36.0),
(2.0, 8.0, 22.0),
(8.0, 8.0, 32.0),
(5.0, 2.5, 24.0),
(3.0, 7.0, 18.0),
];
return AspectRatio(
aspectRatio: 1,
child: Stack(
children: [
ScatterChart(
ScatterChartData(
scatterSpots: data.asMap().entries.map((e) {
final index = e.key;
final (double x, double y, double size) = e.value;
return ScatterSpot(
x,
y,
dotPainter: _getPaint(
_currentPaintType,
size,
selectedSpots.contains(index)
? _availableColors[index % _availableColors.length]
: AppColors.contentColorWhite.withValues(alpha: 0.5),
),
);
}).toList(),
minX: 0,
maxX: 10,
minY: 0,
maxY: 10,
borderData: FlBorderData(
show: false,
),
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
checkToShowHorizontalLine: (value) => true,
getDrawingHorizontalLine: (value) => const FlLine(
color: AppColors.gridLinesColor,
),
drawVerticalLine: true,
checkToShowVerticalLine: (value) => true,
getDrawingVerticalLine: (value) => const FlLine(
color: AppColors.gridLinesColor,
),
),
titlesData: const FlTitlesData(
show: false,
),
showingTooltipIndicators: selectedSpots,
scatterTouchData: ScatterTouchData(
enabled: true,
handleBuiltInTouches: false,
mouseCursorResolver:
(FlTouchEvent touchEvent, ScatterTouchResponse? response) {
return response == null || response.touchedSpot == null
? MouseCursor.defer
: SystemMouseCursors.click;
},
touchTooltipData: ScatterTouchTooltipData(
getTooltipColor: (ScatterSpot touchedBarSpot) {
return touchedBarSpot.dotPainter.mainColor;
},
getTooltipItems: (ScatterSpot touchedBarSpot) {
final bool isBgDark =
switch ((touchedBarSpot.x, touchedBarSpot.y)) {
(4.0, 4.0) => false,
(2.0, 5.0) => false,
(4.0, 5.0) => true,
(8.0, 6.0) => true,
(5.0, 7.0) => true,
(7.0, 2.0) => true,
(3.0, 2.0) => true,
(2.0, 8.0) => false,
(8.0, 8.0) => true,
(5.0, 2.5) => false,
(3.0, 7.0) => true,
_ => false,
};
final color1 = isBgDark ? Colors.grey[100] : Colors.black87;
final color2 = isBgDark ? Colors.white : Colors.black;
return ScatterTooltipItem(
'X: ',
textStyle: TextStyle(
height: 1.2,
color: color1,
fontStyle: FontStyle.italic,
),
bottomMargin: 10,
children: [
TextSpan(
text: '${touchedBarSpot.x.toInt()} \n',
style: TextStyle(
color: color2,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: 'Y: ',
style: TextStyle(
height: 1.2,
color: color1,
fontStyle: FontStyle.italic,
),
),
TextSpan(
text: touchedBarSpot.y.toInt().toString(),
style: TextStyle(
color: color2,
fontStyle: FontStyle.normal,
fontWeight: FontWeight.bold,
),
),
],
);
},
),
touchCallback:
(FlTouchEvent event, ScatterTouchResponse? touchResponse) {
if (touchResponse == null ||
touchResponse.touchedSpot == null) {
return;
}
if (event is FlTapUpEvent) {
final sectionIndex = touchResponse.touchedSpot!.spotIndex;
setState(() {
if (selectedSpots.contains(sectionIndex)) {
selectedSpots.remove(sectionIndex);
} else {
selectedSpots.add(sectionIndex);
}
});
}
},
),
),
),
Align(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: DropdownButton(
value: _currentPaintType,
items: PainterType.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.name),
))
.toList(),
onChanged: (PainterType? value) {
setState(() {
_currentPaintType = value!;
});
},
),
),
),
],
),
);
}
}
enum PainterType {
circle,
square,
cross,
}

View File

@@ -0,0 +1,54 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/presentation/samples/chart_sample.dart';
import 'package:fl_chart_app/util/app_utils.dart';
import 'package:flutter/material.dart';
class ChartHolder extends StatelessWidget {
final ChartSample chartSample;
const ChartHolder({
super.key,
required this.chartSample,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const SizedBox(width: 6),
Text(
chartSample.name,
style: const TextStyle(
color: AppColors.primary,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Expanded(child: Container()),
IconButton(
onPressed: () => AppUtils().tryToLaunchUrl(chartSample.url),
icon: const Icon(
Icons.code,
color: AppColors.primary,
),
tooltip: 'Source code',
),
],
),
const SizedBox(height: 2),
Container(
decoration: const BoxDecoration(
color: AppColors.itemsBackground,
borderRadius:
BorderRadius.all(Radius.circular(AppDimens.defaultRadius)),
),
child: chartSample.builder(context),
),
],
);
}
}

View File

@@ -0,0 +1,64 @@
import 'package:fl_chart_app/presentation/resources/app_colors.dart';
import 'package:flutter/material.dart';
class DownloadNativeAppButton extends StatelessWidget {
const DownloadNativeAppButton({
super.key,
required this.onClose,
required this.onDownload,
});
final VoidCallback onClose;
final VoidCallback onDownload;
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.contentColorYellow,
borderRadius: BorderRadius.circular(999),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(999),
onTap: onDownload,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 4,
horizontal: 8,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 4),
const Icon(
size: 28,
Icons.download_for_offline,
color: AppColors.contentColorBlack,
),
const SizedBox(width: 4),
const Text(
'Download Native App',
style: TextStyle(
color: AppColors.contentColorBlack,
fontSize: 16,
fontWeight: FontWeight.w900,
),
),
IconButton(
onPressed: onClose,
icon: const Icon(
size: 16,
Icons.close,
color: AppColors.contentColorBlack,
),
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
class Indicator extends StatelessWidget {
const Indicator({
super.key,
required this.color,
required this.text,
required this.isSquare,
this.size = 16,
this.textColor,
});
final Color color;
final String text;
final bool isSquare;
final double size;
final Color? textColor;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: isSquare ? BoxShape.rectangle : BoxShape.circle,
color: color,
),
),
const SizedBox(
width: 4,
),
Text(
text,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: textColor,
),
)
],
);
}
}

View File

@@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
class LegendWidget extends StatelessWidget {
const LegendWidget({
super.key,
required this.name,
required this.color,
});
final String name;
final Color color;
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
),
const SizedBox(width: 6),
Text(
name,
style: const TextStyle(
color: Color(0xff757391),
fontSize: 12,
),
),
],
);
}
}
class LegendsListWidget extends StatelessWidget {
const LegendsListWidget({
super.key,
required this.legends,
});
final List<Legend> legends;
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 16,
children: legends
.map(
(e) => LegendWidget(
name: e.name,
color: e.color,
),
)
.toList(),
);
}
}
class Legend {
Legend(this.name, this.color);
final String name;
final Color color;
}

23
example/lib/urls.dart Normal file
View File

@@ -0,0 +1,23 @@
import 'package:fl_chart_app/util/app_helper.dart';
class Urls {
static const flChartUrl = 'https://flchart.dev';
static const flChartGithubUrl = 'https://github.com/imaNNeo/fl_chart';
static String get aboutUrl => '$flChartUrl/about';
static String get downloadUrl => '$flChartUrl/download';
static String getChartSourceCodeUrl(ChartType chartType, int sampleNumber) {
final chartDir = chartType.name.toLowerCase();
return 'https://github.com/imaNNeo/fl_chart/blob/main/example/lib/presentation/samples/$chartDir/${chartDir}_chart_sample$sampleNumber.dart';
}
static String getChartDocumentationUrl(ChartType chartType) {
final chartDir = chartType.name.toLowerCase();
return 'https://github.com/imaNNeo/fl_chart/blob/main/repo_files/documentations/${chartDir}_chart.md';
}
static String getVersionReleaseUrl(String version) =>
'$flChartGithubUrl/releases/tag/$version';
}

View File

@@ -0,0 +1,21 @@
import 'package:fl_chart_app/presentation/resources/app_resources.dart';
import 'package:fl_chart_app/urls.dart';
enum ChartType { line, bar, pie, scatter, radar, candlestick }
extension ChartTypeExtension on ChartType {
String get displayName => '$simpleName Chart';
String get simpleName => switch (this) {
ChartType.line => 'Line',
ChartType.bar => 'Bar',
ChartType.pie => 'Pie',
ChartType.scatter => 'Scatter',
ChartType.radar => 'Radar',
ChartType.candlestick => 'Candlestick',
};
String get documentationUrl => Urls.getChartDocumentationUrl(this);
String get assetIcon => AppAssets.getChartIcon(this);
}

View File

@@ -0,0 +1,28 @@
import 'dart:math' as math;
import 'package:url_launcher/url_launcher.dart';
class AppUtils {
factory AppUtils() {
return _singleton;
}
AppUtils._internal();
static final AppUtils _singleton = AppUtils._internal();
double degreeToRadian(double degree) {
return degree * math.pi / 180;
}
double radianToDegree(double radian) {
return radian * 180 / math.pi;
}
Future<bool> tryToLaunchUrl(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
return await launchUrl(uri);
}
return false;
}
}

View File

@@ -0,0 +1,45 @@
class CsvParser {
static List<List<String>> parse(String rawCsvData) {
final lines =
rawCsvData.split('\n').where((line) => line.isNotEmpty).toList();
final headers = _parseCsvLine(lines.first);
return [
headers,
...lines.skip(1).map((line) => _parseCsvLine(line)),
];
}
static List<String> _parseCsvLine(String line) {
final values = <String>[];
final buffer = StringBuffer();
bool insideQuotes = false;
for (int i = 0; i < line.length; i++) {
final char = line[i];
if (char == '"') {
if (insideQuotes && i + 1 < line.length && line[i + 1] == '"') {
// Handle escaped quotes
buffer.write('"');
i++; // Skip the next quote
} else {
// Toggle the insideQuotes flag
insideQuotes = !insideQuotes;
}
} else if (char == ',' && !insideQuotes) {
// End of value
values.add(buffer.toString());
buffer.clear();
} else {
// Normal character
buffer.write(char);
}
}
// Add the last value
values.add(buffer.toString());
return values;
}
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:universal_platform/universal_platform.dart';
enum FormFactorType { monitor, smallPhone, largePhone, tablet }
// Copied from https://github.com/gskinnerTeam/flutter-folio/blob/main/lib/_utils/device_info.dart
class DeviceOS {
// Syntax sugar, proxy the UniversalPlatform methods so our views can reference a single class
static bool isIOS = UniversalPlatform.isIOS;
static bool isAndroid = UniversalPlatform.isAndroid;
static bool isMacOS = UniversalPlatform.isMacOS;
static bool isLinux = UniversalPlatform.isLinux;
static bool isWindows = UniversalPlatform.isWindows;
// Higher level device class abstractions (more syntax sugar for the views)
static bool isWeb = kIsWeb;
static bool get isDesktop => isWindows || isMacOS || isLinux;
static bool get isMobile => isAndroid || isIOS;
static bool get isDesktopOrWeb => isDesktop || isWeb;
static bool get isMobileOrWeb => isMobile || isWeb;
}
class DeviceScreen {
// Get the device form factor as best we can.
// Otherwise we will use the screen size to determine which class we fall into.
static FormFactorType get(BuildContext context) {
var shortestSide = MediaQuery.of(context).size.shortestSide;
if (shortestSide <= 300) return FormFactorType.smallPhone;
if (shortestSide <= 600) return FormFactorType.largePhone;
if (shortestSide <= 900) return FormFactorType.tablet;
return FormFactorType.monitor;
}
// Shortcuts for various mobile device types
static bool isPhone(BuildContext context) =>
isSmallPhone(context) || isLargePhone(context);
static bool isTablet(BuildContext context) =>
get(context) == FormFactorType.tablet;
static bool isMonitor(BuildContext context) =>
get(context) == FormFactorType.monitor;
static bool isSmallPhone(BuildContext context) =>
get(context) == FormFactorType.smallPhone;
static bool isLargePhone(BuildContext context) =>
get(context) == FormFactorType.largePhone;
}

View File

@@ -0,0 +1,43 @@
import 'dart:ui';
extension ColorExtension on Color {
/// Convert the color to a darken color based on the [percent]
Color darken([int percent = 40]) {
assert(1 <= percent && percent <= 100);
final value = 1 - percent / 100;
return Color.fromARGB(
_floatToInt8(a),
(_floatToInt8(r) * value).round(),
(_floatToInt8(g) * value).round(),
(_floatToInt8(b) * value).round(),
);
}
Color lighten([int percent = 40]) {
assert(1 <= percent && percent <= 100);
final value = percent / 100;
return Color.fromARGB(
_floatToInt8(a),
(_floatToInt8(r) + ((255 - _floatToInt8(r)) * value)).round(),
(_floatToInt8(g) + ((255 - _floatToInt8(g)) * value)).round(),
(_floatToInt8(b) + ((255 - _floatToInt8(b)) * value)).round(),
);
}
Color avg(Color other) {
final red = (_floatToInt8(r) + _floatToInt8(other.r)) ~/ 2;
final green = (_floatToInt8(g) + _floatToInt8(other.g)) ~/ 2;
final blue = (_floatToInt8(b) + _floatToInt8(other.b)) ~/ 2;
final alpha = (_floatToInt8(a) + _floatToInt8(other.a)) ~/ 2;
return Color.fromARGB(alpha, red, green, blue);
}
// Int color components were deprecated in Flutter 3.27.0.
// This method is used to convert the new double color components to the
// old int color components.
//
// Taken from the Color class.
int _floatToInt8(double x) {
return (x * 255.0).round() & 0xff;
}
}

View File

@@ -0,0 +1,3 @@
extension IterableToMapExtension<K, V> on Iterable<MapEntry<K, V>> {
Map<K, V> get asMap => Map.fromEntries(this);
}

View File

@@ -0,0 +1,3 @@
extension ListToMapExtension<K, V> on List<MapEntry<K, V>> {
Map<K, V> get asMap => Map.fromEntries(this);
}