
Install Learning Graph Viewer
- 6 installs
- 2 repo stars
- Updated August 1, 2026
- vishalsachdev/claude-skills
Helps with ai & agent building tasks.
About
install-learning-graph-viewer is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- install-learning-graph-viewer
- AI & Agent Building
- AI-coding skill
Install Learning Graph Viewer by the numbers
- 6 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #12,739 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vishalsachdev/claude-skills --skill install-learning-graph-viewerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 6 |
|---|---|
| repo stars | ★ 2 |
| Last updated | August 1, 2026 |
| Repository | vishalsachdev/claude-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Install Learning Graph Viewer
Overview
This skill installs a complete interactive graph visualization application into the /docs/sims/graph-viewer/ directory of an intelligent textbook project. The viewer provides an interactive way to explore learning graphs with features like node search, category filtering, and real-time statistics.
When to Use This Skill
Use this skill when:
- A learning graph has been generated (learning-graph.json exists in /docs/learning-graph/)
- The textbook needs an interactive visualization tool for exploring concept dependencies
- Students or instructors need to filter, search, and analyze the learning graph structure
Prerequisites:
/docs/learning-graph/learning-graph.jsonmust exist- The JSON file must have metadata with a
titlefield - MkDocs project structure must be in place
Installation Workflow
Step 1: Verify Prerequisites
Before installation, verify that the learning graph JSON file exists:
ls /docs/learning-graph/learning-graph.jsonIf the file doesn't exist, use the learning-graph-generator skill first to create the learning graph.
Step 2: Create Directory Structure
Create the graph-viewer directory:
mkdir -p /docs/sims/graph-viewerStep 3: Install Viewer Files
Copy the four essential files from the skill's assets directory to the target location:
cp assets/main.html /docs/sims/graph-viewer/main.html
cp assets/script.js /docs/sims/graph-viewer/script.js
cp assets/local.css /docs/sims/graph-viewer/local.css
cp assets/index.md /docs/sims/graph-viewer/index.mdStep 4: Extract Title from Learning Graph JSON
Read the title from the learning graph metadata:
# Use Python or jq to extract the title
python3 -c "import json; data = json.load(open('/docs/learning-graph/learning-graph.json')); print(data.get('metadata', {}).get('title', 'Learning Graph'))"If the metadata or title field doesn't exist, use a default title like "Learning Graph" or the course name.
Step 5: Update Title in main.html
Replace the "TITLE" placeholder in main.html with the extracted title:
1. Read the extracted title from Step 4 2. In /docs/sims/graph-viewer/main.html, replace all instances of "TITLE" with the actual course title 3. This appears in two locations:
- The
<title>tag:<title>Learning Graph Viewer for TITLE</title> - The page heading:
<h4>Learning Graph for TITLE</h4>
Step 6: Update MkDocs Navigation (Optional)
If the user wants the graph viewer in the site navigation, add it to mkdocs.yml:
nav:
- MicroSims:
- List of MicroSims: sims/index.md
- Graph Viewer: sims/graph-viewer/index.mdStep 7: Inform the User
Provide the user with instructions to test the installation:
1. Run mkdocs serve to start the local development server 2. Navigate to the appropriate URL based on their repository name:
- Format:
http://localhost:8000/REPO_NAME/sims/graph-viewer/main.html - The REPO_NAME can be extracted from the git repository or inferred from the project structure
3. Alternatively, if added to navigation, they can access it through the MkDocs site menu
Viewer Features
The installed graph viewer provides:
Search Functionality:
- Type-ahead search with dropdown results
- Shows category information for each node
- Focuses and selects matching nodes in the visualization
Category Filtering:
- Sidebar legend with color-coded categories
- Checkboxes to show/hide specific taxonomy groups
- "Check All" and "Uncheck All" bulk operations
- Collapsible sidebar for expanded viewing
Real-time Statistics:
- Visible node count
- Visible edge count
- Orphaned node count (concepts with no dependencies)
Interactive Visualization:
- vis.js network graph with physics simulation
- Color-coded nodes by taxonomy category
- Directed edges showing concept dependencies
- Zoomable and draggable interface
Technical Details
File Structure:
/docs/sims/graph-viewer/
├── main.html # Main application HTML
├── script.js # JavaScript logic for visualization
├── local.css # Styling for the viewer
└── index.md # Documentation page with iframe embedDependencies:
- vis-network.js (loaded from CDN in main.html)
- learning-graph.json (loaded from ../../learning-graph/learning-graph.json)
Data Path: The script.js file loads the learning graph from a relative path: ../../learning-graph/learning-graph.json. This assumes the standard intelligent textbook structure where /docs/sims/ and /docs/learning-graph/ are siblings.
Resources
assets/
This skill includes four asset files that get copied to the target directory:
- main.html - The main viewer application HTML file with vis-network integration
- script.js - Interactive JavaScript for search, filtering, and visualization
- local.css - Stylesheet for the viewer interface
- index.md - Documentation page explaining the viewer features
These files are templates from the learning-graphs repository and are ready to use without modification (except for the TITLE placeholder in main.html).
Learning Graph Viewer
Run the Learning Graph Viewer{ .md-button .md-button--primary }
This viewer reads a learning graph data from ../../learning-graph/learning-graph.json:
1. Search Functionality - Quick node lookup with autocomplete 2. Taxonomy Legend Controls - Filter nodes by category/taxonomy
Features
Search
- Type-ahead search for node names
- Displays matching results in a dropdown
- Shows node group/category in results
- Clicking a result focuses and highlights the node on the graph
- Only searches visible nodes (respects taxonomy filters)
Taxonomy Legend with Checkboxes
- Sidebar legend with all node categories
- Toggle visibility of entire node groups
- Color-coded categories matching the graph
- "Check All" and "Uncheck All" buttons for bulk operations
- Collapsible sidebar to maximize graph viewing area
Graph Statistics
Real-time statistics that update as you filter:
- Nodes: Count of visible nodes
- Edges: Count of visible edges (both endpoints must be visible)
- Orphans: Nodes with no connections (this is an indication that the learning graph needs editing)
Sample Graph Demo
The demo includes a Graph Theory learning graph with 10 taxonomy categories:
- Foundation (Red) - Core concepts in red boxes that should be pinned to the left
- Types (Orange) - Graph types
- Representations (Gold) - Data structures
- Algorithms (Green) - Basic algorithms
- Paths (Blue) - Shortest path algorithms
- Flow (Indigo) - Network flow algorithms
- Advanced (Violet) - Advanced topics
- Metrics (Gray) - Centrality measures
- Spectral (Brown) - Spectral theory
- ML & Networks (Teal) - Machine learning
Usage Tips
1. Hide a category - Uncheck a category in the sidebar to hide all nodes in that group 2. Search within visible nodes - Use search to quickly find specific concepts among visible nodes 3. Focus on a topic - Uncheck all categories, then check only the ones you want to study 4. Collapse sidebar - Click the menu button (☰) to hide the sidebar and expand the graph view 5. Find orphans - Check the statistics to see if any nodes lack connections
Implementation Notes
This viewer follows the standard vis.js architectural patterns:
- Uses
vis.DataSetfor nodes and edges - Implements node
hiddenproperty for filtering - Combines separate search and legend features
- Updates statistics dynamically based on visibility
- Maintains consistent styling across features
Use Cases
- Course planning - Filter by topic area to design lesson sequences
- Concept exploration - Search for specific concepts and see their dependencies
- Gap analysis - Use orphan count to identify disconnected concepts
- Progressive learning - Start with foundation concepts, gradually enable advanced topics
html, body {
height: 100vh;
margin: 0;
padding: 0;
}
body {
font-family: Arial, Helvetica, sans-serif;
display: flex;
}
#sidebar-container {
min-width: 250px;
height: 100%;
display: flex;
flex-direction: column;
}
#sidebar {
width: auto;
max-width: 300px;
padding: 10px;
box-sizing: border-box;
border-right: 1px solid lightgray;
overflow-y: auto;
flex: 1;
}
#main {
flex: 1;
height: 100%;
padding: 10px;
display: flex;
flex-direction: column;
}
#search-container {
width: 100%;
margin-bottom: 10px;
position: relative;
}
#search-input {
width: 100%;
padding: 8px;
box-sizing: border-box;
font-size: 14px;
border: 1px solid silver;
border-radius: 4px;
}
#search-results {
position: absolute;
top: 100%;
left: 0;
width: 100%;
background-color: white;
border: 1px solid silver;
border-top: none;
max-height: 200px;
overflow-y: auto;
z-index: 1000;
display: none;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
#search-results div {
padding: 8px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
}
#search-results div:hover {
background-color: #f0f0f0;
}
#mynetwork {
width: 100%;
flex: 1;
background-color: aliceblue;
border: 1px solid lightgray;
min-height: 400px;
}
#toggle-button {
margin: 5px;
padding: 5px 10px;
cursor: pointer;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 3px;
}
#toggle-button:hover {
background-color: #e0e0e0;
}
.legend-table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
.legend-table td {
padding: 5px;
font-size: 16px;
}
.legend-table input[type="checkbox"] {
margin-right: 5px;
}
.color-indicator {
padding: 3px 8px;
text-align: center;
border-radius: 3px;
font-size: 11px;
}
.control-buttons {
margin: 10px 0;
}
.control-buttons button {
margin-right: 5px;
padding: 5px 10px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 3px;
}
.control-buttons button:hover {
background-color: #45a049;
}
.stats-list {
list-style: none;
padding: 0;
}
.stats-list li {
padding: 5px 0;
font-size: 14px;
}
h3 {
margin: 15px 0 10px 0;
font-size: 16px;
}
h4 {
margin: 0 0 10px 0;
font-size: 18px;
}
<!DOCTYPE html>
<html>
<head>
<title>Learning Graph Viewer for TITLE</title>
<script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
<link rel="stylesheet" href="local.css">
</head>
<body>
<div id="sidebar-container">
<button id="toggle-button" onclick="toggleSidebar()">☰</button>
<div id="sidebar">
<h3>Legend & Controls</h3>
<div class="control-buttons">
<button onclick="checkAllGroups()">Check All</button>
<button onclick="uncheckAllGroups()">Uncheck All</button>
</div>
<table class="legend-table" id="legend-table">
<!-- Legend will be dynamically generated from learning-graph.json -->
</table>
<h3>Graph Statistics</h3>
<ul class="stats-list">
<li><b>Nodes:</b> <span id="nodeCount">0</span></li>
<li><b>Edges:</b> <span id="edgeCount">0</span></li>
<li><b>Orphans:</b> <span id="orphanCount">0</span></li>
</ul>
</div>
</div>
<div id="main">
<h4 id="graph-title">Learning Graph Viewer for TITLE</h4>
<div id="search-container">
<input type="text" id="search-input" placeholder="Search nodes by name...">
<div id="search-results"></div>
</div>
<div id="mynetwork"></div>
<br/>
<a href=".">Back to Lesson Plan</a>
</div>
<script src="script.js"></script>
</body>
</html>
// Global variables
var nodes, edges, network;
// ========== UTILITY FUNCTIONS ==========
function toggleSidebar() {
const sidebar = document.getElementById("sidebar");
const sidebarContainer = document.getElementById("sidebar-container");
const toggleButton = document.getElementById("toggle-button");
if (sidebar.style.display === "none") {
sidebar.style.display = "block";
sidebarContainer.style.minWidth = "250px";
toggleButton.innerHTML = "☰";
} else {
sidebar.style.display = "none";
sidebarContainer.style.minWidth = "auto";
toggleButton.innerHTML = "→";
}
}
// Function to update statistics
function updateStatistics() {
var allNodes = nodes.get();
var allEdges = edges.get();
// Filter visible nodes
var visibleNodes = allNodes.filter(node => !node.hidden);
// Filter visible edges (both connected nodes must be visible)
var visibleEdges = allEdges.filter(edge => {
var fromNode = nodes.get(edge.from);
var toNode = nodes.get(edge.to);
return (!fromNode.hidden && !toNode.hidden);
});
var nodeCount = visibleNodes.length;
var edgeCount = visibleEdges.length;
// Find orphan nodes (nodes with no visible edges)
var connectedNodeIds = new Set();
visibleEdges.forEach(edge => {
connectedNodeIds.add(edge.from);
connectedNodeIds.add(edge.to);
});
var orphanNodes = visibleNodes.filter(node => !connectedNodeIds.has(node.id));
var orphanCount = orphanNodes.length;
// Update the HTML elements with the statistics
document.getElementById('nodeCount').textContent = nodeCount;
document.getElementById('edgeCount').textContent = edgeCount;
document.getElementById('orphanCount').textContent = orphanCount;
}
// Function to toggle groups
function toggleGroup(groupName) {
const visible = document.getElementById(`group${groupName}`).checked;
nodes.forEach(node => {
if (node.group === groupName) {
nodes.update({id: node.id, hidden: !visible});
}
});
updateStatistics();
}
// Function to check all groups
function checkAllGroups() {
const checkboxes = document.querySelectorAll('input[id^="group"]');
checkboxes.forEach(checkbox => {
if (!checkbox.checked) {
checkbox.checked = true;
toggleGroup(checkbox.id.replace('group', ''));
}
});
}
// Function to uncheck all groups
function uncheckAllGroups() {
const checkboxes = document.querySelectorAll('input[id^="group"]');
checkboxes.forEach(checkbox => {
if (checkbox.checked) {
checkbox.checked = false;
toggleGroup(checkbox.id.replace('group', ''));
}
});
}
// Helper function to get readable color name
function getColorName(color) {
const colorNames = {
'red': 'Red',
'orange': 'Orange',
'gold': 'Gold',
'green': 'Green',
'blue': 'Blue',
'indigo': 'Indigo',
'violet': 'Violet',
'gray': 'Gray',
'brown': 'Brown',
'teal': 'Teal',
'cyan': 'Cyan',
// Hex code mappings for custom taxonomies
'#FF6B6B': 'Coral Red',
'#4ECDC4': 'Turquoise',
'#45B7D1': 'Sky Blue',
'#96CEB4': 'Sage Green',
'#FFEAA7': 'Soft Yellow',
'#DDA15E': 'Bronze',
'#BC6C25': 'Brown',
'#9B59B6': 'Purple',
'#E17055': 'Terracotta',
'#74B9FF': 'Light Blue',
'#A29BFE': 'Periwinkle',
'#FD79A8': 'Pink'
};
return colorNames[color.toLowerCase()] || colorNames[color] || color;
}
// Helper function to determine if text should be white or black
function getTextColorForBackground(backgroundColor) {
// Colors that need white text (named colors)
const darkColors = ['red', 'green', 'blue', 'indigo', 'violet', 'gray', 'brown', 'cyan'];
// Hex codes that need white text (dark backgrounds)
const darkHexColors = ['#FF6B6B', '#9B59B6', '#E17055', '#BC6C25'];
return darkColors.includes(backgroundColor.toLowerCase()) || darkHexColors.includes(backgroundColor) ? 'white' : 'black';
}
// Function to generate legend table from groups data
function generateLegend(groups) {
const legendTable = document.getElementById('legend-table');
legendTable.innerHTML = ''; // Clear existing content
// Iterate through groups and create table rows
for (const [groupName, groupStyle] of Object.entries(groups)) {
const row = document.createElement('tr');
// Create checkbox cell
const checkboxCell = document.createElement('td');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `group${groupName}`;
checkbox.checked = true;
checkbox.onchange = function() { toggleGroup(groupName); };
// Get the label from the classifierName in the groups of the learning-graph.json file
const label = document.createTextNode(' ' + (groupStyle.classifierName || groupName));
checkboxCell.appendChild(checkbox);
checkboxCell.appendChild(label);
// Create color indicator cell
const colorCell = document.createElement('td');
colorCell.className = 'color-indicator';
const bgColor = groupStyle.color;
const textColor = getTextColorForBackground(bgColor);
colorCell.style.backgroundColor = bgColor;
colorCell.style.color = textColor;
colorCell.textContent = getColorName(bgColor);
row.appendChild(checkboxCell);
row.appendChild(colorCell);
legendTable.appendChild(row);
}
}
// Function to set metadata (title and description)
function setMetadata(metadata) {
if (metadata && metadata.title) {
const titleElement = document.getElementById('graph-title');
if (titleElement) {
titleElement.textContent = metadata.title;
}
// Update document title as well
if (metadata.title) {
document.title = metadata.title + ' - Learning Graph Viewer';
}
}
}
// ========== INITIALIZATION ==========
function initializeNetwork(graphData) {
// Set metadata (title, description)
if (graphData.metadata) {
setMetadata(graphData.metadata);
}
// Generate legend from groups data
if (graphData.groups) {
generateLegend(graphData.groups);
}
// Create DataSets from loaded data
nodes = new vis.DataSet(graphData.nodes);
edges = new vis.DataSet(graphData.edges);
// Create a network
var container = document.getElementById('mynetwork');
// Provide the data in the vis format
var data = {
nodes: nodes,
edges: edges
};
var options = {
groups: graphData.groups || {}, // Apply group-based styling
edges: {
arrows: {
to: { enabled: true, type: 'arrow', color: 'black', scaleFactor: 1 }
},
color: {
color: 'black',
inherit: false
},
width: 2,
smooth: { type: 'continuous' }
},
physics: {
solver: 'forceAtlas2Based',
forceAtlas2Based: {
springLength: 100
}
}
};
network = new vis.Network(container, data, options);
// Initialize search functionality
initializeSearch();
// Update statistics after network is initialized
updateStatistics();
}
function initializeSearch() {
var searchInput = document.getElementById('search-input');
var searchResults = document.getElementById('search-results');
var searchContainer = document.getElementById('search-container');
searchInput.addEventListener('input', function() {
var query = this.value.toLowerCase();
if (query === '') {
searchResults.style.display = 'none';
searchResults.innerHTML = '';
return;
}
// Only search visible nodes
var matches = nodes.get({
filter: function (item) {
return !item.hidden && item.label.toLowerCase().includes(query);
}
});
if (matches.length > 0) {
searchResults.innerHTML = '';
matches.forEach(function(item) {
var div = document.createElement('div');
div.textContent = item.label + ' (' + item.group + ')';
div.dataset.id = item.id;
div.addEventListener('click', function() {
var nodeId = parseInt(this.dataset.id);
network.focus(nodeId, {
animation: {
duration: 500,
easingFunction: 'easeInOutQuad'
},
scale: 1.5
});
network.selectNodes([nodeId]);
searchResults.style.display = 'none';
searchResults.innerHTML = '';
searchInput.value = '';
});
searchResults.appendChild(div);
});
searchResults.style.display = 'block';
} else {
searchResults.innerHTML = '<div style="color: gray; padding: 8px;">No matches found</div>';
searchResults.style.display = 'block';
}
});
// Close the dropdown when clicking outside
document.addEventListener('click', function(event) {
if (!searchContainer.contains(event.target)) {
searchResults.style.display = 'none';
searchResults.innerHTML = '';
}
});
}
// ========== LOAD DATA AND START APPLICATION ==========
// Load the graph data from JSON file by going up to the learning-graph directory and reading the learning-graph.json file
// Adjust the path as necessary based on your directory structure
fetch('../../learning-graph/learning-graph.json')
.then(response => {
if (!response.ok) {
throw new Error('Failed to load ../../learning-graph/learning-graph.json');
}
return response.json();
})
.then(graphData => {
initializeNetwork(graphData);
})
.catch(error => {
console.error('Error loading graph data:', error);
document.getElementById('mynetwork').innerHTML =
'<div style="padding: 20px; color: red;">Error loading graph data. Please check the console for details.</div>';
});