diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 5fe3c92..32ade45 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,6 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + namespace "com.example.example" compileSdkVersion flutter.compileSdkVersion compileOptions { diff --git a/example/android/build.gradle b/example/android/build.gradle index 24047dc..fb84b31 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.9.10' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:8.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58a..fb5eb59 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip diff --git a/example/lib/main.dart b/example/lib/main.dart index b4583f6..f4c6f89 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'pages/heatmap_calendar_example.dart'; import 'pages/heatmap_example.dart'; @@ -14,6 +15,11 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Heatmap Demo', + localizationsDelegates: GlobalMaterialLocalizations.delegates, + supportedLocales: const [ + Locale('en', ''), + Locale('fr', ''), + ], theme: ThemeData( primarySwatch: Colors.blue, ), diff --git a/example/lib/pages/heatmap_calendar_example.dart b/example/lib/pages/heatmap_calendar_example.dart index d0e91af..fe66ec7 100644 --- a/example/lib/pages/heatmap_calendar_example.dart +++ b/example/lib/pages/heatmap_calendar_example.dart @@ -49,61 +49,64 @@ class _HeatMapCalendarExample extends State { title: const Text('Heatmap Calendar'), ), body: SafeArea( - child: Column( - children: [ - Card( - margin: const EdgeInsets.all(20), - elevation: 20, - child: Padding( - padding: const EdgeInsets.all(20), + child: SingleChildScrollView( + child: Column( + children: [ + Card( + margin: const EdgeInsets.all(20), + elevation: 20, + child: Padding( + padding: const EdgeInsets.all(20), - // HeatMapCalendar - child: HeatMapCalendar( - flexible: true, - datasets: heatMapDatasets, - colorMode: - isOpacityMode ? ColorMode.opacity : ColorMode.color, - colorsets: const { - 1: Colors.red, - 3: Colors.orange, - 5: Colors.yellow, - 7: Colors.green, - 9: Colors.blue, - 11: Colors.indigo, - 13: Colors.purple, - }, + // HeatMapCalendar + child: HeatMapCalendar( + flexible: true, + weekStartsWith: 1, + datasets: heatMapDatasets, + colorMode: + isOpacityMode ? ColorMode.opacity : ColorMode.color, + colorsets: const { + 1: Colors.red, + 3: Colors.orange, + 5: Colors.yellow, + 7: Colors.green, + 9: Colors.blue, + 11: Colors.indigo, + 13: Colors.purple, + }, + ), ), ), - ), - _textField('YYYYMMDD', dateController), - _textField('Heat Level', heatLevelController), - ElevatedButton( - child: const Text('COMMIT'), - onPressed: () { - setState(() { - heatMapDatasets[DateTime.parse(dateController.text)] = - int.parse(heatLevelController.text); - }); - }, - ), + _textField('YYYYMMDD', dateController), + _textField('Heat Level', heatLevelController), + ElevatedButton( + child: const Text('COMMIT'), + onPressed: () { + setState(() { + heatMapDatasets[DateTime.parse(dateController.text)] = + int.parse(heatLevelController.text); + }); + }, + ), - // ColorMode/OpacityMode Switch. - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Color Mode'), - CupertinoSwitch( - value: isOpacityMode, - onChanged: (value) { - setState(() { - isOpacityMode = value; - }); - }, - ), - const Text('Opacity Mode'), - ], - ), - ], + // ColorMode/OpacityMode Switch. + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Color Mode'), + CupertinoSwitch( + value: isOpacityMode, + onChanged: (value) { + setState(() { + isOpacityMode = value; + }); + }, + ), + const Text('Opacity Mode'), + ], + ), + ], + ), ), ), backgroundColor: Colors.white, diff --git a/example/lib/pages/heatmap_example.dart b/example/lib/pages/heatmap_example.dart index cda9da9..7a25bb3 100644 --- a/example/lib/pages/heatmap_example.dart +++ b/example/lib/pages/heatmap_example.dart @@ -49,63 +49,67 @@ class _HeatMapExample extends State { title: const Text('Heatmap'), ), body: SafeArea( - child: Column( - children: [ - Card( - margin: const EdgeInsets.all(20), - elevation: 20, - child: Padding( - padding: const EdgeInsets.all(20), - child: HeatMap( - scrollable: true, - colorMode: - isOpacityMode ? ColorMode.opacity : ColorMode.color, - datasets: heatMapDatasets, - colorsets: const { - 1: Colors.red, - 3: Colors.orange, - 5: Colors.yellow, - 7: Colors.green, - 9: Colors.blue, - 11: Colors.indigo, - 13: Colors.purple, - }, - onClick: (value) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(value.toString()))); - }, + child: SingleChildScrollView( + child: Column( + children: [ + Card( + margin: const EdgeInsets.all(20), + elevation: 20, + child: Padding( + padding: const EdgeInsets.all(20), + child: HeatMap( + scrollable: true, + showText: true, + weekStartsWith: 6, + colorMode: + isOpacityMode ? ColorMode.opacity : ColorMode.color, + datasets: heatMapDatasets, + colorsets: const { + 1: Colors.red, + 3: Colors.orange, + 5: Colors.yellow, + 7: Colors.green, + 9: Colors.blue, + 11: Colors.indigo, + 13: Colors.purple, + }, + onClick: (value) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(value.toString()))); + }, + ), ), ), - ), - _textField('YYYYMMDD', dateController), - _textField('Heat Level', heatLevelController), - ElevatedButton( - child: const Text('COMMIT'), - onPressed: () { - setState(() { - heatMapDatasets[DateTime.parse(dateController.text)] = - int.parse(heatLevelController.text); - }); - }, - ), + _textField('YYYYMMDD', dateController), + _textField('Heat Level', heatLevelController), + ElevatedButton( + child: const Text('COMMIT'), + onPressed: () { + setState(() { + heatMapDatasets[DateTime.parse(dateController.text)] = + int.parse(heatLevelController.text); + }); + }, + ), - // ColorMode/OpacityMode Switch. - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Color Mode'), - CupertinoSwitch( - value: isOpacityMode, - onChanged: (value) { - setState(() { - isOpacityMode = value; - }); - }, - ), - const Text('Opacity Mode'), - ], - ), - ], + // ColorMode/OpacityMode Switch. + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Color Mode'), + CupertinoSwitch( + value: isOpacityMode, + onChanged: (value) { + setState(() { + isOpacityMode = value; + }); + }, + ), + const Text('Opacity Mode'), + ], + ), + ], + ), ), ), backgroundColor: Colors.white, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 31fa076..f7c2fac 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=2.15.1 <3.0.0" + sdk: ">=2.15.1 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -29,12 +29,14 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter flutter_heatmap_calendar: path: ../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + cupertino_icons: dev_dependencies: flutter_test: @@ -45,7 +47,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^1.0.0 + flutter_lints: # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/lib/src/heatmap.dart b/lib/src/heatmap.dart index e884860..4bfabf1 100644 --- a/lib/src/heatmap.dart +++ b/lib/src/heatmap.dart @@ -24,9 +24,12 @@ class HeatMap extends StatefulWidget { /// The color value of every block's default color. final Color? defaultColor; - /// The text color value of every blocks. + /// The text color value of every blocks into the heatmap. final Color? textColor; + /// The text color value for labels (months & week days). + final Color? labelColor; + /// The double value of every block's size. final double? size; @@ -87,6 +90,15 @@ class HeatMap extends StatefulWidget { /// The double value of [HeatMapColorTip]'s tip container's size. final double? colorTipSize; + /// Which day the week should start? + /// weekStartsWith = 1 for Monday, ..., weekStartsWith = 7 for Sunday. + /// Default to 7 (the week starts wih Sunday). + final int weekStartsWith; + + final bool? showWeekText; + + final bool? showMonthText; + const HeatMap({ Key? key, required this.colorsets, @@ -94,6 +106,7 @@ class HeatMap extends StatefulWidget { this.startDate, this.endDate, this.textColor, + this.labelColor, this.size = 20, this.fontSize, this.onClick, @@ -107,6 +120,9 @@ class HeatMap extends StatefulWidget { this.colorTipHelper, this.colorTipCount, this.colorTipSize, + this.weekStartsWith = 7, + this.showWeekText = true, + this.showMonthText = true, }) : super(key: key); @override @@ -133,19 +149,22 @@ class _HeatMap extends State { // Heatmap Widget. _scrollableHeatMap(HeatMapPage( endDate: widget.endDate ?? DateTime.now(), - startDate: widget.startDate ?? - DateUtil.oneYearBefore(widget.endDate ?? DateTime.now()), + startDate: widget.startDate ?? DateUtil.oneYearBefore(widget.endDate ?? DateTime.now()), colorMode: widget.colorMode, size: widget.size, fontSize: widget.fontSize, datasets: widget.datasets, defaultColor: widget.defaultColor, textColor: widget.textColor, + labelColor: widget.labelColor, colorsets: widget.colorsets, borderRadius: widget.borderRadius, onClick: widget.onClick, margin: widget.margin, showText: widget.showText, + weekStartsWith: widget.weekStartsWith, + showWeekText: widget.showWeekText, + showMonthText: widget.showMonthText, )), // Show HeatMapColorTip if showColorTip is true. diff --git a/lib/src/heatmap_calendar.dart b/lib/src/heatmap_calendar.dart index b325e4e..8631cec 100644 --- a/lib/src/heatmap_calendar.dart +++ b/lib/src/heatmap_calendar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' show DateFormat; import './data/heatmap_color_mode.dart'; import './widget/heatmap_calendar_page.dart'; import './widget/heatmap_color_tip.dart'; @@ -87,6 +88,15 @@ class HeatMapCalendar extends StatefulWidget { /// The double value of [HeatMapColorTip]'s tip container's size. final double? colorTipSize; + /// Which day the week should start? + /// weekStartsWith = 1 for Monday, ..., weekStartsWith = 7 for Sunday. + /// Default to 7 (the week starts wih Sunday). + final int weekStartsWith; + + final bool showMonthSelector; + final bool showWeekLabel; + final bool? showText; + const HeatMapCalendar({ Key? key, required this.colorsets, @@ -109,6 +119,10 @@ class HeatMapCalendar extends StatefulWidget { this.colorTipHelper, this.colorTipCount, this.colorTipSize, + this.weekStartsWith = 7, + this.showMonthSelector = true, + this.showWeekLabel = true, + this.showText, }) : super(key: key); @override @@ -118,6 +132,8 @@ class HeatMapCalendar extends StatefulWidget { class _HeatMapCalendar extends State { // The DateTime value of first day of the current month. DateTime? _currentDate; + // The list of localized labels for week days. + final List _localizedWeekDayLabels = []; @override void initState() { @@ -125,21 +141,19 @@ class _HeatMapCalendar extends State { setState(() { // Set _currentDate value to first day of initialized date or // today's month if widget.initDate is null. - _currentDate = - DateUtil.startDayOfMonth(widget.initDate ?? DateTime.now()); + _currentDate = DateUtil.startDayOfMonth(widget.initDate ?? DateTime.now()); }); } void changeMonth(int direction) { setState(() { - _currentDate = - DateUtil.changeMonth(_currentDate ?? DateTime.now(), direction); + _currentDate = DateUtil.changeMonth(_currentDate ?? DateTime.now(), direction); }); if (widget.onMonthChange != null) widget.onMonthChange!(_currentDate!); } /// Header widget which shows left, right buttons and year/month text. - Widget _header() { + Widget _header(BuildContext context) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -153,14 +167,13 @@ class _HeatMapCalendar extends State { ), // Text which shows the current year and month - Text( - DateUtil.MONTH_LABEL[_currentDate?.month ?? 0] + - ' ' + - (_currentDate?.year).toString(), - style: TextStyle( - fontSize: widget.monthFontSize ?? 12, + if (_currentDate != null) + Text( + DateFormat.yMMMM(Localizations.localeOf(context).languageCode).format(_currentDate!), + style: TextStyle( + fontSize: widget.monthFontSize ?? 12, + ), ), - ), // Next month button. IconButton( @@ -174,18 +187,25 @@ class _HeatMapCalendar extends State { ); } - Widget _weekLabel() { + Widget _weekLabel(BuildContext context) { + if (_localizedWeekDayLabels.isEmpty && _currentDate != null) { + final firstWeekDayIndex = -((_currentDate!.weekday - widget.weekStartsWith) % 7); + for (var i = firstWeekDayIndex; i < firstWeekDayIndex + 7; i++) { + _localizedWeekDayLabels.add(DateFormat.E(Localizations.localeOf(context).languageCode) + .format(DateUtil.changeDay(_currentDate!, i))); + } + } + return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - for (String label in DateUtil.WEEK_LABEL.skip(1)) + for (String label in _localizedWeekDayLabels) WidgetUtil.flexibleContainer( widget.flexible ?? false, false, Container( - margin: EdgeInsets.only( - left: widget.margin?.left ?? 2, - right: widget.margin?.right ?? 2), + margin: + EdgeInsets.only(left: widget.margin?.left ?? 2, right: widget.margin?.right ?? 2), width: widget.size ?? 42, alignment: Alignment.center, child: Text( @@ -213,13 +233,14 @@ class _HeatMapCalendar extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - _header(), - _weekLabel(), + widget.showMonthSelector ? _header(context) : SizedBox(), + widget.showWeekLabel ? _weekLabel(context) : SizedBox(), HeatMapCalendarPage( baseDate: _currentDate ?? DateTime.now(), colorMode: widget.colorMode, flexible: widget.flexible, size: widget.size, + showText: widget.showText, fontSize: widget.fontSize, defaultColor: widget.defaultColor, textColor: widget.textColor, @@ -228,6 +249,7 @@ class _HeatMapCalendar extends State { colorsets: widget.colorsets, borderRadius: widget.borderRadius, onClick: widget.onClick, + weekStartsWith: widget.weekStartsWith, ), if (widget.showColorTip == true) HeatMapColorTip( diff --git a/lib/src/util/date_util.dart b/lib/src/util/date_util.dart index 555fcd9..b2be84b 100644 --- a/lib/src/util/date_util.dart +++ b/lib/src/util/date_util.dart @@ -1,49 +1,7 @@ class DateUtil { + // ignore: constant_identifier_names static const int DAYS_IN_WEEK = 7; - static const List MONTH_LABEL = [ - '', - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', - ]; - - static const List SHORT_MONTH_LABEL = [ - '', - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', - ]; - - static const List WEEK_LABEL = [ - '', - 'Sun', - 'Mon', - 'Tue', - 'Wed', - 'Thu', - 'Fri', - 'Sat', - ]; - /// Get start day of month. static DateTime startDayOfMonth(final DateTime referenceDate) => DateTime(referenceDate.year, referenceDate.month, 1); @@ -58,23 +16,28 @@ class DateUtil { /// Separate [referenceDate]'s month to List of every weeks. static List> separatedMonth( - final DateTime referenceDate) { - DateTime _startDate = startDayOfMonth(referenceDate); - DateTime _endDate = DateTime(_startDate.year, _startDate.month, - _startDate.day + DAYS_IN_WEEK - _startDate.weekday % DAYS_IN_WEEK - 1); - DateTime _finalDate = endDayOfMonth(referenceDate); - List> _savedMonth = []; - - while (_startDate.isBefore(_finalDate) || _startDate == _finalDate) { - _savedMonth.add({_startDate: _endDate}); - _startDate = changeDay(_endDate, 1); - _endDate = changeDay( - _endDate, - endDayOfMonth(_endDate).day - _startDate.day >= DAYS_IN_WEEK + final DateTime referenceDate, final int weekStartsWith) { + DateTime startDate = startDayOfMonth(referenceDate); + DateTime endDate = DateTime( + startDate.year, + startDate.month, + startDate.day + + DAYS_IN_WEEK - + ((startDate.weekday - weekStartsWith) % DAYS_IN_WEEK) - + 1); + DateTime finalDate = endDayOfMonth(referenceDate); + List> savedMonth = []; + + while (startDate.isBefore(finalDate) || startDate == finalDate) { + savedMonth.add({startDate: endDate}); + startDate = changeDay(endDate, 1); + endDate = changeDay( + endDate, + endDayOfMonth(endDate).day - startDate.day >= DAYS_IN_WEEK ? DAYS_IN_WEEK - : endDayOfMonth(_endDate).day - _startDate.day + 1); + : endDayOfMonth(endDate).day - startDate.day + 1); } - return _savedMonth; + return savedMonth; } /// Change day of [referenceDate]. diff --git a/lib/src/widget/heatmap_calendar_page.dart b/lib/src/widget/heatmap_calendar_page.dart index cea2c5a..f898dff 100644 --- a/lib/src/widget/heatmap_calendar_page.dart +++ b/lib/src/widget/heatmap_calendar_page.dart @@ -8,6 +8,11 @@ class HeatMapCalendarPage extends StatelessWidget { /// The DateTime value which contains the current calendar's date value. final DateTime baseDate; + /// Which day the week should start? + /// weekStartsWith = 1 for Monday, ..., weekStartsWith = 7 for Sunday. + /// Default to 7 (the week starts wih Sunday). + final int weekStartsWith; + /// The list value of the map value that contains /// separated start and end of every weeks on month. /// @@ -61,9 +66,12 @@ class HeatMapCalendarPage extends StatelessWidget { /// Paratmeter gives clicked [DateTime] value. final Function(DateTime)? onClick; + final bool? showText; + HeatMapCalendarPage({ Key? key, required this.baseDate, + required this.weekStartsWith, required this.colorMode, this.flexible, this.size, @@ -75,9 +83,9 @@ class HeatMapCalendarPage extends StatelessWidget { this.colorsets, this.borderRadius, this.onClick, - }) : separatedDate = DateUtil.separatedMonth(baseDate), - maxValue = DatasetsUtil.getMaxValue( - DatasetsUtil.filterMonth(datasets, baseDate)), + this.showText, + }) : separatedDate = DateUtil.separatedMonth(baseDate, weekStartsWith), + maxValue = DatasetsUtil.getMaxValue(DatasetsUtil.filterMonth(datasets, baseDate)), super(key: key); @override @@ -89,8 +97,10 @@ class HeatMapCalendarPage extends StatelessWidget { HeatMapCalendarRow( startDate: date.keys.first, endDate: date.values.first, + weekStartsWith: weekStartsWith, colorMode: colorMode, size: size, + showText: showText, fontSize: fontSize, defaultColor: defaultColor, colorsets: colorsets, @@ -102,8 +112,7 @@ class HeatMapCalendarPage extends StatelessWidget { onClick: onClick, datasets: Map.from(datasets ?? {}) ..removeWhere( - (key, value) => !(key.isAfter(date.keys.first) && - key.isBefore(date.values.first) || + (key, value) => !(key.isAfter(date.keys.first) && key.isBefore(date.values.first) || key == date.keys.first || key == date.values.first), ), diff --git a/lib/src/widget/heatmap_calendar_row.dart b/lib/src/widget/heatmap_calendar_row.dart index 10f4bd3..0538892 100644 --- a/lib/src/widget/heatmap_calendar_row.dart +++ b/lib/src/widget/heatmap_calendar_row.dart @@ -12,6 +12,11 @@ class HeatMapCalendarRow extends StatelessWidget { /// The integer value of end date of the week final DateTime endDate; + /// Which day the week should start? + /// weekStartsWith = 1 for Monday, ..., weekStartsWith = 7 for Sunday. + /// Default to 7 (the week starts wih Sunday). + final int weekStartsWith; + /// The double value of every [HeatMapContainer]'s width and height. final double? size; @@ -65,10 +70,13 @@ class HeatMapCalendarRow extends StatelessWidget { /// Paratmeter gives clicked [DateTime] value. final Function(DateTime)? onClick; + final bool? showText; + HeatMapCalendarRow({ Key? key, required this.startDate, required this.endDate, + required this.weekStartsWith, required this.colorMode, this.size, this.fontSize, @@ -81,6 +89,7 @@ class HeatMapCalendarRow extends StatelessWidget { this.datasets, this.maxValue, this.onClick, + this.showText, }) : dayContainers = List.generate( 7, // If current week has first day of the month and @@ -91,10 +100,10 @@ class HeatMapCalendarRow extends StatelessWidget { // the last day is not a saturday. (i) => (startDate == DateUtil.startDayOfMonth(startDate) && endDate.day - startDate.day != 7 && - i < (startDate.weekday % 7)) || + i < ((startDate.weekday - weekStartsWith) % 7)) || (endDate == DateUtil.endDayOfMonth(endDate) && endDate.day - startDate.day != 7 && - i > (endDate.weekday % 7)) + i > ((endDate.weekday - weekStartsWith) % 7)) ? Container( width: size ?? 42, height: size ?? 42, @@ -107,7 +116,8 @@ class HeatMapCalendarRow extends StatelessWidget { // // So we have to give every day information to each HeatMapContainer. date: DateTime(startDate.year, startDate.month, - startDate.day - startDate.weekday % 7 + i), + startDate.day - (startDate.weekday - weekStartsWith) % 7 + i), + showText: showText, backgroundColor: defaultColor, size: size, fontSize: fontSize, @@ -119,23 +129,20 @@ class HeatMapCalendarRow extends StatelessWidget { // we have to color the matched HeatMapContainer. // // If datasets is null or doesn't contains the equal DateTime value, send null. - selectedColor: datasets?.keys.contains(DateTime( - startDate.year, - startDate.month, - startDate.day - startDate.weekday % 7 + i)) ?? + selectedColor: datasets?.keys.contains(DateTime(startDate.year, startDate.month, + startDate.day - (startDate.weekday - weekStartsWith) % 7 + i)) ?? false // If colorMode is ColorMode.opacity, ? colorMode == ColorMode.opacity // Color the container with first value of colorsets // and set opacity value to current day's datasets key // devided by maxValue which is the maximum value of the month. - ? colorsets?.values.first.withOpacity((datasets?[ - DateTime( - startDate.year, - startDate.month, - startDate.day + - i - - (startDate.weekday % 7))] ?? + ? colorsets?.values.first.withOpacity((datasets?[DateTime( + startDate.year, + startDate.month, + startDate.day + + i - + ((startDate.weekday - weekStartsWith) % 7))] ?? 1) / (maxValue ?? 1)) // Else if colorMode is ColorMode.Color. @@ -144,10 +151,8 @@ class HeatMapCalendarRow extends StatelessWidget { // Using DatasetsUtil.getColor() : DatasetsUtil.getColor( colorsets, - datasets?[DateTime( - startDate.year, - startDate.month, - startDate.day + i - (startDate.weekday % 7))]) + datasets?[DateTime(startDate.year, startDate.month, + startDate.day + i - ((startDate.weekday - 1) % 7))]) : null, ), ), diff --git a/lib/src/widget/heatmap_column.dart b/lib/src/widget/heatmap_column.dart index 17221f4..8d1dba6 100644 --- a/lib/src/widget/heatmap_column.dart +++ b/lib/src/widget/heatmap_column.dart @@ -22,6 +22,11 @@ class HeatMapColumn extends StatelessWidget { /// The date value of last day of given week. final DateTime endDate; + /// Which day the week should start? + /// weekStartsWith = 1 for Monday, ..., weekStartsWith = 7 for Sunday. + /// Default to 7 (the week starts wih Sunday). + final int weekStartsWith; + /// The double value of every [HeatMapContainer]'s width and height. final double? size; @@ -77,6 +82,7 @@ class HeatMapColumn extends StatelessWidget { Key? key, required this.startDate, required this.endDate, + required this.weekStartsWith, required this.colorMode, required this.numDays, this.size, @@ -111,7 +117,9 @@ class HeatMapColumn extends StatelessWidget { selectedColor: datasets?.keys.contains(DateTime( startDate.year, startDate.month, - startDate.day - startDate.weekday % 7 + i)) ?? + startDate.day - + (startDate.weekday - weekStartsWith) % 7 + + i)) ?? false // If colorMode is ColorMode.opacity, ? colorMode == ColorMode.opacity @@ -121,7 +129,10 @@ class HeatMapColumn extends StatelessWidget { ? colorsets?.values.first.withOpacity((datasets?[DateTime( startDate.year, startDate.month, - startDate.day + i - (startDate.weekday % 7))] ?? + startDate.day + + i - + ((startDate.weekday - weekStartsWith) % + 7))] ?? 1) / (maxValue ?? 1)) // Else if colorMode is ColorMode.Color. @@ -130,8 +141,8 @@ class HeatMapColumn extends StatelessWidget { // Using DatasetsUtil.getColor() : DatasetsUtil.getColor( colorsets, - datasets?[DateTime(startDate.year, startDate.month, - startDate.day + i - (startDate.weekday % 7))]) + datasets?[ + DateTime(startDate.year, startDate.month, startDate.day + i - ((startDate.weekday - weekStartsWith) % 7))]) : null, ), ), diff --git a/lib/src/widget/heatmap_container.dart b/lib/src/widget/heatmap_container.dart index 46b8f40..e78e82b 100644 --- a/lib/src/widget/heatmap_container.dart +++ b/lib/src/widget/heatmap_container.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import '../data/heatmap_color.dart'; class HeatMapContainer extends StatelessWidget { @@ -43,19 +44,22 @@ class HeatMapContainer extends StatelessWidget { width: size, height: size, alignment: Alignment.center, + decoration: BoxDecoration( + color: selectedColor, + borderRadius: + BorderRadius.all(Radius.circular(borderRadius ?? 5)), + ), child: (showText ?? true) ? Text( date.day.toString(), style: TextStyle( color: textColor ?? const Color(0xFF8A8A8A), fontSize: fontSize), + semanticsLabel: DateFormat.yMMMMEEEEd( + Localizations.localeOf(context).languageCode) + .format(date), ) : null, - decoration: BoxDecoration( - color: selectedColor, - borderRadius: - BorderRadius.all(Radius.circular(borderRadius ?? 5)), - ), ), ), onTap: () { diff --git a/lib/src/widget/heatmap_month_text.dart b/lib/src/widget/heatmap_month_text.dart index f9569da..7b9ee53 100644 --- a/lib/src/widget/heatmap_month_text.dart +++ b/lib/src/widget/heatmap_month_text.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart'; -import '../util/date_util.dart'; class HeatMapMonthText extends StatelessWidget { /// List value of every sunday's month information. /// /// From 1: January to 12: December. - final List? firstDayInfos; + final List? firstDayInfos; /// The double value for space between labels. final double? size; @@ -33,7 +32,7 @@ class HeatMapMonthText extends StatelessWidget { List items = []; // Set true if previous week was the first day of the month. - bool _write = false; + bool write = false; // Loop until check every given weeks. for (int label = 0; label < (firstDayInfos?.length ?? 0); label++) { @@ -41,25 +40,26 @@ class HeatMapMonthText extends StatelessWidget { // first week of month, create labels if (label == 0 || (label > 0 && firstDayInfos![label] != firstDayInfos![label - 1])) { - _write = true; + write = true; // Add Text without width margin if first week is end of the month. // Otherwise, add Text with width margin. items.add( - firstDayInfos!.length == 1 || (label == 0 && firstDayInfos![label] != firstDayInfos![label + 1]) - ? _renderText(DateUtil.SHORT_MONTH_LABEL[firstDayInfos![label]]) + firstDayInfos!.length == 1 || + (label == 0 && + firstDayInfos![label] != firstDayInfos![label + 1]) + ? _renderText(firstDayInfos![label]) : Container( width: (((size ?? 20) + (margin?.right ?? 2)) * 2), margin: EdgeInsets.only( left: margin?.left ?? 2, right: margin?.right ?? 2), - child: _renderText( - DateUtil.SHORT_MONTH_LABEL[firstDayInfos![label]]), + child: _renderText(firstDayInfos![label]), ), ); - } else if (_write) { + } else if (write) { // If given week is the next week of labeled week. // do nothing. - _write = false; + write = false; } else { // Else create empty box. items.add(Container( diff --git a/lib/src/widget/heatmap_page.dart b/lib/src/widget/heatmap_page.dart index e1c2984..e5cd861 100644 --- a/lib/src/widget/heatmap_page.dart +++ b/lib/src/widget/heatmap_page.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' show DateFormat; import './heatmap_month_text.dart'; import './heatmap_column.dart'; import '../data/heatmap_color_mode.dart'; @@ -12,11 +13,14 @@ class HeatMapPage extends StatelessWidget { /// List value of every sunday's month information. /// /// From 1: January to 12: December. - final List _firstDayInfos = []; + final List _firstDayInfos = []; /// The number of days between [startDate] and [endDate]. final int _dateDifferent; + /// The list of localized labels for week days. + final List _localizedWeekDayLabels = []; + /// The Date value of start day of heatmap. /// /// HeatMap shows the start day of [startDate]'s week. @@ -45,9 +49,12 @@ class HeatMapPage extends StatelessWidget { /// The default background color value of every blocks. final Color? defaultColor; - /// The text color value of every blocks. + /// The text color value of every blocks into the heatmap. final Color? textColor; + /// The text color value for labels (months & week days). + final Color? labelColor; + /// ColorMode changes the color mode of blocks. /// /// [ColorMode.opacity] requires just one colorsets value and changes color @@ -75,6 +82,15 @@ class HeatMapPage extends StatelessWidget { final bool? showText; + /// Which day the week should start? + /// weekStartsWith = 1 for Monday, ..., weekStartsWith = 7 for Sunday. + /// Default to 7 (the week starts wih Sunday). + final int weekStartsWith; + + final bool? showWeekText; + + final bool? showMonthText; + HeatMapPage({ Key? key, required this.colorMode, @@ -85,55 +101,73 @@ class HeatMapPage extends StatelessWidget { this.datasets, this.defaultColor, this.textColor, + this.labelColor, this.colorsets, this.borderRadius, this.onClick, this.margin, this.showText, + this.weekStartsWith = 7, + this.showWeekText = true, + this.showMonthText = true, }) : _dateDifferent = endDate.difference(startDate).inDays, maxValue = DatasetsUtil.getMaxValue(datasets), super(key: key); /// Get [HeatMapColumn] from [startDate] to [endDate]. - List _heatmapColumnList() { + List _heatmapColumnList(BuildContext context) { // Create empty list. List columns = []; // Set cursor(position) to first day of weeks // until cursor reaches the final week. - for (int datePos = 0 - (startDate.weekday % 7); + for (int datePos = -((startDate.weekday - weekStartsWith) % 7); datePos <= _dateDifferent; datePos += 7) { // Get first day of week by adding cursor's value to startDate. - DateTime _firstDay = DateUtil.changeDay(startDate, datePos); - - columns.add(HeatMapColumn( - // If last day is not saturday, week also includes future Date. - // So we have to make future day on last column blanck. - // - // To make empty space to future day, we have to pass this HeatMapPage's - // endDate to HeatMapColumn's endDate. - startDate: _firstDay, - endDate: datePos <= _dateDifferent - 7 - ? DateUtil.changeDay(startDate, datePos + 6) - : endDate, - colorMode: colorMode, - numDays: min(endDate.difference(_firstDay).inDays + 1, 7), - size: size, - fontSize: fontSize, - defaultColor: defaultColor, - colorsets: colorsets, - textColor: textColor, - borderRadius: borderRadius, - margin: margin, - maxValue: maxValue, - onClick: onClick, - datasets: datasets, - showText: showText, - )); - - // also add first day's month information to _firstDayInfos list. - _firstDayInfos.add(_firstDay.month); + DateTime firstDay = DateUtil.changeDay(startDate, datePos); + + if (_localizedWeekDayLabels.isEmpty) { + // Add an empty string for the first row + // which is used to show the 12 month labels. + _localizedWeekDayLabels.add(''); + for (var i = 0; i < 7; i++) { + _localizedWeekDayLabels.add(DateFormat.E(Localizations.localeOf(context).languageCode) + .format(DateUtil.changeDay(firstDay, i))); + } + } + + final numDays = min(endDate.difference(firstDay).inDays + 1, 7); + if (numDays != 0) { + columns.add(HeatMapColumn( + // If last day is not saturday, week also includes future Date. + // So we have to make future day on last column blanck. + // + // To make empty space to future day, we have to pass this HeatMapPage's + // endDate to HeatMapColumn's endDate. + startDate: firstDay, + endDate: + datePos <= _dateDifferent - 7 ? DateUtil.changeDay(startDate, datePos + 6) : endDate, + weekStartsWith: weekStartsWith, + colorMode: colorMode, + numDays: numDays, + size: size, + fontSize: fontSize, + defaultColor: defaultColor, + colorsets: colorsets, + textColor: textColor, + borderRadius: borderRadius, + margin: margin, + maxValue: maxValue, + onClick: onClick, + datasets: datasets, + showText: showText, + )); + + // also add first day's month information to _firstDayInfos list. + _firstDayInfos + .add(DateFormat.MMM(Localizations.localeOf(context).languageCode).format(firstDay)); + } } return columns; @@ -141,35 +175,34 @@ class HeatMapPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - Row( - mainAxisSize: MainAxisSize.min, + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Show week labels to left side of heatmap. + if (this.showWeekText == true) + HeatMapWeekText( + margin: margin, + fontSize: fontSize, + size: size, + fontColor: labelColor, + weekDayLabels: _localizedWeekDayLabels, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Show week labels to left side of heatmap. - HeatMapWeekText( - margin: margin, - fontSize: fontSize, - size: size, - fontColor: textColor, - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Show month labels to top of heatmap. - HeatMapMonthText( - firstDayInfos: _firstDayInfos, - margin: margin, - fontSize: fontSize, - fontColor: textColor, - size: size, - ), - - // Heatmap itself. - Row( - children: [..._heatmapColumnList()], - ), - ], + // Show month labels to top of heatmap. + if (this.showMonthText == true) + HeatMapMonthText( + firstDayInfos: _firstDayInfos, + margin: margin, + fontSize: fontSize, + fontColor: labelColor, + size: size, + ), + + // Heatmap itself. + Row( + children: [..._heatmapColumnList(context)], ), ], ), diff --git a/lib/src/widget/heatmap_week_text.dart b/lib/src/widget/heatmap_week_text.dart index e884475..3198d8f 100644 --- a/lib/src/widget/heatmap_week_text.dart +++ b/lib/src/widget/heatmap_week_text.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import '../util/date_util.dart'; class HeatMapWeekText extends StatelessWidget { /// The margin value for correctly space between labels. @@ -14,12 +13,16 @@ class HeatMapWeekText extends StatelessWidget { /// The color value of every font's color. final Color? fontColor; + /// The list of labels for week days. + final List weekDayLabels; + const HeatMapWeekText({ Key? key, this.margin, this.fontSize, this.size, this.fontColor, + required this.weekDayLabels, }) : super(key: key); @override @@ -27,7 +30,7 @@ class HeatMapWeekText extends StatelessWidget { return Column( mainAxisSize: MainAxisSize.min, children: [ - for (String label in DateUtil.WEEK_LABEL) + for (String label in weekDayLabels) Container( height: size ?? 20, margin: margin ?? const EdgeInsets.all(2.0), diff --git a/pubspec.yaml b/pubspec.yaml index 1d0ae03..e936198 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,19 +1,20 @@ name: flutter_heatmap_calendar description: Flutter heatmap calendar inspired by github contribution chart which includes traditional mode / calendar mode. -version: 1.0.5 +version: 1.1.0 homepage: https://github.com/devappmin/flutter_heatmap_calendar environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.12.0 <4.0.0" flutter: ">=1.17.0" dependencies: flutter: sdk: flutter + intl: ^0.19.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^1.0.0 + flutter_lints: ^4.0.0 flutter: