From cc1995dafaac637b035c71270e4d4e6f57a15e19 Mon Sep 17 00:00:00 2001 From: Nisha Yerunkar Date: Thu, 22 Aug 2024 11:49:01 -0700 Subject: [PATCH] [Locked labels] View locked labels in an Interactive Graph (#1533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 Screenshot 2024-08-20 at 4 08 28 PM 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: https://github.com/Khan/perseus/pull/1533 --- .changeset/violet-worms-invite.md | 6 +++ .../src/__stories__/flags-for-api-options.ts | 1 + .../interactive-graph-editor.stories.tsx | 6 +-- .../src/components/__tests__/util.test.ts | 11 +++++ .../locked-figures-section.tsx | 7 +++ .../perseus-editor/src/components/util.ts | 10 +++++ packages/perseus/src/index.ts | 1 + packages/perseus/src/perseus-types.ts | 13 +++++- packages/perseus/src/types.ts | 5 +++ .../__stories__/interactive-graph.stories.tsx | 9 ++++ .../interactive-graph.testdata.ts | 17 +++++++ .../__tests__/interactive-graph.test.tsx | 45 +++++++++++++++++++ .../perseus/src/widgets/interactive-graph.tsx | 5 +++ .../graph-locked-labels-layer.tsx | 23 ++++++++++ .../interactive-graphs/graph-locked-layer.tsx | 6 +++ ...interactive-graph-question-builder.test.ts | 37 +++++++++++++++ .../interactive-graph-question-builder.ts | 22 +++++++++ .../locked-figures/locked-label.tsx | 31 +++++++++++++ .../widgets/interactive-graphs/mafs-graph.tsx | 10 ++++- .../stateful-mafs-graph.tsx | 1 + 20 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 .changeset/violet-worms-invite.md create mode 100644 packages/perseus/src/widgets/interactive-graphs/graph-locked-labels-layer.tsx create mode 100644 packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx diff --git a/.changeset/violet-worms-invite.md b/.changeset/violet-worms-invite.md new file mode 100644 index 0000000000..bf762f8c97 --- /dev/null +++ b/.changeset/violet-worms-invite.md @@ -0,0 +1,6 @@ +--- +"@khanacademy/perseus": minor +"@khanacademy/perseus-editor": minor +--- + +[Locked labels] View locked labels in an Interactive Graph diff --git a/packages/perseus-editor/src/__stories__/flags-for-api-options.ts b/packages/perseus-editor/src/__stories__/flags-for-api-options.ts index 479768047a..65e0285a57 100644 --- a/packages/perseus-editor/src/__stories__/flags-for-api-options.ts +++ b/packages/perseus-editor/src/__stories__/flags-for-api-options.ts @@ -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 diff --git a/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx b/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx index f672376960..9ec83c3882 100644 --- a/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx +++ b/packages/perseus-editor/src/__stories__/interactive-graph-editor.stories.tsx @@ -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, }, }, }} @@ -152,13 +152,13 @@ MafsWithLockedFiguresCurrent.parameters = { }, }; -export const MafsWithLockedFiguresM2bFlag = (): React.ReactElement => { +export const MafsWithLockedLabelsFlag = (): React.ReactElement => { return ( ); }; -MafsWithLockedFiguresM2bFlag.parameters = { +MafsWithLockedLabelsFlag.parameters = { chromatic: { // Disabling because this isn't visually testing anything on the // initial load of the editor page. diff --git a/packages/perseus-editor/src/components/__tests__/util.test.ts b/packages/perseus-editor/src/components/__tests__/util.test.ts index 90d54044f7..ff293c6b6a 100644 --- a/packages/perseus-editor/src/components/__tests__/util.test.ts +++ b/packages/perseus-editor/src/components/__tests__/util.test.ts @@ -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", () => { diff --git a/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx b/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx index 2e48b1dcdd..6d1bdcb546 100644 --- a/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx +++ b/packages/perseus-editor/src/components/graph-locked-figures/locked-figures-section.tsx @@ -159,6 +159,13 @@ const LockedFiguresSection = (props: Props) => { {isExpanded && ( {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 ( ( /> ); +export const LockedLabel = (args: StoryArgs): React.ReactElement => ( + +); + export const Sinusoid = (args: StoryArgs): React.ReactElement => ( ); diff --git a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts index 6aba5372bc..96bd5e0b44 100644 --- a/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts +++ b/packages/perseus/src/widgets/__testdata__/interactive-graph.testdata.ts @@ -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) @@ -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 = diff --git a/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx b/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx index aa40acc71e..53a861a01a 100644 --- a/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx +++ b/packages/perseus/src/widgets/__tests__/interactive-graph.test.tsx @@ -35,6 +35,7 @@ import { segmentWithLockedEllipses, segmentWithLockedEllipseWhite, segmentWithLockedFunction, + segmentWithLockedLabels, segmentWithLockedLineQuestion, segmentWithLockedPointsQuestion, segmentWithLockedPointsWithColorQuestion, @@ -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", + }); + }); }); diff --git a/packages/perseus/src/widgets/interactive-graph.tsx b/packages/perseus/src/widgets/interactive-graph.tsx index 02dbf22a45..8dbae8bd1c 100644 --- a/packages/perseus/src/widgets/interactive-graph.tsx +++ b/packages/perseus/src/widgets/interactive-graph.tsx @@ -1821,6 +1821,11 @@ class InteractiveGraph extends React.Component { return ( ; +}; + +export default function GraphLockedLabelsLayer(props: Props) { + const {lockedFigures} = props; + + return lockedFigures.map((figure, i) => { + if (figure.type === "label") { + return ; + } + + // TODO(LEMS-2271): Add support for labels within + // other locked figure types + return null; + }); +} diff --git a/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx b/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx index a6959107ba..2f900a6f81 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graph-locked-layer.tsx @@ -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 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 63f90bc73a..d1190240da 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 @@ -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", + }, + ]); + }); }); 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 0e0e70bc8a..e87624e54d 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 @@ -6,6 +6,7 @@ import type { LockedFigureColor, LockedFigureFillType, LockedFunctionType, + LockedLabelType, LockedLineType, LockedPointType, LockedPolygonType, @@ -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, diff --git a/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx new file mode 100644 index 0000000000..93ca566b41 --- /dev/null +++ b/packages/perseus/src/widgets/interactive-graphs/locked-figures/locked-label.tsx @@ -0,0 +1,31 @@ +import {font} from "@khanacademy/wonder-blocks-tokens"; +import * as React from "react"; + +import {getDependencies} from "../../../dependencies"; +import {lockedFigureColors, type LockedLabelType} from "../../../perseus-types"; +import {pointToPixel} from "../graphs/use-transform"; +import useGraphConfig from "../reducer/use-graph-config"; + +export default function LockedLabel(props: LockedLabelType) { + const {coord, text, color, size} = props; + + const [x, y] = pointToPixel(coord, useGraphConfig()); + + const {TeX} = getDependencies(); + + // Move this all outside the SVG element + return ( + + {text} + + ); +} diff --git a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx index 9420b0835d..0f9d1e987a 100644 --- a/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/mafs-graph.tsx @@ -8,6 +8,7 @@ import AxisLabels from "./backgrounds/axis-labels"; import {AxisTickLabels} from "./backgrounds/axis-tick-labels"; import {Grid} from "./backgrounds/grid"; import {LegacyGrid} from "./backgrounds/legacy-grid"; +import GraphLockedLabelsLayer from "./graph-locked-labels-layer"; import GraphLockedLayer from "./graph-locked-layer"; import { LinearGraph, @@ -35,6 +36,7 @@ import "mafs/core.css"; import "./mafs-styles.css"; export type MafsGraphProps = { + showLabelsFlag?: boolean; box: [number, number]; backgroundImage?: InteractiveGraphProps["backgroundImage"]; lockedFigures?: InteractiveGraphProps["lockedFigures"]; @@ -90,6 +92,12 @@ export const MafsGraph = (props: MafsGraphProps) => { box={props.box} backgroundImage={props.backgroundImage} /> + {/* Locked labels layer */} + {props.showLabelsFlag && props.lockedFigures && ( + + )} { containerSizeClass={props.containerSizeClass} markings={props.markings} /> - {/* Locked layer */} + {/* Locked figures layer */} {props.lockedFigures && (