Skip to content

[Interactive Graph] Fix interactive graph x-axis label overlapping with content below#3567

Draft
ivyolamit wants to merge 2 commits intomainfrom
LEMS-3921/fix-graph-label-missing-pading
Draft

[Interactive Graph] Fix interactive graph x-axis label overlapping with content below#3567
ivyolamit wants to merge 2 commits intomainfrom
LEMS-3921/fix-graph-label-missing-pading

Conversation

@ivyolamit
Copy link
Copy Markdown
Contributor

@ivyolamit ivyolamit commented May 1, 2026

Summary:

  • When the y-range starts at 0 or higher (e.g., [0, 19]), the x-axis label extends below the graph content area and overlaps with text rendered after the widget.
  • The graph container had a hardcoded marginBottom: 30px that didn't account for label overflow.
  • The fix dynamically calculates the bottom margin to clear the overflowing label, so adjacent content (paragraphs, other widgets) retains its natural spacing without overlap.

Root Cause

The x-axis label is absolutely positioned at the end of the x-axis. When yMin >= 0, the x-axis sits at or near the bottom edge of the graph, pushing the label below the graph's content box. The fixed 30px margin was not enough to cover the overflow, especially for:

  • onAxis labels with wholly positive y-ranges (label clamped to height + fontSize * 1.25)
  • alongEdge labels with yMin >= 0 (label offset by fontSize * 3 below the graph)

Changes

packages/perseus/src/widgets/interactive-graphs/backgrounds/utils.ts

  • Added getGraphBottomMargin() — calculates the bottom margin needed to clear the x-axis label overflow so it doesn't overlap with adjacent content.

packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx

  • Replaced the hardcoded marginBottom: "30px" with a dynamic value from getGraphBottomMargin()
  • The calculation considers: label pixel position, graph height, whether labels are shown, and whether there is x-axis label text

packages/perseus/src/widgets/interactive-graphs/backgrounds/axis-labels.test.ts

  • Added 7 tests for getGraphBottomMargin() covering: normal range, yMin=0 (onAxis), wholly positive range (onAxis clamped), alongEdge with yMin=0, no label text, labels hidden, and negative range

packages/perseus/src/widgets/interactive-graphs/__snapshots__/interactive-graph.test.tsx.snap

  • Updated 9 snapshots (CSS class hash changed due to the style value now being dynamic)

Effective margins by scenario

Scenario Label overflow Graph margin Visual gap below label
Default [-10,10], onAxis 0px 30px 30px+ (label inside graph)
yMin=0 [0,19], onAxis 14px 30px ~22px
Wholly positive [6,15], onAxis 31.5px 31.5px ~22px
yMin=0, alongEdge 56px 56px ~22px

Co-authored by Claude Code (Opus)
Issue: LEMS-3921

Test plan

Use the content below:

Caleb measured the height of the snow in his yard before a winter storm began. 

The graph shows the snow height, in centimeters, during a winter storm.

[[☃ interactive-graph 1]]

**Write an equation that represents the height of the snow, $H$, in centimeters, after $t$ hours.**

$H=$ [[☃ expression 1]]
  • All 44 interactive-graph test suites pass (1111 tests)
  • No lint errors
  • No new type errors
  • Verify in Storybook with y-range [0, 19] and x-label $\text{Time (hours)}$
  • Verify with y-range [-1, 19] (workaround case) still looks correct
  • Verify with wholly positive y-range like [5, 15]
  • Verify with alongEdge label location
  • Verify default range [-10, 10] is unchanged
  • Verify graph with no x-axis label text is unchanged

ivyolamit added 2 commits May 1, 2026 16:14
…active graph x-axis label overlapping with content below when y-range starts at 0 or higher
…teractive graph x-axis label overlapping with content below
@ivyolamit ivyolamit self-assigned this May 1, 2026
@github-actions github-actions Bot added schema-change Attached to PRs when we detect Perseus Schema changes in it item-splitting-change labels May 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

npm Snapshot: Published

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

Example:

pnpm add @khanacademy/perseus@PR3567

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

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

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

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

@github-actions github-actions Bot removed the schema-change Attached to PRs when we detect Perseus Schema changes in it label May 1, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🗄️ Schema Change: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

🛠️ Item Splitting: No Changes ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Size Change: +142 B (+0.03%)

Total Size: 503 kB

📦 View Changed
Filename Size Change
packages/perseus/dist/es/index.js 198 kB +142 B (+0.07%)
ℹ️ View Unchanged
Filename Size
packages/kas/dist/es/index.js 20.6 kB
packages/keypad-context/dist/es/index.js 1 kB
packages/kmath/dist/es/index.js 6.36 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 12 kB
packages/perseus-core/dist/es/index.js 25.5 kB
packages/perseus-editor/dist/es/index.js 103 kB
packages/perseus-linter/dist/es/index.js 9.42 kB
packages/perseus-score/dist/es/index.js 9.78 kB
packages/perseus-utils/dist/es/index.js 403 B
packages/perseus/dist/es/strings.js 8.46 kB
packages/pure-markdown/dist/es/index.js 1.39 kB
packages/simple-markdown/dist/es/index.js 6.71 kB

compressed-size-action

@ivyolamit
Copy link
Copy Markdown
Contributor Author

@claude review once

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.

Logic and tests look solid, but this is a visual fix and the test plan still has unchecked Storybook verification items across multiple y-range configurations — worth a human eye on the actual rendering before merging.

Extended reasoning...

Overview

This PR fixes an x-axis label overflow issue in the interactive-graph widget. It introduces a new getGraphBottomMargin() helper in backgrounds/utils.ts that replaces the hardcoded marginBottom: "30px" in mafs-graph.tsx with a value computed from the label position, graph height, and whether axis labels are shown. Seven unit tests cover the new helper, and nine snapshot files update purely because a styled-component CSS hash changes when the inline style becomes dynamic.

Security risks

None. This is a CSS layout calculation in a rendering widget — no auth, crypto, data handling, or user-input parsing involved.

Level of scrutiny

Moderate. The function is small, pure, and has good unit-test coverage covering normal range, yMin=0, wholly-positive ranges, alongEdge labels, and the disabled cases (no label / markings off). The math matches the existing clampLabelPosition and getLabelPosition behaviour. However, this is a visual fix shipping to a heavily used widget, and the PR's own test plan lists six unchecked Storybook verification items spanning different y-range configurations and label modes — those are exactly the cases where a margin regression would be felt by content authors.

Other factors

  • Author explicitly requested review (@claude review once).
  • Bug-hunting system found no issues.
  • Snapshot churn is mechanical (CSS class hash), not behavioral.
  • The default [-10, 10] case still resolves to the original 30px margin, so no behavior change there — but I'd want a human to confirm visual parity in the unchecked Storybook scenarios before approving.

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