Claude-skill-registry Implement Dynamic Graph Question
Create interactive coordinate plane questions using p5.js where students draw linear lines with snap-to-grid.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/implement-dynamic-graph-question" ~/.claude/skills/majiayu000-claude-skill-registry-implement-dynamic-graph-question && rm -rf "$T"
manifest:
skills/data/implement-dynamic-graph-question/SKILL.mdsource content
Implement Dynamic Graph Question
Use this skill when creating questions where students:
- Draw lines on an interactive coordinate plane
- Explore linear relationships by drawing from points
- Create proportional relationships from the origin
- Compare multiple linear scenarios
When to Use This Pattern
Perfect for:
- "Draw a line showing a proportional relationship"
- "Draw a line from (0,0) through (5, 10)"
- "Draw lines to match the given equations"
- Interactive slope/linear function exploration
- Comparing rates by drawing multiple lines
Not suitable for:
- Static graph reading → use implement-static-graph-question
- Simple table completion → use implement-table-question
- Pre-defined graph manipulation (use slider pattern)
Technology Stack
Uses p5.js (NOT D3) for the coordinate plane because:
- Better for real-time interactive drawing
- Simpler mouse/touch event handling
- Built-in animation and rendering loop
- Easier geometric operations
Integrates with D3 for:
- Layout and cards (intro, explanation, etc.)
- State management
- Message protocol
Components Required
Copy these:
P5.js Coordinate Plane (Required)
→ Full p5 sketch in instance modesnippets/coordinate-plane-p5.js
D3 Cards (Optional)
→.claude/skills/question-types/snippets/cards/standard-card.jscreateStandardCard()
→.claude/skills/question-types/snippets/cards/explanation-card.jscreateExplanationCard()
P5.js Library (Required)
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script>
Quick Start
-
Study the base implementation:
cat alex/coordinatePlane/linear-graph-drawing.ts cat .claude/skills/question-types/implement-dynamic-graph-question/snippets/coordinate-plane-p5.js -
Copy the p5 coordinate plane snippet into your chart.js IIFE
-
Follow the integration pattern below
State Shape
function createDefaultState() { return { drawnLines: [], // [{ start: {x, y}, end: {x, y} }, ...] explanation: "" }; }
Core Integration Pattern
1. Load P5.js (in chart.html for testing)
<!DOCTYPE html> <html> <head> <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script> <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script> </head> <body> <div id="chart-root"></div> <script src="chart.js"></script> <script>createChart("#chart-root");</script> </body> </html>
2. Chart.js Structure (IIFE with p5 + D3)
(function () { "use strict"; // ============================================================ // INLINE P5 COORDINATE PLANE COMPONENT HERE // ============================================================ // [Paste full content of coordinate-plane-p5.js here] // ============================================================ // INLINE D3 CARD COMPONENTS (if needed) // ============================================================ // [Paste createStandardCard, createExplanationCard, etc.] // ============================================================ // QUESTION STATE & SETUP // ============================================================ function createDefaultState() { return { drawnLines: [], explanation: "" }; } let chartState = createDefaultState(); let interactivityLocked = false; let d3Promise = null; let messageHandlerRegistered = false; let containerSelectorRef = "#chart-root"; let currentD3 = null; let coordinatePlane = null; // D3 loading... function ensureD3() { /* ... standard D3 loading ... */ } // Message protocol function sendMessage(type, payload) { /* ... standard ... */ } function sendChartState() { sendMessage("response_updated", { lines: chartState.drawnLines.map(line => ({ start: { x: line.start.x, y: line.start.y }, end: { x: line.end.x, y: line.end.y }, slope: calculateSlope(line), intercept: calculateIntercept(line) })), explanation: { text: chartState.explanation } }); } function calculateSlope(line) { const dx = line.end.x - line.start.x; const dy = line.end.y - line.start.y; return dx === 0 ? Infinity : dy / dx; } function calculateIntercept(line) { const slope = calculateSlope(line); if (slope === Infinity) return null; return line.start.y - slope * line.start.x; } function setInteractivity(enabled) { interactivityLocked = !enabled; if (coordinatePlane) { coordinatePlane.setLocked(!enabled); } } function buildLayout(d3, containerSelector) { const container = d3.select(containerSelector); container.html(""); container .style("font-family", "'Inter', system-ui, -apple-system") .style("padding", "20px") .style("overflow", "auto"); // Intro card (using D3) const introCard = createStandardCard(d3, container, { size: "large", title: "Draw Linear Relationships" }); introCard.append("p") .text("Click to start drawing a line, then click again to finish. Lines snap to grid intersections."); // P5 coordinate plane container const planeContainer = container.append("div") .attr("id", "coordinate-plane-container") .style("margin", "20px 0") .node(); // Create p5 coordinate plane coordinatePlane = createCoordinatePlane("coordinate-plane-container", { xMin: 0, xMax: 10, yMin: 0, yMax: 100, xLabel: "Time (hours)", yLabel: "Distance (miles)", gridScaleX: 1, gridScaleY: 10, // Optional: initialPoints, initialEquations, predrawnStartPoint }, { onLineDrawn: (line) => { console.log("Line drawn:", line); }, onLinesChanged: (lines) => { chartState.drawnLines = lines; sendChartState(); } }); // Explanation card (using D3) createExplanationCard(d3, container, { prompt: "Explain your thinking:", value: chartState.explanation, onInput: (value) => { chartState.explanation = value; sendChartState(); }, locked: interactivityLocked }); } function applyInitialState(payload) { if (!payload) return; if (payload.lines && Array.isArray(payload.lines)) { chartState.drawnLines = payload.lines; // Restore lines in p5 coordinate plane // Note: Need to expose setLines method from p5 sketch } chartState.explanation = payload.explanation?.text || ""; } function setupMessageHandlers(d3) { if (messageHandlerRegistered) return; messageHandlerRegistered = true; window.addEventListener("message", (event) => { const { data } = event; if (!data || typeof data !== "object") return; if (data.type === "setInitialState") { applyInitialState(data.payload); } if (data.type === "set_lock") { setInteractivity(data.payload === false); } if (data.type === "check_answer") { sendChartState(); } }); } function createChart(containerSelector) { containerSelectorRef = containerSelector || "#chart-root"; ensureD3() .then((d3) => { currentD3 = d3; buildLayout(d3, containerSelectorRef); window.clearChart = function () { chartState = createDefaultState(); if (coordinatePlane && coordinatePlane.reset) { coordinatePlane.reset(); } sendMessage("selection_cleared", null); sendChartState(); }; window.setInteractivity = setInteractivity; setupMessageHandlers(d3); }) .catch((error) => { console.error(error); const fallback = document.querySelector(containerSelectorRef) || document.body; fallback.innerHTML = "<p style='color:#b91c1c'>Failed to load visualization.</p>"; }); } window.createChart = createChart; window.createArtifact = createChart; })();
Configuration Options
Basic Configuration
{ // Canvas size width: 600, // Canvas width in pixels (default: 600) height: 600, // Canvas height in pixels (default: 600) // Axis ranges xMin: 0, xMax: 10, yMin: 0, yMax: 100, // Grid spacing gridScaleX: 1, // X-axis grid step gridScaleY: 10, // Y-axis grid step // Labels xLabel: "Time (hours)", yLabel: "Distance (miles)", xVariable: "t", // Optional italic variable (e.g., "t" for time) yVariable: "d", // Optional italic variable (e.g., "d" for distance) // Display options showCoordinatesOnHover: true, // Show (x, y) on hover (default: true) allowInput: true, // Enable interactive drawing (default: true) }
With Initial Data
{ xMin: 0, xMax: 10, yMin: 0, yMax: 100, gridScaleX: 1, gridScaleY: 10, xLabel: "X", yLabel: "Y", // Show reference equations (extended lines) initialEquations: [ { slope: 5, intercept: 0, color: [34, 197, 94] }, // y = 5x (green) { slope: 8, intercept: 10, color: [59, 130, 246] } // y = 8x + 10 (blue) ], // Show specific points initialPoints: [ { x: 2, y: 10 }, { x: 5, y: 25 }, { x: 8, y: 40 } ], // Show line segments (can be used instead of equations) initialLines: [ { start: { x: 0, y: 0 }, end: { x: 10, y: 50 }, color: [37, 99, 235] // Optional RGB color } ], // Make it static (no drawing allowed) allowInput: false, }
Force Drawing from Origin
{ xMin: 0, xMax: 10, yMin: 0, yMax: 50, gridScaleX: 1, gridScaleY: 5, xLabel: "X", yLabel: "Y", // Force first line to start from (0, 0) predrawnStartPoint: { x: 0, y: 0 }, }
Common Patterns
Pattern 1: Proportional Relationships (from origin)
const plane = createCoordinatePlane("container", { xMin: 0, xMax: 10, yMin: 0, yMax: 50, xLabel: "X", yLabel: "Y", predrawnStartPoint: { x: 0, y: 0 }, }, { onLineDrawn: (line) => { const slope = (line.end.y - line.start.y) / (line.end.x - line.start.x); console.log(`Constant of proportionality: ${slope}`); } });
Pattern 2: Compare to Reference Line
const plane = createCoordinatePlane("container", { xMin: 0, xMax: 10, yMin: 0, yMax: 100, xLabel: "Time", yLabel: "Distance", initialEquations: [ { slope: 5, intercept: 0, color: [34, 197, 94] } // Reference: 5 mph ], allowInput: true, // Students can draw their own lines }, { onLinesChanged: (lines) => { // Students draw their own speed, compare to reference } });
Pattern 3: Static Display (No Interaction)
const plane = createCoordinatePlane("container", { width: 800, height: 400, xMin: 0, xMax: 12, yMin: 0, yMax: 60, xLabel: "Months", yLabel: "Temperature (°F)", initialPoints: [ { x: 1, y: 32 }, { x: 3, y: 45 }, { x: 6, y: 72 }, { x: 9, y: 58 }, { x: 12, y: 35 } ], initialLines: [ { start: { x: 0, y: 40 }, end: { x: 12, y: 40 }, color: [200, 200, 200] } ], allowInput: false, // No drawing allowed - display only showCoordinatesOnHover: true, // Can still show coordinates on hover drawFullLines: false, // Don't extend lines to edges });
Note: Even in static mode (
allowInput: false), you can still enable showCoordinatesOnHover: true to display coordinate values on hover without allowing any drawing or editing.
Pattern 4: Connect the Dots
const plane = createCoordinatePlane("container", { xMin: 0, xMax: 5, yMin: 0, yMax: 4000, xLabel: "Days", yLabel: "Steps", initialPoints: [ { x: 1, y: 800 }, { x: 3, y: 2400 }, { x: 5, y: 4000 } ], allowInput: true, // Students can draw lines through points });
Implementation Checklist
- Loaded p5.js library in chart.html
- Inlined
into chart.js IIFEcoordinate-plane-p5.js - Inlined D3 card components (if needed)
- Created
withcreateDefaultState()
arraydrawnLines - Created p5 container div with unique ID
- Called
with configurationcreateCoordinatePlane() - Implemented
callback to update chartStateonLinesChanged - Implemented
with line datasendChartState() - Calculated slope/intercept for each line (if needed)
- Implemented
to lock/unlock drawingsetInteractivity() - Implemented
(if state restoration needed)applyInitialState() - Tested locally with chart.html
- Tested drawing lines
- Tested keyboard controls (R to reset, ESC to cancel, H to hide/show instructions)
- Tested state updates trigger
messagesresponse_updated
Tips
- p5 + D3 coexistence - p5 handles the graph, D3 handles everything else
- Use instance mode - Never use global p5 mode when embedding
- Unique container IDs - Each p5 sketch needs its own container
- State extraction - Pull line data from p5 into chartState via callbacks
- Clear instructions - Tell students the interaction model (click-click-click)
- Snap-to-grid - Already built-in, makes drawing easier
- Preview line - Extends to infinity, helps visualize slope
- Initial equations - Great for "match this slope" exercises
- Hide/show instructions - Press H to toggle the instructions box for better visibility
Limitations & Notes
- One p5 instance per question - Don't create multiple coordinate planes
- State restoration - Need to explicitly restore lines to p5 from
setInitialState - Mobile support - p5 handles touch events automatically
- Performance - p5 is fast, but keep canvas size reasonable (600x600 default)
Related Skills
- implement-static-graph-question - For non-interactive graphs
- implement-slider-question - For parameter adjustment
- create-p5-animation - For animated explanations
- create-d3-question - Parent workflow skill
Additional Resources
- snippets/coordinate-plane-p5.js - Full p5 component
- alex/coordinatePlane/ - Original p5 implementation
- p5.js Reference - p5.js documentation