From 6e1ec850c7efb186444dcd9023e2a2c37cd731d2 Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Wed, 31 Jul 2024 15:38:13 -0700 Subject: [PATCH] [Hint Mode: Start Coords] Add start coords UI for quadratic graphs (#1469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: Add the UI to specify start coords for Quadratic graph type. - Add the quadratic graph type to start-coord-settings.tsx - Create a start-coords-quadratic.tsx file with the main UI - Add a utility for getting the quadratic equation from coordinates (less complex than the static method on InteractiveGraph that does the same) - Add quadratic graph to the phase 2 flag Issue: https://khanacademy.atlassian.net/browse/LEMS-2210 ## Test plan: `yarn jest` Storybook - http://localhost:6006/?path=/story/perseuseditor-widgets-interactive-graph--interactive-graph-quadratic-with-starting-coords Author: nishasy Reviewers: mark-fitzgerald, nishasy, benchristel Required Reviewers: Approved By: mark-fitzgerald Checks: ✅ codecov/project, ✅ codecov/patch, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ gerald, ⏭️ Publish npm snapshot, ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1469 --- .changeset/grumpy-rivers-draw.md | 6 ++ .../__tests__/start-coords-settings.test.tsx | 68 +++++++++++++ .../src/components/__tests__/util.test.ts | 32 +++++++ .../src/components/start-coords-quadratic.tsx | 95 +++++++++++++++++++ .../src/components/start-coords-settings.tsx | 11 ++- .../perseus-editor/src/components/util.ts | 38 ++++++++ .../src/widgets/interactive-graph-editor.tsx | 2 +- packages/perseus/src/index.ts | 1 + .../reducer/initialize-graph-state.ts | 2 +- 9 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 .changeset/grumpy-rivers-draw.md create mode 100644 packages/perseus-editor/src/components/start-coords-quadratic.tsx diff --git a/.changeset/grumpy-rivers-draw.md b/.changeset/grumpy-rivers-draw.md new file mode 100644 index 0000000000..5e4e987ca8 --- /dev/null +++ b/.changeset/grumpy-rivers-draw.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": minor +"@khanacademy/perseus-editor": minor +--- + +[Hint Mode: Start Coords] Add start coords UI for quadratic graphs diff --git a/packages/perseus-editor/src/components/__tests__/start-coords-settings.test.tsx b/packages/perseus-editor/src/components/__tests__/start-coords-settings.test.tsx index 8b9b4ea4ad..c0f3cb7cc9 100644 --- a/packages/perseus-editor/src/components/__tests__/start-coords-settings.test.tsx +++ b/packages/perseus-editor/src/components/__tests__/start-coords-settings.test.tsx @@ -455,4 +455,72 @@ describe("StartCoordSettings", () => { }, ); }); + + describe("quadratic graph", () => { + test("shows the start coordinates UI", () => { + // Arrange + + // Act + render( + {}} + />, + {wrapper: RenderStateRoot}, + ); + + // Assert + expect(screen.getByText("Start coordinates")).toBeInTheDocument(); + expect(screen.getByText("Starting equation:")).toBeInTheDocument(); + expect( + // Equation for default start coords + screen.getByText("y = 0.400x^2 + 0.000x + -5.000"), + ).toBeInTheDocument(); + expect(screen.getByText("Point 1:")).toBeInTheDocument(); + expect(screen.getByText("Point 2:")).toBeInTheDocument(); + expect(screen.getByText("Point 3:")).toBeInTheDocument(); + }); + + test.each` + pointIndex | coord + ${0} | ${"x"} + ${0} | ${"y"} + ${1} | ${"x"} + ${1} | ${"y"} + ${2} | ${"x"} + ${2} | ${"y"} + `( + "calls onChange when $coord coord is changed (line $pointIndex)", + async ({pointIndex, coord}) => { + // Arrange + const onChangeMock = jest.fn(); + + // Act + render( + , + ); + + // Assert + const input = screen.getAllByRole("spinbutton", { + name: `${coord}`, + })[pointIndex]; + await userEvent.clear(input); + await userEvent.type(input, "101"); + + const expectedCoords = [ + [-5, 5], + [0, -5], + [5, 5], + ]; + expectedCoords[pointIndex][coord === "x" ? 0 : 1] = 101; + + expect(onChangeMock).toHaveBeenLastCalledWith(expectedCoords); + }, + ); + }); }); diff --git a/packages/perseus-editor/src/components/__tests__/util.test.ts b/packages/perseus-editor/src/components/__tests__/util.test.ts index ce4502237d..bda3983a98 100644 --- a/packages/perseus-editor/src/components/__tests__/util.test.ts +++ b/packages/perseus-editor/src/components/__tests__/util.test.ts @@ -4,6 +4,7 @@ import { radianToDegree, getDefaultGraphStartCoords, getSinusoidEquation, + getQuadraticEquation, } from "../util"; import type {PerseusGraphType, Range} from "@khanacademy/perseus"; @@ -276,3 +277,34 @@ describe("getSinusoidEquation", () => { expect(equation).toBe(expected); }); }); + +describe("getQuadraticEquation", () => { + test.each` + point1 | point2 | point3 | expected + ${[-5, 5]} | ${[0, -5]} | ${[5, 5]} | ${"y = 0.400x^2 + 0.000x + -5.000"} + ${[-5, 5]} | ${[0, 5]} | ${[5, 5]} | ${"y = 0.000x^2 + 0.000x + 5.000"} + ${[-9, 9]} | ${[-7, 7]} | ${[9, 9]} | ${"y = 0.063x^2 + 0.000x + 3.938"} + ${[-9, 4]} | ${[-7, 7]} | ${[9, 9]} | ${"y = -0.076x^2 + 0.278x + 12.688"} + ${[-1, 0]} | ${[0, 1]} | ${[1, 2]} | ${"y = 0.000x^2 + 1.000x + 1.000"} + `( + "should return the correct equation", + ({point1, point2, point3, expected}) => { + // Act + const equation = getQuadraticEquation([point1, point2, point3]); + + expect(equation).toBe(expected); + }, + ); + + test.each` + point1 | point2 | point3 + ${[-5, 5]} | ${[-5, -5]} | ${[5, 5]} + ${[-5, 5]} | ${[0, -5]} | ${[-5, 5]} + ${[-5, 5]} | ${[0, 5]} | ${[0, 5]} + `("should return division by zero error", ({point1, point2, point3}) => { + // Act + const equation = getQuadraticEquation([point1, point2, point3]); + + expect(equation).toBe("Division by zero error"); + }); +}); diff --git a/packages/perseus-editor/src/components/start-coords-quadratic.tsx b/packages/perseus-editor/src/components/start-coords-quadratic.tsx new file mode 100644 index 0000000000..e25eb03ffc --- /dev/null +++ b/packages/perseus-editor/src/components/start-coords-quadratic.tsx @@ -0,0 +1,95 @@ +import {View} from "@khanacademy/wonder-blocks-core"; +import {Strut} from "@khanacademy/wonder-blocks-layout"; +import {color, font, spacing} from "@khanacademy/wonder-blocks-tokens"; +import { + BodyMonospace, + LabelLarge, + LabelMedium, +} from "@khanacademy/wonder-blocks-typography"; +import {StyleSheet} from "aphrodite"; +import * as React from "react"; + +import CoordinatePairInput from "./coordinate-pair-input"; +import {getQuadraticEquation} from "./util"; + +import type {Coord, PerseusGraphType} from "@khanacademy/perseus"; + +type Props = { + startCoords: [Coord, Coord, Coord]; + onChange: (startCoords: PerseusGraphType["startCoords"]) => void; +}; + +const StartCoordsQuadratic = (props: Props) => { + const {startCoords, onChange} = props; + + return ( + <> + {/* Current equation */} + + Starting equation: + + {getQuadraticEquation(startCoords)} + + + + {/* Points UI */} + + Point 1: + + + onChange([value, startCoords[1], startCoords[2]]) + } + /> + + + Point 2: + + + onChange([startCoords[0], value, startCoords[2]]) + } + /> + + + Point 3: + + + onChange([startCoords[0], startCoords[1], value]) + } + /> + + + ); +}; + +const styles = StyleSheet.create({ + tile: { + backgroundColor: color.fadedBlue8, + marginTop: spacing.xSmall_8, + padding: spacing.small_12, + borderRadius: spacing.xSmall_8, + flexDirection: "row", + alignItems: "center", + }, + equationSection: { + marginTop: spacing.small_12, + }, + equationBody: { + backgroundColor: color.fadedOffBlack8, + border: `1px solid ${color.fadedOffBlack32}`, + marginTop: spacing.xSmall_8, + paddingLeft: spacing.xSmall_8, + paddingRight: spacing.xSmall_8, + fontSize: font.size.xSmall, + }, +}); + +export default StartCoordsQuadratic; diff --git a/packages/perseus-editor/src/components/start-coords-settings.tsx b/packages/perseus-editor/src/components/start-coords-settings.tsx index f85ed442ca..a34170fbc6 100644 --- a/packages/perseus-editor/src/components/start-coords-settings.tsx +++ b/packages/perseus-editor/src/components/start-coords-settings.tsx @@ -3,6 +3,7 @@ import { getCircleCoords, getLineCoords, getLinearSystemCoords, + getQuadraticCoords, getSegmentCoords, getSinusoidCoords, } from "@khanacademy/perseus"; @@ -17,6 +18,7 @@ import Heading from "./heading"; import StartCoordsCircle from "./start-coords-circle"; import StartCoordsLine from "./start-coords-line"; import StartCoordsMultiline from "./start-coords-multiline"; +import StartCoordsQuadratic from "./start-coords-quadratic"; import StartCoordsSinusoid from "./start-coords-sinusoid"; import {getDefaultGraphStartCoords} from "./util"; @@ -79,7 +81,14 @@ const StartCoordsSettingsInner = (props: Props) => { onChange={onChange} /> ); - + case "quadratic": + const quadraticCoords = getQuadraticCoords(props, range, step); + return ( + + ); default: return null; } diff --git a/packages/perseus-editor/src/components/util.ts b/packages/perseus-editor/src/components/util.ts index 9c06bc1875..bfeeb3c239 100644 --- a/packages/perseus-editor/src/components/util.ts +++ b/packages/perseus-editor/src/components/util.ts @@ -3,6 +3,7 @@ import { getCircleCoords, getLineCoords, getLinearSystemCoords, + getQuadraticCoords, getSegmentCoords, getSinusoidCoords, } from "@khanacademy/perseus"; @@ -187,6 +188,12 @@ export function getDefaultGraphStartCoords( range, step, ); + case "quadratic": + return getQuadraticCoords( + {...graph, startCoords: undefined}, + range, + step, + ); default: return undefined; } @@ -215,3 +222,34 @@ export const getSinusoidEquation = (startCoords: [Coord, Coord]) => { verticalOffset.toFixed(3) ); }; + +export const getQuadraticEquation = (startCoords: [Coord, Coord, Coord]) => { + const p1 = startCoords[0]; + const p2 = startCoords[1]; + const p3 = startCoords[2]; + + const denom = (p1[0] - p2[0]) * (p1[0] - p3[0]) * (p2[0] - p3[0]); + if (denom === 0) { + // Many of the callers assume that the return value is always defined. + return "Division by zero error"; + } + const a = + (p3[0] * (p2[1] - p1[1]) + + p2[0] * (p1[1] - p3[1]) + + p1[0] * (p3[1] - p2[1])) / + denom; + const b = + (p3[0] * p3[0] * (p1[1] - p2[1]) + + p2[0] * p2[0] * (p3[1] - p1[1]) + + p1[0] * p1[0] * (p2[1] - p3[1])) / + denom; + const c = + (p2[0] * p3[0] * (p2[0] - p3[0]) * p1[1] + + p3[0] * p1[0] * (p3[0] - p1[0]) * p2[1] + + p1[0] * p2[0] * (p1[0] - p2[0]) * p3[1]) / + denom; + + return ( + "y = " + a.toFixed(3) + "x^2 + " + b.toFixed(3) + "x + " + c.toFixed(3) + ); +}; diff --git a/packages/perseus-editor/src/widgets/interactive-graph-editor.tsx b/packages/perseus-editor/src/widgets/interactive-graph-editor.tsx index 202683574e..e7de3e5410 100644 --- a/packages/perseus-editor/src/widgets/interactive-graph-editor.tsx +++ b/packages/perseus-editor/src/widgets/interactive-graph-editor.tsx @@ -70,7 +70,7 @@ const startCoordsUiPhase1Types = [ "segment", "circle", ]; -const startCoordsUiPhase2Types = ["sinusoid"]; +const startCoordsUiPhase2Types = ["sinusoid", "quadratic"]; type Range = [min: number, max: number]; diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index 484605cd1d..71f94e6af2 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -124,6 +124,7 @@ export { getLinearSystemCoords, getSegmentCoords, getSinusoidCoords, + getQuadraticCoords, } from "./widgets/interactive-graphs/reducer/initialize-graph-state"; /** diff --git a/packages/perseus/src/widgets/interactive-graphs/reducer/initialize-graph-state.ts b/packages/perseus/src/widgets/interactive-graphs/reducer/initialize-graph-state.ts index 1e28be4b8e..2b594c827e 100644 --- a/packages/perseus/src/widgets/interactive-graphs/reducer/initialize-graph-state.ts +++ b/packages/perseus/src/widgets/interactive-graphs/reducer/initialize-graph-state.ts @@ -345,7 +345,7 @@ export function getSinusoidCoords( return coords; } -function getQuadraticCoords( +export function getQuadraticCoords( graph: PerseusGraphTypeQuadratic, range: [x: Interval, y: Interval], step: [x: number, y: number],