1.前言
2.程式實作: Score_History_Chart_Page.dart
3.結語
今天的鐵人賽內容,主要延續昨天文章內容D14-Flutter x Dart視覺化魔法,攝護腺量表走勢圖_part1,按照程式設計架構進行實作,並對細部程式碼說明。廢話不多說,讓我們開始吧!
Score_History_Chart_Page.dart
onDateRangeSelected
回調函式處理使用者選擇日期範圍後的數據過濾和更新操作。setState
更新圖表以顯示新的折線圖。fl_chart
庫來創建漂亮的折線圖,並根據使用者的需求自定義外觀。Score_History_Chart_Page.dart
該份程式碼由兩大class
所組成(參考下圖),接下來將依序說明兩個class
的功能。
class ScoreHistoryChartPage extends StatefulWidget {}
class ScoreHistoryChartPage extends StatefulWidget {
// ScoreHistoryChartPage 是一個繼承自 StatefulWidget 的類別: 表示這是一個具有內部狀態的 widget
// 用於存儲分數歷史數據的 FlSpot 列表,FlSpot 是用於 fl_chart 庫的類別,表示圖表中的一個數據點
final List<FlSpot> data;
final List<String> dates; // 存儲每個數據點相對應的日期的列表
// 這個 widget的構造函式,接收數據和日期作為必要的參數
ScoreHistoryChartPage({required this.data, required this.dates});
@override
// _ScoreHistoryChartPageState 是 ScoreHistoryChartPage 的內部狀態類別: 該狀態類別負責實現該 widget的具體 UI 和功能
_ScoreHistoryChartPageState createState() => _ScoreHistoryChartPageState();
}
這段程式碼定義了一個名為 ScoreHistoryChartPage 的 Flutter 狀態 (State)ful widget ,該 widget 用於顯示測驗分數歷史的圖表頁面。
這段程式碼還定義了一個帶有兩個必要參數的 widget ,並建立了一個內部狀態類別以實現該 widget 的功能。該 widget 的目的是顯示分數歷史數據的圖表,並通過參數將數據傳遞給它以進行顯示。
ScoreHistoryChartPage
是一個 StatefulWidget,這表示它是一個具有內部狀態的 widget 。這意味著它可以擁有狀態,並且可以在內部狀態對象中保留數據以供操作和更新。
final List<FlSpot> data;
:這是一個成員變數,用於存儲分數歷史數據的 FlSpot 列表。FlSpot 是用於 fl_chart 套件的類別,代表圖表中的一個數據點。該列表是在 widget 的構造函式中傳遞的。
final List<String> dates;
:這是另一個成員變數,用於存儲每個數據點相對應的日期的列表。同樣,這個列表也是在 widget 的構造函式中傳遞的。
ScoreHistoryChartPage({required this.data, required this.dates});
:這是 widget 的構造函式,接受兩個必要的參數,即分數數據和日期列表。這些參數將在初始化 widget 時提供,以便在 widget 的內部使用。
@override
:這個標註表示下面的函式是對父類的方法進行覆寫,這裡是覆寫了 createState 方法。
_ScoreHistoryChartPageState
:這是 ScoreHistoryChartPage 的內部狀態類別,它負責實現該 widget 的具體 UI 和功能。這個內部狀態類別將在 createState 方法中被實例化,並用於管理 widget 的狀態和處理相關的邏輯。
class _ScoreHistoryChartPageState extends State<ScoreHistoryChartPage> {}
late List<FlSpot> _data;
void initState() {}
Widget build(BuildContext context) {}
DateRangePickerWidget()
LineChart
widget用於顯示折線圖: 根據提供的資料 (widget.data 和 widget.dates) 來顯示分數隨時間變化的情況。class _ScoreHistoryChartPageState extends State<ScoreHistoryChartPage> {
// 宣告兩個狀態變數 _data 和 _date
// 因為 widget.data 是 final 且不能被重新賦值,因此我們需要使用一個狀態變量來存儲和修改數據
late List<FlSpot> _data;
late List<String> _date;
@override
// 初始化狀態
void initState() {
super.initState();
// 這邊進行反轉數據是沒有用的,因為這邊是初始化,正式更新在下方,請搜尋下方: .reversed.toList()
// 初始化 _data
_date = widget.dates; // widget.dates.reversed.toList()
_data = List<FlSpot>.generate(widget.data.length, (index) {
return FlSpot(index.toDouble(), widget.data[widget.data.length - 1 - index].y);
});
}
@override
// build() 方法用於構建分數歷史圖表頁面的 UI
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('分數歷史圖表')),
body: Column(
children: [
DateRangePickerWidget( // 是一個自定義 widget,可以在這個 UI 中用於選擇日期範圍
// 當用戶選擇日期範圍時,你可以根據選擇的日期範圍過濾數據並更新圖表
// onDateRangeSelected: (startDate, endDate) {},
// onDateRangeSelected::這是你在 DateRangePickerWidget 中設定的一個回調函式
// 當使用者選擇日期範圍時,這個回調函式將被觸發
onDateRangeSelected: (startDate, endDate) {
print("onDateRangeSelected called with $startDate to $endDate"); // 有印出來
// 這個條件確保只有在 startDate 和 endDate 都存在的情況下,才執行後續的處理
if (startDate != null && endDate != null) {
// Filter data based on the selected date range.
print("Filtering data from $startDate to $endDate");
print("Total data points before filtering: ${widget.data.length}");
List<FlSpot> filteredData = widget.data.where((spot) {
// 使用 widget.dates 從 spot 的 x 座標獲取日期並解析為 DateTime
DateTime spotDate = DateTime.parse(widget.dates[spot.x.toInt()]);
// 此條件: 確保數據點的日期在選擇的日期範圍內。如果符合這個條件,則該數據點被保留
return spotDate.isAfter(startDate) && spotDate.isBefore(endDate.add(Duration(days: 1)));
}).toList();
print("Data points after filtering: ${filteredData.length}");
// print("Final filtered data: $filteredData");
// 新增過濾後的日期資料: filteredDate
List<String> filteredDate = widget.dates.where((spot) {
DateTime spotDate = DateTime.parse(spot);
return spotDate.isAfter(startDate) && spotDate.isBefore(endDate.add(Duration(days: 1)));
}).toList();
// 在過濾完數據後,你使用 setState 來更新 _data,這將觸發 UI 的重新構建,以顯示新的折線圖
setState(() {
_data = filteredData;
// Reverse the _data list and recalculate the x values based on the index
_data = List<FlSpot>.generate(filteredData.length, (index) {
return FlSpot(index.toDouble(), filteredData[filteredData.length - 1 - index].y);
});
_date = filteredDate.reversed.toList(); // 順利反轉數據
});
}
},
),
Expanded(
// Expanded widget用於佔滿剩餘的可用空間,並在內部放置了一個 AspectRatio widget,用於確保圖表的寬高比例
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: AspectRatio(
aspectRatio: 0.8,
// 使用了 LineChart widget來顯示折線圖: 根據提供的資料(widget.data)和日期(widget.dates)來顯示分數隨時間變化的情況
child: LineChart( // 這是一個 LineChart widget的建構函式,接受一個 LineChartData 物件作為參數,用來配置折線圖的外觀和數據
LineChartData( // 這是一個 LineChartData 的建構函式,它用來配置折線圖的各種屬性,包括標題、軸範圍、線條等
// titlesData::這個屬性用來定義標題的相關設定,包括底部標籤和左側標籤
titlesData: FlTitlesData(
show: true,
// bottomTitles::底部標籤的相關設定,用來顯示日期
bottomTitles: SideTitles(
showTitles: true,
reservedSize: 22,
// 使用 getTitles 函式來定義底部標籤的顯示方式
// 整個程式碼行的目的是從 _date 串列中選擇特定索引位置的日期時間字串,然後擷取其中的日期部分(即 MM-DD),作為回傳值
getTitles: (value) {
// value: 用於存取 _date 串列中特定的日期時間字串
// value.toInt() % 2 == 0 // 每隔 2 個點
// value.toInt() >= 0 // 每筆日期都顯示
// 只有在某些特定條件下(每隔 2 個點或低於日期數量)才顯示日期的月份和日期部分
if (value.toInt() % 2 == 0 && value.toInt() < _data.length) {
return _date[value.toInt()].substring(5, 10); // Display only month and day (MM-DD)
// .toInt():這是將浮點數轉換為整數的方法。由於索引應該是整數,因此您可能會將浮點索引值轉換為整數,以確保正確存取串列
// _date[value.toInt()]:這部分的結果是從 _date 串列中選擇特定索引位置的日期時間字串
}
return '';
},
margin: 8,
),
// leftTitles 屬性定義左側的標籤,顯示整數分數
leftTitles: SideTitles(
showTitles: true,
reservedSize: 38,
getTitles: (value) {
return value.toInt().toString(); // Display score as integer
},
margin: 12,
),
),
// borderData::這個屬性用來定義圖表的邊界線相關設定,例如邊界線的顏色和寬度
borderData: FlBorderData(
show: true,
border: Border.all(color: const Color(0xff37434d), width: 1),
),
// minX 和 maxX 定義了 X 軸的最小值和最大值 注意: 這邊應傳選擇的參數(開始日期&結束日期)
minX: -1,
// 通過 widget.dates.length 設置最大值,確保 X 軸能夠顯示所有日期
maxX: (_date.length + 1).toDouble(),
// minY 和 maxY 定義了 Y 軸的最小值和最大值,Y 軸的最大值是數據中最大的分數
minY: 0,
maxY: 36,
// lineBarsData 屬性定義了要繪製的折線數據
lineBarsData: [ // 它使用 widget.data 中的數據點來繪製折線
LineChartBarData(
// spots 是一個 FlSpot 列表,表示折線上的各個數據點
spots: _data,
isCurved: false, // 是否使用曲線來連接折線上的數據點
colors: [Colors.pinkAccent], // 折線的顏色
barWidth: 4, // 折線的寬度
isStrokeCapRound: true, // 是否將折線的端點設為圓角
belowBarData: BarAreaData(show: true,
colors: [Colors.pinkAccent.withOpacity(0.2)] // 填充區域的顏色,可以使用 Colors 類的顏色,並使用 withOpacity() 方法調整透明度
),
dotData: FlDotData(show: true),
),
],
),
),
),
),
),
],
),
);
}
}
今天文章內容蠻多的,有些詳細的程式碼操作或Flutter框架的特點會在明天的鐵人文章做更詳細的介紹與說明,讓我們期待明天的內容囉!
度過一個充實的周末,讀了許多書和教學影片,也重啟慢跑活動,期待下周啦~
Working without a plan is sailing without a compass. 工作沒有計畫,有如航海沒有羅盤
今天要聽方大同的《You are the sunshine of my life》