Skip to content

[Interactive Graph] Add tangent graph scoring#3356

Merged
ivyolamit merged 3 commits intomainfrom
LEMS-3955/tangent-scoring
Mar 19, 2026
Merged

[Interactive Graph] Add tangent graph scoring#3356
ivyolamit merged 3 commits intomainfrom
LEMS-3955/tangent-scoring

Conversation

@ivyolamit
Copy link
Copy Markdown
Contributor

@ivyolamit ivyolamit commented Mar 13, 2026

Summary:

PR series to add tangent graph support to the Interactive Graph widget:

  1. Foundation — Add tangent graph type definitions and data schema
  2. Math layer — Add tangent math utilities to kmath
  3. State management — Reducer, actions, initialization, and test data
  4. Rendering — The tangent graph component, Storybook story, and AI utils
  5. ▶️ Scoring — Add tangent scoring to the scoring package
  6. Editor — Add tangent to answer type

This is the fifth PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It adds the scoring layer — the final piece needed for tangent exercises to be fully functional (behind the feature flag).


Add tangent graph scoring to support the Tangent graph in Interactive Graph

  • Adds tangent scoring to scoreInteractiveGraph() using getTangentCoefficients and canonicalTangentCoefficients from kmath
  • Follows the sinusoid scoring pattern: extract coefficients from both guess and rubric, canonicalize, then compare
  • 6 new tests covering invalid input, correct/incorrect answers, equivalent curves, and negative amplitude
Implementation notes

Scoring follows the sinusoid pattern exactly. The tangent scoring block extracts coefficients from both the user's guess and the rubric's correct answer using getTangentCoefficients() (from kmath), canonicalizes both with canonicalTangentCoefficients(), and compares with approximateDeepEqual(). This handles equivalent curves that use different control points (e.g., shifted by a full period).

Uses kmath's canonicalTangentCoefficients, NOT the legacy grapher-util version. The kmath version (PR 2) only guarantees b > 0, which is mathematically correct for tangent. The legacy version guarantees both a > 0 and b > 0 using a mathematically incorrect phase shift. See PR 2 implementation notes for details.

References

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

Test plan:

  • pnpm tsc — no type errors
  • pnpm lint — no lint errors
  • pnpm prettier . --check — formatting clean
  • pnpm knip — no unused exports
  • Invalid input tests pass (undefined guess, missing coords)
  • Correct answer test passes
  • Incorrect answer test passes
  • Equivalent curves test passes (period-shifted control points)
  • Negative amplitude test passes

@ivyolamit ivyolamit self-assigned this Mar 13, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

🗄️ Schema Change: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

🛠️ Item Splitting: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

Size Change: +44 B (+0.01%)

Total Size: 488 kB

