Skip to content

Commit

Permalink
[Hint Mode: Start Coords] Add start coords UI for sinusoid graphs (#1468
Browse files Browse the repository at this point in the history
)

## Summary:
Add the UI to specify start coords for Sinusoid graph type.

- Add the sinusoid graph type to start-coord-settings.tsx
- Create a start-coords-sinusoid.tsx file with the main UI
- Add a utility for getting the sinusoid equation from coordinates (less complex
  than the static method on InteractiveGraph that does the same)
- Add start coords UI phase 2 flag

Issue: https://khanacademy.atlassian.net/browse/LEMS-2207

## Test plan:
`yarn jest`

Storybook
- http://localhost:6006/?path=/story/perseuseditor-widgets-interactive-graph--interactive-graph-sinusoid-with-starting-coords

Author: nishasy

Reviewers: nishasy, mark-fitzgerald, benchristel

Required Reviewers:

Approved By: mark-fitzgerald

Checks: ✅ codecov/project, ✅ codecov/patch, ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ gerald, ⏭️  Publish npm snapshot, ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Jest Coverage (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), 🚫 Upload Coverage, ✅ gerald, ⏭️  Publish npm snapshot, ✅ Lint, Typecheck, Format, and Test (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), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Publish Storybook to Chromatic (ubuntu-latest, 20.x), ✅ gerald

Pull Request URL: #1468
  • Loading branch information
nishasy authored Jul 31, 2024
1 parent 94ad04f commit af68a9e
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .changeset/eighty-ducks-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": minor
"@khanacademy/perseus-editor": minor
---

[Hint Mode: Start Coords] Add start coords UI for sinusoid graphs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const flags = {
// Start coords UI flags
// TODO(LEMS-2228): Remove flags once this is fully released
"start-coords-ui-phase-1": true,
"start-coords-ui-phase-2": true,
},
} satisfies APIOptions["flags"];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,4 +391,68 @@ describe("StartCoordSettings", () => {
},
);
});

describe("sinusoid graph", () => {
test("shows the start coordinates UI", () => {
// Arrange

// Act
render(
<StartCoordsSettings
{...defaultProps}
type="sinusoid"
onChange={() => {}}
/>,
{wrapper: RenderStateRoot},
);

// Assert
expect(screen.getByText("Start coordinates")).toBeInTheDocument();
expect(screen.getByText("Starting equation:")).toBeInTheDocument();
expect(
// Equation for default start coords
screen.getByText("y = 2.000sin(0.524x - 0.000) + 0.000"),
).toBeInTheDocument();
expect(screen.getByText("Point 1:")).toBeInTheDocument();
expect(screen.getByText("Point 2:")).toBeInTheDocument();
});

test.each`
pointIndex | coord
${0} | ${"x"}
${0} | ${"y"}
${1} | ${"x"}
${1} | ${"y"}
`(
"calls onChange when $coord coord is changed (line $pointIndex)",
async ({pointIndex, coord}) => {
// Arrange
const onChangeMock = jest.fn();

// Act
render(
<StartCoordsSettings
{...defaultProps}
type="sinusoid"
onChange={onChangeMock}
/>,
);

// Assert
const input = screen.getAllByRole("spinbutton", {
name: `${coord}`,
})[pointIndex];
await userEvent.clear(input);
await userEvent.type(input, "101");

const expectedCoords = [
[0, 0],
[3, 2],
];
expectedCoords[pointIndex][coord === "x" ? 0 : 1] = 101;

expect(onChangeMock).toHaveBeenLastCalledWith(expectedCoords);
},
);
});
});
21 changes: 21 additions & 0 deletions packages/perseus-editor/src/components/__tests__/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
getDefaultFigureForType,
radianToDegree,
getDefaultGraphStartCoords,
getSinusoidEquation,
} from "../util";

import type {PerseusGraphType, Range} from "@khanacademy/perseus";
Expand Down Expand Up @@ -255,3 +256,23 @@ describe("getDefaultGraphStartCoords", () => {
expect(defaultCoords).toEqual({center: [0, 0], radius: 2});
});
});

