見出し画像

MATLAB のグラフをクリックでデータ表示 ~ マウスクリックキャプチャーとデータヒント機能を使う


まえがき

MATLAB でグラフをクリックして、そのデータをスクリプト内で使いたいこととかがあるかと思います。

MATLAB でクリック座標を所得する方法は複数あり、目的によって適切な方法を取る必要があります。

今回はその方法を整理してみます。


データヒント機能と datatip 関数

データヒント機能

MATLAB のグラフには、データヒント機能があります。

データ点にカーソルを近づけるとデータが表示され、クリックすると表示が固定します。

データ点に近づくとポイントが表示
データが表示されクリックで固定

複数固定、個別または全削除もできますし、データ上をドラッグすればデータヒントも移動します。

複数表示、ドラッグでデータポイント移動

便利ですね。

この座標情報は右クリックメニューでワークスペースにエクスポートはできますが、このままではスクリプトから使うのにはちょっと不便です。

ワークスペースへのエクスポート


しかし実は R2019b でこのデータヒントを作成する datatip 関数が追加され、スクリプト中から使ったり、表示をカスタマイズすることができるようになっています。


データヒントカスタマイズ

R2019a 以降であれば、データヒントの表示フォーマットをカスタマイズすることができます。

>> p = plot(rand(10,3));
>> dt = p.DataTipTemplate

dt = 

  DataTipTemplate のプロパティ:

    DataTipRows: [2×1 matlab.graphics.datatip.DataTipTextRow]
    Interpreter: 'tex'
       FontSize: 10
      FontAngle: 'normal'
       FontName: 'Helvetica'
>> dt.DataTipRows(1)

ans = 

  DataTipTextRow のプロパティ:

     Label: 'X'
     Value: 'XData'
    Format: 'auto'

>> dtt.DataTipRows(2)

ans = 

  DataTipTextRow のプロパティ:

     Label: 'Y'
     Value: 'YData'
    Format: 'auto'

デフォルトではこのようになっています。

デフォルトフォーマット

これらを書き換えるか、行を追加・削除することができます。

row1 = dataTipTextRow("X + Y", @(x,y)(x+y));
p(1).DataTipTemplate.DataTipRows(end+1) = row1;  % 行追加
p(2).DataTipTemplate.DataTipRows(2) = [];  % 行削除
p(3).DataTipTemplate.DataTipRows(1).Label = 'x';  % ラベル変更
p(3).DataTipTemplate.DataTipRows(2).Label = 'y';
データヒント表示のカスタマイズ

このように、datatip 関数を使うとマウスクリックから、どのライン、どのデータ点が近い等の処理なしで、直接データを取得・表示、表示のカスタマイズをすることができます。

具体的なデータの取得方法についてはのちほど解説します。


マウスクリックキャプチャー

datatip をスクリプト中から使うには座標を入れる必要があります。
そのような場合も含め、マウスクリック座標のキャプチャー方法を見ていきましょう。

MATLAB でのマウスクリックは、Figure、Axes、各 plot それぞれでキャプチャーを行うことができます。

専用のコールバックが定義されているので、そこに処理用の関数もしくは処理そのものを定義して使います。


Figure

全ての座標が取れるユニバーサルな方法です。

例えばマウスのクリック座標を取るには

f = gcf;
f.WindowButtonDownFcn = @getaxPos;

でコールバック関数を指定し、コールバック関数内で

function pos = getaxPos(src,~)
    figC = get (gcf, 'CurrentPoint')
    axC = get (gca, 'CurrentPoint')
end

などとしてクリック座標を取得します。

コールバックを解除するには

f.WindowButtonDownFcn = '';

とします。


figC は figure 全体の中での位置なので、データ座標を求めるには変換が必要です。axC はデータ座標です。

プロットデータが欲しい場合は、どのラインの近くをクリックしたか等の判定が必要になります。

座標変換は例えば

ax.Units = "normalized";
axWidth = f.Position(3) * ax.Position(3);
axHeight = f.Position(4) * ax.Position(4);
axSX = f.Position(3) * ax.Position(1);
axSY = f.Position(4) * ax.Position(2);
axXmin = ax.XLim(1);
axXmax = ax.XLim(2);
axYmin = ax.YLim(1);
axYmax = ax.YLim(2);

とすれば、

