ItemsControlを使ってWPFでGitHub風ヒートマップを描画する

WPFアプリで日々の実績を図で表現したいと思い、GitHubのContribution Graph(芝生)を参考に描画してみたのでその時の記録を残しておきます。外部データを参照し動的に描画できるように繰り返し描画に便利な ItemsControlを利用しました。xamlはWPFの素の機能だけで書いていますが、裏側はMVVMパターンで実装できるPrismというフレームワークを利用しています。

環境

使用しているプラットフォームやフレームワークは以下の通りです。

実装:xaml

メインテーマであるItemsControlはこんな感じです。

<ItemsControl ItemsSource="{Binding WeekHeatmaps}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>

            <ItemsControl ItemsSource="{Binding HeatmapElements}">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Vertical" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Grid
                            Width="15"
                            Height="15"
                            ToolTip="{Binding Comment}">
                            <Border
                                Margin="1"
                                Background="{Binding Color}"
                                CornerRadius="2" />
                        </Grid>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>

        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

ちょっと説明

ItemsControlを入れ子にしています。外側が芝生の列を表していて、実績データでいう1週間分に相当する部分です。横方向に連ねていくため、水平方向(Horizontal)に伸びるStackPanelを使います。内側は芝生の一つのマスを表していて、実績データでいう1日分に相当する部分です。縦方向に連ねていくため、垂直方向(Vertical)に伸びるStackPanelを使います。どちらのItemsControlも中身のカスタマイズにItemTemplateを利用しています。

実装:バインディングするデータ

バインディングしているデータWeekHeatmapsですが、 ViewModel内で以下のようなリストとして定義しています。

public class MainWindowViewModel : BindableBase
{
    ...

    public List<WeekHeatmap> WeekHeatmaps { get; }

    ...
}

リストの型として使っているWeekHeatmapクラスと関連クラスは以下の通りです。

public class WeekHeatmap
{
    public WeekHeatmap(List<HeatmapElement> heatmapElements)
    {
        if (heatmapElements.Count > 7) throw new ArgumentOutOfRangeException(nameof(heatmapElements), "ヒートマップの数が7を超えています。");
        HeatmapElements = heatmapElements;
    }

    public List<HeatmapElement> HeatmapElements { get; }
}
public class HeatmapElement
{
    public HeatmapElement(string color, string comment)
    {
        Color = color;
        Comment = comment;
    }

    public string Color { get; }
    public string Comment { get; }
}

ちょっと説明

芝生のマスの情報はHeatmapElementクラスに持たせています。芝生の色を表す文字列とマウスオーバーした時に表示されるTooltip用の文字列をプロパティとしています。芝生縦一列の一週間分の情報はWeekHeatmapクラスに持たせています。HeatmapElementのリストをプロパティとしています。機能には関係ないですが、一週間分という性質を補強するためリストの長さが7以上だった場合はエラーになるようにしています。

実行

サンプルのデータを代入し実行すると以下の画像ように芝生を描画できました。補足情報として赤文字でどの情報がどこを表しているか記しています。

実行画面。赤文字は後付け

参考

コード全体はGitHubにアップロードしています。リポジトリをcloneしてきたらVisualStudioでGitHubStyleHeatmap.slnを開いてF5実行すると上に記載した画像のウィンドウが表示されます。

まとめと感想

WPFでGitHubのContribution Graph(芝生)風のグラフを描画してみました。グラフを動的に生成するためにItemsControlを利用しました。StackPanelを入れ子にすることで芝生の行と列を表現しました。ItemsControlのItemTemplateを使ってマスの描画方法を指定しています。今回の取り組みでItemsControlの柔軟性を実感しました。特に入れ子にすることでリストの中身にもItemsControlが使えるのは表現の幅が広がっていいですね。また、バインディングするデータに自作クラスが使えるので工夫次第でいろんなリスト情報を見やすく描画するためにも使えると思います。発展として、WeekHeatmapクラスに「概要」プロパティを追加することで芝生の上部に情報を描画させることができるかもしれません。GitHubのように月を表示できそうです。