Skip to content

Commit

Permalink
[Locked labels] View locked labels in an Interactive Graph (#1533)
Browse files Browse the repository at this point in the history
## Summary:
We want to be able to view a (standalone) locked label on an
Interactive Graph Widget.

- Add "interctive-graph-locked-features-labels" feature flag
- Create LockedLabelType
- Create a layer for locked labels since they can't be part
  of the Mafs graph SVG (can't put TeX in an SVG)
- Create a LockedLabel component to render the labels
- Update the builder to support locked labels
- Add locked labels to stories

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

## Test plan:
`yarn jest packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx`

Storybook
- http://localhost:6006/?path=/story/perseus-widgets-interactive-graph--locked-label
- http://localhost:6006/?path=/story/perseuseditor-widgets-interactive-graph--mafs-with-locked-figures-current
- http://localhost:6006/?path=/story/perseuseditor-widgets-interactive-graph--mafs-with-locked-labels-flag

<img width="476" alt="Screenshot 2024-08-20 at 4 08 28 PM" src="https://github.com/user-attachments/assets/175e66a1-fca2-4a55-9b48-7d869530a54e">

Author: nishasy

Reviewers: benchristel, mark-fitzgerald, SonicScrewdriver

Required Reviewers:

Approved By: benchristel

Checks: ✅ Upload Coverage (ubuntu-latest, 20.x), ✅ Publish npm snapshot (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Jest Coverage (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: #1533
  • Loading branch information
nishasy authored Aug 22, 2024
1 parent 0bf2711 commit cc1995d
Show file tree
Hide file tree
Showing 20 changed files with 261 additions and 5 deletions.
6 changes: 6 additions & 0 deletions .changeset/violet-worms-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@khanacademy/perseus": minor
"@khanacademy/perseus-editor": minor
---

[Locked labels] View locked labels in an Interactive Graph
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const flags = {

// Locked figures flags
"interactive-graph-locked-features-m2b": true,
"interactive-graph-locked-features-labels": true,

// Start coords UI flags
// TODO(LEMS-2228): Remove flags once this is fully released
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export const MafsWithLockedFiguresCurrent = (): React.ReactElement => {
flags: {
mafs: {
...flags.mafs,
"interactive-graph-locked-features-m2b": false,
"interactive-graph-locked-features-labels": false,
},
},
}}
Expand All @@ -152,13 +152,13 @@ MafsWithLockedFiguresCurrent.parameters = {
},
};

export const MafsWithLockedFiguresM2bFlag = (): React.ReactElement => {
export const MafsWithLockedLabelsFlag = (): React.ReactElement => {
return (
<EditorPageWithStorybookPreview question={segmentWithLockedFigures} />
);
};

MafsWithLockedFiguresM2bFlag.parameters = {
MafsWithLockedLabelsFlag.parameters = {
chromatic: {
// Disabling because this isn't visually testing anything on the
// initial load of the editor page.
Expand Down
11 changes: 11 additions & 0 deletions packages/perseus-editor/src/components/__tests__/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ describe("getDefaultFigureForType", () => {
directionalAxis: "x",
});
});

test("should return a 'label' with default values", () => {
const figure = getDefaultFigureForType("label");
expect(figure).toEqual({
type: "label",
coord: [0, 0],
text: "",
color: "grayH",
size: "medium",
});
});
});

describe("degreeToRadian", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ const LockedFiguresSection = (props: Props) => {
{isExpanded && (
<View>
{figures?.map((figure, index) => {
if (figure.type === "label") {
// TODO(LEMS-1795): Add locked label settings.
// Remove this block once label locked figure
// settings are implemented.
return;
}

return (
<LockedFigureSettings
key={`${uniqueId}-locked-${figure}-${index}`}
Expand Down
10 changes: 10 additions & 0 deletions packages/perseus-editor/src/components/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
LockedVectorType,
LockedPolygonType,
LockedFunctionType,
LockedLabelType,
PerseusGraphType,
Coord,
} from "@khanacademy/perseus";
Expand Down Expand Up @@ -73,6 +74,7 @@ export function getDefaultFigureForType(type: "vector"): LockedVectorType;
export function getDefaultFigureForType(type: "ellipse"): LockedEllipseType;
export function getDefaultFigureForType(type: "polygon"): LockedPolygonType;
export function getDefaultFigureForType(type: "function"): LockedFunctionType;
export function getDefaultFigureForType(type: "label"): LockedLabelType;
export function getDefaultFigureForType(type: LockedFigureType): LockedFigure;
export function getDefaultFigureForType(type: LockedFigureType): LockedFigure {
switch (type) {
Expand Down Expand Up @@ -139,6 +141,14 @@ export function getDefaultFigureForType(type: LockedFigureType): LockedFigure {
equation: "x^2",
directionalAxis: "x",
};
case "label":
return {
type: "label",
coord: [0, 0],
text: "",
color: DEFAULT_COLOR,
size: "medium",
};
default:
throw new UnreachableCaseError(type);
}
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 @@ -180,6 +180,7 @@ export type {
LockedEllipseType,
LockedPolygonType,
LockedFunctionType,
LockedLabelType,
PerseusGraphType,
PerseusAnswerArea,
PerseusExpressionWidgetOptions,
Expand Down
13 changes: 12 additions & 1 deletion packages/perseus/src/perseus-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,8 @@ export type LockedFigure =
| LockedVectorType
| LockedEllipseType
| LockedPolygonType
| LockedFunctionType;
| LockedFunctionType
| LockedLabelType;
export type LockedFigureType = LockedFigure["type"];

export type LockedPointType = {
Expand Down Expand Up @@ -780,6 +781,16 @@ export type LockedFunctionType = {
domain?: Interval;
};

// Not associated with a specific figure
export type LockedLabelType = {
type: "label";
coord: Coord;
// TeX-supported string
text: string;
color: LockedFigureColor;
size: "small" | "medium" | "large";
};

export type PerseusGraphType =
| PerseusGraphTypeAngle
| PerseusGraphTypeCircle
Expand Down
5 changes: 5 additions & 0 deletions packages/perseus/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ export const InteractiveGraphLockedFeaturesFlags = [
* new Mafs interactive-graph widget (locked functions).
*/
"interactive-graph-locked-features-m2b",
/**
* Enables/Disables locked features in the new Mafs interactive-graph
* widget (locked labels).
*/
"interactive-graph-locked-features-labels",
] as const;

export const InteractiveGraphEditorFlags = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
segmentWithLockedPolygons,
staticGraphQuestion,
staticGraphQuestionWithAnotherWidget,
segmentWithLockedLabels,
} from "../__testdata__/interactive-graph.testdata";

import type {APIOptions} from "../../types";
Expand All @@ -36,6 +37,7 @@ const enableMafs: APIOptions = {
segment: true,
polygon: true,
angle: true,
"interactive-graph-locked-features-labels": true,
},
},
};
Expand Down Expand Up @@ -154,6 +156,13 @@ export const LockedPolygon = (args: StoryArgs): React.ReactElement => (
/>
);

export const LockedLabel = (args: StoryArgs): React.ReactElement => (
<RendererWithDebugUI
apiOptions={{...enableMafs}}
question={segmentWithLockedLabels}
/>
);

export const Sinusoid = (args: StoryArgs): React.ReactElement => (
<RendererWithDebugUI question={sinusoidQuestion} />
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,22 @@ export const segmentWithLockedFunction = (
.build();
};

export const segmentWithLockedLabels: PerseusRenderer =
interactiveGraphQuestionBuilder()
.addLockedLabel("small \\frac{1}{2}", [-6, 2], {
color: "pink",
size: "small",
})
.addLockedLabel("medium E_0 = mc^2", [1, 2], {
color: "blue",
size: "medium",
})
.addLockedLabel("large \\sqrt{2a}", [-3, -2], {
color: "green",
size: "large",
})
.build();

export const segmentWithLockedFigures: PerseusRenderer =
interactiveGraphQuestionBuilder()
.addLockedPointAt(-7, -7)
Expand All @@ -822,6 +838,7 @@ export const segmentWithLockedFigures: PerseusRenderer =
.addLockedFunction("sin(x)", {
color: "red",
})
.addLockedLabel("\\sqrt{\\frac{1}{2}}", [6, -5])
.build();

export const quadraticQuestion: PerseusRenderer =
Expand Down
45 changes: 45 additions & 0 deletions packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
segmentWithLockedEllipses,
segmentWithLockedEllipseWhite,
segmentWithLockedFunction,
segmentWithLockedLabels,
segmentWithLockedLineQuestion,
segmentWithLockedPointsQuestion,
segmentWithLockedPointsWithColorQuestion,
Expand Down Expand Up @@ -861,4 +862,48 @@ describe("locked layer", () => {
expect(PlotOfXMock).toHaveBeenCalledTimes(0);
expect(PlotOfYMock).toHaveBeenCalledTimes(1);
});

it("should render locked labels", async () => {
// Arrange
const {container} = renderQuestion(segmentWithLockedLabels, {
flags: {
mafs: {
segment: true,
"interactive-graph-locked-features-labels": true,
},
},
});

// Act
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access
const labels = container.querySelectorAll(".locked-label");

// Assert
expect(labels).toHaveLength(3);

// content
expect(labels[0]).toHaveTextContent("small \\frac{1}{2}");
expect(labels[1]).toHaveTextContent("medium E_0 = mc^2");
expect(labels[2]).toHaveTextContent("large \\sqrt{2a}");

// styles
expect(labels[0]).toHaveStyle({
color: lockedFigureColors["pink"],
fontSize: "14px", // small
left: "80px",
top: "160px",
});
expect(labels[1]).toHaveStyle({
color: lockedFigureColors["blue"],
fontSize: "16px", // medium
left: "220px",
top: "160px",
});
expect(labels[2]).toHaveStyle({
color: lockedFigureColors["green"],
fontSize: "20px", // large
left: "140px",
top: "240px",
});
});
});
5 changes: 5 additions & 0 deletions packages/perseus/src/widgets/interactive-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,11 @@ class InteractiveGraph extends React.Component<Props, State> {
return (
<StatefulMafsGraph
{...this.props}
showLabelsFlag={
this.props.apiOptions?.flags?.["mafs"]?.[
"interactive-graph-locked-features-labels"
]
}
ref={this.mafsRef}
gridStep={gridStep}
snapStep={snapStep}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from "react";

import LockedLabel from "./locked-figures/locked-label";

import type {LockedFigure} from "../../perseus-types";

type Props = {
lockedFigures: ReadonlyArray<LockedFigure>;
};

export default function GraphLockedLabelsLayer(props: Props) {
const {lockedFigures} = props;

return lockedFigures.map((figure, i) => {
if (figure.type === "label") {
return <LockedLabel key={`label-${i}`} {...figure} />;
}

// TODO(LEMS-2271): Add support for labels within
// other locked figure types
return null;
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ const GraphLockedLayer = (props: Props) => {
{...figure}
/>
);
case "label":
// This is rendered outside the SVG element, since
// TeX cannot be rendered inside an SVG.
// See graph-locked-labels-layer.tsx for
// the component that renders these.
return null;
default:
/**
* Devlopment-time future-proofing: This should
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -961,4 +961,41 @@ describe("InteractiveGraphQuestionBuilder", () => {
},
]);
});

it("adds a locked label", () => {
const question: PerseusRenderer = interactiveGraphQuestionBuilder()
.addLockedLabel("the text", [1, 2])
.build();
const graph = question.widgets["interactive-graph 1"];

expect(graph.options.lockedFigures).toEqual([
{
type: "label",
text: "the text",
coord: [1, 2],
color: "grayH",
size: "medium",
},
]);
});

it("adds a locked label with options", () => {
const question: PerseusRenderer = interactiveGraphQuestionBuilder()
.addLockedLabel("some other text", [15, 2], {
color: "green",
size: "large",
})
.build();
const graph = question.widgets["interactive-graph 1"];

expect(graph.options.lockedFigures).toEqual([
{
type: "label",
text: "some other text",
coord: [15, 2],
color: "green",
size: "large",
},
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
LockedFigureColor,
LockedFigureFillType,
LockedFunctionType,
LockedLabelType,
LockedLineType,
LockedPointType,
LockedPolygonType,
Expand Down Expand Up @@ -383,6 +384,27 @@ class InteractiveGraphQuestionBuilder {
return this;
}

addLockedLabel(
text: string,
coord: Coord,
options?: {
color?: LockedFigureColor;
size?: "small" | "medium" | "large";
},
) {
const lockedLabel: LockedLabelType = {
type: "label",
coord,
text,
color: "grayH",
size: "medium",
...options,
};

this.addLockedFigure(lockedLabel);
return this;
}

private createLockedPoint(
x: number,
y: number,
Expand Down
Loading

0 comments on commit cc1995d

Please sign in to comment.