@khanacademy/perseus 6.2.1 → 6.2.2

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 (125) hide show
  1. package/CHANGELOG.md +9 -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/es/index.js +67 -66
  7. package/dist/es/index.js.map +1 -1
  8. package/dist/extra-widgets.d.ts +2 -2
  9. package/dist/extra-widgets.js.flow +2 -2
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.js +65 -62
  12. package/dist/index.js.flow +2 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/types.d.ts +2 -2
  15. package/dist/types.js.flow +2 -2
  16. package/dist/util/geometry.d.ts +25 -0
  17. package/dist/util/geometry.js.flow +37 -0
  18. package/dist/widgets/categorizer.d.ts +2 -2
  19. package/dist/widgets/categorizer.js.flow +2 -2
  20. package/dist/widgets/cs-program.d.ts +2 -2
  21. package/dist/widgets/cs-program.js.flow +2 -2
  22. package/dist/widgets/definition.d.ts +2 -2
  23. package/dist/widgets/definition.js.flow +2 -2
  24. package/dist/widgets/dropdown.d.ts +2 -2
  25. package/dist/widgets/dropdown.js.flow +2 -2
  26. package/dist/widgets/explanation.d.ts +2 -2
  27. package/dist/widgets/explanation.js.flow +2 -2
  28. package/dist/widgets/expression.d.ts +2 -2
  29. package/dist/widgets/expression.js.flow +2 -2
  30. package/dist/widgets/graded-group-set.d.ts +2 -2
  31. package/dist/widgets/graded-group-set.js.flow +2 -2
  32. package/dist/widgets/graded-group.d.ts +2 -2
  33. package/dist/widgets/graded-group.js.flow +2 -2
  34. package/dist/widgets/grapher.d.ts +2 -2
  35. package/dist/widgets/grapher.js.flow +2 -2
  36. package/dist/widgets/group.d.ts +2 -2
  37. package/dist/widgets/group.js.flow +2 -2
  38. package/dist/widgets/iframe.d.ts +2 -2
  39. package/dist/widgets/iframe.js.flow +2 -2
  40. package/dist/widgets/image.d.ts +2 -2
  41. package/dist/widgets/image.js.flow +2 -2
  42. package/dist/widgets/input-number.d.ts +2 -2
  43. package/dist/widgets/input-number.js.flow +2 -2
  44. package/dist/widgets/interaction.d.ts +2 -2
  45. package/dist/widgets/interaction.js.flow +2 -2
  46. package/dist/widgets/interactive-graph.d.ts +4 -12
  47. package/dist/widgets/interactive-graph.js.flow +4 -7
  48. package/dist/widgets/label-image.d.ts +2 -2
  49. package/dist/widgets/label-image.js.flow +2 -2
  50. package/dist/widgets/lights-puzzle.d.ts +2 -2
  51. package/dist/widgets/lights-puzzle.js.flow +2 -2
  52. package/dist/widgets/matcher.d.ts +2 -2
  53. package/dist/widgets/matcher.js.flow +2 -2
  54. package/dist/widgets/matrix.d.ts +2 -2
  55. package/dist/widgets/matrix.js.flow +2 -2
  56. package/dist/widgets/measurer.d.ts +2 -2
  57. package/dist/widgets/measurer.js.flow +2 -2
  58. package/dist/widgets/molecule.d.ts +2 -2
  59. package/dist/widgets/molecule.js.flow +2 -2
  60. package/dist/widgets/number-line.d.ts +2 -2
  61. package/dist/widgets/number-line.js.flow +2 -2
  62. package/dist/widgets/numeric-input.d.ts +2 -2
  63. package/dist/widgets/numeric-input.js.flow +2 -2
  64. package/dist/widgets/orderer.d.ts +2 -2
  65. package/dist/widgets/orderer.js.flow +2 -2
  66. package/dist/widgets/passage-ref-target.d.ts +2 -2
  67. package/dist/widgets/passage-ref-target.js.flow +2 -2
  68. package/dist/widgets/passage-ref.d.ts +2 -2
  69. package/dist/widgets/passage-ref.js.flow +2 -2
  70. package/dist/widgets/passage.d.ts +2 -2
  71. package/dist/widgets/passage.js.flow +2 -2
  72. package/dist/widgets/plotter.d.ts +2 -2
  73. package/dist/widgets/plotter.js.flow +2 -2
  74. package/dist/widgets/radio.d.ts +2 -2
  75. package/dist/widgets/radio.js.flow +2 -2
  76. package/dist/widgets/reaction-diagram.d.ts +2 -2
  77. package/dist/widgets/reaction-diagram.js.flow +2 -2
  78. package/dist/widgets/sequence.d.ts +2 -2
  79. package/dist/widgets/sequence.js.flow +2 -2
  80. package/dist/widgets/simulator.d.ts +2 -2
  81. package/dist/widgets/simulator.js.flow +2 -2
  82. package/dist/widgets/sorter.d.ts +2 -2
  83. package/dist/widgets/sorter.js.flow +2 -2
  84. package/dist/widgets/table.d.ts +2 -2
  85. package/dist/widgets/table.js.flow +2 -2
  86. package/dist/widgets/transformer.d.ts +2 -2
  87. package/dist/widgets/transformer.js.flow +2 -2
  88. package/dist/widgets/unit.d.ts +2 -2
  89. package/dist/widgets/unit.js.flow +2 -2
  90. package/dist/widgets/video.d.ts +2 -2
  91. package/dist/widgets/video.js.flow +2 -2
  92. package/package.json +2 -2
  93. package/src/components/prop-check-box.tsx +0 -1
  94. package/src/index.ts +2 -0
  95. package/src/types.ts +19 -2
  96. package/src/util/geometry.ts +293 -0
  97. package/src/widgets/__tests__/numeric-input.test.ts +38 -0
  98. package/src/widgets/expression.tsx +0 -1
  99. package/src/widgets/grapher.tsx +0 -1
  100. package/src/widgets/image.tsx +0 -1
  101. package/src/widgets/input-number.tsx +0 -1
  102. package/src/widgets/interactive-graph.tsx +20 -288
  103. package/src/widgets/matrix.tsx +0 -1
  104. package/src/widgets/numeric-input.tsx +6 -2
  105. package/src/widgets/transformer.tsx +0 -1
  106. package/tsconfig-build.json +0 -1
  107. package/tsconfig-build.tsbuildinfo +1 -1
  108. package/dist/__tests__/mock-asset-loading-widget.d.ts +0 -59
  109. package/dist/__tests__/mock-asset-loading-widget.js.flow +0 -70
  110. package/dist/__tests__/mock-widget.d.ts +0 -30
  111. package/dist/__tests__/mock-widget.js.flow +0 -39
  112. package/dist/__tests__/test-items/image-item.d.ts +0 -14
  113. package/dist/__tests__/test-items/image-item.js.flow +0 -20
  114. package/dist/__tests__/test-items/input-number-1-item.d.ts +0 -9
  115. package/dist/__tests__/test-items/input-number-1-item.js.flow +0 -15
  116. package/dist/__tests__/test-items/input-number-2-item.d.ts +0 -9
  117. package/dist/__tests__/test-items/input-number-2-item.js.flow +0 -15
  118. package/dist/__tests__/test-items/table-item.d.ts +0 -9
  119. package/dist/__tests__/test-items/table-item.js.flow +0 -15
  120. package/dist/interactive2/__tests__/movable-mock.d.ts +0 -5
  121. package/dist/interactive2/__tests__/movable-mock.js.flow +0 -11
  122. package/dist/widgets/__stories__/test-keypad-context-wrapper.d.ts +0 -6
  123. package/dist/widgets/__stories__/test-keypad-context-wrapper.js.flow +0 -12
  124. package/dist/widgets/__tests__/renderQuestion.d.ts +0 -13
  125. package/dist/widgets/__tests__/renderQuestion.js.flow +0 -28
package/src/types.ts CHANGED
@@ -389,18 +389,35 @@ export type WidgetExports<
389
389
  getWidget?: () => T;
390
390
  accessible?: boolean | ((props: any) => boolean);
391
391
  hidden?: boolean;
392
+ // The widget version. Any time the _major_ version changes, the widget
393
+ // should provide a new entry in the propUpgrades map to migrate from the
394
+ // older version to the current (new) version. Minor version changes must
395
+ // be backwards compatible with previous minor versions widget options.
396
+ // This key defaults to `{major: 0, minor: 0}` if not provided.
392
397
  version?: Version;
393
398
  supportedAlignments?: ReadonlyArray<Alignment>;
394
399
  defaultAlignment?: Alignment;
395
400
  getDefaultAlignment?: () => Alignment;
396
401
  isLintable?: boolean;
397
- transform?: WidgetTransform;
398
402
  tracking?: Tracking;
399
- staticTransform?: WidgetTransform; // this is a function of some sort,
403
+
400
404
  traverseChildWidgets?: any; // (Props, traverseRenderer) => NewProps,
405
+
406
+ // transforms the widget options to the props used to render the widget
407
+ transform?: WidgetTransform;
408
+ // transforms the widget options to the props used to render the widget for
409
+ // static renders
410
+ staticTransform?: WidgetTransform; // this is a function of some sort,
411
+
412
+ // A map of major version numbers (as a string, eg "1") to a function that
413
+ // migrates from the _previous_ major version.
414
+ // Example:
415
+ // propUpgrades: {'1': (options) => ({...options})}
416
+ // would migrate from major version 0 to 1.
401
417
  propUpgrades?: {
402
418
  [key: string]: (arg1: any) => any;
403
419
  }; // OldProps => NewProps,
420
+
404
421
  widget: T;
405
422
  }>;
406
423
 
@@ -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";