x = (C(1) - axSX) / axWidth * (axXmax - axXmin) + axXmin;
y = (C(2) - axSY) / axHeight * (axYmax - axYmin) + axYmin;
pos = [x, y];

で求まります。

クリックボタンの判別も、コールバック関数内で

last_seltype = src.SelectionType;

とすればできます。
 normal:左クリック
 alt:右クリック
です。

f.ButtonDownFcn もありますが、これは axes 内は反応しないので、データを取る場合に使うことはないかと思います。


以下に、テスト用のスクリプト click_test_figure.m を示します。

function click_test_figure
f = figure;
f.WindowButtonDownFcn = @getfigWinPos;
f.ButtonDownFcn = @getfigPos;
p = plot(rand(10,3),'ButtonDownFcn',@lineCallback);

ax = gca;
ax.ButtonDownFcn = @getaxPos;

    function getfigWinPos(src,event)
        disp("*** fig WindowButtonDownFcn")
        last_seltype = src.SelectionType
        event
        figC = get (gcf, 'CurrentPoint')
        axC = get (gca, 'CurrentPoint')
    end

    function getfigPos(src,event)
        disp("*** fig ButtonDownFcn")
        last_seltype = src.SelectionType
        event
        figC = get (gcf, 'CurrentPoint')
        axC = get (gca, 'CurrentPoint')
    end

    function getaxPos(src,event)
        disp("*** ax ButtonDownFcn")
        event
        figC = get (gcf, 'CurrentPoint')
        axC = get (gca, 'CurrentPoint')
    end

    function lineCallback(src,event)
        if event.Button == 1
            src.LineWidth = 2;
            d = get(gca,'CurrentPoint')
            dt = datatip(src,d(1,1),d(1,2))
            pause(0.2)
            src.LineWidth = 0.5;
        end
    end

end

データ点以外の axes 内をクリックした場合

*** fig WindowButtonDownFcn

last_seltype =

    'normal'


event = 

  WindowMouseData のプロパティ:

       Source: [1×1 Figure]
    EventName: 'WindowMousePress'


figC =

   144   202


axC =

    2.4620    0.4534    1.0000
    2.4620    0.4534   -1.0000

axes 外をクリックした場合

*** fig WindowButtonDownFcn

last_seltype =

    'normal'


event = 

  WindowMouseData のプロパティ:

       Source: [1×1 Figure]
    EventName: 'WindowMousePress'


figC =

    34   308


axC =

    0.1809    0.7624    1.0000
    0.1809    0.7624   -1.0000

*** fig ButtonDownFcn

last_seltype =

    'normal'


event = 

  MouseData のプロパティ:

       Source: [1×1 Figure]
    EventName: 'ButtonDown'


figC =

    34   308


axC =

    0.1809    0.7624    1.0000
    0.1809    0.7624   -1.0000

lineCallback に関しては、plot の項で説明します。

figure には他のシステムコールバックも用意されており、用途に応じて柔軟な制御が可能です。
WindowButtonDownFcn
WindowButtonMotionFcn
WindowButtonUpFcn
WindowKeyPressFcn
WindowKeyReleaseFcn
WindowScrollWheelFcn


WindowButtonDownFcn、WindowButtonMotionFcn を使い、フィルター周波数・バンド幅をマウスで操作するアプリ例をこの記事に書いています。

さらに WindowButtonUpFcn も使ったお絵かきアプリの例はこちらに。


Axes

ax = gca;
p = plot(ax,rand(10,3));
ax.ButtonDownFcn = "disp('axButtonDown')";

これは、axes 内、データ点以外で呼び出されます。
したがって、データを取ることはできません。


Plot

データの近くでクリックしたときだけコールバックが呼び出されます。
その座標を datatip 関数に入れることにより、実際のデータを得ることができます。
プロットデータを取りたい場合に最も適した方法です。

クリックの種類は、コールバック内で引数のイベントの種類で判別できます。
左クリックは1、右クリックは3です。

以下は、データ近くをクリックすると選択したラインの太さを一時的に変え、近くのデータをデータヒントとして表示する例です。

p = plot(rand(10,3),'ButtonDownFcn',@lineCallback);