describe("getSinusoidEquation", () => {
test.each`
point1 | point2 | expected
${[0, 0]} | ${[3, 2]} | ${"y = 2.000sin(0.524x - 0.000) + 0.000"}
${[0, 0]} | ${[1, 0]} | ${"y = 0.000sin(1.571x - 0.000) + 0.000"}
${[0, 0]} | ${[1, 1]} | ${"y = 1.000sin(1.571x - 0.000) + 0.000"}
${[0, 0]} | ${[1, -1]} | ${"y = -1.000sin(1.571x - 0.000) + 0.000"}
${[0, 0]} | ${[-1, 0]} | ${"y = 0.000sin(-1.571x - 0.000) + 0.000"}
${[-1, 0]} | ${[1, 1]} | ${"y = 1.000sin(0.785x - -0.785) + 0.000"}
${[0, -1]} | ${[1, 1]} | ${"y = 2.000sin(1.571x - 0.000) + -1.000"}
${[-9, -9]} | ${[9, 9]} | ${"y = 18.000sin(0.087x - -0.785) + -9.000"}
${[3, -4]} | ${[6, 7]} | ${"y = 11.000sin(0.524x - 1.571) + -4.000"}
`("should return the correct equation", ({point1, point2, expected}) => {
// Act
const equation = getSinusoidEquation([point1, point2]);

expect(equation).toBe(expected);
});
});
11 changes: 11 additions & 0 deletions packages/perseus-editor/src/components/start-coords-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getLineCoords,
getLinearSystemCoords,
getSegmentCoords,
getSinusoidCoords,
} from "@khanacademy/perseus";
import Button from "@khanacademy/wonder-blocks-button";
import {View} from "@khanacademy/wonder-blocks-core";
Expand All @@ -16,6 +17,7 @@ import Heading from "./heading";
import StartCoordsCircle from "./start-coords-circle";
import StartCoordsLine from "./start-coords-line";
import StartCoordsMultiline from "./start-coords-multiline";
import StartCoordsSinusoid from "./start-coords-sinusoid";
import {getDefaultGraphStartCoords} from "./util";

import type {PerseusGraphType, Range} from "@khanacademy/perseus";
Expand Down Expand Up @@ -69,6 +71,15 @@ const StartCoordsSettingsInner = (props: Props) => {
onChange={onChange}
/>
);
case "sinusoid":
const sinusoidCoords = getSinusoidCoords(props, range, step);
return (
<StartCoordsSinusoid
startCoords={sinusoidCoords}
onChange={onChange}
/>
);

default:
return null;
}
Expand Down
80 changes: 80 additions & 0 deletions packages/perseus-editor/src/components/start-coords-sinusoid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
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 {getSinusoidEquation} from "./util";

import type {Coord, PerseusGraphType} from "@khanacademy/perseus";

type Props = {
startCoords: [Coord, Coord];
onChange: (startCoords: PerseusGraphType["startCoords"]) => void;
};

const StartCoordsSinusoid = (props: Props) => {
const {startCoords, onChange} = props;

return (
<>
{/* Current equation */}
<View style={styles.equationSection}>
<LabelMedium>Starting equation:</LabelMedium>
<BodyMonospace style={styles.equationBody}>
{getSinusoidEquation(startCoords)}
</BodyMonospace>
</View>

{/* Points UI */}
<View style={styles.tile}>
<LabelLarge>Point 1:</LabelLarge>
<Strut size={spacing.small_12} />
<CoordinatePairInput
coord={startCoords[0]}
labels={["x", "y"]}
onChange={(value) => onChange([value, startCoords[1]])}
/>
</View>
<View style={styles.tile}>
<LabelLarge>Point 2:</LabelLarge>
<Strut size={spacing.small_12} />
<CoordinatePairInput
coord={startCoords[1]}
labels={["x", "y"]}
onChange={(value) => onChange([startCoords[0], value])}
/>
</View>
</>
);
};

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 StartCoordsSinusoid;
33 changes: 32 additions & 1 deletion packages/perseus-editor/src/components/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
getLineCoords,
getLinearSystemCoords,
getSegmentCoords,
getSinusoidCoords,
} from "@khanacademy/perseus";
import {UnreachableCaseError} from "@khanacademy/wonder-stuff-core";

Expand All @@ -18,6 +19,7 @@ import type {
LockedPolygonType,
LockedFunctionType,
PerseusGraphType,
Coord,
} from "@khanacademy/perseus";

