背景
- エンジニア業務では複数のスキルが求められる
- コーディング能力、アーキテクチャ設計能力、コミュニケーション能力などなど
- これらのスキル間には因果関係や重みづけが存在する
- 学ぶ順序もある。順方向のみでない。多方向のものもある
- 2次元マップでは表現ができない😭😭😭
対応策
グラフ構造でスキルマップ描く。重みづけも方向も表現できる!😊😊😊
概要
- スキルをグラフ構造でモデル化(スキルグラフ)
- 各能力をスキルとしてノードに定義
- スキル間の因果関係をエッジで表現
イメージこんな感じ
使用するライブラリ
d3.jsをつかいます。
実装
データ部
const skillData = {
nodes: [
// 基本系スキル
// mr: 習熟度(Mastery Rate), cr: 重要度(Criticality Rate)
{ id: 'js_syntax', label: 'JavaScript文法', mr: 0.90, cr: 0.60, category: 'basic' },
{ id: 'git', label: 'Git操作', mr: 0.85, cr: 0.65, category: 'basic' },
{ id: 'terminal', label: 'ターミナル操作', mr: 0.82, cr: 0.60, category: 'basic' },
{ id: 'html_css', label: 'HTML/CSS', mr: 0.88, cr: 0.65, category: 'basic' },
{ id: 'npm', label: 'パッケージ管理', mr: 0.80, cr: 0.70, category: 'basic' },
// 実践系スキル
{ id: 'component_dev', label: 'コンポーネント開発', mr: 0.75, cr: 0.80, category: 'practical' },
{ id: 'state_management', label: '状態管理', mr: 0.72, cr: 0.85, category: 'practical' },
{ id: 'api_integration', label: 'API連携', mr: 0.70, cr: 0.80, category: 'practical' },
{ id: 'testing', label: 'テスト実装', mr: 0.68, cr: 0.75, category: 'practical' },
{ id: 'debugging', label: 'デバッグ', mr: 0.73, cr: 0.80, category: 'practical' },
// 戦略系スキル
{ id: 'arch_design', label: 'アーキテクチャ設計', mr: 0.60, cr: 0.90, category: 'strategic' },
{ id: 'perf_optimization', label: 'パフォーマンス最適化', mr: 0.65, cr: 0.85, category: 'strategic' },
{ id: 'security', label: 'セキュリティ設計', mr: 0.58, cr: 0.88, category: 'strategic' },
{ id: 'proj_management', label: 'プロジェクト管理', mr: 0.62, cr: 0.85, category: 'strategic' },
{ id: 'tech_selection', label: '技術選定', mr: 0.64, cr: 0.87, category: 'strategic' }
],
edges: [
// 基本→基本の関係
{ source: 'js_syntax', target: 'html_css', weight: 0.5 },
{ source: 'terminal', target: 'git', weight: 0.7 },
{ source: 'npm', target: 'git', weight: 0.6 },
// 基本→実践の関係
{ source: 'js_syntax', target: 'component_dev', weight: 0.9 },
{ source: 'js_syntax', target: 'debugging', weight: 0.8 },
{ source: 'html_css', target: 'component_dev', weight: 0.7 },
{ source: 'npm', target: 'api_integration', weight: 0.6 },
// 実践→実践の関係
{ source: 'component_dev', target: 'state_management', weight: 0.8 },
{ source: 'api_integration', target: 'state_management', weight: 0.7 },
{ source: 'debugging', target: 'testing', weight: 0.6 },
// 実践→戦略の関係
{ source: 'component_dev', target: 'arch_design', weight: 0.7 },
{ source: 'state_management', target: 'perf_optimization', weight: 0.8 },
{ source: 'testing', target: 'security', weight: 0.6 },
{ source: 'api_integration', target: 'tech_selection', weight: 0.5 },
// 戦略→戦略の関係
{ source: 'arch_design', target: 'perf_optimization', weight: 0.7 },
{ source: 'security', target: 'arch_design', weight: 0.8 },
{ source: 'tech_selection', target: 'proj_management', weight: 0.6 }
]
};
描画部
import React, { useEffect, useState, useRef } from 'react';
import * as d3 from 'd3';
// スキルグラフのメインコンポーネント
const ComprehensiveSkillGraph = () => {
// SVG要素への参照を保持するref
const svgRef = useRef(null);
// 選択されたノードの状態を管理
const [selectedNode, setSelectedNode] = useState(null);
const skillData = { //省略
};
useEffect(() => {
// SVG要素が存在しない場合は処理を中断
if (!svgRef.current) return;
// 既存のSVG要素内のコンテンツをクリア
d3.select(svgRef.current).selectAll("*").remove();
// SVGのビューポートサイズを設定
const width = 800;
const height = 600;
const svg = d3.select(svgRef.current);
// カテゴリーごとの中心位置を設定
// 画面を3分割し、各カテゴリーを水平方向に配置
const categoryPositions = {
basic: { x: width * 0.2, y: height * 0.5 }, // 左側: 基本スキル
practical: { x: width * 0.5, y: height * 0.5 }, // 中央: 実践スキル
strategic: { x: width * 0.8, y: height * 0.5 } // 右側: 戦略スキル
};
// D3.jsのForce Simulationの設定
// ノードの配置とインタラクションを物理シミュレーションで制御
const simulation = d3.forceSimulation(skillData.nodes)
// リンク(エッジ)の力を設定:関連の強さに応じて距離を調整
.force('link', d3.forceLink(skillData.edges)
.id(d => d.id)
.distance(d => 120 - d.weight * 40)) // weightが大きいほど近づく
// ノード間の反発力を設定:-200は反発の強さ
.force('charge', d3.forceManyBody().strength(-200))
// X軸方向の力:各カテゴリーの中心に向かう力
.force('x', d3.forceX().x(d => categoryPositions[d.category].x).strength(0.1))
// Y軸方向の力:垂直方向の中心に向かう力
.force('y', d3.forceY().y(d => categoryPositions[d.category].y).strength(0.1))
// ノード同士の衝突を防ぐ力:半径50pxの範囲で衝突判定
.force('collision', d3.forceCollide().radius(50));
// 矢印マーカーの定義
// エッジの終端に表示される矢印の形状を定義
svg.append('defs').append('marker')
.attr('id', 'arrowhead')
.attr('viewBox', '-0 -5 10 10') // 矢印の表示領域を設定
.attr('refX', 30) // 矢印の位置調整(ノードとの距離)
.attr('refY', 0) // 矢印の垂直位置
.attr('orient', 'auto') // 矢印の向きを自動調整
.attr('markerWidth', 6) // 矢印の幅
.attr('markerHeight', 6) // 矢印の高さ
.append('path')
.attr('d', 'M 0,-5 L 10,0 L 0,5') // 矢印の形状をパスで定義
.attr('fill', '#999'); // 矢印の色
// SVGコンテナの作成
// ズームとパンに対応するためのグループ要素
const container = svg.append('g');
// ズーム機能の追加
const zoom = d3.zoom()
.scaleExtent([0.5, 2]) // ズームの範囲を制限 0.5倍から2倍までのズームを許可
.on('zoom', (event) => {
// ズーム時にコンテナ全体を変換
container.attr('transform', event.transform);
});
svg.call(zoom); // SVG要素にズーム機能を適用
// カテゴリー背景の作成
// 各スキルカテゴリーの領域を視覚的に区分
const categoryGroups = container.selectAll('.category-group')
.data(['basic', 'practical', 'strategic']) // カテゴリーデータをバインド
.enter()
.append('g')
.attr('class', 'category-group');
// カテゴリー領域の背景矩形を追加
// 半透明の背景色で各カテゴリーを区分
categoryGroups.append('rect')
.attr('x', d => categoryPositions[d].x - 150) // 中心から左右に150pxの幅
.attr('y', 50) // 上端の余白
.attr('width', 300) // 領域の幅
.attr('height', height - 100) // 領域の高さ
.attr('fill', d => {
// カテゴリーごとに異なる色を設定(20%の不透明度)
switch(d) {
case 'basic': return '#8884d820';
case 'practical': return '#82ca9d20';
case 'strategic': return '#ffc65820';
}
})
.attr('rx', 10); // 角を丸める
// カテゴリーラベルの追加
categoryGroups.append('text')
.attr('x', d => categoryPositions[d].x) // カテゴリーの中心に配置
.attr('y', 30) // 上端からの距離
.attr('text-anchor', 'middle') // テキストを中央揃え
.style('font-size', '16px')
.style('fill', '#666')
.text(d => {
// カテゴリー名を日本語で表示
switch(d) {
case 'basic': return '基本系スキル';
case 'practical': return '実践系スキル';
case 'strategic': return '戦略系スキル';
}
});
// エッジの作成
// スキル間の関連性を示す矢印付きの線を描画
const edges = container.append('g')
.selectAll('line')
.data(skillData.edges) // エッジデータをD3にバインド
.join('line') // 新しい線要素を作成
.style('stroke', '#999') // 線の色を設定
.style('stroke-opacity', 0.6) // 線を半透明に
.style('stroke-width', d => d.weight * 3) // 関連の強さに応じて線の太さを変更
.attr('marker-end', 'url(#arrowhead)'); // 線の終端に矢印を追加
// ノードグループの作成
// 各スキルを表す円とラベルのコンテナ
const nodeGroups = container.append('g')
.selectAll('g')
.data(skillData.nodes) // ノードデータをD3にバインド
.join('g') // グループ要素を作成
.call(d3.drag() // ドラッグ&ドロップ機能を追加
.on('start', dragstarted) // ドラッグ開始時の処理
.on('drag', dragged) // ドラッグ中の処理
.on('end', dragended)); // ドラッグ終了時の処理
// ノードの円を作成
// スキルの種類に応じてサイズと色を変更する円を描画
nodeGroups.append('circle')
.attr('r', d => {
// カテゴリーごとに異なる円の大きさを設定
switch(d.category) {
case 'basic': return 25; // 基本スキルは小さめ
case 'practical': return 30; // 実践スキルは中程度
case 'strategic': return 35; // 戦略スキルは大きめ
default: return 25;
}
})
.style('fill', d => {
// カテゴリーごとに異なる色を設定
switch(d.category) {
case 'basic': return '#8884d8'; // 紫色: 基本スキル
case 'practical': return '#82ca9d'; // 緑色: 実践スキル
case 'strategic': return '#ffc658'; // 黄色: 戦略スキル
default: return '#grey';
}
})
.style('fill-opacity', 0.7) // 円を半透明に
.style('stroke', '#fff') // 円の縁を白に
.style('stroke-width', 2) // 縁の太さを2pxに
.on('click', (event, d) => setSelectedNode(d)); // クリックでノードを選択
// ノードのラベルを作成
nodeGroups.append('text')
.text(d => d.label) // ノードのラベルテキストを設定
.attr('text-anchor', 'middle') // テキストを中央揃え
.attr('dy', '.35em') // テキストを垂直方向に微調整
.style('font-size', '10px') // フォントサイズを10pxに
.style('fill', 'white') // テキストの色を白に
.style('pointer-events', 'none'); // テキストへのマウスイベントを無効化
// シミュレーションの更新処理
// フレームごとにノードとエッジの位置を更新
simulation.on('tick', () => {
// エッジの位置を更新
edges
.attr('x1', d => d.source.x) // 開始点のX座標
.attr('y1', d => d.source.y) // 開始点のY座標
.attr('x2', d => d.target.x) // 終点のX座標
.attr('y2', d => d.target.y); // 終点のY座標
// ノードの位置を更新(グループごと移動)
nodeGroups.attr('transform', d => `translate(${d.x},${d.y})`);
});
// ドラッグ開始時の処理
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart(); // シミュレーションを再開
event.subject.fx = event.subject.x; // X座標を固定
event.subject.fy = event.subject.y; // Y座標を固定
}
// ドラッグ中の処理
function dragged(event) {
event.subject.fx = event.x; // ドラッグ位置にX座標を更新
event.subject.fy = event.y; // ドラッグ位置にY座標を更新
}
// ドラッグ終了時の処理
function dragended(event) {
if (!event.active) simulation.alphaTarget(0); // シミュレーションを徐々に停止
event.subject.fx = null; // X座標の固定を解除
event.subject.fy = null; // Y座標の固定を解除
}
return () => {
simulation.stop();
};
}, []);
return (
<div className="w-full max-w-4xl mx-auto p-4">
<div className="relative">
<svg
ref={svgRef}
className="w-full h-[600px] bg-white rounded-lg shadow"
viewBox="0 0 800 600"
/>
{selectedNode && (
<div className="absolute top-4 right-4 bg-white p-4 rounded-lg shadow">
<h3 className="font-bold text-lg mb-2">{selectedNode.label}</h3>
<div className="space-y-2">
<div>
<span className="text-gray-600">マスタリーレート:</span>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${selectedNode.mr * 100}%` }}
/>
</div>
<span className="text-sm text-gray-500">{Math.round(selectedNode.mr * 100)}%</span>
</div>
<div>
<span className="text-gray-600">チャレンジレート:</span>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full"
style={{ width: `${selectedNode.cr * 100}%` }}
/>
</div>
<span className="text-sm text-gray-500">{Math.round(selectedNode.cr * 100)}%</span>
</div>
</div>
</div>
)}
</div>
<div className="mt-4 p-4 bg-gray-100 rounded-lg">
<div className="space-y-2">
<div className="flex space-x-4">
<div className="flex items-center">
<div className="w-4 h-4 rounded-full bg-[#8884d8] opacity-70 mr-2" />
<span>基本系スキル: プログラミング言語、ツール操作等</span>
</div>
</div>
<div className="flex space-x-4">
<div className="flex items-center">
<div className="w-4 h-4 rounded-full bg-[#82ca9d] opacity-70 mr-2" />
<span>実践系スキル: 基本スキルを応用した実装能力</span>
</div>
</div>
<div className="flex space-x-4">
<div className="flex items-center">
<div className="w-4 h-4 rounded-full bg-[#ffc658] opacity-70 mr-2" />
<span>戦略系スキル: アーキテクチャ設計、プロジェクト管理等</span>
</div>
</div>
</div>
</div>
</div>
);
};
export default SkillNetworkGraph;
というプログラムをclaudeが書いてくれました。
データ部を書き換えれば、いろんなスキルグラフが描けますね。
Have a nice graph coding!👋👋👋