【Chart.js】散布図で任意の座標の点の傍に文字列を常に表示する

タイトルの通りです。

やりたいこと

Chart.jsは、JavaScriptでグラフを描画するためのライブラリです。
棒グラフ、円グラフ、レーダーチャートなどといった様々なグラフを描画させることができます。
今回は散布図を使用し、指定した座標に点を表示させます。

以下のgifでは、datasetに渡す値(描画させる座標)を周期的に変え、グラフを描画しなおすことで点を動かしています。
別に今回の目的である「座標の点の傍に文字列を表示させる」ことについて、点を動かす必要は一切ないのですが、文字列を追随させたいよね!ということで。

これにラベルをつけると……、

_人人人人人_
> 動く点P <
 ̄Y^Y^Y^Y ̄

というわけで、ある座標の点の傍に文字列を表示していきます。

散布図を表示するコード

※Chart.jsのバージョンは3.9.1で作成しました。

以下はChart.jsで散布図を描画する関数です。
オプションは、不要なものを非表示にしたり、表示範囲を調整したりと見た目上設定したもので特に深い意味はないです。

JavaScript
// 渡された座標の点を描画する関数
function drawSpectrum(x_value, y_value) { 
    const ctx = document.getElementById('chart'); // canvasのID
    myChart = new Chart(ctx, {
        type: 'scatter', // 散布図
        data: { 
            datasets: [{
                data: [{x: x_value, y: y_value}], // 引数で渡されたx座標y座標
                backgroundColor: "#000000" // 点の色
            }], 
        }, 
        options: { 
            animation: false, 
            plugins:{ 
                legend: { 
                    display: false // 凡例なし
                } 
            }, 
            scales: { 
                x: { 
                    max: 30, // 軸最大値
                    min: -10, // 軸最小値
                    display: true,
                    ticks: {
                        display: false // 軸の値非表示
                    }
                }, 
                y: { 
                    max: 30, 
                    min: -10, 
                    display: true,
                    ticks: {
                        display: false
                    }
                } 
            },
            
        }, 
    });
}

呼び出してる方はこんな感じ。
周期実行してる部分と座標の値を変えている部分は点を動かすためにやっているので、散布図を書く上では一切関係ないです。
ただし、グラフを更新するためには、前のグラフを消す必要があるため、そこだけ注意が必要です(下の12行目)。

JavaScript
let x = 0;
let y = 0;
let myChart;
window.addEventListener('load', async() =>{
    // 描画処理を周期実行
    setInterval(
        () => counterForDraw(), 100);
    }
);

function counterForDraw(){
    if(myChart){ myChart.destroy(); } // 前の描画を消さないと再度グラフが描けない
    // 点を動かすために値を変える
    if(x != 20 && y == 0){
        x += 1;
    }
    else if(x == 20 && y != 20){
        y += 1;
    }
    else if(y == 20 && x != 0){
        x -= 1;
    }
    else if(x == 0 && y != 0){
        y -= 1;
    }
    drawSpectrum(x, y); // Chart.jsでグラフを書く関数に毎度座標の値を変えて渡す
}

ここまでが移動する点を散布図で表示するコードです。

文字列(ラベル)を表示する

今回の本題。点の傍に文字列を表示するコードです。
先ほどのチャート描画部分について、pluginsを追加。Chart.jsの動作をカスタマイズするために使用されるものです。

Plugins | Chart.js
Open source HTML5 Charts for your website

afterDrawイベントに、文字列を表示する関数を指定します。

JavaScript
    myChart = new Chart(ctx, {

        // ↓pluginsを追加
        plugins: [{ afterDraw: (chart) =>{ 
            drawLabel(chart, x_value, y_value); 
        } }],
        type: 'scatter',

呼び出している文字列を表示する関数が以下。

JavaScript
function drawLabel(chart, x_value, y_value){ 

    let x_scale = chart.scales.x;
    let y_scale = chart.scales.y;
    
    var cvs = document.getElementById(chart.canvas.id);
    var ctx = cvs.getContext('2d');
    ctx.save();

    // x, yの位置をキャンバス上の座標に換算してやる
    let x_pos1 = x_scale.getPixelForValue(x_value);
    let y_pos1 = y_scale.getPixelForValue(y_value);

    ctx.beginPath();
    ctx.font = '14px serif';  // 文字列のフォントサイズ、フォントの種類
    ctx.fillText("点P", x_pos1+5, y_pos1-5);  // 文字列の文言、表示位置。ここでは右上に
    ctx.restore(); 
}

これで、ある座標の点の傍に文字列を表示させることができました!

文字列がキャンバス外にはみ出さないようにする

先ほどのグラフは、点も文字もはみ出さないように描画範囲を指定していたため、文字列がはみ出すことはありませんでした。しかしグラフの描画範囲である軸の最大値を、点が移動する最大値に変更すると、上記のコードでは、文字列を表示する位置を点の右上に固定しているため、

文字列が範囲外になり見切れてしまいます。

これを回避するため、基本は右上に表示するけど、見切れちゃう場合は左側や下側に表示させるようにします。
measureText()によって文字の縦幅と横幅を取得し、右上に表示した場合の座標とラベルのサイズの合計値がキャンバス外になるかどうかを計算します。もしキャンバス外になるようなら、文字列が点の左側や下側になるよう足し引きを調整してやります。

JavaScript
function drawLabel(chart, x_value, y_value){ 
    console.log("drawLabel");
    let x_scale = chart.scales.x;
    let y_scale = chart.scales.y;
    
    var cvs = document.getElementById(chart.canvas.id);
    var ctx = cvs.getContext('2d');
    ctx.save();
    let x_pos1 = x_scale.getPixelForValue(x_value);
    let y_pos1 = y_scale.getPixelForValue(y_value);
    ctx.beginPath();
    ctx.font = '14px serif';

    // 文字列のサイズを取得
    var label_size = ctx.measureText("点P"); 
    let label_width = label_size.width;
    let label_height = label_size.actualBoundingBoxAscent + label_size.actualBoundingBoxDescent;

    let x_point = x_pos1 + 5;
    let y_point = y_pos1 - 5;
    // はみ出してないかチェック
    if(x_point + label_width > x_scale.width){
        x_point = x_pos1 - 5 - label_width;
    }
    if(y_point - label_height < 0){
        y_point = y_pos1 + 5 + label_height;
    }

    ctx.fillText("点P", x_point, y_point);
    ctx.restore(); 
}

はみ出す場合は文字列の表示位置を移動させることで、見切れないようにすることができました!

HTMLも含め、全体のコードはこちら(github)にあります。
大した量ではないので、JavaScriptは別ファイルにせず、scriptでhtmlに記載しました。
JavaScript部もHTMLファイル扱いされることが嫌だったため、分割しました。

余談

Chart.jsである点の傍に文字列を表示する方法で悩んだので、記録しときたいな~
→せっかくだし点が動いてもちゃんと文字列が付いて行くようにしたいな~
→ん?動く点と文字列??点Pじゃないか!!!!

みたいなノリで作りました。
ちゃんと点P点Pさせられて良かったです。

あとできるだけぬるぬる動かしたくて色々考えました。
最初は座標をリストで持っておいてインデックスを変えて取得するみたいなことを考えたんですが、あまり綺麗じゃないなということでif文で加算減算するようにしました。
結構気に入ってます。

コメント

タイトルとURLをコピーしました