メインコンテンツへスキップ
見出し画像

fl_chart の棒グラフは x の値で並ばない。日別グラフは、記録のない日も0にして28〜31本渡す

    Flutter の fl_chart の BarChart は、BarChartGroupData の x ではなく、渡したリストの順番と本数で棒の横の位置を決めます。練習した日だけを渡すと、3日と7日の棒が隣どうしに並びます。暦どおりに並べるには、練習しなかった日も0の棒にして、その月の日数ぶん渡します。

    あわせて、maxY を渡さないと上端に半端な数のラベルが増えること、積み上げの色を半透明にすると下に塗られたシアンが透けることも、fl_chart のソース(0.68.0 と 1.2.0)で確かめました。fl_chart で日別・月別のグラフを作る Flutter 開発者に向けた記事です。

    実例は、草野球・少年野球の打撃成績アプリ「.333」の練習ノートです。素振り・ティー・マシンの回数を、日別と月別(6か月)の棒グラフで出しています。

    棒の位置は、x ではなく barGroups の順番で決まる

    fl_chart は棒を描く前に、calculateGroupsX で棒ごとの横の位置を計算します。中身は、グラフの幅から棒の幅を引いた残りを、barGroups の本数で割るだけです。x の値は使いません。x がおもに出てくるのは、下のラベルの getTitlesWidget に渡す値と、タップしたときの group.x です。

    そのため、練習した日だけを x: 日付 で渡すと、こうなります。

    • 9日しか練習していない月は、棒が9本、横いっぱいに等間隔で並ぶ

    • 3日と7日の間の3日ぶんの空きが消える

    • 下のラベルも、棒のある9か所にしか出ない(棒グラフの下のラベルは、棒の位置にだけ置かれる)

    画像
    練習した日だけ渡すと、間の日が詰まって並ぶ

    直し方は、データの側で、先に月の日数ぶんの箱を作ることです。.333 の集計はこう書いています。

    static List<PracticeSummary> daily(
        Iterable<PracticeEntry> entries, int year, int month) {
      final length = DateTime(year, month + 1, 0).day; // その月の日数
      final normal = List<int>.filled(length, 0);
      // tee・machine・記録のあった日も同じ形
      for (final e in entries) {
        if (e.date.year != year || e.date.month != month) continue;
        normal[e.date.day - 1] += e.normal;
      }
      // index 0 が1日。練習しなかった日は0のまま返す
    }

    0の棒は描かれません(fl_chart は toY と fromY が同じ棒を描かない)。場所だけ取るので、休んだ日がそのまま空きに見えます。ラベルは31個並ぶと読めないので、getTitlesWidget で1日と5の倍数の日だけ文字を返し、ほかは SizedBox() にしています。

    月別の推移も同じ考え方で、記録のない月も0の箱にして6か月ぶん渡します。

    maxY を渡さないと、いちばん高い棒が天井に付き、半端なラベルが増える

    maxY を省くと、fl_chart はデータのいちばん大きい値をそのまま上端にします。いちばん練習した日の棒は、グラフの天井に付きます。

    左のラベルの間隔(interval)も省くと、高さ40ピクセルに1つを目安に、1・2・5・10…の刻みから選ばれます。そのうえで、fl_chart は上端の値にもラベルを出します。上端が刻みの倍数でないと、すぐ下のラベルと重なります。0.68.0 では必ず出ます。0.70.2 と 1.2.0 には SideTitles の maxIncluded があり、既定は true です(false にすると上端のラベルを出しません)。

    画像
    maxY を渡さないと、上端の 437 と 400 のラベルが重なる

    .333 では、上端を「4つに割って切りのいい数」から選んでいます。いちばん多い日の1.1倍以上になる最初の候補を上端にするので、いちばん高い棒も天井から少し下がります。

    static double _niceMax(int maxValue) {
      if (maxValue <= 0) return 100;
      const steps = [25, 50, 75, 100, 150, 200, 250, 300, 500, 750, 1000, /* … */];
      for (final step in steps) {
        if (step * 4 >= maxValue * 1.1) return step * 4.0;
      }
      return (maxValue * 1.2).ceilToDouble();
    }

    使う側は、maxY にこの値を、左のラベルの interval と横線の horizontalInterval の両方に maxY / 4 を渡します。ラベルの間隔と横線の間隔は別の設定なので、片方だけ渡すと、線とラベルの位置がずれます。0のラベルは getTitlesWidget で消しています。

    画像

    この候補の表は粗く、1,234回だと上端が2,000で、棒は6割ほどの高さになります。候補に400を足せば1,600まで詰められます。

    積み上げの色を半透明にすると、下に塗られたシアンが透ける

    素振り・ティー・マシンを1本の棒に積むには rodStackItems を使います。fl_chart は、まず棒全体を BarChartRodData の color で塗り、その上に区間ごとの色を重ねて塗ります。color を省くと、棒全体は Colors.cyan で塗られます(0.68.0 と 1.2.0 で同じ)。

    区間の色が不透明なら下は隠れますが、半透明にすると透けます。.333 では、今月の今日より先の日と、月別推移の表示中でない月を薄くしています。ここを withValues(alpha: 0.45) で薄くすると、3色とも青緑がかった色になります。

    画像
    半透明にすると、下に塗られた既定のシアンが透ける

    .333 は、白と混ぜた不透明な色で薄くしています。

    // 透過色にすると重なった部分の色が混ざるため、白と混ぜた不透明色で薄くする
    Color tone(SwingType t) =>
        muted ? Color.lerp(t.color, Colors.white, 0.55)! : t.color;

    カードの地が白なので、見た目は「白の上に半透明」と同じになります。地が白でなければ、BarChartRodData の color に Colors.transparent を渡して、下の棒を塗らせない手もあります。

    日付の箱は、DateTime の繰り上がりに任せて作る

    Dart の DateTime は、範囲の外の月や日を渡すと繰り上げてくれます。箱を作る計算はこれで短く書けます。

    • その月の日数: DateTime(year, month + 1, 0).day(翌月の0日 = その月の末日。うるう年の2月は29)

    • 5か月前の月: DateTime(year, month - 5, 1)(1月なら前の年の8月)

    • 前の日: DateTime(d.year, d.month, d.day - 1)

    前の日を subtract(const Duration(days: 1)) で出すと、24時間を引くことになります。夏時間のある地域では、切り替わりの日をまたぐと日付がずれることがあります(Dart の subtract のドキュメントにも書かれています)。.333 は米国などにも出していて、連続練習日数はこの書き方で数えています。テストには、30日の月・うるう年の2月・年をまたぐ月別を入れています。

    手元の5アプリで、同じ形を探した

    fl_chart を入れている5アプリの lib/ を、次の3つで探しました。

    rg -n "BarChartGroupData\(" lib/   # 棒のリストが、全部の日・月を持っているか
    rg -n "maxY:" lib/                 # 上端を自分で決めているか
    rg -n -A8 "rodStackItems" lib/     # 区間の色に alpha を使っていないか
    画像

    7本の棒グラフは、どれも上端を自分で決めていました。ゲームの5本のうち左のラベルを出しているのは1本だけで、刻みを上端の3等分にしたうえで、上端のラベルを v == meta.max で消していました。記録のない日・月がありうるのは .333 の2本だけで、ゲームは元のデータが全部の月・週を持っています。

    まとめ

    • fl_chart の棒は、x の値ではなく barGroups の順番と本数で並ぶ。日別グラフは、記録のない日も0の棒にして月の日数ぶん渡す

    • maxY を省くと、データの最大値が上端になり、上端の値にもラベルが出る。上端は刻みの倍数にして、interval と horizontalInterval に同じ値を渡す

    • 積み上げの区間を半透明にすると、棒全体に先に塗られた色(既定はシアン)が透ける。不透明な色で薄くする

    • 日付は DateTime(y, m + 1, 0) や DateTime(y, m, d - 1) のように、繰り上がりに任せて作る

    日別グラフの「記録のない日」を、データの側で埋めているか、グラフの側で埋めているか、コメントで教えてください。


    この記事の実例にしたアプリです。

    「.333〜生涯打撃成績〜」は、野球の打撃成績を記録して、打率や OPS を自動で計算するアプリです。練習ノートでは、素振り・ティー・マシンの回数を日別と月別のグラフで見られます。草野球や少年野球の成績管理にどうぞ。

    • App Store:https://apps.apple.com/jp/app/333-生涯打撃成績-野球アプリ/id6453073563

    • Google Play:https://play.google.com/store/apps/details?id=com.basewakamaru.battingrecord

    • 紹介サイト:https://battingrecord.web.app

     
     
    個人でスマホアプリを作っています。草野球の打撃成績アプリ「.333」や、子どものおもちゃを記録する「おもちゃ帳」など、自分や家族が欲しかったものばかり。AIのClaudeと一緒に開発していて、うまくいった話も審査で落ちた話も、ゆるく書いていきます。

    あなたへのおすすめ