From 96151067381fbfbb9ec325ac6b921ba2830cc344 Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Mon, 24 Jun 2024 11:30:14 -0700 Subject: [PATCH] [Interactive Graph] Update the builder with all currently migrated graph types (#1373) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary: I updated the builder utility with all currently migrated graph types. This meant adding linear, linear system, ray, quadratic, and sinusoid. I also added the ability to give them all starting coords. This is necessary to prove that the starting coords can be set at all. Now that it's clear they can be set, we can move onto adding the ability to edit these coordinates in the editor UI in the next task. NOTE: The task also includes start coords for the Points graph type, but that doesn't seem to have the mafs flag on in storybook yet, so I didn't include that here. Issue: https://khanacademy.atlassian.net/browse/LEMS-2051 ## Test plan: Storybook - [segment](http://localhost:6006/?path=/story/perseuseditor-editorpage--interactive-graph-segment-with-starting-coords) - [multiple segments](http://localhost:6006/?path=/story/perseuseditor-editorpage--interactive-graph-segments-with-starting-coords) - [linear](http://localhost:6006/?path=/story/perseuseditor-editorpage--interactive-graph-linear-with-starting-coords) - [linear system](http://localhost:6006/?path=/story/perseuseditor-editorpage--interactive-graph-linear-system-with-starting-coords) - [ray](http://localhost:6006/?path=/story/perseuseditor-editorpage--interactive-graph-ray-with-starting-coords) - [circle](http://localhost:6006/?path=/story/perseuseditor-editorpage--interactive-graph-circle-with-starting-coords) - [quadratic](http://localhost:6006/?path=/story/perseuseditor-editorpage--interactive-graph-quadratic-with-starting-coords) - [sinusoid](http://localhost:6006/?path=/story/perseuseditor-editorpage--interactive-graph-sinusoid-with-starting-coords) ## Storybook previews showing custom start coords | segment | segments | linear | | --- | --- | --- | | Screenshot 2024-06-21 at 5 08 25 PM | Screenshot 2024-06-21 at 5 08 31 PM | Screenshot 2024-06-21 at 5 08 36 PM | | linear system | ray | circle | | Screenshot 2024-06-21 at 5 08 40 PM | Screenshot 2024-06-21 at 5 08 44 PM | Screenshot 2024-06-21 at 5 08 49 PM | | quadratic | sinusoid | | | Screenshot 2024-06-21 at 5 08 53 PM | Screenshot 2024-06-21 at 5 08 58 PM | | Author: nishasy Reviewers: benchristel Required Reviewers: Approved By: benchristel Checks: ✅ codecov/project, ✅ codecov/patch, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: https://github.com/Khan/perseus/pull/1373 --- .changeset/short-badgers-wonder.md | 6 + .../src/__stories__/editor-page.stories.tsx | 83 +++- .../interactive-graph-regression.stories.tsx | 2 +- .../interactive-graph.testdata.ts | 74 ++++ ...interactive-graph-question-builder.test.ts | 367 +++++++++++++++++- .../interactive-graph-question-builder.ts | 187 ++++++++- 6 files changed, 710 insertions(+), 9 deletions(-) create mode 100644 .changeset/short-badgers-wonder.md diff --git a/.changeset/short-badgers-wonder.md b/.changeset/short-badgers-wonder.md new file mode 100644 index 0000000000..68bea28b58 --- /dev/null +++ b/.changeset/short-badgers-wonder.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": patch +"@khanacademy/perseus-editor": patch +--- + +[Interactive Graph] Update the interactive graph builder with all currently migrated graph types diff --git a/packages/perseus-editor/src/__stories__/editor-page.stories.tsx b/packages/perseus-editor/src/__stories__/editor-page.stories.tsx index 286452723b..d57fabbb7d 100644 --- a/packages/perseus-editor/src/__stories__/editor-page.stories.tsx +++ b/packages/perseus-editor/src/__stories__/editor-page.stories.tsx @@ -7,7 +7,17 @@ import {StyleSheet} from "aphrodite"; import * as React from "react"; import {EditorPage} from ".."; -import {segmentWithLockedFigures} from "../../../perseus/src/widgets/__testdata__/interactive-graph.testdata"; +import { + circleWithStartingCoordsQuestion, + linearSystemWithStartingCoordsQuestion, + linearWithStartingCoordsQuestion, + quadraticWithStartingCoordsQuestion, + rayWithStartingCoordsQuestion, + segmentWithLockedFigures, + segmentWithStartingCoordsQuestion, + segmentsWithStartingCoordsQuestion, + sinusoidWithStartingCoordsQuestion, +} from "../../../perseus/src/widgets/__testdata__/interactive-graph.testdata"; import {registerAllWidgetsAndEditorsForTesting} from "../util/register-all-widgets-and-editors-for-testing"; import EditorPageWithStorybookPreview from "./editor-page-with-storybook-preview"; @@ -32,6 +42,77 @@ export const Demo = (): React.ReactElement => { return ; }; +export const InteractiveGraphSegmentWithStartingCoords = + (): React.ReactElement => { + return ( + + ); + }; + +export const InteractiveGraphSegmentsWithStartingCoords = + (): React.ReactElement => { + return ( + + ); + }; + +export const InteractiveGraphLinearWithStartingCoords = + (): React.ReactElement => { + return ( + + ); + }; + +export const InteractiveGraphLinearSystemWithStartingCoords = + (): React.ReactElement => { + return ( + + ); + }; + +export const InteractiveGraphRayWithStartingCoords = (): React.ReactElement => { + return ( + + ); +}; + +export const InteractiveGraphCircleWithStartingCoords = + (): React.ReactElement => { + return ( + + ); + }; + +export const InteractiveGraphQuadraticWithStartingCoords = + (): React.ReactElement => { + return ( + + ); + }; + +export const InteractiveGraphSinusoidWithStartingCoords = + (): React.ReactElement => { + return ( + + ); + }; + export const MafsWithLockedFiguresCurrent = (): React.ReactElement => { return ( ( ); diff --git a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts index 52c801d9bc..eef3e1a1ae 100644 --- a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts +++ b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts @@ -1091,6 +1091,80 @@ export const segmentQuestion: PerseusRenderer = { }, }; +export const segmentWithStartingCoordsQuestion: PerseusRenderer = + interactiveGraphQuestionBuilder() + .withSegments([ + [ + [0, 0], + [2, 2], + ], + ]) + .build(); + +export const segmentsWithStartingCoordsQuestion: PerseusRenderer = + interactiveGraphQuestionBuilder() + .withSegments([ + [ + [0, 0], + [2, 2], + ], + [ + [0, 2], + [2, 0], + ], + ]) + .build(); + +export const linearWithStartingCoordsQuestion: PerseusRenderer = + interactiveGraphQuestionBuilder() + .withLinear([ + [3, 0], + [3, 3], + ]) + .build(); + +export const linearSystemWithStartingCoordsQuestion: PerseusRenderer = + interactiveGraphQuestionBuilder() + .withLinearSystem([ + [ + [-3, 0], + [-3, 3], + ], + [ + [3, 0], + [3, 3], + ], + ]) + .build(); + +export const rayWithStartingCoordsQuestion: PerseusRenderer = + interactiveGraphQuestionBuilder() + .withRay([ + [3, 0], + [3, 3], + ]) + .build(); + +export const circleWithStartingCoordsQuestion: PerseusRenderer = + interactiveGraphQuestionBuilder().withCircle([9, 9]).build(); + +export const quadraticWithStartingCoordsQuestion: PerseusRenderer = + interactiveGraphQuestionBuilder() + .withQuadratic([ + [-1, -1], + [0, 0], + [1, -1], + ]) + .build(); + +export const sinusoidWithStartingCoordsQuestion: PerseusRenderer = + interactiveGraphQuestionBuilder() + .withSinusoid([ + [0, 0], + [1, -1], + ]) + .build(); + export const segmentWithLockedPointsQuestion: PerseusRenderer = { content: "Line segment $\\overline{OG}$ is rotated $180^\\circ$ about the point $(-2,4)$. \n\n**Draw the image of this rotation using the interactive graph.**\n\n*The direction of a rotation by a positive angle is counter-clockwise.* \n\n[[☃ interactive-graph 1]]\n\n", diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts index 755867bc14..bdc0c40f34 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.test.ts @@ -68,9 +68,9 @@ describe("InteractiveGraphQuestionBuilder", () => { expect(graph.options.step).toEqual([7, 8]); }); - it("creates a segment graph with a specified number of segments", () => { + it("creates a default segment graph with a specified number of segments", () => { const question: PerseusRenderer = interactiveGraphQuestionBuilder() - .withSegments(3) + .withNumSegments(3) .build(); const graph = question.widgets["interactive-graph 1"]; @@ -93,6 +93,258 @@ describe("InteractiveGraphQuestionBuilder", () => { ); }); + it("creates a segment graph with start coords", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withSegments([ + [ + [0, 0], + [2, 2], + ], + ]) + .build(); + const graph = question.widgets["interactive-graph 1"]; + + expect(graph.options).toEqual( + expect.objectContaining({ + graph: { + type: "segment", + numSegments: 1, + coords: [ + [ + [0, 0], + [2, 2], + ], + ], + }, + correct: { + type: "segment", + numSegments: 1, + coords: [ + [ + [-7, 7], + [2, 5], + ], + ], + }, + }), + ); + }); + + it("creates a graph with multiple segments with start coords", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withSegments([ + [ + [0, 0], + [2, 2], + ], + [ + [3, 3], + [5, 5], + ], + ]) + .build(); + const graph = question.widgets["interactive-graph 1"]; + + expect(graph.options).toEqual( + expect.objectContaining({ + graph: { + type: "segment", + numSegments: 2, + coords: [ + [ + [0, 0], + [2, 2], + ], + [ + [3, 3], + [5, 5], + ], + ], + }, + correct: { + type: "segment", + numSegments: 2, + coords: [ + [ + [-7, 7], + [2, 5], + ], + [ + [-7, 7], + [2, 5], + ], + ], + }, + }), + ); + }); + + it("creates a linear graph", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withLinear() + .build(); + + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: {type: "linear"}, + correct: { + type: "linear", + coords: [ + [-10, -5], + [10, 5], + ], + }, + }), + ); + }); + + it("creates a linear graph with start coords", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withLinear([ + [3, 0], + [3, 3], + ]) + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: { + type: "linear", + coords: [ + [3, 0], + [3, 3], + ], + }, + correct: { + type: "linear", + coords: [ + [-10, -5], + [10, 5], + ], + }, + }), + ); + }); + + it("creates a linear system graph", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withLinearSystem() + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: {type: "linear-system"}, + correct: { + type: "linear-system", + coords: [ + [ + [-10, -5], + [10, 5], + ], + [ + [-10, 5], + [10, -5], + ], + ], + }, + }), + ); + }); + + it("creates a linear system graph with start coords", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withLinearSystem([ + [ + [-3, 0], + [-3, 3], + ], + [ + [3, 0], + [3, 3], + ], + ]) + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: { + type: "linear-system", + coords: [ + [ + [-3, 0], + [-3, 3], + ], + [ + [3, 0], + [3, 3], + ], + ], + }, + correct: { + type: "linear-system", + coords: [ + [ + [-10, -5], + [10, 5], + ], + [ + [-10, 5], + [10, -5], + ], + ], + }, + }), + ); + }); + + it("creates a ray graph", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withRay() + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: {type: "ray"}, + correct: { + type: "ray", + coords: [ + [-10, -5], + [10, 5], + ], + }, + }), + ); + }); + + it("creates a ray graph with start coords", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withRay([ + [3, 0], + [3, 3], + ]) + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: { + type: "ray", + coords: [ + [3, 0], + [3, 3], + ], + }, + correct: { + type: "ray", + coords: [ + [-10, -5], + [10, 5], + ], + }, + }), + ); + }); + it("creates a circle graph", () => { const question: PerseusRenderer = interactiveGraphQuestionBuilder() .withCircle() @@ -106,6 +358,117 @@ describe("InteractiveGraphQuestionBuilder", () => { ); }); + it("creates a circle graph with start coords", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withCircle([9, 9]) + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: {type: "circle", center: [9, 9], radius: 5}, + correct: {type: "circle", radius: 5, center: [0, 0]}, + }), + ); + }); + + it("creates a quadratic graph", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withQuadratic() + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: {type: "quadratic"}, + correct: { + type: "quadratic", + coords: [ + [-10, 5], + [10, 5], + [0, -5], + ], + }, + }), + ); + }); + + it("creates a quadratic graph with start coords", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withQuadratic([ + [-1, -1], + [0, 0], + [1, -1], + ]) + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: { + type: "quadratic", + coords: [ + [-1, -1], + [0, 0], + [1, -1], + ], + }, + correct: { + type: "quadratic", + coords: [ + [-10, 5], + [10, 5], + [0, -5], + ], + }, + }), + ); + }); + + it("creates a sinusoid graph", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withSinusoid() + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: {type: "sinusoid"}, + correct: { + type: "sinusoid", + coords: [ + [-10, 5], + [10, 5], + ], + }, + }), + ); + }); + + it("creates a sinusoid graph with start coords", () => { + const question: PerseusRenderer = interactiveGraphQuestionBuilder() + .withSinusoid([ + [0, 0], + [1, -1], + ]) + .build(); + const graph = question.widgets["interactive-graph 1"]; + expect(graph.options).toEqual( + expect.objectContaining({ + graph: { + type: "sinusoid", + coords: [ + [0, 0], + [1, -1], + ], + }, + correct: { + type: "sinusoid", + coords: [ + [-10, 5], + [10, 5], + ], + }, + }), + ); + }); + it("creates a polygon graph", () => { const question: PerseusRenderer = interactiveGraphQuestionBuilder() .withPolygon("grid") diff --git a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts index 78022194b3..420e43ba45 100644 --- a/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts +++ b/packages/perseus/src/widgets/interactive-graphs/interactive-graph-question-builder.ts @@ -9,7 +9,9 @@ import type { PerseusGraphType, PerseusRenderer, LockedPolygonType, + CollinearTuple, } from "../../perseus-types"; +import type {Coord} from "@khanacademy/perseus"; import type {Interval, vec} from "mafs"; export function interactiveGraphQuestionBuilder(): InteractiveGraphQuestionBuilder { @@ -97,13 +99,54 @@ class InteractiveGraphQuestionBuilder { return this; } - withSegments(numSegments: number): InteractiveGraphQuestionBuilder { + withSegments( + startCoords: CollinearTuple[], + ): InteractiveGraphQuestionBuilder { + this.interactiveFigureConfig = new SegmentGraphConfig( + startCoords.length, + startCoords, + ); + return this; + } + + withNumSegments(numSegments: number): InteractiveGraphQuestionBuilder { this.interactiveFigureConfig = new SegmentGraphConfig(numSegments); return this; } - withCircle(): InteractiveGraphQuestionBuilder { - this.interactiveFigureConfig = new CircleGraphConfig(); + withLinear(startCoords?: CollinearTuple): InteractiveGraphQuestionBuilder { + this.interactiveFigureConfig = new LinearConfig(startCoords); + return this; + } + + withLinearSystem( + startCoords?: CollinearTuple[], + ): InteractiveGraphQuestionBuilder { + this.interactiveFigureConfig = new LinearSystemConfig(startCoords); + return this; + } + + withRay(startCoords?: CollinearTuple): InteractiveGraphQuestionBuilder { + this.interactiveFigureConfig = new RayConfig(startCoords); + return this; + } + + withCircle(startCoords?: Coord): InteractiveGraphQuestionBuilder { + this.interactiveFigureConfig = new CircleGraphConfig(startCoords); + return this; + } + + withQuadratic( + startCoords?: [Coord, Coord, Coord], + ): InteractiveGraphQuestionBuilder { + this.interactiveFigureConfig = new QuadraticConfig(startCoords); + return this; + } + + withSinusoid( + startCoords?: [Coord, Coord], + ): InteractiveGraphQuestionBuilder { + this.interactiveFigureConfig = new SinusoidGraphConfig(startCoords); return this; } @@ -219,8 +262,11 @@ interface InteractiveFigureConfig { class SegmentGraphConfig implements InteractiveFigureConfig { private numSegments: number; - constructor(numSegments: number) { + private startCoords?: CollinearTuple[]; + + constructor(numSegments: number, startCoords?: CollinearTuple[]) { this.numSegments = numSegments; + this.startCoords = startCoords; } correct(): PerseusGraphType { @@ -235,20 +281,151 @@ class SegmentGraphConfig implements InteractiveFigureConfig { } graph(): PerseusGraphType { - return {type: "segment", numSegments: this.numSegments}; + return { + type: "segment", + numSegments: this.numSegments, + coords: this.startCoords, + }; + } +} + +class LinearConfig implements InteractiveFigureConfig { + private startCoords?: CollinearTuple; + + constructor(startCoords?: CollinearTuple) { + this.startCoords = startCoords; + } + + correct(): PerseusGraphType { + return { + type: "linear", + coords: [ + [-10, -5], + [10, 5], + ], + }; + } + + graph(): PerseusGraphType { + return {type: "linear", coords: this.startCoords}; + } +} + +class LinearSystemConfig implements InteractiveFigureConfig { + private startCoords?: CollinearTuple[]; + + constructor(startCoords?: CollinearTuple[]) { + this.startCoords = startCoords; + } + + correct(): PerseusGraphType { + return { + type: "linear-system", + coords: [ + [ + [-10, -5], + [10, 5], + ], + [ + [-10, 5], + [10, -5], + ], + ], + }; + } + + graph(): PerseusGraphType { + return {type: "linear-system", coords: this.startCoords}; + } +} + +class RayConfig implements InteractiveFigureConfig { + private startCoords?: CollinearTuple; + + constructor(startCoords?: CollinearTuple) { + this.startCoords = startCoords; + } + + correct(): PerseusGraphType { + return { + type: "ray", + coords: [ + [-10, -5], + [10, 5], + ], + }; + } + + graph(): PerseusGraphType { + return {type: "ray", coords: this.startCoords}; } } class CircleGraphConfig implements InteractiveFigureConfig { + private startCoords?: Coord; + + constructor(startCoords?: Coord) { + this.startCoords = startCoords; + } + correct(): PerseusGraphType { return {type: "circle", radius: 5, center: [0, 0]}; } graph(): PerseusGraphType { + if (this.startCoords) { + return {type: "circle", center: this.startCoords, radius: 5}; + } + return {type: "circle"}; } } +class QuadraticConfig implements InteractiveFigureConfig { + private startCoords?: [Coord, Coord, Coord]; + + constructor(startCoords?: [Coord, Coord, Coord]) { + this.startCoords = startCoords; + } + + correct(): PerseusGraphType { + return { + type: "quadratic", + coords: [ + [-10, 5], + [10, 5], + [0, -5], + ], + }; + } + + graph(): PerseusGraphType { + return {type: "quadratic", coords: this.startCoords}; + } +} + +class SinusoidGraphConfig implements InteractiveFigureConfig { + private startCoords?: [Coord, Coord]; + + constructor(startCoords?: [Coord, Coord]) { + this.startCoords = startCoords; + } + + correct(): PerseusGraphType { + return { + type: "sinusoid", + coords: [ + [-10, 5], + [10, 5], + ], + }; + } + + graph(): PerseusGraphType { + return {type: "sinusoid", coords: this.startCoords}; + } +} + class PolygonGraphConfig implements InteractiveFigureConfig { private snapTo: "grid" | "angles" | "sides"; constructor(snapTo: "grid" | "angles" | "sides") {