キャンバスとJavaScriptでインタラクティブグラフを描く


私の仕事では、SNMP、NetFlow、syslogのような様々なツールを通してネットワーク操作とインフラストラクチャを監視します.など顧客のネットワーク上で何が起こっているかを理解する方法の一つは、グラフを介してそれを視覚化することです!これを行うにはいくつかの偉大なライブラリがありますが、私はかなり頻繁に使用する主なものですd3.js .

しかし、これはD 3についてのポストではありませんCanvas 画面に物を描く.より具体的には、グラフ内の一連の接続ノードを描画し、これらのノードをドラッグすることができます.始めましょう!

描画ノード


まず最初に我々がする必要があります私たちのキャンバスのセットアップです.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Map</title>
    <link rel="stylesheet" href="index.css">
    <script defer type="text/javascript" src="load.js"></script>
</head>
<body>
    <canvas></canvas>
</body>
</html>
/** index.css */
:root {
    --root-font-size: 12px;
    --bg: #fafafa;
}

/** Reset */
html, body, nav, ul, h1, h2, h3, h4, a, canvas {
    margin: 0px;
    padding: 0px;
    color: var(--text-color);
}
html, body {
    font-family: Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    font-size: var(--root-font-size);
    background: var(--bg);
    height: 100%;
    width: 100%;
    overflow: hidden;
}
*, body, button, input, select, textarea, canvas {
    text-rendering: optimizeLegibility;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    outline: 0;
}
そして今我々のJavaScript⬇️ 我々は、我々が描きたいノードの配列を保つことによって出発するつもりです.ノードはx , y , radius , fill , strokeからなる.これらのプロパティは、それらを描画するときにキャンバスAPIメソッドに対応します.
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');

var nodes = [];

function resize() {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
}

window.onresize = resize;
resize();
さあ行きましょうdrawNode 今すぐ機能.我々は、使用するつもりですarc 関数は、円、円の半径と角度で描画する.また、fill, stroke . 我々は円弧で円を生成しているので、我々は全体の形をカプセル化することを望むPath これが私たちがBeginPath関数を使用している理由です.
function drawNode(node) {
    context.beginPath();
    context.fillStyle = node.fillStyle;
    context.arc(node.x, node.y, node.radius, 0, Math.PI * 2, true);
    context.strokeStyle = node.strokeStyle;
    context.stroke();
    context.fill();
}

マウス関数


我々はこれがインタラクティブであることを望むので、ユーザーがタッチしたり、キャンバス上でクリックすると、カーソルの位置に右のノードを描画するときに追跡する機能を追加しましょう.
function click(e) {
    let node = {
        x: e.x,
        y: e.y,
        radius: 10,
        fillStyle: '#22cccc',
        strokeStyle: '#009999'
    };
    nodes.push(node);
    drawNode(node);
}

window.onclick = click;

すごい!今、我々はいくつかのノードが画面に描かれているが、我々は彼らを回避するための任意の方法を持っていない.mouseoveでターゲットの位置を利用してmouseoveで移動しましょう.
var selection = undefined;

function within(x, y) {
    return nodes.find(n => {
        return x > (n.x - n.radius) && 
            y > (n.y - n.radius) &&
            x < (n.x + n.radius) &&
            y < (n.y + n.radius);
    });
}

function move(e) {
    if (selection) {
        selection.x = e.x;
        selection.y = e.y;
        drawNode(selection);
    }
}

function down(e) {
    let target = within(e.x, e.y);
    if (target) {
        selection = target;
    }
}

function up(e) {
    selection = undefined;
}

window.onmousemove = move;
window.onmousedown = down;
window.onmouseup = up;

バグフィックス


ドラッグしてノードをレンダリングします


あー!これを修正する必要があるので、これが起こるたびにすべてのノードを再描画します.これを行うには、ほんの少しのビットを加える必要がありますclearRect を描画するdrawNode 我々は、ちょうどそれをドローと呼びます.
function click(e) {
    let node = {
        x: e.x,
        y: e.y,
        radius: 10,
        fillStyle: '#22cccc',
        strokeStyle: '#009999'
    };
    nodes.push(node);
    draw();
}

function move(e) {
    if (selection) {
        selection.x = e.x;
        selection.y = e.y;
        draw();
    }
}

function draw() {
    context.clearRect(0, 0, window.innerWidth, window.innerHeight);
    for (let i = 0; i < nodes.length; i++) {
        let node = nodes[i];
        context.beginPath();
        context.fillStyle = node.fillStyle;
        context.arc(node.x, node.y, node.radius, 0, Math.PI * 2, true);
        context.strokeStyle = node.strokeStyle;
        context.fill();
        context.stroke();
    }
}

クリックしてドラッグすると重複ノードを作成できます


