【コピペでそのまま使う】WordPress にレーダーチャートを挿入する

wordpress でグラフを使った記事を書くための情報、思っているより数が少ない&質が良くないので、備忘録として残しておきます。2022年8月現在動作するソースです。

なぜ、グラフ描画系プラグインがない?

WordPress でグラフを描くプラグイン、不思議なくらい数がないんですよね。

私もちょっとグラフを使いたくてこの手のプラグインを探し求めていたのですが、よく考えたら理由は明白でした。

とても便利なオープンソースのスクリプトが転がっているから、プラグインを作る意味も使う意味もない

具体的に言うと、GoogleCharts, Chart.js……名前ぐらいは聞いたことがあるのではないでしょうか。

wordpress が使えるのに Javascript について無知ってことはないですから、素直にこれらを使えばよく、下手にwebサイトを重くするプラグインをインストールする必要はないわけです。

なんで今まで思い当たらなかったんだろう。

GoogleCharts で描く

実際に使ってみましょう。まずは GoogleCharts です。

GoogleChart のAPIを覗いてみると、Radar Charts という項目がなく、一見「あれ?レーダーチャートはできないのかな?」となりそうですが、GoogleCharts では VegaChart というグループ項目になっています。というのも、これは Vega という別のAPIによって提供されているから。

↑炭水化物と脂質の量を直視すべきではなかった。

ソースは以下の通り(長いので折りたたんでいます)。

Spoiler

