【Tableau】ドット絵と丸棒グラフをシームレスに切り替える

## はじめに
先日、Back 2 Viz BasicsのBar Chartの課題で以下のVizを作成しました。

https://public.tableau.com/app/profile/hideaki.y/viz/B2VB2024Week4AcresofLandOwnedbyAfricanAmericaninGeorgia/1

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="en" dir="ltr"><a href="https://twitter.com/hashtag/B2VB?src=hash&amp;ref_src=twsrc%5Etfw">#B2VB</a> 2024 Week 4<br>Acres of Land Owned by African American in Georgia<br><br>Here&#39;s this week&#39;s B2VB submission!<br>I attempted to depict the portrait of the great W.E.B. Du Bois.<br>Thanks <a href="https://twitter.com/ReadySetData?ref_src=twsrc%5Etfw">@ReadySetData</a>, <a href="https://twitter.com/ItsElisaDavis?ref_src=twsrc%5Etfw">@ItsElisaDavis</a> and <a href="https://twitter.com/datavizfairy?ref_src=twsrc%5Etfw">@datavizfairy</a><a href="https://twitter.com/hashtag/Tableau?src=hash&amp;ref_src=twsrc%5Etfw">#Tableau</a> <a href="https://twitter.com/hashtag/DuBoisChallenge2024?src=hash&amp;ref_src=twsrc%5Etfw">#DuBoisChallenge2024</a><a href="https://t.co/om55pBhUuN">https://t.co/om55pBhUuN</a> <a href="https://t.co/A9LrwQE54I">pic.twitter.com/A9LrwQE54I</a></p>&mdash; Chasoso (@_Chasoso) <a href="https://twitter.com/_Chasoso/status/1760718049014108209?ref_src=twsrc%5Etfw">February 22, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

作り方について、簡単に解説します。

## 1.ドット絵を作る

まずは肖像画のドット絵を作ります。
今回はChat GPTにドットのデータをCSV出力するPythonプログラムを作ってもらいました。
Chat GPTの出力プログラムから、一部ソースコードを手動で修正しています。

### Chat GPTでプログラム作成

> 【Chat GPTに出した要件】
> - 画像を読み込み、ドットに変換する
> - x座標、y座標、色の明るさ(0-255)をCSVに出力する
> 
> 【自分で追加修正した要件】
> - 行IDを追加する

```Python:image_to_dot.py
from PIL import Image


def convert_image_to_dots(image_path, output_csv):
    # 画像を開く
    img = Image.open(image_path)

    # 画像の幅と高さを取得
    width, height = img.size

    index=0
    
    # CSVファイルを書き込みモードで開く
    with open(output_csv, 'w') as csv_file:
        # ヘッダー行を書き込む
        csv_file.write("x,y,id,brightness\n")

        # 各ピクセルの情報を書き込む
        for y in range(height):
            for x in range(width):
                # ピクセルの色情報を取得
                pixel_color = img.getpixel((x, y))

                # グレースケール値を取得
                brightness = pixel_color  # 白(255)か黒(0)のどちらか

                index+=1
                
                # CSVファイルに書き込む
                csv_file.write(f"{x},{y},{index},{brightness}\n")

if __name__ == "__main__":
    # 画像ファイルのパスと出力先のCSVファイル名を指定
    image_path = "image_name.png"
    output_csv = "output_dot_data.csv"
    
    # 画像をドットに変換してCSVに出力
    convert_image_to_dots(image_path, output_csv)
```

:::note warn
`index+=1`をグレースケール値の取得後すぐに入れてしまいました。
CSVファイル書き込みの処理の後に入れた方が、IDが0スタートになるため後々の処理が楽です。
:::

今回は、[W.E.B. Du Bois氏の画像](https://www.loc.gov/collections/african-american-perspectives-rare-books/articles-and-essays/daniel-murray-a-collectors-legacy/w-e-b-du-bois/)をインプットにします。
※全体的に暗い画像だったため、読み込み前に少し明るさ&サイズ調整をしています。

image_pathに画像を指定して実行すると、以下のようにCSVファイルにデータが出力されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/e4f7c448-3c9c-4d30-b799-a8383bc409e5.png)


### Tableauで出力内容確認

せっかくなので、Tableauで出力内容を確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/785d70a6-e16d-3f68-1497-8030e9348c3e.png)

上手く出力できたようです。


## 2.グラフ用データと結合する

### 今回のグラフ用データについて

今回のBack 2 Viz Basicsのテーマは"Build a Bar Chart"です。
データはW.E.B. Do Bois氏が1900年のパリ万博に出展した際のビジュアライゼーションが基になっています。
ジョージア州のアフリカ系アメリカ人の所有する土地面積が拡大してきたことを示す棒グラフです。

