図形をグリグリ動かすためのD3.js+ Electron入門

図形をグリグリ動かすためのD3+ Electron入門

本記事は図形をグリグリ動かせるアプリケーションを作るために図形ライブラリ使ってみたD3.js編です。

JavaScriptを使っているもののデスクトップアプリケーションを作りたかったのでElectronを使用してます。

図形をグリグリ動かせる!無料JS図形ライブラリ5選比較してみた

図形をグリグリ動かすためのSigma.js+ Electron入門

図形をグリグリ動かすためのJSPlumb+ Electron入門

図形をグリグリ動かすためのCytoscape+ Electron入門

図形をグリグリ動かすためのmxGraph+ Electron入門

本記事👉 図形をグリグリ動かすためのD3.js+ Electron入門

今回お試しで作ったやつはこんな感じ

D3の使い方

Node.jsのインストール

本記事では、electron上で目的のライブラリを動作するため、まずはelectronを動かすうえで必要となるnode.jsをインストールしましょう。

Nodejs – どこでもJavaScriptを使おう

ソースコード

package.json

{
  "name": "sample",
  "version": "1.0.0",
  "main": "main.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "electron ."
  },
  "license": "ISC",
  "devDependencies": {
    "electron": "^35.1.4"
  },
  "dependencies": {
    "d3": "^7.9.0"
  }
}

main.js   

const { app, BrowserWindow } = require('electron')
const path = require('path')

function createWindow () {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true
    }
  })

  win.loadFile('index.html')
}

