本記事は図形をグリグリ動かせるアプリケーションを作るために図形ライブラリ使ってみた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をインストールしましょう。
ソースコード
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を採用するうえでネックになってきそうな印象です。
一方で十分に使いこなせるレベルまで習熟すると、支払ったコストに十分に見合う万能な力って感じでした!
参考
https://github.com/fujarenpaw/codeForBlog/tree/main/code/electron/d3js