
Learning Graph Generator
- 6 installs
- 2 repo stars
- Updated August 1, 2026
- vishalsachdev/claude-skills
Helps with ai & agent building tasks.
About
learning-graph-generator is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- learning-graph-generator
- AI & Agent Building
- AI-coding skill
Learning Graph Generator 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 learning-graph-generatorAdd 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
Learning Graph Generator
Version: 0.02
You are tasked with generating a comprehensive high-quality learning graph from a course description. A learning graph is the foundational data structure for intelligent textbooks that can recommend learning paths. A learning graph is like a roadmap of Concepts to help students achieve their learning goals. A learning graph is an DAG Concept graph. Each arrow is a "Learning Dependency" relationship that suggest learning order. The markdown you generate must be compatible with the mkdocs version of markdown. Make sure you put a blank line before any lists.
Follow these steps carefully:
Markdown Generation Rules
1. Always place a blank line before any markdown list. This is required by the mkdocs markdown tools.
Mkdocs Navigation Rules
After you add a markdown file (any file with an extension .md) make sure to add that file to the navigation structure in the mkdocs.yml file. Here is an example of the nav section for the learning graph section:
- Learning Graph:
- Introduction: learning-graph/index.md
- Course Description Assessment: learning-graph/course-description-assessment.md
- Concept Enumeration: learning-graph/list-concepts.md
- Graph Quality Analysis: learning-graph/graph-quality-analysis.md
- Concept Taxonomy: learning-graph/concept-taxonomy.md
- Taxonomy Distribution Report: learning-graph/taxonomy-distribution-report.mdStep 0: Setup
Tell the user that they are running the version graph generator and the version number above.
The default context is that the skill is run from claude code in the home directory of an intelligent textbook that has been checked out from GitHub. There should be a docs directory with a standard mkdocs.yml file in the home git directory. You will create a directory called /docs/learning-graph it it does not already exist. The path is relative to the git home directory. The assumption is that /docs is relative to the directory that claude was started in.
mkdir -p docs/learning-graph; cd docs/learning-graph
You will copy python programs from this skill package into the /docs/learning-graph directory. You will execute python from that directory.
If you do not see the docs directory and the mkdocs.yml file suggest that the user clone a sample textbook from the following location:
git clone https://github.com/dmccreary/intelligent-book-template cd intelligent-book-template
Step 1: Course Description Quality Assessment
Before you begin this step, verify that it has not already been done. To do this, check the yml metadata in the docs/course-description.md file.
Here is a sample of the yml metadata:
---
title: Course Description
description: A detailed course description
quality_score: 95
---
# Course DescriptionIf you see a quality_score above 85 you may tell the user you found a score above 85 and skip this entire step. Tell them this is a way to save tokens.
If the quality score is below 85, analyze the provided course description at course-description.md to ensure it has enough content to generate 200 high-quality concepts:
1. Verify the course has a title, prerequisites, intended audience, objectives, and outcomes ("After this course students will be able to"). If these fields are missing ask the user for this information. 1. Examine the depth and breadth of topics covered 2. Assess whether the material has sufficient granularity for 200 distinct concepts 3. Check for diverse topic areas and learning objectives 4. Provide detailed feedback to the user about:
- List the expected content that you found
- Estimated number of concepts you can derive
- Compare this concept number with similar courses
- Describe areas where the course description is strong
- Any gaps or areas that might be under-represented
- Suggest how the 2001 Bloom taxonomy (remember, understand, apply, analyze, evaluate, create) could improve the outcomes descriptions
- Objective overall quality assessment on a scale of (1-poor to 100-perfect)
- Suggest that the user does not proceed unless a quality score is 70 or above
Use the following rubric for creating a quality score:
2.2 Course Description Quality Scoring System
Evaluate the course description using this 100-point scoring system:
| Element | Points | Criteria |
|---|---|---|
| Title | 5 | Clear, descriptive course title present |
| Target Audience | 5 | Specific audience identified (e.g., "college undergraduate") |
| Prerequisites | 5 | Prerequisites listed or explicitly stated as "None" |
| Main Topics Covered | 10 | Comprehensive list of topics (ideally 5-10 topics) |
| Topics Excluded | 5 | Clear boundaries set for what's NOT covered |
| Learning Outcomes Header | 5 | Clear statement: "After this course, students will be able to..." |
| Remember Level | 10 | Multiple specific outcomes for remembering/recalling |
| Understand Level | 10 | Multiple specific outcomes for understanding/explaining |
| Apply Level | 10 | Multiple specific outcomes for applying/using |
| Analyze Level | 10 | Multiple specific outcomes for analyzing/breaking down |
| Evaluate Level | 10 | Multiple specific outcomes for evaluating/judging |
| Create Level | 10 | Multiple specific outcomes for creating/synthesizing; includes capstone ideas |
| Descriptive Context | 5 | Additional context about course importance, relevance, or value |
Scoring Guidelines:
- Award full points if element is complete and high-quality
- Award partial points if element is present but incomplete or vague
- Award 0 points if element is missing
- For Bloom's Taxonomy levels, require at least 3 specific, actionable outcomes for full points
Tell user what their score was and suggest they improve the course description until the score goes above 80.
Save this report to course-description-assessment.md
5. Ask the user if you should proceed with generating the learning graph
Step 2: Generate Concept Labels
Once the course-description has been approved, generate 200 concept labels from the course content:
Requirements:
- Each Concept label must be in Title Case
- Maximum length: 32 characters
- Labels should be clear, specific, and pedagogically sound
- Cover the full breadth of the course material
- Concept Labels are entity names, not questions
- Do not use questions in the Concept Label. Don't use "What is Git", just use "Git"
!!! note Because these concept labels are used within a network graph, they must not be too long. Otherwise the graph will be hard to read.
Output:
- Save the numbered list to concept-list.md
- Format: Simple numbered list (1-200) in a markdown file
- Make sure that each number is unique so it can be used as a ConceptID
- Inform the user the file has been created
- Tell the user they should view the list and add and remove concepts now
- Tell the user it is best review the concept list before the next steps
Now ask the user to take some time to manually review the entire list of concept labels. If there are concepts that are not appropriate they should be removed now. If there are additional concepts that need to be added, they should be added now. It will require a lot of extra tokens later to change the content later. This is an important review step to ensure the quality of the textbook. Pay special attention to the length of the concept labels and the quality of any abbreviations.
Step 3: Generate Dependency Graph
Create a CSV file mapping dependencies between concepts:
Format:
- Filename: learning-graph.csv
- Columns:
ConceptID,ConceptLabel,Dependencies - ConceptID: Integer (1-200)
- ConceptLabel: The exact label from Step 2
- Dependencies: Pipe-delimited list of ConceptIDs (e.g., "1|3|7")
Dependency Rules:
- Foundational/prerequisite concepts have NO dependencies (empty Dependencies field)
- All other concepts must have at least one dependency
- No concept can depend on itself
- The graph must be a Directed Acyclic Graph (DAG) - no cycles
- Create meaningful learning pathways, not just linear chains
- Consider prerequisite relationships carefully
Note: The JSON file will be created in later steps (Steps 7-8) after the taxonomy is added to the CSV file. The complete JSON will include metadata, groups, nodes, and edges sections conforming to the learning-graph-schema.json.
Step 4: Learning Graph Quality Validation
Perform comprehensive quality checks on the dependency graph by using the Python program analyze-graph.py in this skill. It will do the following checks:
1. Verify DAG structure: Ensure no cycles exist 2. Check for self-dependencies: No concept should depend on itself 3. Foundational concepts: Identify concepts with zero dependencies 4. Orphaned nodes: Identify concepts that nothing depends on (potential dead ends) 5. Disconnected subgraphs: Check if all concepts are connected to the main graph 6. Linear chains: Flag if too many concepts only depend on the immediately prior concept 7. Indegree analysis: Calculate indegree (number of concepts that depend on each concept)
Shell command python analyze-graph.py learning-graph.csv quality-metrics.md
Verify the report has been written to quality-metrics.md
Generate the learning graph quality metrics report:
- Total concepts with zero dependencies - outbound arrows (foundational prerequisites)
- Total concepts with 1+ dependencies
- Average number of dependencies per concept
- Maximum dependency chain length
- Number of orphaned nodes
- Number of disconnected subgraphs
- Top 10 concepts with highest indegree (most depended-upon concepts)
Give the user a general quality score on a scale of 1 (poor) to 100 (perfect). If the learning graph does not get a score above 70, suggest that the user iterates on the process
Step 5: Create Concept Taxonomy
Develop a categorical taxonomy for organizing concepts:
Requirements:
- Target: ~12 categories (can vary by 2-3 if natural groupings emerge)
- Categories should evenly distribute concepts
- Avoid having any single category exceed 30% of total concepts
- Use clear, descriptive category names with title case and spaces
- Create 3-5 letter abbreviations for each category (TaxonomyID)
- Note that a JSON representation of the taxonomy will be created to form the groups section of the learning graph
Output:
- Save taxonomy to concept-taxonomy.md
- Format as markdown with:
- Category name
- TaxonomyID abbreviation (3-5 letters uppercase)
- Brief description of what concepts belong in this category
Step 6: Add Taxonomy to CSV
Update the dependencies CSV file:
1. Add a new column: TaxonomyID to the existing CSV file if it does not exist 2. For each concept, assign the best matching TaxonomyID 3. Use "MISC" for concepts without a clear category match 4. Save the updated file to learning-graph.csv
You can use the Python Program add-taxonomy.py as a template that will do the substitution.
Final CSV columns: ConceptID,ConceptLabel,Dependencies,TaxonomyID
Step 7: Create the metadata section of the learning-graph.json file
The metadata section contains Dublin Core-inspired fields for the textbook extracted from the course-description.md file. The JSON schema for the learning graph is located in the file learning-graph-schema.json within this skill.
Required fields:
title: Extract from the course description titledescription: Extract or summarize from the course description
Optional but recommended fields:
creator: Author or organization namedate: Current date in YYYY-MM-DD formatversion: Version number (e.g., "1.0")format: "Learning Graph JSON v1.0"schema: URL to the JSON schemalicense: License information (e.g., "CC BY-NC-SA 4.0 DEED")
Here is an example of the metadata section:
"metadata": {
"title": "Title Text From Course Description",
"description": "A description of the course in a few sentences.",
"creator": "Your Name",
"date": "2025-11-01",
"version": "1.0",
"format": "Learning Graph JSON v1.0",
"schema": "https://raw.githubusercontent.com/dmccreary/learning-graphs/refs/heads/main/src/schema/learning-graph-schema.json",
"license": "CC BY-NC-SA 4.0 DEED"
}You can create a metadata.json file with these fields to pass to the csv-to-json.py program in Step 9.
Step 8: Create the groups section of the JSON file
Convert the taxonomy categories into JSON format for the groups section of the learning-graph.json file. The JSON schema for the learning graph is located in the file learning-graph-schema.json within this skill.
The groups section creates a legend of concept types with distinct colors for visualization.
Important:
- The groups section uses taxonomy IDs (e.g., "FOUND", "DEF") as keys
- Each group must have a
classifierNamefield containing the human-readable name (e.g., "Foundation Concepts") - Each group must have a
colorfield (CSS color value) - Each group should have a
fontobject with acolorfield for text readability
Key structure:
- Group key: Use the TaxonomyID from the CSV (uppercase, no spaces, e.g., "FOUND")
- classifierName: Display name with Title Case and spaces (e.g., "Foundation Concepts")
- color: Choose distinct colors for each taxonomy
- font.color: "white" for dark backgrounds, "black" for light backgrounds
Below is an example of the groups section:
"groups": {
"FOUND": {
"classifierName": "Foundation Concepts",
"color": "red",
"font": {
"color": "white"
}
},
"DEF": {
"classifierName": "Definitions",
"color": "orange",
"font": {
"color": "black"
}
},
"CORE": {
"classifierName": "Core Concepts",
"color": "gold",
"font": {
"color": "black"
}
},
"INTER": {
"classifierName": "Intermediate",
"color": "green",
"font": {
"color": "white"
}
},
"ADV": {
"classifierName": "Advanced",
"color": "blue",
"font": {
"color": "white"
}
},
"MISC": {
"classifierName": "Miscellaneous Concepts",
"color": "indigo",
"font": {
"color": "white"
}
},
"PROJ": {
"classifierName": "Project Ideas",
"color": "violet",
"font": {
"color": "white"
}
},
"CAP": {
"classifierName": "Capstone Projects",
"color": "gray",
"font": {
"color": "white"
}
}
}Note: The csv-to-json.py program will automatically generate the groups section based on the taxonomies found in your CSV file. You can customize colors by creating an optional color-config.json file.
Step 9: Generate the Complete Learning Graph JSON
Now that you have created the metadata.json file (Step 7) and have the taxonomy-enriched CSV (Step 6), run the csv-to-json.py program to generate the complete learning-graph.json file:
python csv-to-json.py learning-graph.csv learning-graph.json metadata.jsonThis command will: 1. Read the learning-graph.csv file (with ConceptID, ConceptLabel, Dependencies, TaxonomyID columns) 2. Use the metadata from metadata.json 3. Auto-generate the groups section based on the taxonomies in the CSV 4. Create nodes with proper group references (using TaxonomyIDs) 5. Create edges based on the dependencies 6. Output a complete learning-graph.json file conforming to the schema
Verify that the file learning-graph.json is present and valid.
Optional: You can validate the JSON against the schema using:
./validate-learning-graph.sh learning-graph.jsonStep 10: Taxonomy Distribution Report
Generate a distribution analysis:
1. Count concepts in each category 2. Calculate percentages 3. Identify over-represented categories (>30%) 4. Suggest alternative categorization if needed
Use the python report in this skill called taxonomy-distribution.py
Output:
- Save to taxonomy-distribution.md
- Format as markdown table with columns:
- Category Name
- TaxonomyID
- Count
- Percentage
Step 11: Create new index.md from index-template.md
Create a new index.md file in the learning-graph directory from the file index-template.md in this skill. Customize the new index.md file to reflect the name of this intelligent book. Look for values in all uppercase (TEXTBOOK_NAME) and replace them with the appropriate values.
Step 12: Write session log
Export the session log to logs/learning-graph-generator-VERSION-DATE.md
Where:
1. VERSION is the version of this skill. 2. DATE is today's date in ISO format yyyy-mm-dd.
Note that the session log should also list what version of any Python program was used. For example, not what version of the csv-to-json.py Python program was used in the session log. This is important for debugging.
Step 13: Completion
Inform the user that the learning graph generation is complete! Congratulate them and wish them success on their textbook or course material. Tell them that the next step is the book-chapter-generator skill, but that it is critical to review the concept lists, the concept taxonomies and the learning graph before they do this next step.
Files created:
- course-description-assessment.md - quality assessment of the course description
- concept-list.md - Numbered list of 200 concepts
- learning-graph.csv - Full dependency graph with taxonomy
- metadata.json - Metadata for the learning graph (title, description, creator, etc.)
- learning-graph.json - Complete learning graph with metadata, groups, nodes, and edges in vis-network.js JSON format
- concept-taxonomy.md - Category definitions
- quality-metrics.md - Quality validation report
- taxonomy-distribution.md - Category distribution analysis
- index.md - Introduction page for the learning graph section
Important Notes
- Maintain pedagogical integrity throughout the process
- Dependencies should reflect actual prerequisite knowledge
- Balance between granularity and comprehensiveness
- Ensure concepts build upon each other logically
- The learning graph should support multiple learning pathways, not just one linear path
#!/usr/bin/env python3
"""
Add Taxonomy IDs to Concept Dependency CSV
Reads the concept dependency CSV and assigns appropriate taxonomy IDs
based on concept labels and position in the concept hierarchy.
"""
import csv
import re
from typing import Dict, List
def assign_taxonomy(concept_id: int, concept_label: str, taxonomy_config: dict = None) -> str:
"""
Assign taxonomy ID based on concept ID and label.
Args:
concept_id: The concept identifier
concept_label: The concept name/label
taxonomy_config: Optional dictionary mapping taxonomy IDs to their configurations.
Each config contains 'range' (tuple) and 'keywords' (list).
If not provided, returns 'MISC' for all concepts.
Returns:
Taxonomy ID string
Example taxonomy_config:
{
'MATH': {
'range': (1, 20),
'keywords': ['number', 'algebra', 'calculus', 'derivative']
},
'CORE': {
'range': (21, 50),
'keywords': ['fundamental', 'basic', 'foundation']
}
}
"""
if taxonomy_config is None:
return 'MISC'
label_lower = concept_label.lower()
# Check each taxonomy in order
for tax_id, config in taxonomy_config.items():
range_start, range_end = config.get('range', (0, 0))
keywords = config.get('keywords', [])
exclude_keywords = config.get('exclude', [])
# Check if in range or matches keywords
in_range = range_start <= concept_id <= range_end
matches_keywords = any(kw in label_lower for kw in keywords)
has_exclusions = any(kw in label_lower for kw in exclude_keywords)
if (in_range or matches_keywords) and not has_exclusions:
return tax_id
# Default to MISC
return 'MISC'
def add_taxonomy_to_csv(input_csv: str, output_csv: str, taxonomy_config: dict = None):
"""
Read CSV, add taxonomy column, and write updated CSV.
Args:
input_csv: Path to input CSV file
output_csv: Path to output CSV file
taxonomy_config: Optional taxonomy configuration dictionary
Returns:
Dictionary of taxonomy counts
"""
rows = []
with open(input_csv, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
concept_id = int(row['ConceptID'])
concept_label = row['ConceptLabel']
taxonomy_id = assign_taxonomy(concept_id, concept_label, taxonomy_config)
rows.append({
'ConceptID': concept_id,
'ConceptLabel': concept_label,
'Dependencies': row['Dependencies'],
'TaxonomyID': taxonomy_id
})
# Write updated CSV
with open(output_csv, 'w', encoding='utf-8', newline='') as f:
fieldnames = ['ConceptID', 'ConceptLabel', 'Dependencies', 'TaxonomyID']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
# Count concepts per taxonomy
taxonomy_counts = {}
for row in rows:
tax = row['TaxonomyID']
taxonomy_counts[tax] = taxonomy_counts.get(tax, 0) + 1
print(f"✅ Taxonomy added to CSV: {output_csv}")
print(f"\n📊 Taxonomy Distribution:")
for tax in sorted(taxonomy_counts.keys()):
count = taxonomy_counts[tax]
percentage = (count / len(rows)) * 100
print(f" {tax:6s}: {count:3d} concepts ({percentage:5.1f}%)")
return taxonomy_counts
if __name__ == "__main__":
import sys
import json
# Parse command line arguments
if len(sys.argv) < 3:
print("Usage: python add-taxonomy.py <input_csv> <output_csv> [taxonomy_config.json]")
print("\nExample taxonomy_config.json format:")
print(json.dumps({
'FOUNDATION': {
'range': [1, 20],
'keywords': ['basic', 'fundamental', 'introduction'],
'exclude': []
},
'ADVANCED': {
'range': [21, 50],
'keywords': ['advanced', 'complex', 'detailed'],
'exclude': []
}
}, indent=2))
sys.exit(1)
input_csv = sys.argv[1]
output_csv = sys.argv[2]
# Load taxonomy config if provided
taxonomy_config = None
if len(sys.argv) > 3:
config_file = sys.argv[3]
with open(config_file, 'r', encoding='utf-8') as f:
taxonomy_config = json.load(f)
print(f"📋 Loaded taxonomy config from: {config_file}")
else:
print("⚠️ No taxonomy config provided. All concepts will be marked as 'MISC'.")
print(" Provide a taxonomy config JSON file as the third argument.")
taxonomy_counts = add_taxonomy_to_csv(input_csv, output_csv, taxonomy_config)
#!/usr/bin/env python3
"""
Learning Graph Quality Analysis Script
Analyzes the concept dependency graph and generates quality metrics including:
- DAG verification
- Indegree/outdegree analysis
- Dependency chain analysis
- Orphaned node detection
- Connected component analysis
"""
import csv
from collections import defaultdict, deque
from typing import Dict, List, Set, Tuple
def load_graph(csv_path: str) -> Tuple[Dict[int, str], Dict[int, List[int]]]:
"""Load the dependency graph from CSV file."""
concepts = {} # id -> label
dependencies = defaultdict(list) # id -> list of prerequisite ids
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
concept_id = int(row['ConceptID'])
concepts[concept_id] = row['ConceptLabel']
if row['Dependencies']:
deps = [int(d) for d in row['Dependencies'].split('|')]
dependencies[concept_id] = deps
return concepts, dependencies
def calculate_indegree(concepts: Dict[int, str],
dependencies: Dict[int, List[int]]) -> Dict[int, int]:
"""Calculate indegree (number of concepts that depend on each concept)."""
indegree = {cid: 0 for cid in concepts}
for concept_id, prereqs in dependencies.items():
for prereq in prereqs:
indegree[prereq] += 1
return indegree
def calculate_outdegree(concepts: Dict[int, str],
dependencies: Dict[int, List[int]]) -> Dict[int, int]:
"""Calculate outdegree (number of prerequisites for each concept)."""
outdegree = {cid: len(dependencies.get(cid, [])) for cid in concepts}
return outdegree
def find_orphaned_nodes(concepts: Dict[int, str],
indegree: Dict[int, int],
dependencies: Dict[int, List[int]]) -> List[Tuple[int, str]]:
"""Find concepts that nothing depends on (potential dead ends)."""
orphaned = [(cid, label) for cid, label in concepts.items()
if indegree[cid] == 0 and cid in dependencies]
return orphaned
def verify_dag(concepts: Dict[int, str],
dependencies: Dict[int, List[int]]) -> Tuple[bool, List[List[int]]]:
"""Verify the graph is a DAG using topological sort. Returns (is_dag, cycles_found)."""
indeg = {cid: 0 for cid in concepts}
# Calculate indegree (number of prerequisites for each concept)
for concept_id, prereqs in dependencies.items():
indeg[concept_id] = len(prereqs)
# Kahn's algorithm for topological sort
queue = deque([cid for cid in concepts if indeg[cid] == 0])
processed = []
while queue:
node = queue.popleft()
processed.append(node)
# For each concept that depends on this node
for concept_id, prereqs in dependencies.items():
if node in prereqs:
indeg[concept_id] -= 1
if indeg[concept_id] == 0:
queue.append(concept_id)
is_dag = len(processed) == len(concepts)
cycles = [] if is_dag else find_cycles(concepts, dependencies)
return is_dag, cycles
def find_cycles(concepts: Dict[int, str],
dependencies: Dict[int, List[int]]) -> List[List[int]]:
"""Find cycles in the graph using DFS."""
visited = set()
rec_stack = set()
cycles = []
def dfs(node, path):
visited.add(node)
rec_stack.add(node)
path.append(node)
# Check all nodes that this node depends on (reverse edges in dependency graph)
for next_node, prereqs in dependencies.items():
if node in prereqs:
if next_node not in visited:
if dfs(next_node, path[:]):
return True
elif next_node in rec_stack:
cycle_start = path.index(next_node)
cycles.append(path[cycle_start:] + [next_node])
return True
rec_stack.remove(node)
return False
for node in concepts:
if node not in visited:
dfs(node, [])
return cycles
def find_longest_chain(concepts: Dict[int, str],
dependencies: Dict[int, List[int]]) -> Tuple[int, List[int]]:
"""Find the longest dependency chain using DFS."""
memo = {}
def dfs(node):
if node in memo:
return memo[node]
if node not in dependencies or not dependencies[node]:
memo[node] = (1, [node])
return memo[node]
max_length = 0
max_path = []
for prereq in dependencies[node]:
length, path = dfs(prereq)
if length > max_length:
max_length = length
max_path = path
memo[node] = (max_length + 1, max_path + [node])
return memo[node]
max_chain_length = 0
max_chain_path = []
for concept_id in concepts:
length, path = dfs(concept_id)
if length > max_chain_length:
max_chain_length = length
max_chain_path = path
return max_chain_length, max_chain_path
def find_connected_components(concepts: Dict[int, str],
dependencies: Dict[int, List[int]]) -> List[Set[int]]:
"""Find connected components (treating graph as undirected)."""
visited = set()
components = []
def bfs(start):
component = set()
queue = deque([start])
component.add(start)
visited.add(start)
while queue:
node = queue.popleft()
# Add all neighbors (both directions)
if node in dependencies:
for prereq in dependencies[node]:
if prereq not in visited:
visited.add(prereq)
component.add(prereq)
queue.append(prereq)
for concept_id, prereqs in dependencies.items():
if node in prereqs and concept_id not in visited:
visited.add(concept_id)
component.add(concept_id)
queue.append(concept_id)
return component
for concept_id in concepts:
if concept_id not in visited:
component = bfs(concept_id)
components.append(component)
return components
def generate_report(csv_path: str, output_path: str):
"""Generate comprehensive quality metrics report."""
concepts, dependencies = load_graph(csv_path)
# Calculate metrics
indegree = calculate_indegree(concepts, dependencies)
outdegree = calculate_outdegree(concepts, dependencies)
orphaned = find_orphaned_nodes(concepts, indegree, dependencies)
is_dag, cycles = verify_dag(concepts, dependencies)
max_chain_length, max_chain_path = find_longest_chain(concepts, dependencies)
components = find_connected_components(concepts, dependencies)
# Foundational concepts
foundational = [(cid, label) for cid, label in concepts.items()
if outdegree[cid] == 0]
# Top concepts by indegree
top_indegree = sorted([(cid, label, indegree[cid])
for cid, label in concepts.items()],
key=lambda x: x[2], reverse=True)[:10]
# Calculate average dependencies
total_deps = sum(len(deps) for deps in dependencies.values())
avg_deps = total_deps / len(dependencies) if dependencies else 0
# Generate markdown report
with open(output_path, 'w', encoding='utf-8') as f:
f.write("# Learning Graph Quality Metrics Report\n\n")
f.write("## Overview\n\n")
f.write(f"- **Total Concepts**: {len(concepts)}\n")
f.write(f"- **Foundational Concepts** (no dependencies): {len(foundational)}\n")
f.write(f"- **Concepts with Dependencies**: {len(dependencies)}\n")
f.write(f"- **Average Dependencies per Concept**: {avg_deps:.2f}\n\n")
f.write("## Graph Structure Validation\n\n")
f.write(f"- **Valid DAG Structure**: {'✅ Yes' if is_dag else '❌ No'}\n")
f.write(f"- **Self-Dependencies**: None detected ✅\n")
f.write(f"- **Cycles Detected**: {len(cycles)}\n\n")
if cycles:
f.write("### Detected Cycles:\n\n")
for i, cycle in enumerate(cycles, 1):
cycle_labels = [concepts[cid] for cid in cycle]
f.write(f"{i}. {' → '.join(cycle_labels)}\n")
f.write("\n")
f.write("## Foundational Concepts\n\n")
f.write("These concepts have no prerequisites:\n\n")
for cid, label in foundational:
f.write(f"- **{cid}**: {label}\n")
f.write("\n")
f.write("## Dependency Chain Analysis\n\n")
f.write(f"- **Maximum Dependency Chain Length**: {max_chain_length}\n\n")
f.write("### Longest Learning Path:\n\n")
for i, cid in enumerate(max_chain_path, 1):
f.write(f"{i}. **{concepts[cid]}** (ID: {cid})\n")
f.write("\n")
f.write("## Orphaned Nodes Analysis\n\n")
f.write(f"- **Total Orphaned Nodes**: {len(orphaned)}\n\n")
if orphaned:
f.write("Concepts that are not prerequisites for any other concept:\n\n")
for cid, label in orphaned[:20]: # Show first 20
f.write(f"- **{cid}**: {label}\n")
if len(orphaned) > 20:
f.write(f"\n*...and {len(orphaned) - 20} more*\n")
else:
f.write("✅ No orphaned nodes detected.\n")
f.write("\n")
f.write("## Connected Components\n\n")
f.write(f"- **Number of Connected Components**: {len(components)}\n\n")
if len(components) == 1:
f.write("✅ All concepts are connected in a single graph.\n\n")
else:
f.write("⚠️ Multiple disconnected subgraphs detected:\n\n")
for i, component in enumerate(components, 1):
f.write(f"### Component {i} ({len(component)} concepts)\n\n")
for cid in sorted(list(component)[:10]):
f.write(f"- {concepts[cid]}\n")
if len(component) > 10:
f.write(f"- *...and {len(component) - 10} more*\n")
f.write("\n")
f.write("## Indegree Analysis\n\n")
f.write("Top 10 concepts that are prerequisites for the most other concepts:\n\n")
f.write("| Rank | Concept ID | Concept Label | Indegree |\n")
f.write("|------|-----------|---------------|----------|\n")
for i, (cid, label, ind) in enumerate(top_indegree, 1):
f.write(f"| {i} | {cid} | {label} | {ind} |\n")
f.write("\n")
f.write("## Outdegree Distribution\n\n")
outdeg_dist = defaultdict(int)
for deg in outdegree.values():
outdeg_dist[deg] += 1
f.write("| Dependencies | Number of Concepts |\n")
f.write("|--------------|--------------------|\n")
for deg in sorted(outdeg_dist.keys()):
f.write(f"| {deg} | {outdeg_dist[deg]} |\n")
f.write("\n")
f.write("## Recommendations\n\n")
if len(components) > 1:
f.write("- ⚠️ **Connect disconnected components**: Add dependencies to link separate subgraphs\n")
if len(orphaned) > 50:
f.write(f"- ⚠️ **Many orphaned nodes** ({len(orphaned)}): Consider if these should be prerequisites for advanced concepts\n")
if is_dag:
f.write("- ✅ **DAG structure verified**: Graph supports valid learning progressions\n")
if max_chain_length > 15:
f.write(f"- ℹ️ **Long dependency chains** ({max_chain_length}): Ensure students can follow extended learning paths\n")
if avg_deps < 1.5:
f.write("- ℹ️ **Consider adding cross-dependencies**: More connections could create richer learning pathways\n")
f.write("\n---\n\n")
f.write("*Report generated by learning-graph-reports/analyze_graph.py*\n")
print(f"✅ Quality metrics report generated: {output_path}")
return is_dag, len(foundational), len(orphaned), max_chain_length
if __name__ == "__main__":
import sys
# Parse command line arguments
if len(sys.argv) < 3:
print("Usage: python analyze-graph.py <input_csv> <output_report.md>")
print("\nExample:")
print(" python analyze-graph.py learning-graph.csv quality-metrics.md")
sys.exit(1)
csv_path = sys.argv[1]
output_path = sys.argv[2]
generate_report(csv_path, output_path)
#!/usr/bin/env python3
"""
Convert CSV Learning Graph to JSON for vis-network.js
Converts the concept dependency CSV into the JSON format
used by the existing graph viewer (vis.js network format).
"""
VERSION = "0.02"
import csv
import json
from typing import Dict, List
from datetime import datetime
def csv_to_json(csv_path: str, json_path: str, color_config: dict = None, metadata: dict = None):
"""
Convert CSV dependency graph to vis.js JSON format with metadata and groups.
Args:
csv_path: Path to input CSV file with columns: ConceptID, ConceptLabel, Dependencies, TaxonomyID
json_path: Path to output JSON file
color_config: Optional dictionary mapping taxonomy IDs to colors.
If not provided, uses default color scheme.
metadata: Optional dictionary with metadata fields (title, description, creator, etc.)
If not provided, creates minimal metadata.
"""
# Default taxonomy group colors for visualization
# Supports both text codes (FOUND, DEF, etc.) and numeric IDs (1, 2, etc.)
default_colors = {
# Custom taxonomies for intelligent textbook skills course
'AIFND': '#FF6B6B', # Coral red - AI Foundations
'SKILL': '#4ECDC4', # Turquoise - Claude Skills
'IBOOK': '#45B7D1', # Sky blue - Intelligent Textbooks
'MKDOC': '#96CEB4', # Sage green - MkDocs
'GRAPH': '#FFEAA7', # Soft yellow - Learning Graphs
'EDTHY': '#DDA15E', # Bronze - Educational Theory
'CONTE': '#BC6C25', # Brown - Content Creation
'RSRCE': '#9B59B6', # Purple - Resources
'INTER': '#E17055', # Terracotta - Interactive
'VERCT': '#74B9FF', # Light blue - Version Control
'TOOLS': '#A29BFE', # Periwinkle - Tools
'DATAS': '#FD79A8', # Pink - Data & Scripting
# Standard/default text codes
'FOUND': 'red',
'DEF': 'orange',
'CORE': 'gold',
'ADV': 'blue',
'APPL': 'cyan',
'SPEC': 'indigo',
'PROJ': 'violet',
'CAP': 'gray',
'MISC': 'brown',
# Numeric IDs (same mapping)
'1': 'red',
'2': 'orange',
'3': 'gold',
'4': 'green',
'5': 'blue',
'6': 'cyan',
'7': 'indigo',
'8': 'violet',
'9': 'gray',
'10': 'brown',
}
taxonomy_colors = color_config if color_config is not None else default_colors
# Taxonomy ID to classifier name mapping
# Supports both text codes (FOUND, DEF, etc.) and numeric IDs (1, 2, etc.)
# These are the display names (classifierName in schema) for each taxonomy
taxonomy_names = {
# Custom taxonomies for intelligent textbook skills course
'AIFND': 'AI Foundations',
'SKILL': 'Claude Skills System',
'IBOOK': 'Intelligent Textbooks',
'MKDOC': 'MkDocs Platform',
'GRAPH': 'Learning Graphs',
'EDTHY': 'Educational Theory',
'CONTE': 'Content Creation',
'RSRCE': 'Educational Resources',
'INTER': 'Interactive Elements',
'VERCT': 'Version Control',
'TOOLS': 'Development Tools',
'DATAS': 'Data & Scripting',
# Standard/default text codes
'FOUND': 'Foundation Concepts',
'DEF': 'Definitions',
'CORE': 'Core Concepts',
'ADV': 'Advanced',
'APPL': 'Applied',
'SPEC': 'Specialized',
'CAP': 'Capstone Projects',
'PROJ': 'Project Ideas',
'MISC': 'Miscellaneous Concepts',
# Numeric IDs (common mapping)
'1': 'Foundation Concepts',
'2': 'Definitions',
'3': 'Core Concepts',
'4': 'Intermediate',
'5': 'Advanced',
'6': 'Applied',
'7': 'Specialized',
'8': 'Capstone Projects',
'9': 'Miscellaneous Concepts',
'10': 'Extended Topics',
}
# Read CSV
nodes = []
edges = []
foundational_ids = []
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
concept_id = int(row['ConceptID'])
# Support both ConceptLabel and ConceptName column names
label = row.get('ConceptLabel') or row.get('ConceptName', '')
taxonomy = row['TaxonomyID']
dependencies_str = row['Dependencies']
# Determine if foundational (no dependencies)
is_foundational = (dependencies_str == '')
if is_foundational:
foundational_ids.append(concept_id)
# Create node - use taxonomy ID directly as group reference
node = {
'id': concept_id,
'label': label,
'group': taxonomy
}
# Special styling for foundational concepts
if is_foundational:
node['shape'] = 'box'
nodes.append(node)
# Create edges (from concept to its prerequisites)
if dependencies_str:
prereq_ids = [int(pid) for pid in dependencies_str.split('|')]
for prereq_id in prereq_ids:
edge = {
'from': concept_id,
'to': prereq_id
}
edges.append(edge)
# Create metadata section
default_metadata = {
'title': 'Learning Graph',
'description': f'Learning graph with {len(nodes)} concepts generated from CSV',
'creator': 'CSV to JSON Converter',
'date': datetime.now().strftime('%Y-%m-%d'),
'version': '1.0',
'format': 'Learning Graph JSON v1.0',
'schema': 'https://raw.githubusercontent.com/dmccreary/learning-graphs/refs/heads/main/src/schema/learning-graph-schema.json',
'license': 'CC BY-NC-SA 4.0 DEED'
}
# Merge user-provided metadata with defaults
if metadata:
default_metadata.update(metadata)
# Create groups section from taxonomy colors
groups = {}
# Determine which taxonomy IDs are actually used
used_taxonomies = set(node['group'] for node in nodes)
for tax_id, color in taxonomy_colors.items():
# Only include groups that are actually used
if tax_id in used_taxonomies:
# Get the classifier name for this taxonomy
classifier_name = taxonomy_names.get(tax_id, tax_id)
# Determine font color based on background color
# Dark colors need white text (named colors)
dark_colors = ['red', 'blue', 'indigo', 'violet', 'cyan', 'brown']
# Hex codes that need white text (dark backgrounds)
dark_hex_colors = ['#FF6B6B', '#9B59B6', '#E17055', '#BC6C25']
font_color = 'white' if (color in dark_colors or color in dark_hex_colors) else 'black'
groups[tax_id] = {
'classifierName': classifier_name,
'color': color,
'font': {
'color': font_color
}
}
# Create final JSON structure
graph_data = {
'metadata': default_metadata,
'groups': groups,
'nodes': nodes,
'edges': edges
}
# Write JSON
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(graph_data, f, indent=2)
print(f"✅ JSON graph created: {json_path} (csv-to-json v{VERSION})")
print(f" - Title: {default_metadata['title']}")
print(f" - {len(groups)} groups/taxonomies")
print(f" - {len(nodes)} nodes")
print(f" - {len(edges)} edges")
print(f" - {len(foundational_ids)} foundational concepts")
print(f"\nFoundational concept IDs: {foundational_ids}")
print(f"Groups: {list(groups.keys())}")
return graph_data
def create_taxonomy_legend(groups: dict = None):
"""
Generate a legend of taxonomy colors for documentation.
Args:
groups: Dictionary of groups from the generated JSON (contains classifierName, color, etc.)
"""
if not groups:
print("\n⚠️ No groups to display in legend")
return
print("\n## Taxonomy Color Legend\n")
print("| Category | TaxonomyID | Color |")
print("|----------|------------|-------|")
for tax_id in sorted(groups.keys()):
group_info = groups[tax_id]
name = group_info.get('classifierName', tax_id)
color = group_info.get('color', 'gray')
print(f"| {name} | {tax_id} | {color} |")
if __name__ == "__main__":
import sys
# Parse command line arguments
if len(sys.argv) < 3:
print("Usage: python csv-to-json.py <input_csv> <output_json> [color_config.json] [metadata.json]")
print("Looking for CSV column names: ConceptID, ConceptLabel, Dependencies, TaxonomyID")
print("\nExample:")
print(" python csv-to-json.py learning-graph.csv learning-graph.json")
print("\nOptional color_config.json format:")
print(json.dumps({
'FOUND': 'red',
'CORE': 'yellow',
'ADV': 'blue'
}, indent=2))
print("\nOptional metadata.json format:")
print(json.dumps({
'title': 'My Learning Graph',
'description': 'A comprehensive learning graph',
'creator': 'Your Name',
'license': 'CC BY 4.0'
}, indent=2))
sys.exit(1)
csv_path = sys.argv[1]
json_path = sys.argv[2]
# Load color config if provided
color_config = None
if len(sys.argv) > 3:
config_file = sys.argv[3]
try:
with open(config_file, 'r', encoding='utf-8') as f:
color_config = json.load(f)
print(f"📋 Loaded color config from: {config_file}")
except FileNotFoundError:
print(f"⚠️ Color config file not found: {config_file}, using defaults")
# Load metadata config if provided
metadata = None
if len(sys.argv) > 4:
metadata_file = sys.argv[4]
try:
with open(metadata_file, 'r', encoding='utf-8') as f:
metadata = json.load(f)
print(f"📋 Loaded metadata from: {metadata_file}")
except FileNotFoundError:
print(f"⚠️ Metadata file not found: {metadata_file}, using defaults")
graph_data = csv_to_json(csv_path, json_path, color_config, metadata)
create_taxonomy_legend(graph_data['groups'])
print("\n✅ CSV to JSON format complete. Ready to use with graph-viewer!")
print(f" Validate with: ./src/schema/validate-learning-graph.sh {json_path}")
Learning Graph for TEXTBOOK_NAME
This section contains the learning graph for this textbook. A learning graph is a graph of concepts used in this textbook. Each concept is represented by a node in a network graph. Concepts are connected by directed edges that indicate what concepts each node depends on before that concept is understood by the student.
A learning graph is the foundational data structure for intelligent textbooks that can recommend learning paths. A learning graph is like a roadmap of concepts to help students arrive at their learning goals.
At the left of the learning graph are prerequisite or foundational concepts. They have no outbound edges. They only have inbound edges for other concepts that depend on understanding these foundational prerequisite concepts. At the far right we have the most advanced concepts in the course. To master these concepts you must understand all the concepts that they point to.
Here are other files used by the learning graph.
Course Description
We use the Course Description as the source document for the concepts that are included in this course. The course description uses the 2001 Bloom taxonomy to order learning objectives.
List of Concepts
We use generative AI to convert the course description into a Concept List. Each concept is in the form of a short Title Case label with most labels under 32 characters long.
Concept Dependency List
We next use generative AI to create a Directed Acyclic Graph (DAG). DAGs do not have cycles where concepts depend on themselves. We provide the DAG in two formats. One is a CSV file and the other format is a JSON file that uses the vis-network JavaScript library format. The vis-network format uses nodes, edges and metadata elements with edges containing from and to properties. This makes it easy for you to view and edit the learning graph using an editor built with the vis-network tools.
Analysis & Documentation
Course Description Quality Assessment
This report rates the overall quality of the course description for the purpose of generating a learning graph.
- Course description fields and content depth analysis
- Validates course description has sufficient depth for generating 200 concepts
- Compares course description against similar courses
- Identifies content gaps and strengths
- Suggests areas of improvement
Vew the Course Description Quality Assessment
Learning Graph Quality Validation
This report gives you an overall assessment of the quality of the learning graph. It uses graph algorithms to look for specific quality patterns in the graph.
- Graph structure validation - all concepts are connected
- DAG validation (no cycles detected)
- Foundational concepts: 10 entry points
- Indegree distribution analysis
- Longest dependency chains
- Connectivity: percent of nodes connected to the main cluster
View the Learning Graph Quality Validation
Concept Taxonomy
In order to see patterns in the learning graph, it is useful to assign colors to each concept based on the concept type. We use generative AI to create about a dozen categories for our concepts and then place each concept into a single primary classifier.
- A concept classifier taxonomy with approximately 12 categories (+/- 1 or 2)
- Category organization - foundational elements first, course capstone project ideas last
- Balanced categories (3.5% - 15% each)
- All categories under 30% threshold
- Pedagogical flow recommendations
- Clear 3-5 letter abbreviations for use in CSV file
- A Miscellaneous (MOSC) category is sometimes added
View the Concept Taxonomy
Taxonomy Distribution
This reports shows how many concepts fit into each category of the taxonomy. Our goal is a somewhat balanced taxonomy where each category holds an equal number of concepts. We also don't want any category to contain over 30% of our concepts.
- Statistical breakdown
- Detailed concept listing by category
- Visual distribution table
- Balance verification
View the Taxonomy Distribution Report
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://dmccreary.github.io/geometry-course/learning-graph/learning-graph-schema.json",
"title": "Learning Graph Schema",
"description": "Schema for learning graph JSON files that define concept nodes, dependencies, and taxonomies for educational content",
"type": "object",
"required": ["metadata", "groups", "nodes", "edges"],
"properties": {
"metadata": {
"type": "object",
"description": "Metadata about the learning graph",
"required": ["title"],
"properties": {
"title": {
"type": "string",
"description": "The title of the course or subject"
},
"description": {
"type": "string",
"description": "A brief description of the course content"
},
"creator": {
"type": "string",
"description": "The name of the person or organization who created this learning graph"
},
"date": {
"type": "string",
"format": "date",
"description": "The creation or last modification date (YYYY-MM-DD)"
},
"version": {
"type": "string",
"description": "Version number of the learning graph"
},
"format": {
"type": "string",
"description": "The format specification of this learning graph"
},
"schema": {
"type": "string",
"format": "uri",
"description": "URL to the JSON schema for this learning graph"
},
"license": {
"type": "string",
"description": "The license under which this learning graph is distributed"
}
}
},
"groups": {
"type": "object",
"description": "Taxonomy groups/categories for organizing concepts",
"patternProperties": {
"^[A-Z]+$": {
"type": "object",
"required": ["classifierName", "color"],
"properties": {
"classifierName": {
"type": "string",
"description": "Human-readable name for this taxonomy category"
},
"color": {
"type": "object",
"description": "Color styling for nodes in this group",
"required": ["color"],
"properties": {
"color": {
"type": "string",
"description": "Color value (hex code like #ff0000 or CSS color name like red)",
"pattern": "^(#[0-9A-Fa-f]{6}|[a-z]+)$"
},
"colorName": {
"type": "string",
"description": "Human-readable color name for display in legends"
},
"font": {
"type": "object",
"description": "Font styling for text in nodes",
"properties": {
"color": {
"type": "string",
"description": "Font color (typically 'white' or 'black')",
"pattern": "^(#[0-9A-Fa-f]{6}|[a-z]+)$"
}
}
}
}
},
"font": {
"type": "object",
"description": "Additional font styling (deprecated - use color.font instead)",
"properties": {
"color": {
"type": "string",
"pattern": "^(#[0-9A-Fa-f]{6}|[a-z]+)$"
}
}
}
}
}
}
},
"nodes": {
"type": "array",
"description": "Array of concept nodes in the learning graph",
"items": {
"type": "object",
"required": ["id", "label", "group"],
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier for the node",
"minimum": 1
},
"label": {
"type": "string",
"description": "The name/label of the concept",
"minLength": 1
},
"group": {
"type": "string",
"description": "The taxonomy group this concept belongs to (must match a key in groups)",
"pattern": "^[A-Z]+$"
},
"shape": {
"type": "string",
"description": "Optional vis.js shape for the node (e.g., 'box', 'circle', 'ellipse')",
"enum": ["box", "circle", "ellipse", "database", "diamond", "dot", "square", "star", "triangle", "hexagon"]
},
"title": {
"type": "string",
"description": "Optional tooltip text shown when hovering over the node"
},
"level": {
"type": "integer",
"description": "Optional hierarchical level for layout purposes"
}
}
}
},
"edges": {
"type": "array",
"description": "Array of directed edges representing dependencies between concepts",
"items": {
"type": "object",
"required": ["from", "to"],
"properties": {
"from": {
"type": "integer",
"description": "Source node ID (prerequisite concept)",
"minimum": 1
},
"to": {
"type": "integer",
"description": "Target node ID (dependent concept)",
"minimum": 1
},
"arrows": {
"type": "string",
"description": "Arrow direction (typically 'to' for directed edges)",
"enum": ["to", "from", "middle", "to,from"]
},
"label": {
"type": "string",
"description": "Optional label for the edge"
},
"title": {
"type": "string",
"description": "Optional tooltip for the edge"
},
"smooth": {
"type": "object",
"description": "Edge smoothing options for vis.js",
"properties": {
"type": {
"type": "string",
"enum": ["dynamic", "continuous", "discrete", "diagonalCross", "straightCross", "horizontal", "vertical", "curvedCW", "curvedCCW", "cubicBezier"]
}
}
}
}
}
}
}
}
JSON Schema for Learning Graph
The file learning-graph-schema.json contains a JSON schema for validating a learning graph.
Unix Shell Script
We have provided a UNIX shell script for running a validator on any learning graph. Just pass the file name as the first parameter to the validate-learning-graph.sh file.
./validate-learning-graph.sh ../../docs/vis/combined-viewer/learning-graph.json Sample Response:
Validating learning graph...
Input file: ../../docs/vis/combined-viewer/learning-graph.json
Schema file: $HOME/Documents/ws/learning-graphs/src/schema/learning-graph-schema.json
✓ Validation successful!
Summary:
Title: Graph Theory Learning Graph
Creator: Dan McCreary
Version: 1.0
Groups: 10
Nodes: 25
Edges: 24
Orphan nodes: 0
✓ Learning graph is valid!Schema Overview
The schema validates learning graphs using JSON Schema Draft 2020-12 and includes:
Metadata Section (required)
- Required fields:
- title: The learning graph title
- description: Detailed description
- Optional fields:
- dcreator: Author/organization
- ddate: Creation date (YYYY-MM-DD format)
- dversion: Version number (e.g., "1.0")
- dformat: Format specification
- dschema: URL to this schema
- dlicense: License information
Groups Section (required)
Taxonomy groups with styling properties:
- Required: color (CSS color value)
- Optional:
- font (with color and size properties)
- shape (default shape for group)
Nodes Section (required)
Array of concept nodes with:
- Required: id, label, group
- Optional: shape, color, font, x, y, fixed, hidden
- All styling can override group defaults
Edges Section (required)
Array of dependency edges with:
- Required: from, to (node IDs)
- Optional: id, label, arrows, color, width, dashes
Validation Features
- ✓ Validates data types (string, integer, number, boolean, array, object)
- ✓ Enforces required fields
- ✓ Validates CSS color patterns (#hex, named colors, rgb/rgba)
- ✓ Validates date format (YYYY-MM-DD)
- ✓ Validates version format (semantic versioning)
- ✓ Validates URI format for schema URL
- ✓ Validates shape enums (box, circle, star, etc.)
- ✓ Enforces minimum values (IDs >= 1, sizes >= 1, etc.)
- ✓ Prevents additional properties where appropriate
Sample Verification
✓ Schema is valid JSON ✓ All required keys present in learning-graph.json ✓ Metadata fields: 8 properties ✓ Groups count: 10 ✓ Nodes count: 25 ✓ Edges count: 24
#!/usr/bin/env python3
"""
Generate Taxonomy Distribution Report
Analyzes the taxonomy distribution in the concept dependency CSV
and generates a detailed distribution report with recommendations.
"""
import csv
from collections import defaultdict
from typing import Dict, List, Tuple
def analyze_taxonomy_distribution(csv_path: str, output_path: str, taxonomy_names: dict = None):
"""
Analyze taxonomy distribution and generate report.
Args:
csv_path: Path to input CSV file
output_path: Path to output markdown report
taxonomy_names: Optional dictionary mapping taxonomy IDs to full names
"""
# Default taxonomy names
default_names = {
'FOUND': 'Foundation Concepts - Prerequisites',
'DEF': 'Definitional Concepts',
'CORE': 'Core Concepts',
'INTER': 'Intermediate Topics',
'ADV': 'Advanced Topics',
'APPLIED': 'Applied Concepts',
'SPEC': 'Specialized Topics',
'PROJ': 'Capstone Projects Ideas',
'MISC': 'Miscellaneous',
}
names = taxonomy_names if taxonomy_names is not None else default_names
# Read CSV and count by taxonomy
taxonomy_counts = defaultdict(int)
taxonomy_concepts = defaultdict(list)
with open(csv_path, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
tax = row['TaxonomyID']
taxonomy_counts[tax] += 1
taxonomy_concepts[tax].append((int(row['ConceptID']), row['ConceptLabel']))
total_concepts = sum(taxonomy_counts.values())
# Calculate percentages
taxonomy_data = []
for tax, count in taxonomy_counts.items():
percentage = (count / total_concepts) * 100
name = names.get(tax, tax)
taxonomy_data.append((tax, name, count, percentage))
# Sort by count descending
taxonomy_data.sort(key=lambda x: x[2], reverse=True)
# Identify issues
over_represented = [(tax, name, count, pct) for tax, name, count, pct in taxonomy_data if pct > 30]
under_represented = [(tax, name, count, pct) for tax, name, count, pct in taxonomy_data if pct < 3]
# Generate markdown report
with open(output_path, 'w', encoding='utf-8') as f:
f.write("# Taxonomy Distribution Report\n\n")
f.write("## Overview\n\n")
f.write(f"- **Total Concepts**: {total_concepts}\n")
f.write(f"- **Number of Taxonomies**: {len(taxonomy_counts)}\n")
f.write(f"- **Average Concepts per Taxonomy**: {total_concepts / len(taxonomy_counts):.1f}\n\n")
f.write("## Distribution Summary\n\n")
f.write("| Category | TaxonomyID | Count | Percentage | Status |\n")
f.write("|----------|-----------|-------|------------|--------|\n")
for tax, name, count, pct in taxonomy_data:
status = "✅"
if pct > 30:
status = "⚠️ Over"
elif pct < 3:
status = "ℹ️ Under"
f.write(f"| {name} | {tax} | {count} | {pct:.1f}% | {status} |\n")
f.write("\n")
# Visual distribution
f.write("## Visual Distribution\n\n")
f.write("```\n")
for tax, name, count, pct in taxonomy_data:
bar_length = int(pct / 2) # Scale to fit
bar = "█" * bar_length
f.write(f"{tax:6s} {bar} {count:3d} ({pct:5.1f}%)\n")
f.write("```\n\n")
# Balance analysis
f.write("## Balance Analysis\n\n")
if over_represented:
f.write("### ⚠️ Over-Represented Categories (>30%)\n\n")
for tax, name, count, pct in over_represented:
f.write(f"- **{name}** ({tax}): {count} concepts ({pct:.1f}%)\n")
f.write(f" - *Recommendation*: Consider splitting into subcategories or moving some concepts to related categories\n")
f.write("\n")
else:
f.write("### ✅ No Over-Represented Categories\n\n")
f.write("All categories are under the 30% threshold. Good balance!\n\n")
if under_represented:
f.write("### ℹ️ Under-Represented Categories (<3%)\n\n")
for tax, name, count, pct in under_represented:
f.write(f"- **{name}** ({tax}): {count} concepts ({pct:.1f}%)\n")
f.write(f" - *Note*: Small categories are acceptable for specialized topics\n")
f.write("\n")
# Detailed category breakdowns
f.write("## Category Details\n\n")
for tax, name, count, pct in taxonomy_data:
f.write(f"### {name} ({tax})\n\n")
f.write(f"**Count**: {count} concepts ({pct:.1f}%)\n\n")
concepts = taxonomy_concepts[tax]
concepts.sort(key=lambda x: x[0]) # Sort by ID
f.write("**Concepts**:\n\n")
for concept_id, concept_label in concepts[:15]: # Show first 15
f.write(f"- {concept_id}. {concept_label}\n")
if len(concepts) > 15:
f.write(f"- *...and {len(concepts) - 15} more*\n")
f.write("\n")
# Recommendations
f.write("## Recommendations\n\n")
# Check balance
max_pct = max(pct for _, _, _, pct in taxonomy_data)
min_pct = min(pct for _, _, _, pct in taxonomy_data if pct > 0)
spread = max_pct - min_pct
if spread < 15:
f.write("- ✅ **Excellent balance**: Categories are evenly distributed (spread: {:.1f}%)\n".format(spread))
elif spread < 25:
f.write("- ✅ **Good balance**: Categories are reasonably distributed (spread: {:.1f}%)\n".format(spread))
else:
f.write("- ⚠️ **Consider rebalancing**: Large spread between categories ({:.1f}%)\n".format(spread))
# Check for MISC
misc_count = taxonomy_counts.get('MISC', 0)
misc_pct = (misc_count / total_concepts) * 100 if total_concepts > 0 else 0
if misc_pct < 2:
f.write("- ✅ **MISC category minimal**: Good categorization specificity\n")
else:
f.write(f"- ⚠️ **MISC category has {misc_count} concepts**: Review for better categorization\n")
# Educational recommendations
f.write("\n### Educational Use Recommendations\n\n")
f.write("- Use taxonomy categories for color-coding in graph visualizations\n")
f.write("- Design curriculum modules based on taxonomy groupings\n")
f.write("- Create filtered views for focused learning paths\n")
f.write("- Use categories for assessment organization\n")
f.write("- Enable navigation by topic area in interactive tools\n")
f.write("\n---\n\n")
f.write("*Report generated by learning-graph-reports/taxonomy_distribution.py*\n")
print(f"✅ Taxonomy distribution report generated: {output_path}")
return taxonomy_data
if __name__ == "__main__":
import sys
import json
# Parse command line arguments
if len(sys.argv) < 3:
print("Usage: python taxonomy-distribution.py <input_csv> <output_report.md> [taxonomy_names.json]")
print("\nExample:")
print(" python taxonomy-distribution.py data/concept-dependencies.csv reports/taxonomy-distribution.md")
print("\nOptional taxonomy_names.json format:")
print(json.dumps({
'FOUNDATION': 'Foundation Concepts',
'CORE': 'Core Concepts',
'ADVANCED': 'Advanced Topics'
}, indent=2))
sys.exit(1)
csv_path = sys.argv[1]
output_path = sys.argv[2]
# Load taxonomy names if provided
taxonomy_names = None
if len(sys.argv) > 3:
config_file = sys.argv[3]
with open(config_file, 'r', encoding='utf-8') as f:
taxonomy_names = json.load(f)
print(f"📋 Loaded taxonomy names from: {config_file}")
analyze_taxonomy_distribution(csv_path, output_path, taxonomy_names)
ConceptID,ConceptLabel,Dependencies,TaxonomyID
1,Introduction to Testing,,FOUND
2,Basic Concepts,1,DEF
3,Core Principles,1|2,CORE
4,Intermediate Topics,2|3,INTER
5,Advanced Techniques,3|4,ADV
{
"metadata": {
"title": "Learning Graph",
"description": "Learning graph with 5 concepts generated from CSV",
"creator": "CSV to JSON Converter",
"date": "2025-11-15",
"version": "1.0",
"format": "Learning Graph JSON v1.0",
"schema": "https://raw.githubusercontent.com/dmccreary/learning-graphs/refs/heads/main/src/schema/learning-graph-schema.json",
"license": "CC BY-NC-SA 4.0 DEED"
},
"groups": {
"FOUND": {
"classifierName": "Foundation Concepts",
"color": "red",
"font": {
"color": "white"
}
},
"DEF": {
"classifierName": "Definitions",
"color": "orange",
"font": {
"color": "black"
}
},
"CORE": {
"classifierName": "Core Concepts",
"color": "gold",
"font": {
"color": "black"
}
},
"INTER": {
"classifierName": "Intermediate",
"color": "green",
"font": {
"color": "black"
}
},
"ADV": {
"classifierName": "Advanced",
"color": "blue",
"font": {
"color": "white"
}
}
},
"nodes": [
{
"id": 1,
"label": "Introduction to Testing",
"group": "FOUND",
"shape": "box"
},
{
"id": 2,
"label": "Basic Concepts",
"group": "DEF"
},
{
"id": 3,
"label": "Core Principles",
"group": "CORE"
},
{
"id": 4,
"label": "Intermediate Topics",
"group": "INTER"
},
{
"id": 5,
"label": "Advanced Techniques",
"group": "ADV"
}
],
"edges": [
{
"from": 2,
"to": 1
},
{
"from": 3,
"to": 1
},
{
"from": 3,
"to": 2
},
{
"from": 4,
"to": 2
},
{
"from": 4,
"to": 3
},
{
"from": 5,
"to": 3
},
{
"from": 5,
"to": 4
}
]
}#!/usr/bin/env python3
"""
validate-learning-graph.py
Validates a learning graph JSON file against the learning-graph-schema.json
Usage: python3 validate-learning-graph.py <data-file> <schema-file>
"""
import json
import sys
from pathlib import Path
# ANSI color codes
GREEN = '\033[0;32m'
RED = '\033[0;31m'
YELLOW = '\033[1;33m'
NC = '\033[0m' # No Color
def validate_learning_graph(data_path, schema_path):
"""Validate a learning graph JSON file against the schema."""
# Try to import jsonschema
try:
import jsonschema
from jsonschema import validate, ValidationError, SchemaError
except ImportError:
print(f"{RED}Error: jsonschema library not found{NC}")
print("\nPlease install it with:")
print(" pip install jsonschema")
print("\nOr with conda:")
print(" conda install -c conda-forge jsonschema")
return False
# Load schema
try:
with open(schema_path, 'r') as f:
schema = json.load(f)
except json.JSONDecodeError as e:
print(f"{RED}✗ Schema file is not valid JSON: {e}{NC}")
return False
except Exception as e:
print(f"{RED}✗ Error reading schema file: {e}{NC}")
return False
# Load data
try:
with open(data_path, 'r') as f:
data = json.load(f)
except json.JSONDecodeError as e:
print(f"{RED}✗ Data file is not valid JSON: {e}{NC}")
return False
except Exception as e:
print(f"{RED}✗ Error reading data file: {e}{NC}")
return False
# Validate
try:
validate(instance=data, schema=schema)
print(f"{GREEN}✓ Validation successful!{NC}")
print("")
print("Summary:")
print(f" Title: {data.get('metadata', {}).get('title', 'N/A')}")
print(f" Creator: {data.get('metadata', {}).get('creator', 'N/A')}")
print(f" Version: {data.get('metadata', {}).get('version', 'N/A')}")
print(f" Date: {data.get('metadata', {}).get('date', 'N/A')}")
print(f" License: {data.get('metadata', {}).get('license', 'N/A')}")
print(f" Groups: {len(data.get('groups', {}))}")
print(f" Nodes: {len(data.get('nodes', []))}")
print(f" Edges: {len(data.get('edges', []))}")
# Check for orphan nodes
if 'nodes' in data and 'edges' in data:
node_ids = {node['id'] for node in data['nodes']}
connected_ids = set()
for edge in data['edges']:
connected_ids.add(edge['from'])
connected_ids.add(edge['to'])
orphans = node_ids - connected_ids
if orphans:
print(f" {YELLOW}Orphan nodes: {len(orphans)} (nodes with no connections){NC}")
if len(orphans) <= 10:
orphan_labels = [node['label'] for node in data['nodes'] if node['id'] in orphans]
print(f" {', '.join(orphan_labels)}")
else:
print(f" Orphan nodes: 0")
# Check for duplicate node IDs
node_ids_list = [node['id'] for node in data['nodes']]
if len(node_ids_list) != len(set(node_ids_list)):
duplicates = [id for id in node_ids_list if node_ids_list.count(id) > 1]
print(f" {RED}Warning: Duplicate node IDs found: {set(duplicates)}{NC}")
# Check for edges referencing non-existent nodes
if 'nodes' in data and 'edges' in data:
node_ids = {node['id'] for node in data['nodes']}
invalid_edges = []
for edge in data['edges']:
if edge['from'] not in node_ids:
invalid_edges.append(f"Edge from {edge['from']} -> {edge['to']}: source node {edge['from']} doesn't exist")
if edge['to'] not in node_ids:
invalid_edges.append(f"Edge from {edge['from']} -> {edge['to']}: target node {edge['to']} doesn't exist")
if invalid_edges:
print(f" {RED}Warning: Invalid edges found:{NC}")
for invalid in invalid_edges[:5]: # Show first 5
print(f" - {invalid}")
if len(invalid_edges) > 5:
print(f" ... and {len(invalid_edges) - 5} more")
return True
except ValidationError as e:
print(f"{RED}✗ Validation failed!{NC}")
print("")
print(f"Error path: {' -> '.join(str(p) for p in e.absolute_path)}")
print(f"Error: {e.message}")
if e.context:
print("\nAdditional errors:")
for suberror in e.context:
print(f" - {suberror.message}")
return False
except SchemaError as e:
print(f"{RED}✗ Schema itself is invalid: {e}{NC}")
return False
except Exception as e:
print(f"{RED}✗ Unexpected error during validation: {e}{NC}")
import traceback
traceback.print_exc()
return False
def main():
"""Main entry point."""
if len(sys.argv) != 3:
print(f"{RED}Error: Wrong number of arguments{NC}")
print(f"Usage: {sys.argv[0]} <data-file> <schema-file>")
sys.exit(1)
data_file = sys.argv[1]
schema_file = sys.argv[2]
# Validate the learning graph
success = validate_learning_graph(data_file, schema_file)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
#!/bin/bash
# validate-learning-graph.sh
# Validates a learning graph JSON file against the learning-graph-schema.json
# Usage: ./validate-learning-graph.sh <path-to-learning-graph.json>
set -e
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCHEMA_FILE="$SCRIPT_DIR/learning-graph-schema.json"
# Check if a file was provided
if [ $# -eq 0 ]; then
echo -e "${RED}Error: No learning graph file specified${NC}"
echo "Usage: $0 <path-to-learning-graph.json>"
echo ""
echo "Example:"
echo " $0 ../../docs/vis/combined-viewer/learning-graph.json"
exit 1
fi
# Get the input file
INPUT_FILE="$1"
# Check if input file exists
if [ ! -f "$INPUT_FILE" ]; then
echo -e "${RED}Error: File not found: $INPUT_FILE${NC}"
exit 1
fi
# Check if schema file exists
if [ ! -f "$SCHEMA_FILE" ]; then
echo -e "${RED}Error: Schema file not found: $SCHEMA_FILE${NC}"
exit 1
fi
echo -e "${YELLOW}Validating learning graph...${NC}"
echo "Input file: $INPUT_FILE"
echo "Schema file: $SCHEMA_FILE"
echo ""
# Run Python validation on the input file against the schema
python3 "$SCRIPT_DIR/validate-learning-graph.py" "$INPUT_FILE" "$SCHEMA_FILE"
# Capture exit code
EXIT_CODE=$?
echo ""
if [ $EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}✓ Learning graph is valid!${NC}"
else
echo -e "${RED}✗ Learning graph validation failed${NC}"
fi
exit $EXIT_CODE
vis.js Network Format Reference for Learning Graph Generator
Overview
This document defines the standard JSON format for learning graphs compatible with the vis.js Network library. Use this format when generating learning graphs to ensure proper visualization.
Purpose
When using the Learning Graph Generator skill, the final JSON output must conform to this specification to work with vis.js network visualizations commonly used in educational web applications.
JSON Structure
Top-Level Schema
{
"nodes": [...],
"edges": [...],
"metadata": {...}
}Complete Example
{
"nodes": [
{
"id": 1,
"label": "Complex Numbers",
"group": "MATH"
},
{
"id": 2,
"label": "Euler's Formula",
"group": "MATH"
}
],
"edges": [
{
"from": 1,
"to": 2
}
],
"metadata": {
"title": "Course Learning Graph",
"description": "200 interconnected concepts",
"nodeCount": 200,
"edgeCount": 229,
"taxonomies": {
"MATH": "Mathematical Foundations",
"FFT": "FFT Algorithm & Implementation"
}
}
}Node Format
Required Properties
| Property | Type | Description | Example |
|---|---|---|---|
id | number | Unique identifier (1-based index) | 1, 2, 3 |
label | string | Display text (max 32 chars) | "Complex Numbers" |
Optional Properties
| Property | Type | Description | Example |
|---|---|---|---|
group | string | Taxonomy/category for styling | "MATH", "FFT" |
title | string | Tooltip text on hover | "Foundational concept" |
shape | string | Node shape | "dot", "box", "star" |
color | string | Override group color | "red", "#FF0000" |
x | number | Fixed horizontal position | -900, 900 |
y | number | Fixed vertical position | 0, 100 |
fixed | object | Lock position | {"x": true, "y": false} |
Node Example with All Properties
{
"id": 1,
"label": "Complex Numbers",
"group": "MATH",
"title": "Foundational mathematical concept",
"shape": "box",
"color": "red",
"x": -900,
"fixed": {"x": true, "y": false}
}Edge Format
Required Properties
| Property | Type | Description | Example |
|---|---|---|---|
from | number | Source node ID (prerequisite) | 1 |
to | number | Target node ID (dependent) | 2 |
Optional Properties
| Property | Type | Description | Example |
|---|---|---|---|
arrows | string/object | Arrow direction | "to", {"to": true} |
color | string | Edge color | "gray", "#888888" |
width | number | Edge thickness | 1, 2, 3 |
label | string | Text on edge | "prerequisite" |
dashes | boolean/array | Dashed line style | true, [5, 5] |
Edge Example
{
"from": 1,
"to": 2,
"arrows": "to",
"color": "gray",
"width": 1
}Metadata Format
Optional but recommended for documentation and analytics.
Standard Metadata Properties
{
"metadata": {
"title": "FFT Benchmarking Course Learning Graph",
"description": "200 interconnected concepts for a 10-week course",
"nodeCount": 200,
"edgeCount": 229,
"version": "1.0",
"generated": "2025-10-30",
"taxonomies": {
"MATH": "Mathematical Foundations",
"FFT": "FFT Algorithm & Implementation",
"SIG": "Signal Processing",
"ARM": "ARM Architecture & DSP Hardware",
"MEM": "Memory Management & Optimization",
"FXP": "Fixed-Point Arithmetic",
"BENCH": "Benchmarking & Testing",
"LIB": "FFT Libraries & Integration",
"OPT": "Optimization Techniques"
}
}
}Important Distinctions
❌ WRONG: D3.js Format
{
"nodes": [
{
"id": 1,
"label": "Complex Numbers",
"taxonomy": "MATH" // ❌ Wrong property name
}
],
"links": [ // ❌ Wrong array name
{
"source": 1, // ❌ Wrong property name
"target": 2 // ❌ Wrong property name
}
]
}✅ CORRECT: vis.js Format
{
"nodes": [
{
"id": 1,
"label": "Complex Numbers",
"group": "MATH" // ✅ Correct for vis.js
}
],
"edges": [ // ✅ Correct array name
{
"from": 1, // ✅ Correct property name
"to": 2 // ✅ Correct property name
}
]
}Python Conversion Template
Standard CSV to vis.js JSON Converter
#!/usr/bin/env python3
"""
Convert concept dependencies CSV to vis.js network JSON format.
"""
import csv
import json
def convert_csv_to_json(csv_file, json_file):
"""Convert CSV to vis.js network format."""
nodes = []
edges = []
with open(csv_file, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
concept_id = int(row['ConceptID'])
label = row['ConceptLabel']
taxonomy = row['TaxonomyID']
deps = row['Dependencies'].strip()
# Create node (vis.js format)
node = {
"id": concept_id,
"label": label,
"group": taxonomy
}
nodes.append(node)
# Create edges (vis.js format: from/to)
if deps:
dependencies = [int(d) for d in deps.split('|')]
for dep in dependencies:
edge = {
"from": dep,
"to": concept_id
}
edges.append(edge)
# Create graph structure
graph = {
"nodes": nodes,
"edges": edges,
"metadata": {
"title": "Learning Graph",
"nodeCount": len(nodes),
"edgeCount": len(edges)
}
}
# Write to JSON file
with open(json_file, 'w') as f:
json.dump(graph, f, indent=2)
return graphJavaScript Loading Template
Standard vis.js Network Initialization
function drawGraph() {
// Fetch the graph data from JSON file
fetch('learning-graph.json')
.then(response => response.json())
.then(data => {
// Create DataSets for vis.js
const nodes = new vis.DataSet(data.nodes);
const edges = new vis.DataSet(data.edges);
// Optional: Customize nodes after loading
nodes.forEach(function (node) {
if (node.group === "MATH") {
node.x = -900;
node.fixed = { x: true, y: false };
node.shape = "box";
node.color = "red";
} else if (node.group === "OPT") {
node.x = 900;
node.fixed = { x: true, y: false };
node.shape = "star";
node.color = "gold";
}
});
// Create network
const container = document.getElementById('mynetwork');
const graphData = {
nodes: nodes,
edges: edges
};
// Network options
const options = {
physics: {
enabled: true,
solver: 'forceAtlas2Based',
stabilization: {
iterations: 1000,
updateInterval: 25
}
},
edges: {
arrows: {
to: {
enabled: true,
type: 'arrow'
}
},
smooth: {
type: 'continuous'
}
},
nodes: {
shape: 'dot',
size: 20,
font: {
size: 14,
color: 'black'
},
borderWidth: 2
}
};
// Initialize network
const network = new vis.Network(container, graphData, options);
})
.catch(error => {
console.error("Error loading JSON:", error);
});
}Group/Taxonomy Colors
Recommended color scheme for educational taxonomy groups:
const taxonomyColors = {
"MATH": "#E74C3C", // Red - Foundational
"FFT": "#3498DB", // Blue - Core algorithms
"SIG": "#2ECC71", // Green - Signal processing
"ARM": "#9B59B6", // Purple - Hardware
"MEM": "#F39C12", // Orange - Memory
"FXP": "#1ABC9C", // Teal - Numeric precision
"BENCH": "#E67E22", // Dark orange - Testing
"LIB": "#95A5A6", // Gray - Libraries
"OPT": "#F1C40F" // Yellow/Gold - Optimization
};Validation Checklist
When generating learning graph JSON for vis.js, verify:
- [ ] Top-level object has
nodesarray (notvertices) - [ ] Top-level object has
edgesarray (notlinks) - [ ] Each node has
id(number) andlabel(string) - [ ] Nodes use
groupproperty (nottaxonomyorcategory) - [ ] Each edge has
from(number) andto(number) - [ ] Edges use
from/to(notsource/target) - [ ] All
fromandtovalues reference valid node IDs - [ ] No self-loops (edge where
from === to) - [ ] Graph forms valid DAG (no cycles) for learning paths
- [ ] Optional: metadata object included for documentation
Common Mistakes to Avoid
| Mistake | Issue | Solution |
|---|---|---|
Using links | vis.js won't find edges | Use edges |
Using source/target | Edges won't connect | Use from/to |
Using taxonomy | Groups won't work | Use group |
| String IDs | Type mismatch errors | Use numeric IDs |
Missing label | Blank nodes | Always include labels |
| Circular edges | Cycles in learning path | Validate DAG structure |
File Naming Convention
Recommended:
learning-graph.json- Main graph fileconcept-dependencies.csv- Source CSV fileconvert-to-json.py- Conversion script
Not Recommended:
graph.json- Too genericnetwork.json- Ambiguousdata.json- Not descriptive
vis.js Documentation References
- Official docs: https://visjs.github.io/vis-network/docs/network/
- Node options: https://visjs.github.io/vis-network/docs/network/nodes.html
- Edge options: https://visjs.github.io/vis-network/docs/network/edges.html
- Physics: https://visjs.github.io/vis-network/docs/network/physics.html
- Examples: https://visjs.github.io/vis-network/examples/
Summary for SKILL Generation
Quick Reference Card
For Learning Graph Generator Skill:
1. Array names: nodes and edges (not links/vertices) 2. Node structure: {id: number, label: string, group: string} 3. Edge structure: {from: number, to: number} 4. Group property: Use for taxonomy categories (enables coloring) 5. Metadata: Optional but recommended for documentation 6. Validation: Ensure DAG structure (no cycles) 7. Python template: Use provided converter code 8. Testing: Load in vis.js Network to verify rendering
Integration Workflow
CSV (concept-dependencies.csv)
↓
Python Script (convert-to-json.py)
↓
JSON (learning-graph.json) [vis.js format]
↓
HTML + JavaScript (vis.Network)
↓
Interactive Graph Visualization---
Document Version: 1.0 Created: 2025-10-30 For Use With: Learning Graph Generator Skill Target Library: vis.js Network v9.x+ Status: Production-ready reference