これは非常によく動作しますが、問題は私たちがすぐにノードをクリックしたときに我々はmousedownして移動するときに表示されます.代わりに、新しいノードを作成したいときに、状態をクリアするMotionイベントに依存してみましょう.
私たちは窓を取り外すつもりだ.OnClickとコードをクリックし、代わりにmousedown , mouseup , mousemove 選択を扱うイベントは、状態を作成します.時mouseup イベントが何も選択されていない場合に発生し、まだ移動されていない場合は新しいノードを作成します.
/** remove the onclick code and update move and up code */
function move(e) {
    if (selection) {
        selection.x = e.x;
        selection.y = e.y;
        selection.moving = true;
        draw();
    }
}

function up(e) {
    if (!selection || !selection.moving) {
        let node = {
            x: e.x,
            y: e.y,
            radius: 10,
            fillStyle: '#22cccc',
            strokeStyle: '#009999',
            selectedFill: '#88aaaa'
        };
        nodes.push(node);
        draw();
    }
    if (selection) {
        delete selection.moving;
        delete selection.selected;
    }
    selection = undefined;
    draw();
}

すごい!あなたが更新するならdraw コードをキーオフにするselected このようにフィルを変更できます
context.fillStyle = node.selected ? node.selectedFill : node.fillStyle;

接続の追加


次のことは、このグラフのいくつかの端にある.私たちは1つのノードから別のノードに行を接続することができます.これを行うには、今のところ単純な行を使用し、これらの接続を定義するエッジ配列を持ちます.
我々が成し遂げたい行動は

  • マウスが表示され、マウスが現在選択中である➡️ 更新の選択xとy

  • MouseDown、選択された状態を選択し、選択した状態を選択し、選択した状態を設定して

  • MouseUp、選択がない場合は、新しいノードを作成して描画します.そうでない場合は、現在の選択が選択されていない(マウスダウンのため)選択をクリアしてから描画します

  • さらに、選択が新しいノードに変わるとき、mousedownして、我々はすでに選択される何かを持っています
  • function move(e) {
        if (selection && e.buttons) {
            selection.x = e.x;
            selection.y = e.y;
            draw();
        }
    }
    
    function down(e) {
        let target = within(e.x, e.y);
        if (selection && selection.selected) {
            selection.selected = false;
        }
        if (target) {
            selection = target;
            selection.selected = true;
            draw();
        }
    }
    
    function up(e) {
        if (!selection) {
            let node = {
                x: e.x,
                y: e.y,
                radius: 10,
                fillStyle: '#22cccc',
                strokeStyle: '#009999',
                selectedFill: '#88aaaa',
                selected: false
            };
            nodes.push(node);
            draw();
        }
        if (selection && !selection.selected) {
            selection = undefined;
        }
        draw();
    }
    
    これは以前とほぼ同じ結果です.私が望むことは、現在の選択と新しい選択が新しい端と線をつくるように我々が端を加えることができるということです.
    var edges = [];
    
    function draw() {
        context.clearRect(0, 0, window.innerWidth, window.innerHeight);
    
        for (let i = 0; i < edges.length; i++) {
            let fromNode = edges[i].from;
            let toNode = edges[i].to;
            context.beginPath();
            context.strokeStyle = fromNode.strokeStyle;
            context.moveTo(fromNode.x, fromNode.y);
            context.lineTo(toNode.x, toNode.y);
            context.stroke();
        }
    
        for (let i = 0; i < nodes.length; i++) {
            let node = nodes[i];
            context.beginPath();
            context.fillStyle = node.selected ? node.selectedFill : node.fillStyle;
            context.arc(node.x, node.y, node.radius, 0, Math.PI * 2, true);
            context.strokeStyle = node.strokeStyle;
            context.fill();
            context.stroke();
        }
    }
    
    function down(e) {
        let target = within(e.x, e.y);
        if (selection && selection.selected) {
            selection.selected = false;
        }
        if (target) {
            if (selection && selection !== target) {
                edges.push({ from: selection, to: target });
            }
            selection = target;
            selection.selected = true;
            draw();
        }
    }
    

    それだ!今、我々はノード間のいくつかの端を持っている!このポストへのフォローアップにおいて、私はベジエ曲線について話します、そして、どのように、あなたはこれらのカーブの間で若干のきちんとした滑らかな内挿を作成することができます.
    乾杯!🍻
    あなたがこの記事が好きならば、私にフォローとハート/ユニコーンを与えてください.また、もしあなたが傾斜している場合は、同様に、他の更新プログラムをチェック!
    あなたがこのキャンバスチュートリアルを楽しんだならば、下記のキャンバスAPIの上で私の他の記事をチェックしてください

    再びありがとう!🏕