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") {