app.whenReady().then(() => {
  createWindow()
  
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow()
    }
  })
})

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>D3.js Example</title>
    <style>
        body {
            font-family: Arial, sans-serif; 
            margin: 0;
            padding: 20px;
            background-color: #f5f5f5;
        }
        .container {
            display: flex;
            gap: 20px;
        }
        .control-panel {
            background-color: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            width: 300px;
        }
        .instruction {
            margin-bottom: 15px;
            padding: 10px;
            background-color: #f8f9fa;
            border-radius: 4px;
        }
        .color-palette {
            display: flex;
            gap: 10px;
            margin-top: 15px;
        }
        .color-box {
            width: 30px;
            height: 30px;
            border-radius: 4px;
            cursor: pointer;
        }
        .status {
            margin-top: 20px;
            padding: 10px;
            background-color: #e9ecef;
            border-radius: 4px;
        }
        .feature-list {
            list-style-type: none;
            padding: 0;
        }
        .feature-list li {
            margin-bottom: 10px;
            padding: 8px;
            background-color: #f8f9fa;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="control-panel">
            <h2>操作説明</h2>
            <ul class="feature-list">
                <li>🖱️ ドラッグ: 円を移動</li>
                <li>🖱️ ホイール: 円のサイズ変更</li>
                <li>🖱️ ダブルクリック: 透明度切り替え</li>
                <li>🖱️ 右クリック: 色変更</li>
                <li>🖱️ マウスオーバー: 円の強調表示</li>
            </ul>
            
            <div class="color-palette">
                <div class="color-box" style="background-color: steelblue;"></div>
                <div class="color-box" style="background-color: red;"></div>
                <div class="color-box" style="background-color: green;"></div>
                <div class="color-box" style="background-color: purple;"></div>
                <div class="color-box" style="background-color: orange;"></div>
            </div>

            <div class="status">
                <h3>現在の状態</h3>
                <p>円の数: <span id="circleCount">5</span></p>
                <p>接続線の数: <span id="connectionCount">10</span></p>
            </div>
        </div>
        <div id="visualization"></div>
    </div>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script src="script.js"></script>
</body>
</html>

script.js

const width = 800;
const height = 600;

// SVG要素の作成
const svg = d3.select("#visualization")
    .append("svg")
    .attr("width", width)
    .attr("height", height);

// 接続線用のグループ
const linksGroup = svg.append("g");

// ドラッグ機能の定義
const drag = d3.drag()
    .on("start", function() {
        d3.select(this).classed("dragging", true);
    })
    .on("drag", function(event) {
        d3.select(this)
            .attr("cx", event.x)
            .attr("cy", event.y);
        updateConnections();
        updateStatus();
    })
    .on("end", function() {
        d3.select(this).classed("dragging", false);
    });

// 円のデータを生成
const circleData = d3.range(5).map(i => ({
    id: i,
    value: Math.floor(Math.random() * 100),
    group: Math.floor(i / 2)
}));

// カラースケールの定義
const colorScale = d3.scaleOrdinal()
    .domain([0, 1])
    .range(["steelblue", "orange"]);

// 円の作成
const circles = svg.selectAll("circle")
    .data(circleData)
    .enter()
    .append("circle")
    .attr("r", 30)
    .attr("cx", () => Math.random() * (width - 60) + 30)
    .attr("cy", () => Math.random() * (height - 60) + 30)
    .attr("fill", d => colorScale(d.group))
    .attr("opacity", 1)
    .attr("data-id", d => d.id)
    .call(drag);

// ラベルの作成
const labels = svg.selectAll("text")
    .data(circleData)
    .enter()
    .append("text")
    .text(d => d.value)
    .attr("text-anchor", "middle")
    .attr("dy", ".3em")
    .style("fill", "white")
    .style("pointer-events", "none");

// 接続線の更新関数
function updateConnections() {
    const links = [];
    circles.each(function(d1, i) {
        circles.each(function(d2, j) {
            if (i < j) {
                links.push({
                    source: d1,
                    target: d2
                });
            }
        });
    });

    const lines = linksGroup.selectAll("line")
        .data(links);

    lines.enter()
        .append("line")
        .merge(lines)
        .attr("stroke", "gray")
        .attr("stroke-width", 1)
        .attr("x1", d => d3.select(`circle[data-id="${d.source.id}"]`).attr("cx"))
        .attr("y1", d => d3.select(`circle[data-id="${d.source.id}"]`).attr("cy"))
        .attr("x2", d => d3.select(`circle[data-id="${d.target.id}"]`).attr("cx"))
        .attr("y2", d => d3.select(`circle[data-id="${d.target.id}"]`).attr("cy"));

    lines.exit().remove();
    
    // 接続線の数を更新
    d3.select("#connectionCount").text(links.length);
}

// 状態更新関数
function updateStatus() {
    d3.select("#circleCount").text(circles.size());
}

// 初期接続線の描画
updateConnections();
updateStatus();

// ホイールスクロールでのサイズ変更
circles.on("wheel", function(event) {
    event.preventDefault();
    const currentRadius = +d3.select(this).attr("r");
    const newRadius = Math.max(10, Math.min(50, currentRadius + (event.deltaY > 0 ? -5 : 5)));
    d3.select(this)
        .attr("r", newRadius)
        .transition()
        .duration(200)
        .attr("r", newRadius);
});

// ダブルクリックでの透明度変更
circles.on("dblclick", function() {
    const currentOpacity = +d3.select(this).attr("opacity");
    const newOpacity = currentOpacity === 1 ? 0.5 : 1;
    d3.select(this)
        .transition()
        .duration(500)
        .attr("opacity", newOpacity);
});

// 右クリックでの色変更
circles.on("contextmenu", function(event) {
    event.preventDefault();
    const colors = ["steelblue", "red", "green", "purple", "orange"];
    const currentColor = d3.select(this).attr("fill");
    const currentIndex = colors.indexOf(currentColor);
    const nextColor = colors[(currentIndex + 1) % colors.length];
    d3.select(this)
        .transition()
        .duration(500)
        .attr("fill", nextColor);
});

// ラベルの位置更新
function updateLabels() {
    labels.attr("x", d => d3.select(`circle[data-id="${d.id}"]`).attr("cx"))
          .attr("y", d => d3.select(`circle[data-id="${d.id}"]`).attr("cy"));
}

// アニメーション効果
circles.on("mouseover", function() {
    d3.select(this)
        .transition()
        .duration(200)
        .attr("r", 35)
        .attr("stroke", "black")
        .attr("stroke-width", 2);
})
.on("mouseout", function() {
    d3.select(this)
        .transition()
        .duration(200)
        .attr("r", 30)
        .attr("stroke", "none");
});

// 定期的なラベル更新
d3.interval(updateLabels, 100);

// カラーパレットのクリックイベント
d3.selectAll(".color-box").on("click", function() {
    const color = d3.select(this).style("background-color");
    circles.transition()
        .duration(500)
        .attr("fill", color);
}); 

ソースコード解説

基本的なセットアップ

const width = 800;
const height = 600;

// SVG要素の作成
const svg = d3.select("#visualization")
    .append("svg")
    .attr("width", width)
    .attr("height", height);
  • d3.select()でDOM要素を選択

  • SVGコンテナを作成し、サイズを設定

データバインディング

const circleData = d3.range(5).map(i => ({
    id: i,
    value: Math.floor(Math.random() * 100),
    group: Math.floor(i / 2)
}));

const circles = svg.selectAll("circle")
    .data(circleData)
    .enter()
    .append("circle")
  • d3.range()でデータを生成

  • selectAll().data().enter().append()パターンでデータと要素を紐付け

インタラクションの実装

const drag = d3.drag()
    .on("start", function() {
        d3.select(this).classed("dragging", true);
    })
    .on("drag", function(event) {
        d3.select(this)
            .attr("cx", event.x)
            .attr("cy", event.y);
    });
  • d3.drag()でドラッグ機能を実装

  • イベントハンドラで要素の位置を更新

データの動的更新

function updateConnections() {
    const links = [];
    circles.each(function(d1, i) {
        circles.each(function(d2, j) {
            if (i < j) {
                links.push({
                    source: d1,
                    target: d2
                });
            }
        });
    });
    // ...
}
  • データの変更に応じて可視化を更新

  • 要素の追加・削除・更新を管理

実行

ライブラリのインストール

npm install

アプリを実行!

npm start

終わりに

今回お試ししたライブラリの中でD3.jsは一番機能が充実していると感じたもののはソースコード量が多い・・・。

図形ライブラリ使いたいときって、あまり高頻度ではなくスポット的に作ってみたい!ってことが多い気がするので、覚える事の多さやソースコード量の多さはD3を採用するうえでネックになってきそうな印象です。

一方で十分に使いこなせるレベルまで習熟すると、支払ったコストに十分に見合う万能な力って感じでした!

参考

D3

https://github.com/fujarenpaw/codeForBlog/tree/main/code/electron/d3js

本記事で使用したソースコードはGithubにあります。

最新情報をチェックしよう!