iT邦幫忙

2023 iThome 鐵人賽

DAY 15
0
Mobile Development

攜手神隊友ChatGPT:攝護腺自我照護App開發歷程!系列 第 15

D15-Flutter x Dart視覺化魔法,攝護腺量表走勢圖_part2

  • 分享至 

  • xImage
  •  

Part1:今日目標

1.前言
2.程式實作: Score_History_Chart_Page.dart
3.結語

Part2:今日內容

1.前言

今天的鐵人賽內容,主要延續昨天文章內容D14-Flutter x Dart視覺化魔法,攝護腺量表走勢圖_part1,按照程式設計架構進行實作,並對細部程式碼說明。廢話不多說,讓我們開始吧!

2.程式實作: Score_History_Chart_Page.dart

(1) 主要功能概述

  • 程式碼的主要功能是顯示分數歷史的折線圖,並允許使用者選擇日期範圍以動態更新數據。
  • onDateRangeSelected 回調函式處理使用者選擇日期範圍後的數據過濾和更新操作。
  • 數據可以按日期範圍進行過濾,並使用 setState 更新圖表以顯示新的折線圖。
  • 使用 fl_chart 庫來創建漂亮的折線圖,並根據使用者的需求自定義外觀。

(2) 程式實作: Score_History_Chart_Page.dart

該份程式碼由兩大class所組成(參考下圖),接下來將依序說明兩個class的功能。
https://ithelp.ithome.com.tw/upload/images/20230918/20120073lHhlCnMpjO.png

<1>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 的狀態和處理相關的邏輯。

<2>class _ScoreHistoryChartPageState extends State<ScoreHistoryChartPage> {}
  • 此段程式碼共有5大部分: (參考下圖)
    • 宣告狀態變數: late List<FlSpot> _data;
    • 初始化狀態: void initState() {}
    • 構建分數歷史圖介面的UI: Widget build(BuildContext context) {}
    • 選擇圖表開始和結束日期的widget: DateRangePickerWidget()
    • LineChart widget用於顯示折線圖: 根據提供的資料 (widget.data 和 widget.dates) 來顯示分數隨時間變化的情況。
      https://ithelp.ithome.com.tw/upload/images/20230918/20120073cJDFufCoyp.png
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),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

3.結語

今天文章內容蠻多的,有些詳細的程式碼操作或Flutter框架的特點會在明天的鐵人文章做更詳細的介紹與說明,讓我們期待明天的內容囉!

度過一個充實的周末,讀了許多書和教學影片,也重啟慢跑活動,期待下周啦~
Working without a plan is sailing without a compass. 工作沒有計畫,有如航海沒有羅盤
今天要聽方大同的《You are the sunshine of my life》


上一篇
D14-Flutter x Dart視覺化魔法,攝護腺量表走勢圖_part1
下一篇
D16-Flutter x Dart視覺化魔法,攝護腺量表走勢圖_part3
系列文
攜手神隊友ChatGPT:攝護腺自我照護App開發歷程!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言