Skip to content

Commit 0d5ab0b

Browse files
authored
[CX] Allow option to rotate axis labels on interactive graphs (#2284)
## Summary: This PR is part of the Interactive Graph Project. The purpose of this PR is to add a new option that will allow Content Creators to change the location of the axis labels between "onAxis" and "alongEdge". Note: While working on this ticket, a long-standing bug was found where the axis labels will continue to stretch further away from their desired locations on largely positive or negative graphs. This will require clamping the min/max values for our calculations. However, adding that logic was making this ticket seem quite complicated so I have opted to create a separate ticket for handling this. See https://khanacademy.atlassian.net/browse/LEMS-2912 for more details. ## Screenshots: ### onAxis (Default) ![Screenshot 2025-03-12 at 10 52 46 AM](https://github.com/user-attachments/assets/c8b65da1-c34e-424a-b802-dcf550afad58) ### alongEdge ![Screenshot 2025-03-12 at 10 52 29 AM](https://github.com/user-attachments/assets/920e7f68-f90c-4be6-b82e-d592c451acfa) Issue: https://khanacademy.atlassian.net/browse/LEMS-2718 ## Test plan: - New tests - Tests Pass - Manual Testing Author: nishasy Reviewers: SonicScrewdriver Required Reviewers: Approved By: SonicScrewdriver Checks: ✅ 8 checks were successful Pull Request URL: #2284
1 parent 9737eb4 commit 0d5ab0b

13 files changed

+324
-23
lines changed

.changeset/beige-stingrays-cry.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@khanacademy/perseus": minor
3+
"@khanacademy/perseus-core": minor
4+
"@khanacademy/perseus-editor": minor
5+
---
6+
7+
Add new labelLocation value for Interactive Graphs

packages/perseus-core/src/data-schema.ts

+8
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,7 @@ export type PerseusImageBackground = {
431431
* - none: shows no markings
432432
*/
433433
export type MarkingsType = "axes" | "graph" | "grid" | "none";
434+
export type AxisLabelLocation = "onAxis" | "alongEdge";
434435

435436
export type PerseusCategorizerWidgetOptions = {
436437
// Translatable text; a list of items to categorize. e.g. ["banana", "yellow", "apple", "purple", "shirt"]
@@ -734,6 +735,13 @@ export type PerseusInteractiveGraphWidgetOptions = {
734735
markings: MarkingsType;
735736
// How to label the X and Y axis. default: ["x", "y"]
736737
labels?: ReadonlyArray<string>;
738+
/**
739+
* Specifies the location of the labels on the graph. default: "onAxis".
740+
* - "onAxis": Labels are positioned on the axis at the right (x) and top (y) of the graph.
741+
* - "alongEdge": Labels are centered along the bottom (x) and left (y) edges of the graph.
742+
* The y label is rotated. Typically used when the range min is near 0 with longer labels.
743+
*/
744+
labelLocation?: AxisLabelLocation;
737745
// Whether to show the Protractor tool overlayed on top of the graph
738746
showProtractor: boolean;
739747
/**

packages/perseus-editor/src/widgets/interactive-graph-editor/components/interactive-graph-settings.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import Heading from "../../../components/heading";
2020
import LabeledRow from "../locked-figures/labeled-row";
2121

2222
import type {
23+
AxisLabelLocation,
2324
MarkingsType,
2425
PerseusImageBackground,
2526
} from "@khanacademy/perseus-core";
@@ -49,6 +50,13 @@ type Props = {
4950
* The labels for the x and y axes.
5051
*/
5152
labels: ReadonlyArray<string>;
53+
/**
54+
* Specifies the location of the labels on the graph. default: "onAxis".
55+
* - "onAxis": Labels are positioned on the axis at the right (x) and top (y) of the graph.
56+
* - "alongEdge": Labels are centered along the bottom (x) and left (y) edges of the graph.
57+
* The y label is rotated. Typically used when the range min is near 0 with longer labels.
58+
*/
59+
labelLocation: AxisLabelLocation;
5260
/**
5361
* The range of the graph.
5462
*/
@@ -99,6 +107,7 @@ type Props = {
99107
type State = {
100108
isExpanded: boolean;
101109
labelsTextbox: ReadonlyArray<string>;
110+
labelLocation: AxisLabelLocation;
102111
gridStepTextbox: [x: number, y: number];
103112
snapStepTextbox: [x: number, y: number];
104113
stepTextbox: [x: number, y: number];
@@ -116,6 +125,7 @@ class InteractiveGraphSettings extends React.Component<Props, State> {
116125
static stateFromProps(props: Props) {
117126
return {
118127
labelsTextbox: props.labels,
128+
labelLocation: props.labelLocation,
119129
gridStepTextbox: props.gridStep,
120130
snapStepTextbox: props.snapStep,
121131
stepTextbox: props.step,
@@ -140,6 +150,7 @@ class InteractiveGraphSettings extends React.Component<Props, State> {
140150
interactiveSizes.defaultBoxSizeSmall,
141151
],
142152
labels: ["$x$", "$y$"],
153+
labelLocation: "onAxis",
143154
range: [
144155
[-10, 10],
145156
[-10, 10],
@@ -165,6 +176,7 @@ class InteractiveGraphSettings extends React.Component<Props, State> {
165176
UNSAFE_componentWillReceiveProps(nextProps) {
166177
if (
167178
!_.isEqual(this.props.labels, nextProps.labels) ||
179+
!_.isEqual(this.props.labelLocation, nextProps.labelLocation) ||
168180
!_.isEqual(this.props.gridStep, nextProps.gridStep) ||
169181
!_.isEqual(this.props.snapStep, nextProps.snapStep) ||
170182
!_.isEqual(this.props.step, nextProps.step) ||
@@ -399,6 +411,7 @@ class InteractiveGraphSettings extends React.Component<Props, State> {
399411

400412
changeGraph = () => {
401413
const labels = this.state.labelsTextbox;
414+
const labelLocation = this.state.labelLocation;
402415
const range = _.map(this.state.rangeTextbox, function (range) {
403416
return _.map(range, Number);
404417
});
@@ -425,6 +438,7 @@ class InteractiveGraphSettings extends React.Component<Props, State> {
425438
this.change({
426439
valid: true,
427440
labels: labels,
441+
labelLocation: labelLocation,
428442
range: range,
429443
step: step,
430444
gridStep: gridStep,
@@ -452,6 +466,26 @@ class InteractiveGraphSettings extends React.Component<Props, State> {
452466
{this.state.isExpanded && (
453467
<View>
454468
<div className="graph-settings">
469+
<div className="perseus-widget-row">
470+
<LabeledRow label="Label Location">
471+
<ButtonGroup
472+
value={this.props.labelLocation}
473+
allowEmpty={false}
474+
buttons={[
475+
{
476+
value: "onAxis",
477+
content: "On Axis",
478+
},
479+
{
480+
value: "alongEdge",
481+
content: "Along Graph Edge",
482+
},
483+
]}
484+
onChange={this.change("labelLocation")}
485+
/>
486+
</LabeledRow>
487+
</div>
488+
455489
<div className="perseus-widget-row">
456490
<div className="perseus-widget-left-col">
457491
<LabeledRow label="x Label">

packages/perseus-editor/src/widgets/interactive-graph-editor/interactive-graph-editor.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
type PerseusGraphType,
1515
type MarkingsType,
1616
type InteractiveGraphDefaultWidgetOptions,
17+
type AxisLabelLocation,
1718
interactiveGraphLogic,
1819
} from "@khanacademy/perseus-core";
1920
import {Id, View} from "@khanacademy/wonder-blocks-core";
@@ -70,6 +71,13 @@ export type Props = {
7071
* The labels for the x and y axes.
7172
*/
7273
labels: ReadonlyArray<string>;
74+
/**
75+
* Specifies the location of the labels on the graph. default: "onAxis".
76+
* - "onAxis": Labels are positioned on the axis at the right (x) and top (y) of the graph.
77+
* - "alongEdge": Labels are centered along the bottom (x) and left (y) edges of the graph.
78+
* The y label is rotated. Typically used when the range min is near 0 with longer labels.
79+
*/
80+
labelLocation?: AxisLabelLocation;
7381
/**
7482
* The range of the graph in the x and y directions.
7583
*/
@@ -194,6 +202,7 @@ class InteractiveGraphEditor extends React.Component<Props> {
194202
"backgroundImage",
195203
"markings",
196204
"labels",
205+
"labelLocation",
197206
"showProtractor",
198207
"showTooltips",
199208
"range",
@@ -291,6 +300,7 @@ class InteractiveGraphEditor extends React.Component<Props> {
291300
box: this.props.box,
292301
range: this.props.range,
293302
labels: this.props.labels,
303+
labelLocation: this.props.labelLocation,
294304
step: this.props.step,
295305
gridStep: gridStep,
296306
snapStep: snapStep,
@@ -741,6 +751,7 @@ class InteractiveGraphEditor extends React.Component<Props> {
741751
box={getInteractiveBoxFromSizeClass(sizeClass)}
742752
range={this.props.range}
743753
labels={this.props.labels}
754+
labelLocation={this.props.labelLocation}
744755
step={this.props.step}
745756
gridStep={gridStep}
746757
snapStep={snapStep}

packages/perseus/src/widgets/interactive-graphs/__snapshots__/interactive-graph.test.tsx.snap

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ exports[`Interactive Graph A none-type graph renders predictably: first render 1
3737
</span>
3838
<span
3939
aria-label="Y-axis"
40-
style="position: absolute; left: 200px; top: -24px; font-size: 14px; transform: translate(-50%, 0px);"
40+
style="position: absolute; left: 200px; top: -28px; font-size: 14px; transform: translate(-50%, 0px);"
4141
>
4242
<span
4343
class="mock-TeX"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {getLabelPosition, fontSize} from "./axis-labels";
2+
3+
import type {GraphDimensions} from "../types";
4+
5+
describe("getLabelPosition", () => {
6+
it("should return the correct position for the default graph", () => {
7+
const graphInfo: GraphDimensions = {
8+
range: [
9+
[-10, 10],
10+
[-10, 10],
11+
],
12+
width: 400,
13+
height: 400,
14+
};
15+
const labelLocation = "onAxis";
16+
const expected = [
17+
[400, 200], // X Label at [Right edge of the graph, vertical center of the graph]
18+
[200, -2 * fontSize], // Y Label at [Horizontal center of the graph, 2x fontSize above the top edge]
19+
];
20+
21+
expect(getLabelPosition(graphInfo, labelLocation)).toEqual(expected);
22+
});
23+
24+
it("should return the correct position for labels set to alongEdge", () => {
25+
const graphInfo: GraphDimensions = {
26+
range: [
27+
[-10, 10],
28+
[-10, 10],
29+
],
30+
width: 400,
31+
height: 400,
32+
};
33+
const labelLocation = "alongEdge";
34+
const expected = [
35+
[200, 400 + fontSize], // X Label at [Horizontal center of the graph, 1x fontSize below the bottom edge]
36+
[-fontSize, 200 - fontSize], // Y label at [1x fontSize to the left of the left edge, vertical center of the graph]
37+
];
38+
39+
expect(getLabelPosition(graphInfo, labelLocation)).toEqual(expected);
40+
});
41+
42+
it("should return the correct position for labels set to alongEdge with wholly negative ranges", () => {
43+
const graphInfo: GraphDimensions = {
44+
range: [
45+
[-10, -5],
46+
[-10, -5],
47+
],
48+
width: 400,
49+
height: 400,
50+
};
51+
const labelLocation = "alongEdge";
52+
const expected = [
53+
[200, 400 + fontSize], // X Label at [Horizontal center of the graph, 1x fontSize below the bottom edge]
54+
[-fontSize, 200 - fontSize], // Y label at [1x fontSize to the left of the left edge, vertical center of the graph]
55+
];
56+
57+
expect(getLabelPosition(graphInfo, labelLocation)).toEqual(expected);
58+
});
59+
60+
it("should return the correct position for labels set to alongEdge with wholly positive ranges", () => {
61+
const graphInfo: GraphDimensions = {
62+
range: [
63+
[5, 10],
64+
[5, 10],
65+
],
66+
width: 400,
67+
height: 400,
68+
};
69+
const labelLocation = "alongEdge";
70+
const expected = [
71+
[200, 400 + 3 * fontSize], // X Label at [Horizontal center of the graph, 3x fontSize below the bottom edge]
72+
[-3 * fontSize, 200 - fontSize], // Y label at [3x fontSize to the left of the left edge, vertical center of the graph]
73+
];
74+
75+
expect(getLabelPosition(graphInfo, labelLocation)).toEqual(expected);
76+
});
77+
78+
it("should return the correct position for labels set to alongEdge with min ranges at 0", () => {
79+
// Should result in same position as the wholly positive range test
80+
const graphInfo: GraphDimensions = {
81+
range: [
82+
[0, 10],
83+
[0, 10],
84+
],
85+
width: 400,
86+
height: 400,
87+
};
88+
const labelLocation = "alongEdge";
89+
const expected = [
90+
[200, 400 + 3 * fontSize], // X Label at [Horizontal center of the graph, 3x fontSize below the bottom edge]
91+
[-3 * fontSize, 200 - fontSize], // Y label at [3x fontSize to the left of the left edge, vertical center of the graph]
92+
];
93+
94+
expect(getLabelPosition(graphInfo, labelLocation)).toEqual(expected);
95+
});
96+
});

0 commit comments

Comments
 (0)