[Interactive Graph] Add tangent math utilities to kmath#3347
Conversation
🗄️ Schema Change: No Changes ✅ |
|
Size Change: +62 B (+0.01%) Total Size: 486 kB
ℹ️ View Unchanged
|
🛠️ Item Splitting: No Changes ✅ |
npm Snapshot: PublishedGood news!! We've packaged up the latest commit from this PR (8c91c25) and published it to npm. You Example: pnpm add @khanacademy/perseus@PR3347If you are working in Khan Academy's frontend, you can run the below command. ./dev/tools/bump_perseus_version.ts -t PR3347If you are working in Khan Academy's webapp, you can run the below command. ./dev/tools/bump_perseus_version.js -t PR3347 |
722f758 to
6247b68
Compare
|
Summary of the tangent implementation
|
There was a problem hiding this comment.
LGTM — clean addition of tangent math utilities following established patterns with good test coverage.
Extended reasoning...
Overview
This PR adds tangent coefficient extraction (getTangentCoefficients) and canonical normalization (canonicalTangentCoefficients) to the kmath package, along with corresponding types (TangentCoefficient, NamedTangentCoefficient) and 13 new tests. The changes span 6 files but are self-contained within the kmath math library with no UI dependencies.
Security risks
None. This is pure math utility code (arithmetic operations on numbers) with no I/O, network calls, user input handling, or authentication concerns.
Level of scrutiny
Low scrutiny is appropriate. The code is a straightforward addition of math functions that closely mirror the existing getSinusoidCoefficients and canonicalSineCoefficients patterns already in the codebase. The tangent-specific math (odd function identity for b-normalization, π-period phase normalization) is correct and well-documented in comments.
Other factors
- The one bug found (while-loop phase normalization fragility) is explicitly pre-existing — the identical pattern exists in
canonicalSineCoefficientsat lines 288-293. The new code intentionally follows this established convention. - Test coverage is thorough: 5 tests for coefficient extraction and 8 tests for canonical normalization, including edge cases (negative b, phase wrapping, equivalent curves, zero phase).
- The PR description provides excellent context on why the normalization differs from the legacy Grapher widget version, with correct mathematical reasoning.
- No outstanding reviewer comments to address.
| number, // verticalOffset | ||
| ]; | ||
|
|
||
| export function canonicalTangentCoefficients([ |
There was a problem hiding this comment.
Need to resolve naming or update grapher to use this kmath function instead to avoid code duplication.
see: #3333 (comment)
There was a problem hiding this comment.
Oh great point, I need to make sure I check this for exponential too
| number, // verticalOffset | ||
| ]; | ||
|
|
||
| export function canonicalTangentCoefficients([ |
There was a problem hiding this comment.
It would be preferable to not have multiple functions doing roughly the same thing (this is also in perseus/packages/perseus-core/src/utils/grapher-util.ts). If similar logic is needed in multiple places, it should be moved somewhere where it can be shared rather than duplicated.
I noticed that the implementation is different between the two - are they doing different things? I think it's probably just different code with the same objective...it's just a little confusing (part of why you don't want two functions trying to do the same thing).
There was a problem hiding this comment.
Thanks @handeyeco for your feedback,. My plan is to do a separate PR specifically for changing the Grapher side of things and use this function in kmath, right after this tangent PRs. The follow-up PR would include:
- Update usages in grapher to use the kmatch functions
- Delete the duplicate the functions in grapher utils,
canonicalSineCoefficientsandcanonicalTangentCoefficients
Although the grapher is partially deprecated (editors can't add new grapher content) we still have a significant number of exercises that use it. And the reason for my approach, so that this PR only focuses changes related to tangent, and the follow-up PR would focus only the grapher.
See related discussion in #3333 (comment)
There was a problem hiding this comment.
I noticed that the implementation is different between the two - are they doing different things? I think it's probably just different code with the same objective...it's just a little confusing (part of why you don't want two functions trying to do the same thing).
I was going through my plan.md and did add a note about this last week:
1. Canonical normalization difference from Grapher widget
The notes currently reference the grapher-util version at a high level. Add details about the specific difference:
- grapher-util guarantees both a > 0 and b > 0 using a phase += π/2 step
- This is mathematically incorrect (tan(x + π/2) = -cot(x), not -tan(x))
- It works for Grapher scoring because areEqual applies the same normalization to both sides (bug cancels out)
- kmath version only guarantees b > 0, which is correct per the odd function identity
- We intentionally do not unify them to avoid breaking existing Grapher tangent exercise scoring
With our discussion now, i think there's no harm on fixing the math there and use this same function for both. Here's more details about this from Claude:
That changes the picture. The getTangentCoefficients raw extraction is fine — identical formulas. But there's a critical gap to address for the eventual migration: canonicalTangentCoefficients in grapher-util.ts also normalizes a > 0, while kmath's version does not.
Here's the behavioral difference when a < 0, b > 0:
| Input | grapher-util.ts canonical | kmath/geometry.ts canonical |
| ----------- | ------------------------- | ----------------------------- |
| [-2, 1, 0, 0] │ [2, 1, π/2, 0] │ [-2, 1, 0, 0] │
The legacy output is actually mathematically wrong (2*tan(x - π/2) = -2*cot(x), not -2*tan(x)), but areEqual applies the same wrong normalization to both sides so it cancels out.
For the migration to work correctly, when you replace grapher-util.ts's canonicalTangentCoefficients with kmath's, you have two options:
Option A (recommended): Keep them separate. Use kmath's for Interactive Graph scoring, keep grapher-util's local version for backward compatibility with existing Grapher exercises. No changes to coefficients.ts.
Option B: Add a > 0 to kmath's canonicalTangentCoefficients — but this is mathematically incorrect and the plan explicitly avoided it. It would also cause a subtle behavior difference where -2*tan(x) would incorrectly be treated as equivalent to 2*tan(x - π/2).
The plan.md (line 57) anticipated this exact concern:
▎ We intentionally do NOT replace the legacy version to avoid changing scoring behavior for existing Grapher tangent exercises.
So no changes are needed to coefficients.ts for the migration of the raw extraction function. But for canonicalTangentCoefficients, the plan recommends keeping grapher-util's local version rather than migrating it to kmath.
There was a problem hiding this comment.
here's a draft follow-up (i still need to review and test this): #3373
#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
…ent graph type definitions and data schema
…ties to kmath for supporting Tangent graph in Interactive Graph
…ilities to kmath for supporting Tangent graph in Interactive Graph
787bfc8 to
d93ccd0
Compare
| startCoords?: Coord[]; | ||
| }; | ||
|
|
||
| export type PerseusGraphTypeTangent = { |
There was a problem hiding this comment.
i messed up my rebase 😢 got duplicated so i'm removing the change from this PR.
Co-authored-by: Matthew <matthewcurtis@khanacademy.org>
SonicScrewdriver
left a comment
There was a problem hiding this comment.
Looks good, since you already discussed the naming duplication
…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
…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
## 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
… 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
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
#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
## 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
…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
…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
## 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
… 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
Summary:
PR series to add tangent graph support to the Interactive Graph widget:
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
getTangentCoefficients()to extract[a, b, c, d]from two control points forf(x) = a * tan(b*x - c) + dcanonicalTangentCoefficients()to normalize coefficients for scoring comparison (guaranteesb > 0, phase in[0, π))TangentCoefficientandNamedTangentCoefficienttypesImplementation notes
Canonical normalization differs from the legacy Grapher widget version. The legacy
canonicalTangentCoefficientsingrapher-util.tsguarantees botha > 0andb > 0using aphase += π/2step. However, this is mathematically incorrect for tangent —tan(x + π/2) = -cot(x), not-tan(x). The legacy version still works because itsareEqualapplies the same normalization to both sides, so the error cancels out.Our version only guarantees
b > 0, using the odd function identitytan(-x) = -tan(x):b < 0: flip signs ofa,b, andccto[0, π)We intentionally do not replace the legacy version to avoid changing scoring behavior for existing Grapher tangent exercises.
References
Co-Authored by Claude Code (Opus)
Issue: LEMS-3955
Test plan:
pnpm tsc— no type errorspnpm lint— no lint errorspnpm prettier . --check— formatting cleanpnpm knip— no unused exportscanonicalTangentCoefficientstests pass (negative b, phase normalization, equivalent curves, preserved negative amplitude)getTangentCoefficientstests pass (basic, vertical offset, phase shift, negative amplitude, negative angular frequency)Test plan: