From 9069a68c40b9daa701439cfde6d5eff8891fb864 Mon Sep 17 00:00:00 2001 From: Anakaren Rojas Date: Fri, 10 Jan 2025 14:57:42 -0800 Subject: [PATCH 01/30] add description for sinusoid --- packages/perseus/src/strings.ts | 23 ++++ .../interactive-graphs/graphs/sinusoid.tsx | 113 +++++++++++++++++- 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/packages/perseus/src/strings.ts b/packages/perseus/src/strings.ts index 93d0160457..fb8ea437be 100644 --- a/packages/perseus/src/strings.ts +++ b/packages/perseus/src/strings.ts @@ -261,6 +261,19 @@ export type PerseusStrings = { srSinusoidGraphAriaLabel: string; srSinusoidExtremumPoint: ({x, y}: {x: string; y: string}) => string; srSinusoidMidlineIntersection: ({x, y}: {x: string; y: string}) => string; + srSinusoidDescription: ({ + minValue, + maxValue, + cycleType, + xMinCoord, + xMaxCoord, + }: { + minValue: string; + maxValue: string; + cycleType: string; + xMinCoord: string; + xMaxCoord: string; + }) => string; // The above strings are used for interactive graph SR descriptions. }; @@ -484,6 +497,8 @@ export const strings: { srSinusoidGraphAriaLabel: "A sinusoid function on a coordinate plane.", srSinusoidExtremumPoint: "Extremum Point at %(x)s comma %(y)s.", srSinusoidMidlineIntersection: "Midline Intersection at %(x)s comma %(y)s.", + srSinusoidDescription: + "The graph shows a wave with a minimum value of %(minValue)s and a maximum value of %(maxValue)s. The wave completes a %(cycleType)s cycle from %(xMinCoord)s to %(xMaxCoord)s.", // The above strings are used for interactive graph SR descriptions. }; @@ -705,5 +720,13 @@ export const mockStrings: PerseusStrings = { srSinusoidExtremumPoint: ({x, y}) => `Extremum Point at ${x} comma ${y}.`, srSinusoidMidlineIntersection: ({x, y}) => `Midline Intersection at ${x} comma ${y}.`, + srSinusoidDescription: ({ + minValue, + maxValue, + cycleType, + xMinCoord, + xMaxCoord, + }) => + `The graph shows a wave with a minimum value of ${minValue} and a maximum value of ${maxValue}. The wave completes a ${cycleType} cycle from ${xMinCoord} to ${xMaxCoord}.`, // The above strings are used for interactive graph SR descriptions. }; diff --git a/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx b/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx index 8484ba7239..6e9e9e28a7 100644 --- a/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx +++ b/packages/perseus/src/widgets/interactive-graphs/graphs/sinusoid.tsx @@ -62,6 +62,7 @@ function SinusoidGraph(props: SinusoidGraphProps) { } const {strings, locale} = usePerseusI18n(); + const uniqueId = React.useId(); function getMoveablePointAriaLabel( index: number, @@ -77,8 +78,38 @@ function SinusoidGraph(props: SinusoidGraphProps) { : strings.srSinusoidMidlineIntersection(coordsObj); } + function getWholeGraphDescription(): string { + const minMaxVals = calculateMinAndMaxValues( + coeffRef.current.amplitude, + coeffRef.current.verticalOffset, + ); + const minMaxCoords = calculateStartAndEndPoints( + coeffRef.current.amplitude, + coeffRef.current.phase, + ); + const minCoordStringWithPi = formatAsMultipleOfPi( + minMaxCoords[0], + locale, + ); + const maxCoordStringWithPi = formatAsMultipleOfPi( + minMaxCoords[1], + locale, + ); + const descriptionObj = { + minValue: srFormatNumber(minMaxVals[0], locale), + maxValue: srFormatNumber(minMaxVals[1], locale), + cycleType: "full", + xMinCoord: minCoordStringWithPi, + xMaxCoord: maxCoordStringWithPi, + }; + return strings.srSinusoidDescription(descriptionObj); + } + return ( - + computeSine(x, coeffRef.current)} color={color.blue} @@ -94,6 +125,9 @@ function SinusoidGraph(props: SinusoidGraphProps) { } /> ))} + + {getWholeGraphDescription()} + ); } @@ -134,3 +168,80 @@ export const getSinusoidCoefficients = ( return {amplitude, angularFrequency, phase, verticalOffset}; }; + +/** + * Sine and cosine oscillate between [-1, 1], which is scaled by the graph's amplitude [-A, A] and shifted by the vertical offset [-A+D, A+D] + * @param amplitude Distance from the center to either extreme + * @param verticalOffset aka vertical shift - moves the range up or down + * @returns array of min and max values + */ +export function calculateMinAndMaxValues( + amplitude: number, + verticalOffset: number, +) { + const absAmp = Math.abs(amplitude); + return [-absAmp + verticalOffset, absAmp + verticalOffset]; +} + +/** + * @param angularFrequency Determines how stretched or compressed the graph is + * @returns Period of a sinusoid graph as a number + */ +export function calculatePeriod(angularFrequency: number) { + return (2 * Math.PI) / Math.abs(angularFrequency); +} + +/** + * Formats integer or fractional multiples of PI. + * @param input - number + * @param locale - i18n locale + * @returns integer or fractional multiples of PI. if input is not a multiple of PI, returns input formatted as a sr string + */ + +export function formatAsMultipleOfPi(input: number, locale: string): string { + const multiple = input / Math.PI; + const faultTolerance = 1e-15; // Math.PI goes to 15 decimal places + + if (input === 0 || multiple === 0) { + return `0`; + } + + // check for integer multiple of PI + if (Math.abs(multiple - Math.round(multiple)) < faultTolerance) { + const roundedMultiple = Math.round(multiple); + if (roundedMultiple === 1) { + return `pi`; + } else if (roundedMultiple === -1) { + return `negative pi`; + } + + return `${Math.round(multiple)} pi`; + } + + // Check for fractional multiple of PI + const maxDenominator = 1000; + + for (let denominator = 1; denominator < maxDenominator; denominator++) { + const numerator = Math.round(multiple * denominator); + if (Math.abs(multiple - numerator / denominator) < faultTolerance) { + return `${numerator}/${denominator} pi`; + } + } + + return `${srFormatNumber(input, locale)}`; +} + +/** + * Calculates the start and end points for a full cycle of a sinusoid wave + * @param angularFrequency Determines how stretched or compressed the graph is + * @param phase Determines how to wave is shifted horizontally + * @returns array of start and end points for the full cycle + */ +export function calculateStartAndEndPoints( + angularFrequency: number, + phase: number, +) { + const phaseShift = -phase / angularFrequency; + const period = calculatePeriod(angularFrequency); + return [phaseShift, phaseShift + period]; +} From ef36b079abb2fc4c217074c2500fe710d339b01c Mon Sep 17 00:00:00 2001 From: Anakaren Rojas Date: Mon, 13 Jan 2025 14:19:17 -0800 Subject: [PATCH 02/30] docs(changeset): adds aria description for sinusoid graph --- .changeset/mighty-taxis-breathe.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mighty-taxis-breathe.md diff --git a/.changeset/mighty-taxis-breathe.md b/.changeset/mighty-taxis-breathe.md new file mode 100644 index 0000000000..6f623a5754 --- /dev/null +++ b/.changeset/mighty-taxis-breathe.md @@ -0,0 +1,5 @@ +--- +"@khanacademy/perseus": minor +--- + +adds aria description for sinusoid graph From 637652c5a53a069ae762e12f07c111d7a9f569af Mon Sep 17 00:00:00 2001 From: Anakaren Rojas Date: Mon, 13 Jan 2025 14:37:27 -0800 Subject: [PATCH 03/30] remove negative and update strings --- packages/perseus/src/strings.ts | 7 ++----- .../__snapshots__/explanation.test.ts.snap | 8 ++++---- .../__snapshots__/graded-group-set-jipt.test.ts.snap | 12 ++++++------ .../__snapshots__/graded-group-set.test.ts.snap | 4 ++-- .../__snapshots__/graded-group.test.ts.snap | 4 ++-- .../widgets/interactive-graphs/graphs/sinusoid.tsx | 7 ++----- 6 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/perseus/src/strings.ts b/packages/perseus/src/strings.ts index fb8ea437be..3daa5e1947 100644 --- a/packages/perseus/src/strings.ts +++ b/packages/perseus/src/strings.ts @@ -264,13 +264,11 @@ export type PerseusStrings = { srSinusoidDescription: ({ minValue, maxValue, - cycleType, xMinCoord, xMaxCoord, }: { minValue: string; maxValue: string; - cycleType: string; xMinCoord: string; xMaxCoord: string; }) => string; @@ -498,7 +496,7 @@ export const strings: { srSinusoidExtremumPoint: "Extremum Point at %(x)s comma %(y)s.", srSinusoidMidlineIntersection: "Midline Intersection at %(x)s comma %(y)s.", srSinusoidDescription: - "The graph shows a wave with a minimum value of %(minValue)s and a maximum value of %(maxValue)s. The wave completes a %(cycleType)s cycle from %(xMinCoord)s to %(xMaxCoord)s.", + "The graph shows a wave with a minimum value of %(minValue)s and a maximum value of %(maxValue)s. The wave completes a full cycle from %(xMinCoord)s to %(xMaxCoord)s.", // The above strings are used for interactive graph SR descriptions. }; @@ -723,10 +721,9 @@ export const mockStrings: PerseusStrings = { srSinusoidDescription: ({ minValue, maxValue, - cycleType, xMinCoord, xMaxCoord, }) => - `The graph shows a wave with a minimum value of ${minValue} and a maximum value of ${maxValue}. The wave completes a ${cycleType} cycle from ${xMinCoord} to ${xMaxCoord}.`, + `The graph shows a wave with a minimum value of ${minValue} and a maximum value of ${maxValue}. The wave completes a full cycle from ${xMinCoord} to ${xMaxCoord}.`, // The above strings are used for interactive graph SR descriptions. }; diff --git a/packages/perseus/src/widgets/explanation/__snapshots__/explanation.test.ts.snap b/packages/perseus/src/widgets/explanation/__snapshots__/explanation.test.ts.snap index 3383f7cfa0..e96e0672b6 100644 --- a/packages/perseus/src/widgets/explanation/__snapshots__/explanation.test.ts.snap +++ b/packages/perseus/src/widgets/explanation/__snapshots__/explanation.test.ts.snap @@ -21,12 +21,12 @@ exports[`Explanation should snapshot when expanded: expanded 1`] = ` aria-controls=":r1:" aria-disabled="false" aria-expanded="true" - class="button_vr44p2-o_O-shared_lwskrm-o_O-default_1hl5pu8-o_O-small_14crccx-o_O-inlineStyles_1s8anjv" + class="button_vr44p2-o_O-shared_lwskrm-o_O-default_qjb97o-o_O-small_14crccx-o_O-inlineStyles_1s8anjv" role="button" type="button" > Hide explanation! @@ -94,12 +94,12 @@ exports[`Explanation should snapshot: initial render 1`] = ` aria-controls=":r0:" aria-disabled="false" aria-expanded="false" - class="button_vr44p2-o_O-shared_lwskrm-o_O-default_1hl5pu8-o_O-small_14crccx-o_O-inlineStyles_1s8anjv" + class="button_vr44p2-o_O-shared_lwskrm-o_O-default_qjb97o-o_O-small_14crccx-o_O-inlineStyles_1s8anjv" role="button" type="button" > Explanation diff --git a/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set-jipt.test.ts.snap b/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set-jipt.test.ts.snap index 6d4b738751..fcf253b7dd 100644 --- a/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set-jipt.test.ts.snap +++ b/packages/perseus/src/widgets/graded-group-set/__snapshots__/graded-group-set-jipt.test.ts.snap @@ -251,12 +251,12 @@ exports[`graded-group-set should render all graded groups 1`] = ` />