diff --git a/.changeset/proud-parents-hear.md b/.changeset/proud-parents-hear.md new file mode 100644 index 0000000000..6b52625e94 --- /dev/null +++ b/.changeset/proud-parents-hear.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": minor +"@khanacademy/perseus-editor": minor +--- + +[Interactive Graph Editor] Implement the UI for adding start coords for circle 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 246e3bfe26..3cc6c53f24 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 @@ -8,7 +8,7 @@ import {testDependencies} from "../../../../../testing/test-dependencies"; import {clone} from "../../util/object-utils"; import StartCoordsSettings from "../start-coords-settings"; -import type {CollinearTuple, Range} from "@khanacademy/perseus"; +import type {CollinearTuple, Coord, Range} from "@khanacademy/perseus"; import type {UserEvent} from "@testing-library/user-event"; const defaultProps = { @@ -329,4 +329,66 @@ describe("StartCoordSettings", () => { }, ); }); + + describe("circle graph", () => { + test("shows the start coordinates UI", () => { + // Arrange + + // Act + render( + {}} + />, + {wrapper: RenderStateRoot}, + ); + + // Assert + expect(screen.getByText("Start coordinates")).toBeInTheDocument(); + expect(screen.getByText("Center:")).toBeInTheDocument(); + expect(screen.getByText("x")).toBeInTheDocument(); + expect(screen.getByText("y")).toBeInTheDocument(); + }); + + test.each` + coordIndex | coord + ${0} | ${"x"} + ${1} | ${"y"} + `( + `calls onChange when $coord coord is changed`, + async ({coordIndex, coord}) => { + // Arrange + const onChangeMock = jest.fn(); + + const start = {center: [2, 2], radius: 5} satisfies { + center: Coord; + radius: number; + }; + + // Act + render( + , + {wrapper: RenderStateRoot}, + ); + + // Assert + const input = screen.getByRole("spinbutton", { + name: coord, + }); + await userEvent.clear(input); + await userEvent.type(input, "101"); + + const expectedCoords = clone(start); + expectedCoords.center[coordIndex] = 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 dd7ed8c04a..399e369d85 100644 --- a/packages/perseus-editor/src/components/__tests__/util.test.ts +++ b/packages/perseus-editor/src/components/__tests__/util.test.ts @@ -239,4 +239,19 @@ describe("getDefaultGraphStartCoords", () => { ], ]); }); + + test("should get default start coords for a circle graph", () => { + // Arrange + const graph: PerseusGraphType = {type: "circle"}; + const range = [ + [-10, 10], + [-10, 10], + ] satisfies [Range, Range]; + const step = [1, 1] satisfies [number, number]; + + // Act + const defaultCoords = getDefaultGraphStartCoords(graph, range, step); + + expect(defaultCoords).toEqual({center: [0, 0], radius: 2}); + }); }); diff --git a/packages/perseus-editor/src/components/start-coords-circle.tsx b/packages/perseus-editor/src/components/start-coords-circle.tsx new file mode 100644 index 0000000000..d3eadf1ec7 --- /dev/null +++ b/packages/perseus-editor/src/components/start-coords-circle.tsx @@ -0,0 +1,77 @@ +import {View} from "@khanacademy/wonder-blocks-core"; +import {TextField} from "@khanacademy/wonder-blocks-form"; +import {Strut} from "@khanacademy/wonder-blocks-layout"; +import {color, spacing} from "@khanacademy/wonder-blocks-tokens"; +import {LabelLarge} from "@khanacademy/wonder-blocks-typography"; +import {StyleSheet} from "aphrodite"; +import * as React from "react"; + +import CoordinatePairInput from "./coordinate-pair-input"; + +import type {Coord, PerseusGraphType} from "@khanacademy/perseus"; + +type Props = { + startCoords: { + center: Coord; + radius: number; + }; + // center: number; + onChange: (startCoords: PerseusGraphType["startCoords"]) => void; +}; + +const StartCoordsCircle = (props: Props) => { + const {startCoords, onChange} = props; + + return ( + + {/* Center */} + + Center: + + + onChange({center: coord, radius: startCoords.radius}) + } + /> + + + + {/* Radius */} + + Radius: + + { + onChange({ + center: startCoords.center, + radius: parseFloat(value), + }); + }} + style={styles.textField} + /> + + + ); +}; + +const styles = StyleSheet.create({ + tile: { + backgroundColor: color.fadedBlue8, + marginTop: spacing.xSmall_8, + padding: spacing.small_12, + borderRadius: spacing.xSmall_8, + }, + row: { + flexDirection: "row", + alignItems: "center", + }, + textField: { + width: spacing.xxxLarge_64, + }, +}); + +export default StartCoordsCircle; diff --git a/packages/perseus-editor/src/components/start-coords-settings.tsx b/packages/perseus-editor/src/components/start-coords-settings.tsx index 581ba98fe9..c83ff4bc67 100644 --- a/packages/perseus-editor/src/components/start-coords-settings.tsx +++ b/packages/perseus-editor/src/components/start-coords-settings.tsx @@ -1,4 +1,6 @@ +import {vector as kvector} from "@khanacademy/kmath"; import { + getCircleCoords, getLineCoords, getLinearSystemCoords, getSegmentCoords, @@ -11,6 +13,7 @@ import arrowCounterClockwise from "@phosphor-icons/core/bold/arrow-counter-clock import * as React from "react"; import Heading from "./heading"; +import StartCoordsCircle from "./start-coords-circle"; import StartCoordsLine from "./start-coords-line"; import StartCoordsMultiline from "./start-coords-multiline"; import {getDefaultGraphStartCoords} from "./util"; @@ -55,6 +58,17 @@ const StartCoordsSettingsInner = (props: Props) => { onChange={onChange} /> ); + case "circle": + const circleCoords = getCircleCoords(props); + const radius = kvector.length( + kvector.subtract(circleCoords.radiusPoint, circleCoords.center), + ); + return ( + + ); default: return null; } diff --git a/packages/perseus-editor/src/components/util.ts b/packages/perseus-editor/src/components/util.ts index c4754af151..f585004da0 100644 --- a/packages/perseus-editor/src/components/util.ts +++ b/packages/perseus-editor/src/components/util.ts @@ -1,4 +1,6 @@ +import {vector as kvector} from "@khanacademy/kmath"; import { + getCircleCoords, getLineCoords, getLinearSystemCoords, getSegmentCoords, @@ -168,6 +170,16 @@ export function getDefaultGraphStartCoords( range, step, ); + case "circle": + const startCoords = getCircleCoords({ + ...graph, + startCoords: undefined, + }); + const radius = kvector.length( + kvector.subtract(startCoords.radiusPoint, startCoords.center), + ); + + return {center: startCoords.center, radius}; default: return undefined; } diff --git a/packages/perseus/src/index.ts b/packages/perseus/src/index.ts index 55355c6a2b..572d510d88 100644 --- a/packages/perseus/src/index.ts +++ b/packages/perseus/src/index.ts @@ -119,6 +119,7 @@ export { export {convertWidgetNameToEnum} from "./util/widget-enum-utils"; export {addWidget, QUESTION_WIDGETS} from "./util/snowman-utils"; export { + getCircleCoords, getLineCoords, getLinearSystemCoords, getSegmentCoords, diff --git a/packages/perseus/src/perseus-types.ts b/packages/perseus/src/perseus-types.ts index 1ee2136a1d..94be90aedc 100644 --- a/packages/perseus/src/perseus-types.ts +++ b/packages/perseus/src/perseus-types.ts @@ -802,7 +802,10 @@ export type PerseusGraphTypeCircle = { center?: Coord; radius?: number; // The initial coordinates the graph renders with. - startCoords?: Coord; + startCoords?: { + center: Coord; + radius: number; + }; } & PerseusGraphTypeCommon; export type PerseusGraphTypeLinear = { diff --git a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts index 23de0af435..d55be9abc4 100644 --- a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts +++ b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts @@ -340,7 +340,7 @@ export const rayWithStartingCoordsQuestion: PerseusRenderer = export const circleWithStartingCoordsQuestion: PerseusRenderer = interactiveGraphQuestionBuilder() - .withCircle({startCoords: [9, 9]}) + .withCircle({startCoords: {center: [9, 9], radius: 5}}) .build(); export const quadraticWithStartingCoordsQuestion: PerseusRenderer = 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 38d60c3f30..cb494b822d 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 @@ -424,12 +424,22 @@ describe("InteractiveGraphQuestionBuilder", () => { it("creates a circle graph with options", () => { const question: PerseusRenderer = interactiveGraphQuestionBuilder() - .withCircle({center: [-1, -1], radius: 3, startCoords: [9, 9]}) + .withCircle({ + center: [-1, -1], + radius: 3, + startCoords: { + center: [9, 9], + radius: 5, + }, + }) .build(); const graph = question.widgets["interactive-graph 1"]; expect(graph.options).toEqual( expect.objectContaining({ - graph: {type: "circle", startCoords: [9, 9], radius: 5}, + graph: { + type: "circle", + startCoords: {center: [9, 9], radius: 5}, + }, correct: {type: "circle", radius: 3, center: [-1, -1]}, }), ); 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 5723445445..7db0b1dbf1 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 @@ -183,7 +183,10 @@ class InteractiveGraphQuestionBuilder { withCircle(options?: { center?: Coord; radius?: number; - startCoords?: Coord; + startCoords?: { + center: Coord; + radius: number; + }; }): InteractiveGraphQuestionBuilder { this.interactiveFigureConfig = new CircleGraphConfig(options); return this; @@ -528,14 +531,20 @@ class RayGraphConfig implements InteractiveFigureConfig { } class CircleGraphConfig implements InteractiveFigureConfig { - private startCoords?: Coord; + private startCoords?: { + center: Coord; + radius: number; + }; private correctCenter: Coord; private correctRadius: number; constructor(options?: { center?: Coord; radius?: number; - startCoords?: Coord; + startCoords?: { + center: Coord; + radius: number; + }; }) { this.startCoords = options?.startCoords; this.correctCenter = options?.center ?? [0, 0]; @@ -552,7 +561,10 @@ class CircleGraphConfig implements InteractiveFigureConfig { graph(): PerseusGraphType { if (this.startCoords) { - return {type: "circle", startCoords: this.startCoords, radius: 5}; + return { + type: "circle", + startCoords: this.startCoords, + }; } return {type: "circle"}; 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 74d5b0a0ff..4b0a1042a7 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 @@ -367,7 +367,7 @@ function getQuadraticCoords( return normalizePoints(range, step, defaultCoords, true); } -function getCircleCoords(graph: PerseusGraphTypeCircle): { +export function getCircleCoords(graph: PerseusGraphTypeCircle): { center: Coord; radiusPoint: Coord; } { @@ -378,10 +378,13 @@ function getCircleCoords(graph: PerseusGraphTypeCircle): { }; } - if (graph.startCoords) { + if (graph.startCoords?.center && graph.startCoords.radius) { return { - center: graph.startCoords, - radiusPoint: vec.add(graph.startCoords, [2, 0]), + center: graph.startCoords.center, + radiusPoint: vec.add(graph.startCoords.center, [ + graph.startCoords.radius, + 0, + ]), }; }