Skip to content

[Interactive Graph] Add tangent graph option in the Interactive Graph Editor#3358

Open
ivyolamit wants to merge 4 commits intoLEMS-3955/tangent-scoringfrom
LEMS-3956/add-tangent-in-interactive-graph-editor
Open

[Interactive Graph] Add tangent graph option in the Interactive Graph Editor#3358
ivyolamit wants to merge 4 commits intoLEMS-3955/tangent-scoringfrom
LEMS-3956/add-tangent-in-interactive-graph-editor

Conversation

@ivyolamit
Copy link
Contributor

@ivyolamit ivyolamit commented Mar 14, 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 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)
Implementation notes

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.

References

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)

…Add tangent graph option in the Interactive Graph Editor
…h] Add tangent graph option in the Interactive Graph Editor
@ivyolamit ivyolamit self-assigned this Mar 14, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 14, 2026

🗄️ Schema Change: No Changes ✅

@github-actions
Copy link
Contributor

github-actions bot commented Mar 14, 2026

Size Change: +223 B (+0.05%)

Total Size: 488 kB

Filename Size Change
packages/perseus-editor/dist/es/index.js 100 kB +218 B (+0.22%)
packages/perseus/dist/es/index.js 188 kB +5 B (0%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.8 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-linter/dist/es/index.js 8.82 kB
packages/perseus-score/dist/es/index.js 9.3 kB
packages/perseus-utils/dist/es/index.js 403 B
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
Contributor

github-actions bot commented Mar 14, 2026

🛠️ Item Splitting: No Changes ✅

@github-actions
Copy link
Contributor

github-actions bot commented Mar 14, 2026

npm Snapshot: Published

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

Example:

pnpm add @khanacademy/perseus@PR3358

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

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

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

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

@ivyolamit ivyolamit marked this pull request as ready for review March 14, 2026 00:20
@ivyolamit
Copy link
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 for review
5 Scoring — Add tangent scoring to the scoring package #3356 base PR (for review)
6 Editor — Add tangent to answer type #3358 this PR

Copy link

@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 graph type to the editor, closely mirroring the existing sinusoid pattern and gated behind a feature flag.

Extended reasoning...

Overview

This PR adds tangent as a selectable graph type in the Interactive Graph Editor. It touches 8 files: a new StartCoordsTangent component, a getTangentEquation utility, feature-flag-gated dropdown option in GraphTypeSelector, apiOptions prop threading, export of getTangentCoords from perseus, and updates to shouldShowStartCoordsUI and getDefaultGraphStartCoords to handle the tangent type.

Security risks

None. All changes are editor-only UI code behind a feature flag (interactive-graph-tangent). No auth, crypto, permissions, or user-facing data handling is involved.

Level of scrutiny

Low scrutiny is appropriate. This is the sixth and final PR in a well-structured series. Every change follows the established sinusoid pattern exactly — the new component, equation function, and coord handling are mechanical mirrors of their sinusoid counterparts. The feature flag gating follows the same pattern as other gated features in the codebase.

Other factors

The two bugs found are both minor: (1) a nit about tangent missing from the GraphTypesThatHaveStartCoords type union, which has no runtime or compile-time impact since tangent's startCoords type (Coord[]) is structurally identical to sinusoid's already-included type; (2) a pre-existing formatting issue in equation string building that exists identically in getSinusoidEquation and is explicitly accepted by existing tests. Neither warrants blocking the PR. The CODEOWNERS file only has general team ownership for this path, with no specific individual owners for the interactive graph editor files.

Comment on lines 125 to 153
};

export const getTangentEquation = (startCoords: [Coord, Coord]) => {
// Get coefficients
// It's assumed that p1 is the inflection point and p2 is a quarter-period away
const p1 = startCoords[0];
const p2 = startCoords[1];

// Resulting coefficients are canonical for this tangent curve
const amplitude = p2[1] - p1[1];
const angularFrequency = Math.PI / (4 * (p2[0] - p1[0]));
const phase = p1[0] * angularFrequency;
const verticalOffset = p1[1];

return (
"y = " +
amplitude.toFixed(3) +
"tan(" +
angularFrequency.toFixed(3) +
"x - " +
phase.toFixed(3) +
") + " +
verticalOffset.toFixed(3)
);
};

export const getQuadraticEquation = (startCoords: [Coord, Coord, Coord]) => {
const p1 = startCoords[0];
const p2 = startCoords[1];
Copy link

Choose a reason for hiding this comment

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

🟣 Pre-existing issue: getTangentEquation inherits two formatting quirks from getSinusoidEquation — (1) hardcoded "x - " and ") + " produce ugly strings like "x - -0.785" and "+ -1.000" when phase/offset are negative, and (2) no division-by-zero guard when both points share the same x-coordinate, producing "Infinity" in the equation. Both issues exist identically in getSinusoidEquation (lines 105-123) and are explicitly verified by existing sinusoid tests (line 269). Since this PR intentionally mirrors the sinusoid pattern, this is a pre-existing issue — but worth noting for a future cleanup across both functions.

Extended reasoning...

What the bug is

getTangentEquation (lines 125-153) builds an equation string using hardcoded operators "x - " and ") + ". When phase is negative, the output becomes "x - -0.785" instead of the mathematically cleaner "x + 0.785". Similarly, when verticalOffset is negative, it produces "+ -1.000" instead of "- 1.000". Additionally, if p2[0] === p1[0], the denominator in Math.PI / (4 * (p2[0] - p1[0])) is zero, producing Infinity for angularFrequency and phase, yielding an equation like "y = 0.000tan(Infinityx - Infinity) + 0.000".

Why this is pre-existing

Both issues are direct copies of the identical pattern in getSinusoidEquation (lines 105-123 of the same file). The sinusoid function uses Math.PI / (2 * (p2[0] - p1[0])) with the same division-by-zero vulnerability, and the same hardcoded "x - " and ") + " operators. The PR description explicitly states "StartCoordsTangent mirrors StartCoordsSinusoid exactly," confirming this was an intentional design choice to maintain consistency.

Proof via concrete example

For the formatting issue: given startCoords = [[1, -1], [3, 1]], we get amplitude = 2, angularFrequency = π/8 ≈ 0.393, phase = 0.393, verticalOffset = -1. The output is "y = 2.000tan(0.393x - 0.393) + -1.000" — note the "+ -1.000". For the division-by-zero: given startCoords = [[2, 0], [2, 3]], the denominator 4 * (2 - 2) = 0, so angularFrequency = Infinity, and the equation displays nonsensically.

Existing test evidence

The sinusoid tests in util.test.ts (around line 269) explicitly assert the ugly format: "y = 1.000sin(0.785x - -0.785) + 0.000" and "y = 2.000sin(1.571x - 0.000) + -1.000". This demonstrates the team has accepted this formatting as the current behavior. Changing only getTangentEquation would create an inconsistency between tangent and sinusoid equation displays.

Impact and recommended fix

The impact is purely cosmetic — these equations are only shown in the editor's start coordinates section, not to students. The division-by-zero case requires a content creator to manually type identical x-coordinates into the CoordinatePairInput fields, which is an unusual edge case (interactive dragging prevents it). For comparison, getQuadraticEquation (line 156-159) already has a denom === 0 guard that returns "Division by zero error". A future cleanup could add sign-aware formatting and division-by-zero guards to both getSinusoidEquation and getTangentEquation together for consistency.

);
};

const styles = StyleSheet.create({
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Move this in packages/perseus-editor/src/widgets/interactive-graph-editor/start-coords/start-coords-tangent.css

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.

1 participant