function lineCallback(src,event)
if event.Button == 1
    src.LineWidth = 2;
    d = get(gca,'CurrentPoint');
    d(1,1)
    d(1,2)
    fprintf("Clicked\t:X=%f Y=%f\n",d(1,1),d(1,2))
    dt = datatip(src,d(1,1),d(1,2));
    fprintf("data\t:X=%f Y=%f\n",dt.X,dt.Y)
    pause(0.2)
    src.LineWidth = 0.5;
end
end
実行例
Clicked	:X=2.017544 Y=0.902000
data	:X=2.000000 Y=0.923380


また、PickableParts、 HitTest プロパティに設定によって、制御を親に渡したりすることもできます。

詳しくはこちらをご参照ください。


関数プロット(fplot / fimplicit)

これらの関数は、データ点を指定することなく、適宜関数のグラフを描画してくれます。

データの密度は MeshDensity プロパティで指定できますが、ズーム操作によっても適宜変更されます。

MeshDensity とズーム率でデータ点は自動的に決まってしまう制限はありますが、datatip 関数を使うと、自動的にクリック点に近い関数の値を取ることができて便利です。

クリック可能な点データは

fp.XData / YData に入っています。

クリックすると、fp.Children に DataTip が追加されていきます。

プロットオブジェクトにコールバックを設定することにより、プロットが複数あっても、src 引数でどのプロットが選択されたかを識別できます。

表示形式のカスタマイズは、上のデータヒントカスタマイズと同じです。

行を追加したい場合は dataTipTextRow を使って
raw = dataTipTextRow(label,value) の形式で指定します。

fp.DataTipTemplate.DataTipRows(end+1) = row;


無名関数を使うことも可能です。

row = dataTipTextRow("X + Y", @(x,y)(x+y));
fp.DataTipTemplate.DataTipRows(end+1) = row;

この場合、x,y はそれぞれオブジェクトの XData / YData を示すことに注意してください。
他に x,y を変数として使っていても、それとは異なります。

表示フォーマットも指定できます。

f = @(x,y) x.^5 + y.^6 - 1;
fp = fimplicit(f);
axis equal

y2 = cos(fp.XData);
row = dataTipTextRow('Cosine',y2,'%+4.4g');
fp.DataTipTemplate.DataTipRows(end+1) = row;
表示フォーマット指定


ただなぜか、関数プロットの場合、各ラインの初回クリック時にデータヒントが表示されないようです。
(どこか1回クリックすると表示される)

対策としては、
dt.Visible = "off";
 ~
dt.Visible = "on";
を入れておくと、一回目から表示されます。
間に何か処理か pause を入れないとダメなようですが。

データ点取得にのみ datatip を使い、表示は text を使うという手もあります。

figure(Visible="on");
r_values = 1:3;
axis equal
hold on;
fp = arrayfun(@(r) fimplicit(@(x,y) x.^2 + y.^2 - r.^2, [-r r -r r]), r_values);
hold off;
for Ia=r_values
    set(fp(Ia), 'ButtonDownFcn',@(src, event) lineCallback(src,event))
    % append new data tip if necessary
    row = dataTipTextRow("X + Y", @(x,y)(x+y));
    fp(Ia).DataTipTemplate.DataTipRows(end+1) = row;
end


function lineCallback(src,event)
if event.Button == 1 % left click
    C = get (gca, 'CurrentPoint');
    x = C(1, 1);  y = C(1, 2);
    dt = datatip(src,x,y);
    dataX = dt.X;  dataY = dt.Y;
    z = dataX + dataY

    % if no need datatip display
    dt.Visible = "off";
    hold on
    plot(dataX,dataY,'x',MarkerSize=4, MarkerEdgeColor="black" )
    hold off

    text(dataX+0.1, dataY, "z = " + num2str(z));
end
end
text 表示の例


あとがき

MATLAB でクリック座標を所得する方法を、データヒント機能と一緒にまとめてみました。

色々あってちょっと複雑かもしれませんが、データ点取得以外のマウスインタラクティブ処理もしたいのであれば figure の各種コールバック、グラフデータを取るなら plot の @lineCallback が便利です。datatip を併用することにより、実際のデータ座標も簡単に取ることができます。

こういうことができるということを覚えておくと、何かの時に役に立つかもしれません。(u_u)


タイトル画像モデル:結貴
The title image was created using Adobe Generative Fill based on this picture.

Original Image

いいなと思ったら応援しよう!