(煩雑なのでソース内説明を多々省略しています)


    <script src='https://www.gstatic.com/charts/loader.js'></script>
    <script>
    $(function(){
      google.charts.load('upcoming', {'packages': ['vegachart']}).then(loadCharts);

      const marie = [
        ["1枚当たり標準重量(g)", 5.4],
        ["エネルギー(×10kcal)", 2.4],
        ["たんぱく質(g)", 0.4],
        ["脂質(g)", 0.6],
        ["炭水化物(g)", 4.2],
      ];

      const choice = [
        ["1枚当たり標準重量(g)", 8.7],
        ["エネルギー(×10kcal)", 4.5],
        ["たんぱく質(g)", 0.6],
        ["脂質(g)", 2.1],
        ["炭水化物(g)", 5.8],
      ];

      const moonlight = [
        ["1枚当たり標準重量(g)", 8.1],
        ["エネルギー(×10kcal)", 4.2],
        ["たんぱく質(g)", 0.5],
        ["脂質(g)", 2.2],
        ["炭水化物(g)", 5.1],
      ];

      function loadCharts() {
        for(let i=0;i<marie.length;i++){
            marie[i][2] = "marie";
            choice[i][2] = "choice";
            moonlight[i][2] = "moonlight";
        }
        addChart(marie[0][2], marie, "#d00000");
        addChart(choice[0][2], choice, "#ffba08");
        addChart(moonlight[0][2], moonlight, "#296eb4");
      };

      function addChart(title, data, color) {
        const dataTable = new google.visualization.DataTable();
        dataTable.addColumn({type: 'string', 'id': 'key'});
        dataTable.addColumn({type: 'number', 'id': 'value'});
        dataTable.addColumn({type: 'string', 'id': 'category'});
        dataTable.addRows(data);

        const options = {
          'vega': {
            "$schema": "https://vega.github.io/schema/vega/v5.json",
            "width": 350,
            "height": 300,
            "autosize": "none",
            "title": {
              "text": title,
              "anchor": "middle",
              "fontSize": 14,
              "dy": -8,
              "dx": {"signal": "-width/4"},
              "subtitle": "森永ビスケットの栄養価"
            },
            "signals": [
              {"name": "radius", "update": "90"}
            ],
            "data": [
              {
                "name": "table",
                "source": "datatable",
              },
              {
                "name": "keys",
                "source": "table",
                "transform": [
                  {
                    "type": "aggregate",
                    "groupby": ["key"]
                  }
                ]
              }
            ],
            "scales": [
              {
                "name": "angular",
                "type": "point",
                "range": {"signal": "[-0.7*PI, 1.3*PI]"},  //グラフの向きに関係する変数
                "padding": 0.5,
                "domain": {"data": "table", "field": "key"}
              },
              {
                "name": "radial",
                "type": "linear",
                "range": {"signal": "[0, radius]"},
                "zero": true,
                "nice": false,
                "domain": [0, 8],  //グラフの範囲を指定する変数
              }
            ],
            "encode": {
              "enter": {
                "x": {"signal": "width/2"},
                "y": {"signal": "height/2 + 20"}
              }
            },
            "marks": [
              {
                "type": "group",
                "name": "categories",
                "zindex": 1,
                "from": {
                  "facet": {"data": "table", "name": "facet", "groupby": ["category"]}
                },
                "marks": [
                  {
                    "type": "line",
                    "name": "category-line",
                    "from": {"data": "facet"},
                    "encode": {
                      "enter": {
                        "interpolate": {"value": "linear-closed"},
                        "x": {"signal": "scale('radial', datum.value) * cos(scale('angular', datum.key))"},
                        "y": {"signal": "scale('radial', datum.value) * sin(scale('angular', datum.key))"},
                        "stroke": {"value": color},
                        "strokeWidth": {"value": 1.5},
                        "fill": {"value": color},
                        "fillOpacity": {"value": 0.1}
                      }
                    }
                  },
                  {
                    "type": "text",
                    "name": "value-text",
                    "from": {"data": "category-line"},
                    "encode": {
                      "enter": {
                        "x": {"signal": "datum.x + 14 * cos(scale('angular', datum.datum.key))"},
                        "y": {"signal": "datum.y + 14 * sin(scale('angular', datum.datum.key))"},
                        "text": {"signal": "datum.datum.value"},
                        "opacity": {"signal": "datum.datum.value > 0.01 ? 1 : 0"},
                        "align": {"value": "center"},
                        "baseline": {"value": "middle"},
                        "fontWeight": {"value": "bold"},
                        "fill": {"value": color},
                      }
                    }
                  }
                ]
              },
              {
                "type": "rule",
                "name": "radial-grid",
                "from": {"data": "keys"},
                "zindex": 0,
                "encode": {
                  "enter": {
                    "x": {"value": 0},
                    "y": {"value": 0},
                    "x2": {"signal": "radius * cos(scale('angular', datum.key))"},
                    "y2": {"signal": "radius * sin(scale('angular', datum.key))"},
                    "stroke": {"value": "lightgray"},
                    "strokeWidth": {"value": 1}
                  }
                }
              },
              {
                "type": "text",
                "name": "key-label",
                "from": {"data": "keys"},
                "zindex": 1,
                "encode": {
                  "enter": {
                    "x": {"signal": "(radius + 11) * cos(scale('angular', datum.key))"},
                    "y": [
                      {
                        "test": "sin(scale('angular', datum.key)) > 0",
                        "signal": "5 + (radius + 11) * sin(scale('angular', datum.key))"
                      },
                      {
                        "test": "sin(scale('angular', datum.key)) < 0",
                        "signal": "-5 + (radius + 11) * sin(scale('angular', datum.key))"
                      },
                      {
                        "signal": "(radius + 11) * sin(scale('angular', datum.key))"
                      }
                    ],
                    "text": {"field": "key"},
                    "align":
                      {
                        "value": "center"
                      },
                    "baseline": [
                      {
                        "test": "scale('angular', datum.key) > 0", "value": "top"
                      },
                      {
                        "test": "scale('angular', datum.key) == 0", "value": "middle"
                      },
                      {
                        "value": "bottom"
                      }
                    ],
                    "fill": {"value": "black"},
                    "fontSize": {"value": 12}
                  }
                }
              },
              {
                "type": "line",
                "name": "twenty-line",
                "from": {"data": "keys"},
                "encode": {
                  "enter": {
                    "interpolate": {"value": "linear-closed"},
                    "x": {"signal": "0.2 * radius * cos(scale('angular', datum.key))"},
                    "y": {"signal": "0.2 * radius * sin(scale('angular', datum.key))"},
                    "stroke": {"value": "lightgray"},
                    "strokeWidth": {"value": 1}
                  }
                }
              },
              {
                "type": "line",
                "name": "fourty-line",
                "from": {"data": "keys"},
                "encode": {
                  "enter": {
                    "interpolate": {"value": "linear-closed"},
                    "x": {"signal": "0.4 * radius * cos(scale('angular', datum.key))"},
                    "y": {"signal": "0.4 * radius * sin(scale('angular', datum.key))"},
                    "stroke": {"value": "lightgray"},
                    "strokeWidth": {"value": 1}
                  }
                }
              },
              {
                "type": "line",
                "name": "sixty-line",
                "from": {"data": "keys"},
                "encode": {
                  "enter": {
                    "interpolate": {"value": "linear-closed"},
                    "x": {"signal": "0.6 * radius * cos(scale('angular', datum.key))"},
                    "y": {"signal": "0.6 * radius * sin(scale('angular', datum.key))"},
                    "stroke": {"value": "lightgray"},
                    "strokeWidth": {"value": 1}
                  }
                }
              },
              {
                "type": "line",
                "name": "eighty-line",
                "from": {"data": "keys"},
                "encode": {
                  "enter": {
                    "interpolate": {"value": "linear-closed"},
                    "x": {"signal": "0.8 * radius * cos(scale('angular', datum.key))"},
                    "y": {"signal": "0.8 * radius * sin(scale('angular', datum.key))"},
                    "stroke": {"value": "lightgray"},
                    "strokeWidth": {"value": 1}
                  }
                }
              },
              {
                "type": "line",
                "name": "outer-line",
                "from": {"data": "radial-grid"},
                "encode": {
                  "enter": {
                    "interpolate": {"value": "linear-closed"},
                    "x": {"field": "x2"},
                    "y": {"field": "y2"},
                    "stroke": {"value": "lightgray"},
                    "strokeWidth": {"value": 1}
                  }
                }
              }
            ]
          }
        };

        const elem = document.createElement("div");
        elem.setAttribute("style", "display: inline-block; width: 350px; height: 300px; padding: 20px;");

        const chart = new google.visualization.VegaChart(elem);
        chart.draw(dataTable, options);

        document.getElementById("chart-area").appendChild(elem);
      }
    });
    </script>
    <div id="chart-area"></div>

