1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

エンジニアのスキルマップをグラフ構造で描きたい

Posted at

背景

  • エンジニア業務では複数のスキルが求められる
    • コーディング能力、アーキテクチャ設計能力、コミュニケーション能力などなど
  • これらのスキル間には因果関係や重みづけが存在する
    • 学ぶ順序もある。順方向のみでない。多方向のものもある
    • 2次元マップでは表現ができない😭😭😭

対応策

グラフ構造でスキルマップ描く。重みづけも方向も表現できる!😊😊😊

概要

  • スキルをグラフ構造でモデル化(スキルグラフ)
    • 各能力をスキルとしてノードに定義
    • スキル間の因果関係をエッジで表現

イメージこんな感じ

image.png

使用するライブラリ

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!👋👋👋

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?