https://github.com/ajstarks/dubois-data-portraits/blob/master/challenge/2024/challenge03/original-plate-19.jpg

データは年と面積の2列しかない、単純なCSVです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/f6a35558-b1b4-9956-18e0-c9c395549063.png)

### データ結合方法の選択と結合キー

アニメーションでドット絵とグラフを切り替える場合、データの結合方法が重要になります。
以下の2点を念頭に、元データを観察しながら結合方法を考えます。

- データを欠損させない
- グラフ描画結果が元データと乖離しない

今回のグラフ用データをよく見ると、1881年のデータが欠落しているようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/dc86a157-2685-a193-133c-107bf1d2cfd8.png)

データの欠落も踏まえ、今回は以下のリレーションシップで対応することにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/99f916c8-687a-2626-849e-ce8fff7f9083.png)


:::note warn
あれ？データは25行だから25で割った余りを使って結合しなきゃダメじゃね？
と、記事を書いているときに気が付きました。
よく見ると、`[ID]%26 = 25` のデータは、丸棒グラフへ変化したときに1874年の丸棒グラフの付け根に移動しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/ca454c5b-7eb4-7a2f-9fe4-1e57c1476c22.png)

そのため、**以降は `([ID]-1)%25` を結合キーとした場合の実装方法**をご説明します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/243d2675-d02c-3bcd-7cc5-28936413f3fa.png)

```:関係の計算（DuBois Data.csv）
// 1874年から連番を付与。ただし1881年は飛ばす
IF [Year]<1881 THEN [Year]-1874 ELSE [Year]-1875 END
```
```:関係の計算（output_dot_data.csv）
// CSVのidを0開始にした場合、-1は不要
([Id]-1)%25
```
:::

## 3.シート作成

Vizは以下のような構成となっています。
左：線グラフ（丸棒グラフ）、右：円
![20240225.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/16309f74-0a5c-0068-6e34-64a3b718055a.gif)

白背景のように見えていたものは、白い線グラフの集合体です。

### パラメーター作成

今回はドット絵とグラフを同一シートで切り替えられるようにしたいです。
そのため、描画を切り替えるためのパラメーターが必要になります。
今回はTableauのパラメーター機能を使って、描画形式を切り替えていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/2ae1fe60-54ea-17c7-7432-ad8079e84ed7.png)


### ドット絵作成

順番に計算フィールドを作っていきます。

```:First Id per Year
{FIXED [Year]:MIN([Id])}
```

```:Last Id per Year
{FIXED [Year]:MAX([Id])}
```

```:X axis
// ([Id]-[First Id per Year])/([Last Id per Year]-[First Id per Year]) は[Id]の値に応じて0~1を返す
// 5/10^5はグラフの横幅を調整するためのマジックナンバー
IF [パラメーター]
THEN MAX([X])
ELSE MAX([Acres Owned])*MAX(([Id]-[First Id per Year])/([Last Id per Year]-[First Id per Year]))*5/10^5
END
```

```:Y axis
// 2.5はグラフの高さを調整するためのマジックナンバー
IF [パラメーター]
THEN MAX([Y])
ELSE MAX([Year]-1874)*2.5
END
```

```:Size
// ドットのサイズを調整する
// パラメーターがFalseの時はサイズが一番小さくなる255を指定
IF [パラメーター] THEN [Brightness] ELSE 255 END
```

これらの計算フィールドを列・行・マークシェルフに入れます。
円の色、サイズを調整して軸を固定すれば、ドット絵の完成です。

![20240225_02.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/ce210035-64ee-2b65-5755-fb19716622cb.gif)


### 丸棒グラフ作成

丸棒グラフは、線グラフで作成します。
丸棒グラフもドット絵と同じ計算フィールドで作成可能と思っていたのですが……

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/fd4caace-1565-6e60-e3e4-c1580f15df4e.png)

なぜか中心に大きな点が余計に出てしまいます。
しばらく調べましたが、この点が出る理由がわからず……
ただ、線グラフは始点と終点さえマークが表示されていればよいので、線グラフの真ん中の部分は表示させないようにします。

今回は、グループ機能を使ってグルーピングし、データの除去に使います。

まずは、ドット絵表示（パラメーターがTrueの状態）でグループを作成します。
![20240225_03.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/0c944e93-697b-7853-d855-8851596b68c0.gif)

そして、丸棒グラフ用のX軸の計算フィールドを以下のように作成します。