Filename Size Change
packages/perseus-score/dist/es/index.js 9.44 kB +44 B (+0.47%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.5 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 6.03 kB
packages/math-input/dist/es/index.js 98.5 kB
packages/math-input/dist/es/strings.js 1.61 kB
packages/perseus-core/dist/es/index.item-splitting.js 11.9 kB
packages/perseus-core/dist/es/index.js 25 kB
packages/perseus-editor/dist/es/index.js 100 kB
packages/perseus-linter/dist/es/index.js 8.82 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/index.js 189 kB
packages/perseus/dist/es/strings.js 7.66 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.71 kB

compressed-size-action

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (3a9b485) and published it to npm. You
can install it using the tag PR3356.

Example:

pnpm add @khanacademy/perseus@PR3356

If you are working in Khan Academy's frontend, you can run the below command.

./dev/tools/bump_perseus_version.ts -t PR3356

If you are working in Khan Academy's webapp, you can run the below command.

./dev/tools/bump_perseus_version.js -t PR3356

@ivyolamit ivyolamit marked this pull request as ready for review March 13, 2026 23:45
@ivyolamit
Copy link
Copy Markdown
Contributor Author

Summary of the tangent implementation

PR Scope Note
1 Foundation — Add tangent graph type definitions and data schema #3345 for review
2 Math layer — Add tangent math utilities to kmath #3347 for review
3 State management — Reducer, actions, initialization, and test data #3353 for review
4 Rendering — The tangent graph component, Storybook story, and AI utils #3354 base PR (for review)
5 Scoring — Add tangent scoring to the scoring package #3356 this PR
6 Editor — Add tangent to answer type 🔜

Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — straightforward addition of tangent scoring that mirrors the existing sinusoid pattern exactly.

Extended reasoning...

Overview

This PR adds tangent graph scoring to scoreInteractiveGraph() in the perseus-score package. It touches 3 files: a changeset, the scoring implementation, and tests. The implementation adds ~26 lines of scoring logic (a new else if branch) and ~140 lines of tests (6 test cases).

Security risks

None. This is pure scoring logic that compares mathematical coefficients — no user-facing I/O, no network calls, no auth, no data persistence.

Level of scrutiny

Low scrutiny warranted. The tangent scoring block is a near-verbatim copy of the sinusoid scoring block, substituting getTangentCoefficients / canonicalTangentCoefficients for the sinusoid equivalents. The pattern is well-established in this codebase. The change is behind a feature flag per the PR description.

Other factors

  • 6 tests cover the key cases: undefined guess, missing coords, correct answer, incorrect answer, equivalent curves (period-shifted), and negative amplitude
  • No outstanding reviewer comments or prior reviews
  • No bugs found by the bug hunting system
  • Bundle size impact is negligible (+43 B)
  • The mathematical correctness of getTangentCoefficients and canonicalTangentCoefficients is the responsibility of the kmath package (separate PRs in this series), not this scoring layer

ivyolamit added a commit that referenced this pull request Mar 17, 2026
#3345)

## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. ▶️ [Foundation — Add tangent graph type definitions and data schema](#3345)
2. [Math layer —  Add tangent math utilities to kmath](#3347)
3. [State management — Reducer, actions, initialization, and test data](#3353)
4. [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. [Editor — Add tangent to answer type](#3358)

This is the first PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It establishes the type foundation with zero runtime behavior change. The feature flag (`interactive-graph-tangent`) is added in #3344.

---

- Adds `PerseusGraphTypeTangent` and `TangentGraphCorrect` types to the data schema, following the sinusoid pattern
- Adds the JSON parser for the tangent graph type (`parsePerseusGraphTypeTangent`)
- Defines `TangentGraphState` interface (not yet exported or added to the `InteractiveGraphState` union — deferred to a later PR when reducer handlers are implemented)
- Adds `generateIGTangentGraph()` test data generator with unit tests
- Adds placeholder `case "tangent"` branches in all exhaustiveness switches affected by the new `PerseusGraphType` union member

<details>
<summary>Implementation notes</summary>

Adding `PerseusGraphTypeTangent` to the `PerseusGraphType` union triggers `UnreachableCaseError` in several switch statements. Placeholder cases were added to keep the build green:
- `interactive-graph-editor.tsx` — graph merging
- `start-coords/util.ts` — `shouldShowStartCoordsUI` returns `false` for tangent (downstream components `StartCoordsSettingsInner` and `getDefaultGraphStartCoords` don't handle tangent yet — returning `true` would show an empty section with a broken reset button)
- `interactive-graph-ai-utils.ts` — `getGraphOptionsForProps` + `getUserInput`
- `interactive-graph.tsx` — `getEquationString` (returns `""`)
- `initialize-graph-state.ts` — returns `type: "none"`

These placeholders will be replaced with real implementations in subsequent PRs.

`TangentGraphState` is intentionally **not exported** and **not added to the `InteractiveGraphState` union** in this PR. That union has its own set of exhaustiveness checks (`renderGraphElements`, `mafsStateToInteractiveGraph`, `getGradableGraph`), and adding to it requires reducer handlers to exist first. This happens in PR 3.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] Generator tests pass (`generateIGTangentGraph` default + all props)

Author: ivyolamit

Reviewers: ivyolamit, claude[bot], handeyeco, SonicScrewdriver

Required Reviewers:

Approved By: handeyeco

Checks: ✅ 10 checks were successful, ⏭️  1 check has been skipped

Pull Request URL: #3345
Comment on lines +2 to +6
"@khanacademy/kmath": minor
"@khanacademy/perseus": minor
"@khanacademy/perseus-core": minor
"@khanacademy/perseus-editor": minor
"@khanacademy/perseus-score": minor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"@khanacademy/kmath": minor
"@khanacademy/perseus": minor
"@khanacademy/perseus-core": minor
"@khanacademy/perseus-editor": minor
"@khanacademy/perseus-score": minor
"@khanacademy/perseus-score": minor

ivyolamit added a commit that referenced this pull request Mar 19, 2026
## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. [Foundation — Add tangent graph type definitions and data schema](#3345)
2. ▶️ [Math layer —  Add tangent math utilities to kmath](#3347)
3. [State management — Reducer, actions, initialization, and test data](#3353)
4. [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. [Editor — Add tangent to answer type](#3358)

This is the second PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It adds the pure math layer with no UI dependencies.

---

Added the tangent math utilities to kmath for supporting Tangent graph in Interactive Graph

- Adds `getTangentCoefficients()` to extract `[a, b, c, d]` from two control points for `f(x) = a * tan(b*x - c) + d`
- Adds `canonicalTangentCoefficients()` to normalize coefficients for scoring comparison (guarantees `b > 0`, phase in `[0, π)`)
- Adds `TangentCoefficient` and `NamedTangentCoefficient` types
- 13 new tests covering coefficient extraction and canonical normalization edge cases

<details>
<summary>Implementation notes</summary>

**Canonical normalization differs from the legacy Grapher widget version.** The legacy `canonicalTangentCoefficients` in `grapher-util.ts` guarantees both `a > 0` and `b > 0` using a `phase += π/2` step. However, this is mathematically incorrect for tangent — `tan(x + π/2) = -cot(x)`, not `-tan(x)`. The legacy version still works because its `areEqual` applies the same normalization to both sides, so the error cancels out.

Our version only guarantees `b > 0`, using the odd function identity `tan(-x) = -tan(x)`:
- If `b < 0`: flip signs of `a`, `b`, and `c`
- Normalize `c` to `[0, π)`

We intentionally do **not** replace the legacy version to avoid changing scoring behavior for existing Grapher tangent exercises.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] `canonicalTangentCoefficients` tests pass (negative b, phase normalization, equivalent curves, preserved negative amplitude)
- [ ] `getTangentCoefficients` tests pass (basic, vertical offset, phase shift, negative amplitude, negative angular frequency)


## Test plan:

Author: ivyolamit

Reviewers: claude[bot], ivyolamit, handeyeco, SonicScrewdriver

Required Reviewers:

Approved By: SonicScrewdriver

Checks: ⏭️  1 check has been skipped, ✅ 10 checks were successful

Pull Request URL: #3347
ivyolamit added a commit that referenced this pull request Mar 19, 2026
…3353)

## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. [Foundation — Add tangent graph type definitions and data schema](#3345)
2. [Math layer —  Add tangent math utilities to kmath](#3347)
3. ▶️ [State management — Reducer, actions, initialization, and test data](#3353)
4. [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. [Editor — Add tangent to answer type](#3358)

This is the third PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It wires up the state management layer — the reducer, initialization, actions, and gradable graph serialization.

---

Add tangent graph state management and reducer for supporting Tangent graph in Interactive Graph

- Exports `TangentGraphState` and adds it to the `InteractiveGraphState` union
- Adds `tangent.movePoint` action and reducer case with same-x constraint (prevents division by zero in `getTangentCoefficients`)
- Adds real tangent initialization in `initializeGraphState` via `getTangentCoords()` (default coords `[[0.5, 0.5], [0.75, 0.75]]`)
- Adds tangent case to `getGradableGraph` for scoring serialization
- Adds `withTangent()` builder method and `TangentGraphConfig` class for test data construction
- Adds `tangentQuestion` and `tangentQuestionWithDefaultCorrect` test data
- Adds tangent serialization in `mafsStateToInteractiveGraph`
- 13 new tests across 5 test files

<details>
<summary>Implementation notes</summary>

**InteractiveGraphState union update.** Adding `TangentGraphState` to the union triggers `UnreachableCaseError` in `renderGraphElements` (mafs-graph.tsx). A placeholder case returns `null` (no rendering) — replaced with real rendering in PR 4.

**Same-x constraint.** The `doMovePoint` tangent case rejects moves that would place both control points on the same vertical line. This mirrors the sinusoid constraint and prevents `getTangentCoefficients` from producing `Infinity` for `angularFrequency` (since it divides by `p2[0] - p1[0]`).

**`getTangentCoords()` is not exported.** It's only called within `initialize-graph-state.ts`. It will be exported in PR 6 (editor) when `start-coords/util.ts` needs it.

**`mafsStateToInteractiveGraph` tangent case is the real implementation** (not a placeholder) — it simply returns `{ ...originalGraph, coords: state.coords }`, same as sinusoid.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] Initialization tests pass (given coords, startCoords, defaults)
- [ ] Reducer tests pass (same-x rejection, out-of-bounds rejection, valid move)
- [ ] `getGradableGraph` tangent test passes
- [ ] `mafsStateToInteractiveGraph` tangent serialization test passes
- [ ] Tangent renders in parameterized "should render" tests

Author: ivyolamit

Reviewers: claude[bot], handeyeco, ivyolamit, SonicScrewdriver

Required Reviewers:

Approved By: handeyeco

Checks: ✅ 10 checks were successful, ⏭️  1 check has been skipped

Pull Request URL: #3353
@ivyolamit ivyolamit force-pushed the LEMS-3955/add-tangent-graph-component branch from 50d3194 to f815d9f Compare March 19, 2026 20:00
ivyolamit added a commit that referenced this pull request Mar 19, 2026
…tion string (#3354)

## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. [Foundation — Add tangent graph type definitions and data schema](#3345)
2. [Math layer —  Add tangent math utilities to kmath](#3347)
3. [State management — Reducer, actions, initialization, and test data](#3353)
4. ▶️ [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. [Editor — Add tangent to answer type](#3358)

This is the fourth PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It adds the rendering layer — the Mafs component, accessibility strings, equation string generation, and Storybook coverage.

---

Created the tangent graph visual component, add Storybook coverage, SR strings, and equation string for supporting Tangent graph in Interactive Graph.

- Adds the tangent graph visual component (`tangent.tsx`) with `renderTangentGraph()`, `computeTangent()`, keyboard constraints, and screen reader descriptions
- Adds 5 screen reader strings for tangent graph accessibility (`srTangentGraph`, `srTangentInflectionPoint`, `srTangentSecondPoint`, `srTangentDescription`, `srTangentInteractiveElements`)
- Replaces the `mafs-graph.tsx` placeholder with real `renderTangentGraph()` call
- Replaces the `interactive-graph.tsx` equation string placeholder with `getTangentEquationString()`
- Adds Tangent Storybook story
- 7 new tests for the tangent graph component

<details>
<summary>Implementation notes</summary>

**Tangent component follows the sinusoid pattern.** `tangent.tsx` mirrors `sinusoid.tsx` structurally: two movable control points, coefficient calculation with a ref-based fallback for invalid states, and the same keyboard constraint logic that prevents same-x points.

**Asymptote handling (vertical line bug fix).** Mafs `Plot.OfX` renders a single SVG `<path>` that draws vertical lines across discontinuities at asymptotes. To fix this, the tangent curve is split into segments between asymptotes:
- `getAsymptotePositions()` computes asymptote x-positions within the visible range: `x = (c + π/2 + nπ) / b`
- `getPlotSegments()` splits the x-range into segments between asymptotes with a small epsilon margin (0.01)
- Each segment is rendered as a separate `Plot.OfX` with a `domain` prop, so Mafs never draws across a discontinuity
- `computeTangent()` also returns NaN near asymptotes as a defensive backup. The proximity formula was corrected from the POC — the POC's `((arg / Math.PI + 0.5) % 1) - 0.5` measures distance from zero crossings (inflection points), not asymptotes. The corrected formula `((arg - Math.PI/2) / Math.PI) % 1` correctly targets asymptotes at `arg = π/2 + nπ`.
- This approach was validated in the POC (commit 204f3f2)

**Two `getTangentCoefficients` functions exist.** The one in `tangent.tsx` returns `NamedTangentCoefficient | undefined` (named object with `undefined` fallback for same-x points) for rendering use. The one in `kmath/coefficients.ts` returns `TangentCoefficient` (numeric tuple, returns `Infinity` for same-x) for scoring use. The UI prevents the same-x case via the reducer's same-x guard, so the difference only matters as a defensive measure.

**Screen reader descriptions.** The tangent graph uses "inflection point" for the first control point (where the curve crosses the midline) and "control point" for the second point (a quarter-period away). This differs from sinusoid which uses "midline intersection" and "maximum/minimum point" — tangent doesn't have a meaningful max/min since it approaches ±∞.

**Equation string.** `getTangentEquationString()` formats `y = a*tan(b*x - c) + d` using the same pattern as `getSinusoidEquationString()` but using `getTangentCoefficients` from kmath.

**No feature flag gate in rendering.** The tangent graph renders unconditionally once the graph type is set to "tangent". The feature flag gate is in the editor (PR 6), which controls whether content creators can select "tangent" as a graph type. This follows the existing pattern — no other graph types check feature flags at render time.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] SR tests pass (aria labels for graph, inflection point, control point, description, interactive elements)
- [ ] Coefficient calculation test passes
- [ ] Tangent computation test passes
- [ ] Invalid coefficient test passes (same-x returns undefined)
- [ ] Keyboard constraint test passes (avoids same-x)
- [ ] Tangent story renders in Storybook (`pnpm storybook`)

Author: ivyolamit

Reviewers: claude[bot], ivyolamit, handeyeco, SonicScrewdriver

Required Reviewers:

Approved By: handeyeco

Checks: ⏭️  1 check has been skipped, ✅ 10 checks were successful

Pull Request URL: #3354
Base automatically changed from LEMS-3955/add-tangent-graph-component to main March 19, 2026 20:30
@ivyolamit ivyolamit force-pushed the LEMS-3955/tangent-scoring branch from 355ee58 to d96aca9 Compare March 19, 2026 20:32
@ivyolamit ivyolamit merged commit a022e75 into main Mar 19, 2026
11 checks passed
@ivyolamit ivyolamit deleted the LEMS-3955/tangent-scoring branch March 19, 2026 20:39
ivyolamit added a commit that referenced this pull request Mar 19, 2026
… Editor (#3358)

## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. [Foundation — Add tangent graph type definitions and data schema](#3345)
2. [Math layer —  Add tangent math utilities to kmath](#3347)
3. [State management — Reducer, actions, initialization, and test data](#3353)
4. [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. ▶️ [Editor — Add tangent to answer type](#3358)

This is the sixth and final PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3956). It adds editor support so content creators can create tangent exercises.

---

Add tangent graph option in the Interactive Graph Editor

- Adds tangent as a selectable graph type in the editor, gated by the `interactive-graph-tangent` feature flag
- Adds `StartCoordsTangent` component for configuring tangent start coordinates (inflection point + quarter-period point)
- Adds tangent equation display in the start coordinates editor section
- Exports `getTangentCoords()` from perseus for use by the editor's start-coords utilities
- 0 new tests (existing parameterized tests cover the new code paths)

<details>
<summary>Implementation notes</summary>

**Feature flag gating.** The tangent `OptionItem` in `GraphTypeSelector` only renders when `isFeatureOn("interactive-graph-tangent")` is true. This is the only place the feature flag is checked — once a tangent graph type is persisted in content JSON, it renders and scores regardless of the flag. This follows the pattern used by other gated features (e.g., `image-widget-upgrade-scale`).

**`apiOptions` prop threading.** `GraphTypeSelector` needed access to `apiOptions` for `isFeatureOn`. Added `apiOptions` as an optional prop and threaded it from `InteractiveGraphEditor`.

**`getTangentCoords()` export.** This function was internal to `initialize-graph-state.ts` (as noted in PR 3's plan). Now exported and re-exported from perseus index, used by `getDefaultGraphStartCoords` and `StartCoordsSettingsInner` in the editor.

**`StartCoordsTangent` mirrors `StartCoordsSinusoid` exactly.** Two coordinate pair inputs (Point 1 / Point 2) and an equation display using `getTangentEquation()`. The equation helper follows the same pattern as `getSinusoidEquation()`.

**`shouldShowStartCoordsUI` flipped.** Changed from `false` (set in PR 1 as a placeholder) to `true` for tangent, now that `StartCoordsSettingsInner` and `getDefaultGraphStartCoords` both handle the tangent type.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3956

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] Tangent option appears in graph type dropdown when feature flag is on
- [ ] Tangent option does NOT appear when feature flag is off
- [ ] Start coordinates section appears for tangent graph type
- [ ] Start coordinates reset button works for tangent
- [ ] Tangent equation updates when start coordinates change
- [ ] Existing editor tests pass (422 tests)

Author: ivyolamit

Reviewers: claude[bot], ivyolamit, handeyeco, SonicScrewdriver

Required Reviewers:

Approved By: SonicScrewdriver

Checks: ⏭️  1 check has been skipped, ✅ 10 checks were successful

Pull Request URL: #3358
ivyolamit added a commit that referenced this pull request Mar 24, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @khanacademy/perseus-editor@30.0.0

### Major Changes

- [#3332](#3332)
[`604b3a6c25`](604b3a6)
Thanks [@benchristel](https://github.com/benchristel)! - The `options`
parameter of the `serialize` method of `EditorPage` and `Editor` has
been removed.


- [#3386](#3386)
[`7e76fbbc2f`](7e76fbb)
Thanks [@benchristel](https://github.com/benchristel)! - The `serialize`
methods of classes in `@khanacademy/perseus-editor` no longer use arrow
function syntax. Callers should not unbind them from the class instance.

Additionally, the `Editor` component no longer accepts a `replace` prop
(used for hints), and its serialize method no longer returns `replace`.
The `replace` prop was only used in `serialize`. Users of the `Editor`
component should manage hints' `replace` setting themselves.

### Minor Changes

- [#3395](#3395)
[`97223334ea`](9722333)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Implementation of Editor support for Exponential Graph


- [#3352](#3352)
[`b681e00a4f`](b681e00)
Thanks [@handeyeco](https://github.com/handeyeco)! - Add editor support
for AbsoluteValue


- [#3348](#3348)
[`b1557c2a73`](b1557c2)
Thanks [@handeyeco](https://github.com/handeyeco)! - Add schema for
AbsoluteValue graph


- [#3345](#3345)
[`dde985f3b5`](dde985f)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent type
definitions, this is the initial implementation for supporting Tangent
graph in Interactive Graph


- [#3358](#3358)
[`8c503171b1`](8c50317)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent graph
option in the Interactive Graph Editor


- [#3376](#3376)
[`8aa0a77886`](8aa0a77)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of new Types, Schema, and Kmath utilities for Exponential Graph

### Patch Changes

- [#3396](#3396)
[`35fa9133db`](35fa913)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Add a
linter warning for images with no size


- [#3390](#3390)
[`d22c50dc2a`](d22c50d)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Make
the 125 character alt text warning less aggressive


- [#3372](#3372)
[`3cdb09813d`](3cdb098)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (UX) |
Upscale Graphies within Explore Image Modal


- [#3391](#3391)
[`2f285ee161`](2f285ee)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Add
character counter to alt text field


- [#3374](#3374)
[`cd73c99ba3`](cd73c99)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Remove incorrect
usage of the feature flag setting in one of the test

- Updated dependencies
\[[`f18c0d9b6f`](f18c0d9),
[`a022e751d6`](a022e75),
[`35fa9133db`](35fa913),
[`54db3fd4bd`](54db3fd),
[`97223334ea`](9722333),
[`027a5edbda`](027a5ed),
[`ae0538d0a7`](ae0538d),
[`005e13d784`](005e13d),
[`3cdb09813d`](3cdb098),
[`afcff9f96f`](afcff9f),
[`75f184e5a7`](75f184e),
[`4b2a7c85db`](4b2a7c8),
[`5e1acd01f8`](5e1acd0),
[`b681e00a4f`](b681e00),
[`d99f1c0259`](d99f1c0),
[`54eee35d65`](54eee35),
[`b1557c2a73`](b1557c2),
[`dde985f3b5`](dde985f),
[`56e7dbe9a2`](56e7dbe),
[`85f9cd46fc`](85f9cd4),
[`8c503171b1`](8c50317),
[`3aca3dcdf4`](3aca3dc),
[`9f29bc7161`](9f29bc7),
[`7034844845`](7034844),
[`8aa0a77886`](8aa0a77),
[`003aca7612`](003aca7)]:
    -   @khanacademy/perseus-linter@4.9.0
    -   @khanacademy/perseus-score@8.4.0
    -   @khanacademy/perseus-core@23.7.0
    -   @khanacademy/perseus@76.1.0
    -   @khanacademy/kmath@2.3.0
    -   @khanacademy/keypad-context@3.2.40
    -   @khanacademy/math-input@26.4.10

## @khanacademy/kmath@2.3.0

### Minor Changes

- [#3351](#3351)
[`005e13d784`](005e13d)
Thanks [@handeyeco](https://github.com/handeyeco)! - Add scoring for
AbsoluteValue


- [#3347](#3347)
[`d99f1c0259`](d99f1c0)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add the tangent
math utilities to kmath for supporting Tangent graph in Interactive
Graph


- [#3376](#3376)
[`8aa0a77886`](8aa0a77)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of new Types, Schema, and Kmath utilities for Exponential Graph

### Patch Changes

- Updated dependencies
\[[`54db3fd4bd`](54db3fd),
[`ae0538d0a7`](ae0538d),
[`005e13d784`](005e13d),
[`b1557c2a73`](b1557c2),
[`dde985f3b5`](dde985f),
[`8aa0a77886`](8aa0a77)]:
    -   @khanacademy/perseus-core@23.7.0

## @khanacademy/perseus@76.1.0

### Minor Changes

- [#3350](#3350)
[`75f184e5a7`](75f184e)
Thanks [@handeyeco](https://github.com/handeyeco)! - Implement
AbsoluteValue rendering


- [#3354](#3354)
[`4b2a7c85db`](4b2a7c8)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Created the tangent
graph visual component, add Storybook coverage, SR strings, and equation
string for supporting Tangent graph in Interactive Graph


- [#3353](#3353)
[`5e1acd01f8`](5e1acd0)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent graph
state management and reducer for supporting Tangent graph in Interactive
Graph


- [#3352](#3352)
[`b681e00a4f`](b681e00)
Thanks [@handeyeco](https://github.com/handeyeco)! - Add editor support
for AbsoluteValue


- [#3348](#3348)
[`b1557c2a73`](b1557c2)
Thanks [@handeyeco](https://github.com/handeyeco)! - Add schema for
AbsoluteValue graph


- [#3345](#3345)
[`dde985f3b5`](dde985f)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent type
definitions, this is the initial implementation for supporting Tangent
graph in Interactive Graph


- [#3349](#3349)
[`56e7dbe9a2`](56e7dbe)
Thanks [@handeyeco](https://github.com/handeyeco)! - Add state
management for AbsoluteValue


- [#3377](#3377)
[`85f9cd46fc`](85f9cd4)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Implementation of state management logic for new Exponential graph


- [#3358](#3358)
[`8c503171b1`](8c50317)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent graph
option in the Interactive Graph Editor


- [#3393](#3393)
[`9f29bc7161`](9f29bc7)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Rendering logic for new Exponential Graph


- [#3376](#3376)
[`8aa0a77886`](8aa0a77)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of new Types, Schema, and Kmath utilities for Exponential Graph

### Patch Changes

- [#3329](#3329)
[`027a5edbda`](027a5ed)
Thanks [@Myranae](https://github.com/Myranae)! - Fix image bug by
batching setState calls in setupGraphie


- [#3372](#3372)
[`3cdb09813d`](3cdb098)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (UX) |
Upscale Graphies within Explore Image Modal


- [#3365](#3365)
[`afcff9f96f`](afcff9f)
Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Improve
ordering of Props type for `Renderer` component


- [#3367](#3367)
[`54eee35d65`](54eee35)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (UX) | Show
image in explore modal even when size is undefined


- [#3407](#3407)
[`3aca3dcdf4`](3aca3dc)
Thanks [@Myranae](https://github.com/Myranae)! - Improve a11y with
graded group set


- [#3385](#3385)
[`003aca7612`](003aca7)
Thanks [@Myranae](https://github.com/Myranae)! - Small fix to prevent
pip duplication in Graded Group Sets

- Updated dependencies
\[[`f18c0d9b6f`](f18c0d9),
[`a022e751d6`](a022e75),
[`35fa9133db`](35fa913),
[`54db3fd4bd`](54db3fd),
[`97223334ea`](9722333),
[`ae0538d0a7`](ae0538d),
[`005e13d784`](005e13d),
[`d99f1c0259`](d99f1c0),
[`b1557c2a73`](b1557c2),
[`dde985f3b5`](dde985f),
[`7034844845`](7034844),
[`8aa0a77886`](8aa0a77)]:
    -   @khanacademy/perseus-linter@4.9.0
    -   @khanacademy/perseus-score@8.4.0
    -   @khanacademy/perseus-core@23.7.0
    -   @khanacademy/kmath@2.3.0
    -   @khanacademy/keypad-context@3.2.40
    -   @khanacademy/math-input@26.4.10

## @khanacademy/perseus-core@23.7.0

### Minor Changes

- [#3405](#3405)
[`54db3fd4bd`](54db3fd)
Thanks [@benchristel](https://github.com/benchristel)! -
`@khanacademy/perseus-core` now exports a
`removeOrphanedWidgetsFromPerseusItem` function, which removes
unreferenced widgets from a `PerseusItem`'s question and hints.


- [#3351](#3351)
[`005e13d784`](005e13d)
Thanks [@handeyeco](https://github.com/handeyeco)! - Add scoring for
AbsoluteValue


- [#3348](#3348)
[`b1557c2a73`](b1557c2)
Thanks [@handeyeco](https://github.com/handeyeco)! - Add schema for
AbsoluteValue graph


- [#3345](#3345)
[`dde985f3b5`](dde985f)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent type
definitions, this is the initial implementation for supporting Tangent
graph in Interactive Graph


- [#3376](#3376)
[`8aa0a77886`](8aa0a77)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Creation of new Types, Schema, and Kmath utilities for Exponential Graph

### Patch Changes

- [#3357](#3357)
[`ae0538d0a7`](ae0538d)
Thanks [@jeremywiebe](https://github.com/jeremywiebe)! - Improve code
documentation for all data-schema and user-input types

## @khanacademy/perseus-linter@4.9.0

### Minor Changes

- [#3381](#3381)
[`f18c0d9b6f`](f18c0d9)
Thanks [@anakaren-rojas](https://github.com/anakaren-rojas)! - Adds new
linters for parsed objects


- [#3395](#3395)
[`97223334ea`](9722333)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Implementation of Editor support for Exponential Graph

### Patch Changes

- [#3396](#3396)
[`35fa9133db`](35fa913)
Thanks [@nishasy](https://github.com/nishasy)! - [Image] | (CX) | Add a
linter warning for images with no size

- Updated dependencies
\[[`54db3fd4bd`](54db3fd),
[`ae0538d0a7`](ae0538d),
[`005e13d784`](005e13d),
[`d99f1c0259`](d99f1c0),
[`b1557c2a73`](b1557c2),
[`dde985f3b5`](dde985f),
[`8aa0a77886`](8aa0a77)]:
    -   @khanacademy/perseus-core@23.7.0
    -   @khanacademy/kmath@2.3.0

## @khanacademy/perseus-score@8.4.0

### Minor Changes

- [#3356](#3356)
[`a022e751d6`](a022e75)
Thanks [@ivyolamit](https://github.com/ivyolamit)! - Add tangent graph
scoring to support the Tangent graph in Interactive Graph


- [#3351](#3351)
[`005e13d784`](005e13d)
Thanks [@handeyeco](https://github.com/handeyeco)! - Add scoring for
AbsoluteValue


- [#3394](#3394)
[`7034844845`](7034844)
Thanks [@SonicScrewdriver](https://github.com/SonicScrewdriver)! -
Implementation of new scoring logic for Exponential Graph

### Patch Changes

- Updated dependencies
\[[`54db3fd4bd`](54db3fd),
[`ae0538d0a7`](ae0538d),
[`005e13d784`](005e13d),
[`d99f1c0259`](d99f1c0),
[`b1557c2a73`](b1557c2),
[`dde985f3b5`](dde985f),
[`8aa0a77886`](8aa0a77)]:
    -   @khanacademy/perseus-core@23.7.0
    -   @khanacademy/kmath@2.3.0

## @khanacademy/keypad-context@3.2.40

### Patch Changes

- Updated dependencies
\[[`54db3fd4bd`](54db3fd),
[`ae0538d0a7`](ae0538d),
[`005e13d784`](005e13d),
[`b1557c2a73`](b1557c2),
[`dde985f3b5`](dde985f),
[`8aa0a77886`](8aa0a77)]:
    -   @khanacademy/perseus-core@23.7.0

## @khanacademy/math-input@26.4.10

### Patch Changes

- Updated dependencies
\[[`54db3fd4bd`](54db3fd),
[`ae0538d0a7`](ae0538d),
[`005e13d784`](005e13d),
[`b1557c2a73`](b1557c2),
[`dde985f3b5`](dde985f),
[`8aa0a77886`](8aa0a77)]:
    -   @khanacademy/perseus-core@23.7.0
    -   @khanacademy/keypad-context@3.2.40
catandthemachines pushed a commit that referenced this pull request Apr 1, 2026
#3345)

## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. ▶️ [Foundation — Add tangent graph type definitions and data schema](#3345)
2. [Math layer —  Add tangent math utilities to kmath](#3347)
3. [State management — Reducer, actions, initialization, and test data](#3353)
4. [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. [Editor — Add tangent to answer type](#3358)

This is the first PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It establishes the type foundation with zero runtime behavior change. The feature flag (`interactive-graph-tangent`) is added in #3344.

---

- Adds `PerseusGraphTypeTangent` and `TangentGraphCorrect` types to the data schema, following the sinusoid pattern
- Adds the JSON parser for the tangent graph type (`parsePerseusGraphTypeTangent`)
- Defines `TangentGraphState` interface (not yet exported or added to the `InteractiveGraphState` union — deferred to a later PR when reducer handlers are implemented)
- Adds `generateIGTangentGraph()` test data generator with unit tests
- Adds placeholder `case "tangent"` branches in all exhaustiveness switches affected by the new `PerseusGraphType` union member

<details>
<summary>Implementation notes</summary>

Adding `PerseusGraphTypeTangent` to the `PerseusGraphType` union triggers `UnreachableCaseError` in several switch statements. Placeholder cases were added to keep the build green:
- `interactive-graph-editor.tsx` — graph merging
- `start-coords/util.ts` — `shouldShowStartCoordsUI` returns `false` for tangent (downstream components `StartCoordsSettingsInner` and `getDefaultGraphStartCoords` don't handle tangent yet — returning `true` would show an empty section with a broken reset button)
- `interactive-graph-ai-utils.ts` — `getGraphOptionsForProps` + `getUserInput`
- `interactive-graph.tsx` — `getEquationString` (returns `""`)
- `initialize-graph-state.ts` — returns `type: "none"`

These placeholders will be replaced with real implementations in subsequent PRs.

`TangentGraphState` is intentionally **not exported** and **not added to the `InteractiveGraphState` union** in this PR. That union has its own set of exhaustiveness checks (`renderGraphElements`, `mafsStateToInteractiveGraph`, `getGradableGraph`), and adding to it requires reducer handlers to exist first. This happens in PR 3.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] Generator tests pass (`generateIGTangentGraph` default + all props)

Author: ivyolamit

Reviewers: ivyolamit, claude[bot], handeyeco, SonicScrewdriver

Required Reviewers:

Approved By: handeyeco

Checks: ✅ 10 checks were successful, ⏭️  1 check has been skipped

Pull Request URL: #3345
catandthemachines pushed a commit that referenced this pull request Apr 1, 2026
## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. [Foundation — Add tangent graph type definitions and data schema](#3345)
2. ▶️ [Math layer —  Add tangent math utilities to kmath](#3347)
3. [State management — Reducer, actions, initialization, and test data](#3353)
4. [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. [Editor — Add tangent to answer type](#3358)

This is the second PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It adds the pure math layer with no UI dependencies.

---

Added the tangent math utilities to kmath for supporting Tangent graph in Interactive Graph

- Adds `getTangentCoefficients()` to extract `[a, b, c, d]` from two control points for `f(x) = a * tan(b*x - c) + d`
- Adds `canonicalTangentCoefficients()` to normalize coefficients for scoring comparison (guarantees `b > 0`, phase in `[0, π)`)
- Adds `TangentCoefficient` and `NamedTangentCoefficient` types
- 13 new tests covering coefficient extraction and canonical normalization edge cases

<details>
<summary>Implementation notes</summary>

**Canonical normalization differs from the legacy Grapher widget version.** The legacy `canonicalTangentCoefficients` in `grapher-util.ts` guarantees both `a > 0` and `b > 0` using a `phase += π/2` step. However, this is mathematically incorrect for tangent — `tan(x + π/2) = -cot(x)`, not `-tan(x)`. The legacy version still works because its `areEqual` applies the same normalization to both sides, so the error cancels out.

Our version only guarantees `b > 0`, using the odd function identity `tan(-x) = -tan(x)`:
- If `b < 0`: flip signs of `a`, `b`, and `c`
- Normalize `c` to `[0, π)`

We intentionally do **not** replace the legacy version to avoid changing scoring behavior for existing Grapher tangent exercises.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] `canonicalTangentCoefficients` tests pass (negative b, phase normalization, equivalent curves, preserved negative amplitude)
- [ ] `getTangentCoefficients` tests pass (basic, vertical offset, phase shift, negative amplitude, negative angular frequency)


## Test plan:

Author: ivyolamit

Reviewers: claude[bot], ivyolamit, handeyeco, SonicScrewdriver

Required Reviewers:

Approved By: SonicScrewdriver

Checks: ⏭️  1 check has been skipped, ✅ 10 checks were successful

Pull Request URL: #3347
catandthemachines pushed a commit that referenced this pull request Apr 1, 2026
…3353)

## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. [Foundation — Add tangent graph type definitions and data schema](#3345)
2. [Math layer —  Add tangent math utilities to kmath](#3347)
3. ▶️ [State management — Reducer, actions, initialization, and test data](#3353)
4. [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. [Editor — Add tangent to answer type](#3358)

This is the third PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It wires up the state management layer — the reducer, initialization, actions, and gradable graph serialization.

---

Add tangent graph state management and reducer for supporting Tangent graph in Interactive Graph

- Exports `TangentGraphState` and adds it to the `InteractiveGraphState` union
- Adds `tangent.movePoint` action and reducer case with same-x constraint (prevents division by zero in `getTangentCoefficients`)
- Adds real tangent initialization in `initializeGraphState` via `getTangentCoords()` (default coords `[[0.5, 0.5], [0.75, 0.75]]`)
- Adds tangent case to `getGradableGraph` for scoring serialization
- Adds `withTangent()` builder method and `TangentGraphConfig` class for test data construction
- Adds `tangentQuestion` and `tangentQuestionWithDefaultCorrect` test data
- Adds tangent serialization in `mafsStateToInteractiveGraph`
- 13 new tests across 5 test files

<details>
<summary>Implementation notes</summary>

**InteractiveGraphState union update.** Adding `TangentGraphState` to the union triggers `UnreachableCaseError` in `renderGraphElements` (mafs-graph.tsx). A placeholder case returns `null` (no rendering) — replaced with real rendering in PR 4.

**Same-x constraint.** The `doMovePoint` tangent case rejects moves that would place both control points on the same vertical line. This mirrors the sinusoid constraint and prevents `getTangentCoefficients` from producing `Infinity` for `angularFrequency` (since it divides by `p2[0] - p1[0]`).

**`getTangentCoords()` is not exported.** It's only called within `initialize-graph-state.ts`. It will be exported in PR 6 (editor) when `start-coords/util.ts` needs it.

**`mafsStateToInteractiveGraph` tangent case is the real implementation** (not a placeholder) — it simply returns `{ ...originalGraph, coords: state.coords }`, same as sinusoid.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] Initialization tests pass (given coords, startCoords, defaults)
- [ ] Reducer tests pass (same-x rejection, out-of-bounds rejection, valid move)
- [ ] `getGradableGraph` tangent test passes
- [ ] `mafsStateToInteractiveGraph` tangent serialization test passes
- [ ] Tangent renders in parameterized "should render" tests

Author: ivyolamit

Reviewers: claude[bot], handeyeco, ivyolamit, SonicScrewdriver

Required Reviewers:

Approved By: handeyeco

Checks: ✅ 10 checks were successful, ⏭️  1 check has been skipped

Pull Request URL: #3353
catandthemachines pushed a commit that referenced this pull request Apr 1, 2026
…tion string (#3354)

## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. [Foundation — Add tangent graph type definitions and data schema](#3345)
2. [Math layer —  Add tangent math utilities to kmath](#3347)
3. [State management — Reducer, actions, initialization, and test data](#3353)
4. ▶️ [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. [Editor — Add tangent to answer type](#3358)

This is the fourth PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It adds the rendering layer — the Mafs component, accessibility strings, equation string generation, and Storybook coverage.

---

Created the tangent graph visual component, add Storybook coverage, SR strings, and equation string for supporting Tangent graph in Interactive Graph.

- Adds the tangent graph visual component (`tangent.tsx`) with `renderTangentGraph()`, `computeTangent()`, keyboard constraints, and screen reader descriptions
- Adds 5 screen reader strings for tangent graph accessibility (`srTangentGraph`, `srTangentInflectionPoint`, `srTangentSecondPoint`, `srTangentDescription`, `srTangentInteractiveElements`)
- Replaces the `mafs-graph.tsx` placeholder with real `renderTangentGraph()` call
- Replaces the `interactive-graph.tsx` equation string placeholder with `getTangentEquationString()`
- Adds Tangent Storybook story
- 7 new tests for the tangent graph component

<details>
<summary>Implementation notes</summary>

**Tangent component follows the sinusoid pattern.** `tangent.tsx` mirrors `sinusoid.tsx` structurally: two movable control points, coefficient calculation with a ref-based fallback for invalid states, and the same keyboard constraint logic that prevents same-x points.

**Asymptote handling (vertical line bug fix).** Mafs `Plot.OfX` renders a single SVG `<path>` that draws vertical lines across discontinuities at asymptotes. To fix this, the tangent curve is split into segments between asymptotes:
- `getAsymptotePositions()` computes asymptote x-positions within the visible range: `x = (c + π/2 + nπ) / b`
- `getPlotSegments()` splits the x-range into segments between asymptotes with a small epsilon margin (0.01)
- Each segment is rendered as a separate `Plot.OfX` with a `domain` prop, so Mafs never draws across a discontinuity
- `computeTangent()` also returns NaN near asymptotes as a defensive backup. The proximity formula was corrected from the POC — the POC's `((arg / Math.PI + 0.5) % 1) - 0.5` measures distance from zero crossings (inflection points), not asymptotes. The corrected formula `((arg - Math.PI/2) / Math.PI) % 1` correctly targets asymptotes at `arg = π/2 + nπ`.
- This approach was validated in the POC (commit 204f3f2)

**Two `getTangentCoefficients` functions exist.** The one in `tangent.tsx` returns `NamedTangentCoefficient | undefined` (named object with `undefined` fallback for same-x points) for rendering use. The one in `kmath/coefficients.ts` returns `TangentCoefficient` (numeric tuple, returns `Infinity` for same-x) for scoring use. The UI prevents the same-x case via the reducer's same-x guard, so the difference only matters as a defensive measure.

**Screen reader descriptions.** The tangent graph uses "inflection point" for the first control point (where the curve crosses the midline) and "control point" for the second point (a quarter-period away). This differs from sinusoid which uses "midline intersection" and "maximum/minimum point" — tangent doesn't have a meaningful max/min since it approaches ±∞.

**Equation string.** `getTangentEquationString()` formats `y = a*tan(b*x - c) + d` using the same pattern as `getSinusoidEquationString()` but using `getTangentCoefficients` from kmath.

**No feature flag gate in rendering.** The tangent graph renders unconditionally once the graph type is set to "tangent". The feature flag gate is in the editor (PR 6), which controls whether content creators can select "tangent" as a graph type. This follows the existing pattern — no other graph types check feature flags at render time.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] SR tests pass (aria labels for graph, inflection point, control point, description, interactive elements)
- [ ] Coefficient calculation test passes
- [ ] Tangent computation test passes
- [ ] Invalid coefficient test passes (same-x returns undefined)
- [ ] Keyboard constraint test passes (avoids same-x)
- [ ] Tangent story renders in Storybook (`pnpm storybook`)

Author: ivyolamit

Reviewers: claude[bot], ivyolamit, handeyeco, SonicScrewdriver

Required Reviewers:

Approved By: handeyeco

Checks: ⏭️  1 check has been skipped, ✅ 10 checks were successful

Pull Request URL: #3354
catandthemachines pushed a commit that referenced this pull request Apr 1, 2026
## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. [Foundation — Add tangent graph type definitions and data schema](#3345)
2. [Math layer —  Add tangent math utilities to kmath](#3347)
3. [State management — Reducer, actions, initialization, and test data](#3353)
4. [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. ▶️ [Scoring — Add tangent scoring to the scoring package](#3356)
6. [Editor — Add tangent to answer type](#3358)

This is the fifth PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3955). It adds the scoring layer — the final piece needed for tangent exercises to be fully functional (behind the feature flag).

---

Add tangent graph scoring to support the Tangent graph in Interactive Graph

- Adds tangent scoring to `scoreInteractiveGraph()` using `getTangentCoefficients` and `canonicalTangentCoefficients` from kmath
- Follows the sinusoid scoring pattern: extract coefficients from both guess and rubric, canonicalize, then compare
- 6 new tests covering invalid input, correct/incorrect answers, equivalent curves, and negative amplitude

<details>
<summary>Implementation notes</summary>

**Scoring follows the sinusoid pattern exactly.** The tangent scoring block extracts coefficients from both the user's guess and the rubric's correct answer using `getTangentCoefficients()` (from kmath), canonicalizes both with `canonicalTangentCoefficients()`, and compares with `approximateDeepEqual()`. This handles equivalent curves that use different control points (e.g., shifted by a full period).

**Uses kmath's `canonicalTangentCoefficients`, NOT the legacy grapher-util version.** The kmath version (PR 2) only guarantees `b > 0`, which is mathematically correct for tangent. The legacy version guarantees both `a > 0` and `b > 0` using a mathematically incorrect phase shift. See PR 2 implementation notes for details.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3955

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] Invalid input tests pass (undefined guess, missing coords)
- [ ] Correct answer test passes
- [ ] Incorrect answer test passes
- [ ] Equivalent curves test passes (period-shifted control points)
- [ ] Negative amplitude test passes

Author: ivyolamit

Reviewers: claude[bot], handeyeco, SonicScrewdriver

Required Reviewers:

Approved By: handeyeco

Checks: ⏭️  1 check has been skipped, ✅ 10 checks were successful

Pull Request URL: #3356
catandthemachines pushed a commit that referenced this pull request Apr 1, 2026
… Editor (#3358)

## Summary:

PR series to add tangent graph support to the Interactive Graph widget:

1. [Foundation — Add tangent graph type definitions and data schema](#3345)
2. [Math layer —  Add tangent math utilities to kmath](#3347)
3. [State management — Reducer, actions, initialization, and test data](#3353)
4. [Rendering — The tangent graph component, Storybook story, and AI utils](#3354)
5. [Scoring — Add tangent scoring to the scoring package](#3356)
6. ▶️ [Editor — Add tangent to answer type](#3358)

This is the sixth and final PR in a series to add tangent graph support to the Interactive Graph widget (LEMS-3956). It adds editor support so content creators can create tangent exercises.

---

Add tangent graph option in the Interactive Graph Editor

- Adds tangent as a selectable graph type in the editor, gated by the `interactive-graph-tangent` feature flag
- Adds `StartCoordsTangent` component for configuring tangent start coordinates (inflection point + quarter-period point)
- Adds tangent equation display in the start coordinates editor section
- Exports `getTangentCoords()` from perseus for use by the editor's start-coords utilities
- 0 new tests (existing parameterized tests cover the new code paths)

<details>
<summary>Implementation notes</summary>

**Feature flag gating.** The tangent `OptionItem` in `GraphTypeSelector` only renders when `isFeatureOn("interactive-graph-tangent")` is true. This is the only place the feature flag is checked — once a tangent graph type is persisted in content JSON, it renders and scores regardless of the flag. This follows the pattern used by other gated features (e.g., `image-widget-upgrade-scale`).

**`apiOptions` prop threading.** `GraphTypeSelector` needed access to `apiOptions` for `isFeatureOn`. Added `apiOptions` as an optional prop and threaded it from `InteractiveGraphEditor`.

**`getTangentCoords()` export.** This function was internal to `initialize-graph-state.ts` (as noted in PR 3's plan). Now exported and re-exported from perseus index, used by `getDefaultGraphStartCoords` and `StartCoordsSettingsInner` in the editor.

**`StartCoordsTangent` mirrors `StartCoordsSinusoid` exactly.** Two coordinate pair inputs (Point 1 / Point 2) and an equation display using `getTangentEquation()`. The equation helper follows the same pattern as `getSinusoidEquation()`.

**`shouldShowStartCoordsUI` flipped.** Changed from `false` (set in PR 1 as a placeholder) to `true` for tangent, now that `StartCoordsSettingsInner` and `getDefaultGraphStartCoords` both handle the tangent type.

</details>

### References
- [Tangent Notes](https://github.com/Khan/perseus/blob/main/packages/perseus/src/widgets/interactive-graphs/__docs__/notes/tangent.md)
- POC: #3311

Co-Authored by Claude Code (Opus)

Issue: LEMS-3956

## Test plan:
- [ ] `pnpm tsc` — no type errors
- [ ] `pnpm lint` — no lint errors
- [ ] `pnpm prettier . --check` — formatting clean
- [ ] `pnpm knip` — no unused exports
- [ ] Tangent option appears in graph type dropdown when feature flag is on
- [ ] Tangent option does NOT appear when feature flag is off
- [ ] Start coordinates section appears for tangent graph type
- [ ] Start coordinates reset button works for tangent
- [ ] Tangent equation updates when start coordinates change
- [ ] Existing editor tests pass (422 tests)

Author: ivyolamit

Reviewers: claude[bot], ivyolamit, handeyeco, SonicScrewdriver

Required Reviewers:

Approved By: SonicScrewdriver

Checks: ⏭️  1 check has been skipped, ✅ 10 checks were successful

Pull Request URL: #3358
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants