@khanacademy/perseus 6.2.1 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/all-widgets.d.ts +2 -2
  3. package/dist/all-widgets.js.flow +2 -2
  4. package/dist/basic-widgets.d.ts +2 -2
  5. package/dist/basic-widgets.js.flow +2 -2
  6. package/dist/components/math-input.d.ts +4 -1
  7. package/dist/components/math-input.js.flow +4 -1
  8. package/dist/components/tex-buttons.d.ts +5 -1
  9. package/dist/components/tex-buttons.js.flow +1 -1
  10. package/dist/es/index.js +172 -205
  11. package/dist/es/index.js.map +1 -1
  12. package/dist/extra-widgets.d.ts +2 -2
  13. package/dist/extra-widgets.js.flow +2 -2
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +170 -201
  16. package/dist/index.js.flow +2 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/strings.js +23 -0
  19. package/dist/types.d.ts +2 -2
  20. package/dist/types.js.flow +2 -2
  21. package/dist/util/geometry.d.ts +25 -0
  22. package/dist/util/geometry.js.flow +37 -0
  23. package/dist/widgets/categorizer.d.ts +2 -2
  24. package/dist/widgets/categorizer.js.flow +2 -2
  25. package/dist/widgets/cs-program.d.ts +2 -2
  26. package/dist/widgets/cs-program.js.flow +2 -2
  27. package/dist/widgets/definition.d.ts +2 -2
  28. package/dist/widgets/definition.js.flow +2 -2
  29. package/dist/widgets/dropdown.d.ts +2 -2
  30. package/dist/widgets/dropdown.js.flow +2 -2
  31. package/dist/widgets/explanation.d.ts +2 -2
  32. package/dist/widgets/explanation.js.flow +2 -2
  33. package/dist/widgets/expression.d.ts +2 -2
  34. package/dist/widgets/expression.js.flow +2 -2
  35. package/dist/widgets/graded-group-set.d.ts +2 -2
  36. package/dist/widgets/graded-group-set.js.flow +2 -2
  37. package/dist/widgets/graded-group.d.ts +2 -2
  38. package/dist/widgets/graded-group.js.flow +2 -2
  39. package/dist/widgets/grapher.d.ts +2 -2
  40. package/dist/widgets/grapher.js.flow +2 -2
  41. package/dist/widgets/group.d.ts +2 -2
  42. package/dist/widgets/group.js.flow +2 -2
  43. package/dist/widgets/iframe.d.ts +2 -2
  44. package/dist/widgets/iframe.js.flow +2 -2
  45. package/dist/widgets/image.d.ts +2 -2
  46. package/dist/widgets/image.js.flow +2 -2
  47. package/dist/widgets/input-number.d.ts +2 -2
  48. package/dist/widgets/input-number.js.flow +2 -2
  49. package/dist/widgets/interaction.d.ts +2 -2
  50. package/dist/widgets/interaction.js.flow +2 -2
  51. package/dist/widgets/interactive-graph.d.ts +4 -12
  52. package/dist/widgets/interactive-graph.js.flow +4 -7
  53. package/dist/widgets/label-image.d.ts +2 -2
  54. package/dist/widgets/label-image.js.flow +2 -2
  55. package/dist/widgets/lights-puzzle.d.ts +2 -2
  56. package/dist/widgets/lights-puzzle.js.flow +2 -2
  57. package/dist/widgets/matcher.d.ts +2 -2
  58. package/dist/widgets/matcher.js.flow +2 -2
  59. package/dist/widgets/matrix.d.ts +2 -2
  60. package/dist/widgets/matrix.js.flow +2 -2
  61. package/dist/widgets/measurer.d.ts +2 -2
  62. package/dist/widgets/measurer.js.flow +2 -2
  63. package/dist/widgets/molecule.d.ts +2 -2
  64. package/dist/widgets/molecule.js.flow +2 -2
  65. package/dist/widgets/number-line.d.ts +2 -2
  66. package/dist/widgets/number-line.js.flow +2 -2
  67. package/dist/widgets/numeric-input.d.ts +2 -2
  68. package/dist/widgets/numeric-input.js.flow +2 -2
  69. package/dist/widgets/orderer.d.ts +2 -2
  70. package/dist/widgets/orderer.js.flow +2 -2
  71. package/dist/widgets/passage-ref-target.d.ts +2 -2
  72. package/dist/widgets/passage-ref-target.js.flow +2 -2
  73. package/dist/widgets/passage-ref.d.ts +2 -2
  74. package/dist/widgets/passage-ref.js.flow +2 -2
  75. package/dist/widgets/passage.d.ts +2 -2
  76. package/dist/widgets/passage.js.flow +2 -2
  77. package/dist/widgets/plotter.d.ts +2 -2
  78. package/dist/widgets/plotter.js.flow +2 -2
  79. package/dist/widgets/radio.d.ts +2 -2
  80. package/dist/widgets/radio.js.flow +2 -2
  81. package/dist/widgets/reaction-diagram.d.ts +2 -2
  82. package/dist/widgets/reaction-diagram.js.flow +2 -2
  83. package/dist/widgets/sequence.d.ts +2 -2
  84. package/dist/widgets/sequence.js.flow +2 -2
  85. package/dist/widgets/simulator.d.ts +2 -2
  86. package/dist/widgets/simulator.js.flow +2 -2
  87. package/dist/widgets/sorter.d.ts +2 -2
  88. package/dist/widgets/sorter.js.flow +2 -2
  89. package/dist/widgets/table.d.ts +2 -2
  90. package/dist/widgets/table.js.flow +2 -2
  91. package/dist/widgets/transformer.d.ts +2 -2
  92. package/dist/widgets/transformer.js.flow +2 -2
  93. package/dist/widgets/unit.d.ts +2 -2
  94. package/dist/widgets/unit.js.flow +2 -2
  95. package/dist/widgets/video.d.ts +2 -2
  96. package/dist/widgets/video.js.flow +2 -2
  97. package/package.json +4 -5
  98. package/src/components/__tests__/math-input.test.tsx +60 -0
  99. package/src/components/math-input.tsx +88 -118
  100. package/src/components/prop-check-box.tsx +0 -1
  101. package/src/components/tex-buttons.tsx +73 -12
  102. package/src/index.ts +2 -0
  103. package/src/types.ts +19 -2
  104. package/src/util/geometry.ts +293 -0
  105. package/src/widgets/__tests__/numeric-input.test.ts +38 -0
  106. package/src/widgets/expression.tsx +0 -1
  107. package/src/widgets/grapher.tsx +0 -1
  108. package/src/widgets/image.tsx +0 -1
  109. package/src/widgets/input-number.tsx +0 -1
  110. package/src/widgets/interactive-graph.tsx +20 -288
  111. package/src/widgets/matrix.tsx +0 -1
  112. package/src/widgets/numeric-input.tsx +6 -2
  113. package/src/widgets/transformer.tsx +0 -1
  114. package/tsconfig-build.json +0 -1
  115. package/tsconfig-build.tsbuildinfo +1 -1
  116. package/dist/__tests__/mock-asset-loading-widget.d.ts +0 -59
  117. package/dist/__tests__/mock-asset-loading-widget.js.flow +0 -70
  118. package/dist/__tests__/mock-widget.d.ts +0 -30
  119. package/dist/__tests__/mock-widget.js.flow +0 -39
  120. package/dist/__tests__/test-items/image-item.d.ts +0 -14
  121. package/dist/__tests__/test-items/image-item.js.flow +0 -20
  122. package/dist/__tests__/test-items/input-number-1-item.d.ts +0 -9
  123. package/dist/__tests__/test-items/input-number-1-item.js.flow +0 -15
  124. package/dist/__tests__/test-items/input-number-2-item.d.ts +0 -9
  125. package/dist/__tests__/test-items/input-number-2-item.js.flow +0 -15
  126. package/dist/__tests__/test-items/table-item.d.ts +0 -9
  127. package/dist/__tests__/test-items/table-item.js.flow +0 -15
  128. package/dist/interactive2/__tests__/movable-mock.d.ts +0 -5
  129. package/dist/interactive2/__tests__/movable-mock.js.flow +0 -11
  130. package/dist/widgets/__stories__/test-keypad-context-wrapper.d.ts +0 -6
  131. package/dist/widgets/__stories__/test-keypad-context-wrapper.js.flow +0 -12
  132. package/dist/widgets/__tests__/renderQuestion.d.ts +0 -13
  133. package/dist/widgets/__tests__/renderQuestion.js.flow +0 -28
@@ -0,0 +1,293 @@
1
+ /**
2
+ * A collection of geomtry-related utility functions
3
+ */
4
+
5
+ import {number as knumber} from "@khanacademy/kmath";
6
+ import _ from "underscore";
7
+
8
+ import Util from "../util";
9
+
10
+ import type {Coord, Line} from "../interactive2/types";
11
+
12
+ const {eq, deepEq} = Util;
13
+
14
+ // This should really be a readonly tuple of [number, number]
15
+ export type Range = [number, number];
16
+
17
+ export type SineCoefficient = [
18
+ number, // amplitude
19
+ number, // angularFrequency
20
+ number, // phase
21
+ number, // verticalOffset
22
+ ];
23
+ export type QuadraticCoefficient = [number, number, number]; // a, b, c
24
+
25
+ export function sign(val: number): 0 | 1 | -1 {
26
+ if (eq(val, 0)) {
27
+ return 0;
28
+ }
29
+ return val > 0 ? 1 : -1;
30
+ }
31
+
32
+ export function ccw(a: Coord, b: Coord, c: Coord): number {
33
+ return (b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1]);
34
+ }
35
+
36
+ export function collinear(a: Coord, b: Coord, c: Coord): boolean {
37
+ return eq(ccw(a, b, c), 0);
38
+ }
39
+
40
+ // Given rect bounding points A and B, whether point C is inside the rect
41
+ function pointInRect(a: Coord, b: Coord, c: Coord): boolean {
42
+ return (
43
+ c[0] <= Math.max(a[0], b[0]) &&
44
+ c[0] >= Math.min(a[0], b[0]) &&
45
+ c[1] <= Math.max(a[1], b[1]) &&
46
+ c[1] >= Math.min(a[1], b[1])
47
+ );
48
+ }
49
+
50
+ // Whether line segment AB intersects line segment CD
51
+ // http://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/
52
+ export function intersects(ab: Line, cd: Line): boolean {
53
+ const triplets = [
54
+ [ab[0], ab[1], cd[0]] as const,
55
+ [ab[0], ab[1], cd[1]] as const,
56
+ [cd[0], cd[1], ab[0]] as const,
57
+ [cd[0], cd[1], ab[1]] as const,
58
+ ];
59
+
60
+ const orientations = _.map(triplets, function (triplet) {
61
+ return sign(ccw(...triplet));
62
+ });
63
+
64
+ if (
65
+ orientations[0] !== orientations[1] &&
66
+ orientations[2] !== orientations[3]
67
+ ) {
68
+ return true;
69
+ }
70
+
71
+ for (let i = 0; i < 4; i++) {
72
+ if (orientations[i] === 0 && pointInRect(...triplets[i])) {
73
+ return true;
74
+ }
75
+ }
76
+
77
+ return false;
78
+ }
79
+
80
+ export function vector(a, b) {
81
+ return _.map(_.zip(a, b), function (pair) {
82
+ return pair[0] - pair[1];
83
+ });
84
+ }
85
+
86
+ export function magnitude(v: ReadonlyArray<Coord>): number {
87
+ return Math.sqrt(
88
+ _.reduce(
89
+ v,
90
+ function (memo, el) {
91
+ // @ts-expect-error [FEI-5003] - TS2345 - Argument of type 'Coord' is not assignable to parameter of type 'number'.
92
+ return memo + Math.pow(el, 2);
93
+ },
94
+ 0,
95
+ ),
96
+ );
97
+ }
98
+
99
+ function dotProduct(a: Coord, b: Coord): number {
100
+ return _.reduce(
101
+ _.zip(a, b),
102
+ function (memo, pair) {
103
+ return memo + pair[0] * pair[1];
104
+ },
105
+ 0,
106
+ );
107
+ }
108
+
109
+ function sideLengths(coords: ReadonlyArray<Coord>): ReadonlyArray<number> {
110
+ const segments = _.zip(coords, rotate(coords));
111
+ return segments.map(function (segment) {
112
+ // @ts-expect-error [FEI-5003] - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
113
+ return magnitude(vector(...segment));
114
+ });
115
+ }
116
+
117
+ // Based on http://math.stackexchange.com/a/151149
118
+ export function angleMeasures(
119
+ coords: ReadonlyArray<Coord>,
120
+ ): ReadonlyArray<number> {
121
+ const triplets = _.zip(rotate(coords, -1), coords, rotate(coords, 1));
122
+
123
+ const offsets = _.map(triplets, function (triplet) {
124
+ const p = vector(triplet[1], triplet[0]);
125
+ const q = vector(triplet[2], triplet[1]);
126
+ // @ts-expect-error [FEI-5003] - TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'Coord'. | TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'. | TS2345 - Argument of type 'number[]' is not assignable to parameter of type 'readonly Coord[]'.
127
+ const raw = Math.acos(dotProduct(p, q) / (magnitude(p) * magnitude(q)));
128
+ // @ts-expect-error [FEI-5003] - TS2556 - A spread argument must either have a tuple type or be passed to a rest parameter.
129
+ return sign(ccw(...triplet)) > 0 ? raw : -raw;
130
+ });
131
+
132
+ const sum = _.reduce(
133
+ offsets,
134
+ function (memo, arg) {
135
+ return memo + arg;
136
+ },
137
+ 0,
138
+ );
139
+
140
+ return _.map(offsets, function (offset) {
141
+ return sum > 0 ? Math.PI - offset : Math.PI + offset;
142
+ });
143
+ }
144
+
145
+ // Whether two polygons are similar (or if specified, congruent)
146
+ export function similar(
147
+ coords1: ReadonlyArray<Coord>,
148
+ coords2: ReadonlyArray<Coord>,
149
+ tolerance: number,
150
+ ): boolean {
151
+ if (coords1.length !== coords2.length) {
152
+ return false;
153
+ }
154
+
155
+ const n = coords1.length;
156
+
157
+ const angles1 = angleMeasures(coords1);
158
+ const angles2 = angleMeasures(coords2);
159
+
160
+ const sides1 = sideLengths(coords1);
161
+ const sides2 = sideLengths(coords2);
162
+
163
+ for (let i = 0; i < 2 * n; i++) {
164
+ let angles = angles2.slice();
165
+ let sides = sides2.slice();
166
+
167
+ // Reverse angles and sides to allow matching reflected polygons
168
+ if (i >= n) {
169
+ angles.reverse();
170
+ sides.reverse();
171
+ // Since sides are calculated from two coordinates,
172
+ // simply reversing results in an off by one error
173
+ // @ts-expect-error [FEI-5003] - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
174
+ sides = rotate(sides, 1);
175
+ }
176
+
177
+ // @ts-expect-error [FEI-5003] - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
178
+ angles = rotate(angles, i);
179
+ // @ts-expect-error [FEI-5003] - TS4104 - The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
180
+ sides = rotate(sides, i);
181
+
182
+ if (deepEq(angles1, angles)) {
183
+ const sidePairs = _.zip(sides1, sides);
184
+
185
+ const factors = _.map(sidePairs, function (pair) {
186
+ return pair[0] / pair[1];
187
+ });
188
+
189
+ const same = _.all(factors, function (factor) {
190
+ return eq(factors[0], factor);
191
+ });
192
+
193
+ const congruentEnough = _.all(sidePairs, function (pair) {
194
+ return knumber.equal(pair[0], pair[1], tolerance);
195
+ });
196
+
197
+ if (same && congruentEnough) {
198
+ return true;
199
+ }
200
+ }
201
+ }
202
+
203
+ return false;
204
+ }
205
+
206
+ // Given triangle with sides ABC return angle opposite side C in degrees
207
+ export function lawOfCosines(a: number, b: number, c: number): number {
208
+ return (Math.acos((a * a + b * b - c * c) / (2 * a * b)) * 180) / Math.PI;
209
+ }
210
+
211
+ export function canonicalSineCoefficients([
212
+ amplitude,
213
+ angularFrequency,
214
+ phase,
215
+ verticalOffset,
216
+ ]: [any, any, any, any]) {
217
+ // For a curve of the form f(x) = a * Sin(b * x - c) + d,
218
+ // this function ensures that a, b > 0, and c is its
219
+ // smallest possible positive value.
220
+
221
+ // Guarantee a > 0
222
+ if (amplitude < 0) {
223
+ amplitude *= -1;
224
+ angularFrequency *= -1;
225
+ phase *= -1;
226
+ }
227
+
228
+ const period = 2 * Math.PI;
229
+ // Guarantee b > 0
230
+ if (angularFrequency < 0) {
231
+ angularFrequency *= -1;
232
+ phase *= -1;
233
+ phase += period / 2;
234
+ }
235
+
236
+ // Guarantee c is smallest possible positive value
237
+ while (phase > 0) {
238
+ phase -= period;
239
+ }
240
+ while (phase < 0) {
241
+ phase += period;
242
+ }
243
+
244
+ return [amplitude, angularFrequency, phase, verticalOffset];
245
+ }
246
+
247
+ // e.g. rotate([1, 2, 3]) -> [2, 3, 1]
248
+ export function rotate<T>(
249
+ array: ReadonlyArray<T>,
250
+ n?: number,
251
+ ): ReadonlyArray<T> {
252
+ n = typeof n === "undefined" ? 1 : n % array.length;
253
+ return array.slice(n).concat(array.slice(0, n));
254
+ }
255
+
256
+ export function getLineEquation(first: Coord, second: Coord): string {
257
+ if (eq(first[0], second[0])) {
258
+ return "x = " + first[0].toFixed(3);
259
+ }
260
+ const m = (second[1] - first[1]) / (second[0] - first[0]);
261
+ const b = first[1] - m * first[0];
262
+ return "y = " + m.toFixed(3) + "x + " + b.toFixed(3);
263
+ }
264
+
265
+ // Stolen from the wikipedia article
266
+ // http://en.wikipedia.org/wiki/Line-line_intersection
267
+ export function getLineIntersection(
268
+ // TODO(LP-10725): update these to be 2-tuples
269
+ firstPoints: ReadonlyArray<Coord>,
270
+ secondPoints: ReadonlyArray<Coord>,
271
+ ): string {
272
+ const x1 = firstPoints[0][0];
273
+ const y1 = firstPoints[0][1];
274
+ const x2 = firstPoints[1][0];
275
+ const y2 = firstPoints[1][1];
276
+ const x3 = secondPoints[0][0];
277
+ const y3 = secondPoints[0][1];
278
+ const x4 = secondPoints[1][0];
279
+ const y4 = secondPoints[1][1];
280
+
281
+ const determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
282
+
283
+ if (Math.abs(determinant) < 1e-9) {
284
+ return "Lines are parallel";
285
+ }
286
+ const x =
287
+ ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) /
288
+ determinant;
289
+ const y =
290
+ ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) /
291
+ determinant;
292
+ return "Intersection: (" + x.toFixed(3) + ", " + y.toFixed(3) + ")";
293
+ }
@@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
3
3
 
4
4
  import {testDependencies} from "../../../../../testing/test-dependencies";
5
5
  import * as Dependencies from "../../dependencies";
6
+ import {errors} from "../../util/answer-types";
6
7
  import {
7
8
  question1AndAnswer,
8
9
  multipleAnswers,
@@ -212,6 +213,43 @@ describe("static function validate", () => {
212
213
  `);
213
214
  });
214
215
 
216
+ // Don't default to validating the answer as a pi answer
217
+ // if answerForm isn't set on the answer.
218
+ // The answer value, userInput.currentValue, and
219
+ // the omission of answerForms in the answer are
220
+ // important to the test.
221
+ // https://khanacademy.atlassian.net/browse/LC-691
222
+ it("doesn't default to validating pi", () => {
223
+ const rubric: Rubric = {
224
+ answers: [
225
+ {
226
+ maxError: null,
227
+ message: "",
228
+ simplify: "required",
229
+ status: "correct",
230
+ strict: true,
231
+ value: 45.289,
232
+ },
233
+ ],
234
+ labelText: "",
235
+ size: "normal",
236
+ static: false,
237
+ coefficient: false,
238
+ };
239
+
240
+ const userInput = {
241
+ // (pi / 12) * 173 = 45.291
242
+ // within the 0.01 margin of error
243
+ // to trigger the pi validation flow
244
+ currentValue: "45.282",
245
+ } as const;
246
+
247
+ const score = NumericInput.validate(userInput, rubric);
248
+
249
+ expect(score.message).not.toBe(errors.APPROXIMATED_PI_ERROR);
250
+ expect(score.message?.includes("pi")).toBeFalsy();
251
+ });
252
+
215
253
  it("with a strict answer", () => {
216
254
  const rubric: Rubric = {
217
255
  answers: [
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/prop-types */
2
1
  import * as KAS from "@khanacademy/kas";
3
2
  import {KeypadInput, KeypadType, Keys as Key} from "@khanacademy/math-input";
4
3
  import {linterContextDefault} from "@khanacademy/perseus-linter";
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/prop-types */
2
1
  /* eslint-disable react/sort-comp */
3
2
  import {
4
3
  number as knumber,
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/prop-types */
2
1
  /* eslint-disable react/sort-comp */
3
2
  import {linterContextDefault} from "@khanacademy/perseus-linter";
4
3
  import {StyleSheet, css} from "aphrodite";
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/prop-types */
2
1
  /* eslint-disable react/sort-comp */
3
2
  import {linterContextDefault} from "@khanacademy/perseus-linter";
4
3
  import * as i18n from "@khanacademy/wonder-blocks-i18n";