---
name: Implement Dynamic Graph Question
description: Create interactive coordinate plane questions using p5.js where students draw linear lines with snap-to-grid.
---

# 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](../implement-static-graph-question/SKILL.md)
- Simple table completion → use [implement-table-question](../implement-table-question/SKILL.md)
- 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)
- `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)
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.7.0/p5.js"></script>
```

## Quick Start

1. **Study the base implementation**:
   ```bash
   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

```javascript
function createDefaultState() {
  return {
    drawnLines: [],  // [{ start: {x, y}, end: {x, y} }, ...]
    explanation: ""
  };
}
```

## Core Integration Pattern

### 1. Load P5.js (in chart.html for testing)

```html
<!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)

```javascript
(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

```javascript
{
  // 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

```javascript
{
  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

```javascript
{
  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)

```javascript
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

```javascript
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)

```javascript
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

```javascript
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

- [implement-static-graph-question](../implement-static-graph-question/SKILL.md) - For non-interactive graphs
- [implement-slider-question](../implement-slider-question/SKILL.md) - For parameter adjustment
- [create-p5-animation](../../create-p5-animation/SKILL.md) - For animated explanations
- [create-d3-question](../../create-d3-question/SKILL.md) - Parent workflow skill

## Additional Resources

- [snippets/coordinate-plane-p5.js](snippets/coordinate-plane-p5.js) - Full p5 component
- [alex/coordinatePlane/](../../../../../alex/coordinatePlane/) - Original p5 implementation
- [p5.js Reference](https://p5js.org/reference/) - p5.js documentation
