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.md
source 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:

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)

  • snippets/coordinate-plane-p5.js
    → Full p5 sketch in instance mode

D3 Cards (Optional)

  • .claude/skills/question-types/snippets/cards/standard-card.js
    createStandardCard()
  • .claude/skills/question-types/snippets/cards/explanation-card.js
    createExplanationCard()

P5.js Library (Required)

<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script>

Quick Start

  1. 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
    
  2. Copy the p5 coordinate plane snippet into your chart.js IIFE

  3. 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
    coordinate-plane-p5.js
    into chart.js IIFE
  • Inlined D3 card components (if needed)
  • Created
    createDefaultState()
    with
    drawnLines
    array
  • Created p5 container div with unique ID
  • Called
    createCoordinatePlane()
    with configuration
  • Implemented
    onLinesChanged
    callback to update chartState
  • Implemented
    sendChartState()
    with line data
  • Calculated slope/intercept for each line (if needed)
  • Implemented
    setInteractivity()
    to lock/unlock drawing
  • Implemented
    applyInitialState()
    (if state restoration needed)
  • 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
    response_updated
    messages

Tips

  1. p5 + D3 coexistence - p5 handles the graph, D3 handles everything else
  2. Use instance mode - Never use global p5 mode when embedding
  3. Unique container IDs - Each p5 sketch needs its own container
  4. State extraction - Pull line data from p5 into chartState via callbacks
  5. Clear instructions - Tell students the interaction model (click-click-click)
  6. Snap-to-grid - Already built-in, makes drawing easier
  7. Preview line - Extends to infinity, helps visualize slope
  8. Initial equations - Great for "match this slope" exercises
  9. 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

Additional Resources