export function focusWithChromeStickyFocusBugWorkaround(element: Element) {
Expand Down Expand Up @@ -178,9 +180,38 @@ export function getDefaultGraphStartCoords(
const radius = kvector.length(
kvector.subtract(startCoords.radiusPoint, startCoords.center),
);

return {center: startCoords.center, radius};
case "sinusoid":
return getSinusoidCoords(
{...graph, startCoords: undefined},
range,
step,
);
default:
return undefined;
}
}

export const getSinusoidEquation = (startCoords: [Coord, Coord]) => {
// Get coefficients
// It's assumed that p1 is the root and p2 is the first peak
const p1 = startCoords[0];
const p2 = startCoords[1];

// Resulting coefficients are canonical for this sine curve
const amplitude = p2[1] - p1[1];
const angularFrequency = Math.PI / (2 * (p2[0] - p1[0]));
const phase = p1[0] * angularFrequency;
const verticalOffset = p1[1];

return (
"y = " +
amplitude.toFixed(3) +
"sin(" +
angularFrequency.toFixed(3) +
"x - " +
phase.toFixed(3) +
") + " +
verticalOffset.toFixed(3)
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,64 @@ describe("InteractiveGraphEditor", () => {
mafs: {
...flags.mafs,
"start-coords-ui-phase-1": shouldRender,
"start-coords-ui-phase-2": false,
},
},
}}
graph={{type}}
correct={{type}}
/>,
{
wrapper: RenderStateRoot,
},
);

// Assert
if (shouldRender) {
expect(
await screen.findByRole("button", {
name: "Use default start coordinates",
}),
).toBeInTheDocument();
} else {
expect(
screen.queryByRole("button", {
name: "Use default start coordinates",
}),
).toBeNull();
}
},
);

test.each`
type | shouldRender
${"linear"} | ${false}
${"ray"} | ${false}
${"linear-system"} | ${false}
${"segment"} | ${false}
${"circle"} | ${false}
${"quadratic"} | ${false}
${"sinusoid"} | ${true}
${"polygon"} | ${false}
${"angle"} | ${false}
${"point"} | ${false}
`(
"should render for $type graphs if phase2 flag is on: $shouldRender",
async ({type, shouldRender}) => {
// Arrange

// Act
render(
<InteractiveGraphEditor
{...baseProps}
apiOptions={{
...ApiOptions.defaults,
flags: {
...flags,
mafs: {
...flags.mafs,
"start-coords-ui-phase-1": false,
"start-coords-ui-phase-2": shouldRender,
},
},
}}
Expand Down
20 changes: 14 additions & 6 deletions packages/perseus-editor/src/widgets/interactive-graph-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const startCoordsUiPhase1Types = [
"segment",
"circle",
];
const startCoordsUiPhase2Types = ["sinusoid"];

type Range = [min: number, max: number];

Expand Down Expand Up @@ -269,6 +270,18 @@ class InteractiveGraphEditor extends React.Component<Props> {
graph = <div className="perseus-error">{this.props.valid}</div>;
}

const startCoordsPhase1 =
this.props.apiOptions?.flags?.mafs?.["start-coords-ui-phase-1"];
const startCoordsPhase2 =
this.props.apiOptions?.flags?.mafs?.["start-coords-ui-phase-2"];

const displayStartCoordsUI =
this.props.graph &&
((startCoordsPhase1 &&
startCoordsUiPhase1Types.includes(this.props.graph.type)) ||
(startCoordsPhase2 &&
startCoordsUiPhase2Types.includes(this.props.graph.type)));

return (
<View>
<LabeledRow label="Type of Graph:">
Expand Down Expand Up @@ -483,12 +496,7 @@ class InteractiveGraphEditor extends React.Component<Props> {
)}
{this.props.graph?.type &&
// TODO(LEMS-2228): Remove flags once this is fully released
this.props.apiOptions?.flags?.mafs?.[
"start-coords-ui-phase-1"
] &&
startCoordsUiPhase1Types.includes(
this.props.graph.type,
) && (
displayStartCoordsUI && (
<StartCoordsSettings
{...this.props.graph}
range={this.props.range}
Expand Down
1 change: 1 addition & 0 deletions packages/perseus/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export {
getLineCoords,
getLinearSystemCoords,
getSegmentCoords,
getSinusoidCoords,
} from "./widgets/interactive-graphs/reducer/initialize-graph-state";

/**
Expand Down
Loading

0 comments on commit af68a9e

Please sign in to comment.