まるまるコピペしてカスタムHTMLブロックにでも貼り付ければ動作するソースになっています。

……正直、本当にめんどくさいです。Excel 感覚で描けることを期待する人にはおすすめできません。なんせ線の一本一本の座標情報を細かく指定して描画しています。設定しなければデフォルトの値で、とかいう甘えは許されず、設定しなければ表示されません。レーダー作って軸は5分割して~、みたいな簡単な作業ではありません。

逆に言うと拡張性は非常に高く、使いこなせればどんなグラフでも描けるようになるでしょう。

Chart.js で描く

続いて Chart.js で同じことをやってみます。

<div style="width: 50%; text-align: center;">
<canvas id="radar_chart"></canvas>
</div>

<script src="https://www.gstatic.com/charts/loader.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script>

        $(function () {

            var title = "大学入学共通テスト成績(%)"
            var labels = ["英語", "国語", "算数", "社会", "理科"];  //軸ラベルはここを変更
            var dataVal = [86, 74, 95, 90, 97];  //データセットはここを変更

            var config = {
                type: 'radar',
                data: {
                    labels: labels,
                    datasets: [{
                        label: title,
                        backgroundColor: 'rgba(224, 0, 0, 0.4)',  //データカラーはここを変更
                        data: dataVal,
                    }]
                },
                options: {
                    plugins: {
                        legend: {
                            display: false,  //グラフタイトルを表示したい場合はここをtrueに変更
                        },
                        tooltip: {
                            enabled: false,  //ツールチップを表示したい場合はここをtrueに変更
                        },
                    },
                    scales: {
                        r: {
                            min: 0,
                            max: 100,  //グラフの上端を指定する場合はここを変更
                            angleLines: {
                                color: 'rgb(224, 0, 0)',  //縦軸についている色を消したい場合はここを消す
                            }
                        },
                    },
                },

            };

            var chart;
            chart = document.getElementById("radar_chart");
            ctx = chart.getContext("2d");

            var radarChart = new Chart(ctx, config);
            document.getElementById('chart_image_submit').href = radarChart.toBase64Image();
            radarChart.update();

         });

</script>

こちらの方がはるかに簡単ですね。ただし、APIが難読です。

バージョンアップで過去のバージョンと互換性がなくなっている部分があり、web上に転がっているソースをそのままコピーして貼り付けても動かないことが多々あります。

このページで使っているソースは2022年8月現在のものです。

WordPressで描くときの注意

カスタムHTMLブロックに丸ごと放り込めば動くはずですが、以下の点にはご注意ください。

  • 要素の名前が被っていると動かない場合があります。
  • エディターのプレビューには何も表示されません。「新しいタブでプレビュー」を選択し、スクリプトを読み込んでください。
  • canvasを取得する際、jQueryの文法は使用できません。document.getElementById で取得してください(上のソースでchart = $(“#radar_chart”);と書くと完全なる無が出力されます)。

どっちにしてもドキュメントを読みながらソースを書くのが本当に面倒なので、スクリプトを組んでツール化してしまうのがいいかもしれません。気が向けば作ります。