```:X axis(Bar Chart)
// 作成したグループをIF条件に入れる
// マークシェルフの詳細に[X]と[Y]が入っているので、MAX関数の集計粒度は[X]と[Y]になる
IF MAX([Year, X, Y (グループ)]="その他")
THEN [X axis]
END
```

そして、列シェルフに`[X axis(Bar Chart)]`を入れて、マークシェルフ内の色にパラメーターを入れて色変更すれば丸棒グラフの完成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/9b06be01-8ef4-a4a6-6c39-6182f00a93f6.png)

### 二重軸の設定

後は`[X axis]`と`[X axis(Bar Chart)]`を二重軸にすれば、ドット絵と丸棒グラフ部分はほぼ完成です。

![20240225_04.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/1842b520-b057-de9e-4c04-40b58a2ac6ac.gif)

### 年ラベルの作成

実は年ラベルは別シートで作成し、ダッシュボード上で位置を微調整しています。

まずは、`[Year]`を複製し、日付形式に変換します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/3cd86cb0-5488-f410-b318-5b1c2339314d.png)

そして、行シェルフとマークシェルフのテキストに入れた日付形式の`[Year (コピー)]`フィールドの **「欠落した値を表示」** にチェックを入れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/41163ffe-4d79-a4d6-c548-43172bb4923e.png)

その後、円のサイズ、不透明度、テキストの配置を中央に調整します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/4cbd0d84-59dd-2c63-91b5-8d707ee5017a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/00ccc559-9211-9bb5-d752-a0cf6f871f12.png)

最後に、行ヘッダーを非表示にすればシートは完成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/f947ca36-9aee-aa60-d9c1-0337dd7b583b.png)

### ダッシュボード配置用の仕込み

ドット絵が表示されているときには、年ラベルが表示されたまま残ってほしくありません。
そのため、ダッシュボードにシートを配置する前に、計算フィールドの仕込みをします。

今回は動的ゾーン表示を使って表示を消すので、パラメーターがドット絵表示(False)になっているときに`True`を返す計算フィールドを作成します。

```:Inverse Parameter
NOT [パラメーター]
```

## 4.ダッシュボード作成

最後に、ダッシュボードに作成した2シートを配置します。
この時、年ラベルを表示したシートには、`[Inverse Parameter]`を使って動的ゾーン表示設定をしておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/129ef03d-71ea-ea77-8bbc-7fd6efdde28b.png)

これで、ダッシュボードの大枠は完成です。

![20240225_05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/b8eda7ea-085e-fb93-a60d-952c98297454.gif)

あとはデザイン調整やパラメーター変更用シートの作成をすれば、Vizの完成です。

![20240225_06.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2898507/f1cd8302-9d9c-cd3f-601c-893eec84ff7c.gif)


## 最後に（Vizを作った感想と課題）

以前のMakeoverMondayの課題でも、マークのアニメーションを駆使したVizを作成しました。

https://public.tableau.com/app/profile/hideaki.y/viz/MoM2024Week7TheGenderGapinPerceptionsofEatingSpecificAnimalsasMorallyWrong/Dashboard

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="en" dir="ltr"><a href="https://twitter.com/hashtag/MakeoverMonday?src=hash&amp;ref_src=twsrc%5Etfw">#MakeoverMonday</a> 2024 Week7<br>The Gender Gap in Perceptions of Eating Specific Animals as Morally Wrong<br><br>Here&#39;s this week&#39;s MoM submission!<br>I tried using the animation feature to make people&#39;s icons move. <a href="https://twitter.com/hashtag/Tableau?src=hash&amp;ref_src=twsrc%5Etfw">#Tableau</a><a href="https://t.co/pgdjnRewXc">https://t.co/pgdjnRewXc</a> <a href="https://t.co/6mHvBWkVvo">pic.twitter.com/6mHvBWkVvo</a></p>&mdash; Chasoso (@_Chasoso) <a href="https://twitter.com/_Chasoso/status/1757069391865536521?ref_src=twsrc%5Etfw">February 12, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ただ、このVizはマーク数が多く、Tableau PublicのWebサイト上ではアニメーションが再生されたりされなかったりという結果となりました。
今回のB2VBの課題では、データ加工の時点からできるだけマーク数を減らすことを念頭に作業しました。
その結果、Tableau PublicのWebサイト上でもアニメーションが動くVizを作成できたので、その点は満足しています。

今回はドット絵&グラフのシートと年ラベルのシートを分けて作成しましたが、おそらく1シートで作成することが可能です。
ダッシュボード上で細かな位置調整をする必要もなくなるので、今回のVizに関しては1シートで作成した方が良かったかもしれません。

今後も、Tableauで出来る表現の可能性を探っていきたいです。