@khanacademy/perseus 7.1.1 → 7.2.1
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.
- package/CHANGELOG.md +18 -0
- package/dist/es/index.css +61 -61
- package/dist/es/index.js +100 -168
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +61 -61
- package/dist/index.js +89 -126
- package/dist/index.js.map +1 -1
- package/dist/util.d.ts +7 -12
- package/dist/util.graphie.d.ts +6 -0
- package/dist/util.graphie.js.flow +14 -0
- package/dist/util.js.flow +7 -12
- package/dist/widgets/plotter.d.ts +47 -13
- package/dist/widgets/plotter.js.flow +47 -13
- package/package.json +2 -2
- package/src/__tests__/item-renderer.test.tsx +1 -1
- package/src/components/__stories__/image-loader.stories.tsx +1 -0
- package/src/components/graph.tsx +1 -0
- package/src/components/icon.tsx +1 -0
- package/src/components/image-loader.tsx +1 -0
- package/src/components/number-input.tsx +1 -0
- package/src/components/sortable.tsx +1 -0
- package/src/components/svg-image.tsx +1 -0
- package/src/components/tex-buttons.tsx +1 -0
- package/src/components/text-input.tsx +1 -0
- package/src/components/text-list-editor.tsx +1 -0
- package/src/components/zoomable.tsx +1 -0
- package/src/hints-renderer.tsx +1 -0
- package/src/index.ts +1 -0
- package/src/item-renderer.tsx +1 -0
- package/src/mixins/provide-keypad.tsx +1 -0
- package/src/multi-items/multi-renderer.tsx +1 -0
- package/src/renderer.tsx +1 -0
- package/src/server-item-renderer.tsx +1 -0
- package/src/styles/articles.less +8 -4
- package/src/styles/perseus-renderer.less +47 -38
- package/src/styles/util.less +1 -1
- package/src/styles/variables.boostrap.less +158 -182
- package/src/styles/variables.less +20 -20
- package/src/styles/variables.shared.less +162 -168
- package/src/styles/widgets/categorizer.less +1 -2
- package/src/styles/widgets/expression.less +7 -4
- package/src/styles/widgets/grapher.less +2 -1
- package/src/styles/widgets/image.less +3 -2
- package/src/styles/widgets/interactive-graph.less +4 -2
- package/src/styles/widgets/label-image.less +2 -1
- package/src/styles/widgets/matrix.less +11 -7
- package/src/styles/widgets/orderer.less +2 -1
- package/src/styles/widgets/passage.less +2 -1
- package/src/styles/widgets/plotter.less +4 -3
- package/src/styles/widgets/radio.less +4 -2
- package/src/styles/widgets/sortable.less +1 -1
- package/src/styles/widgets/table.less +2 -1
- package/src/styles/widgets/transformer.less +2 -1
- package/src/styles/widgets/unit.less +2 -1
- package/src/util.graphie.test.ts +45 -0
- package/src/util.graphie.ts +59 -0
- package/src/util.ts +8 -63
- package/src/widgets/__stories__/plotter.stories.tsx +27 -0
- package/src/widgets/__testdata__/plotter.testdata.ts +36 -0
- package/src/widgets/__tests__/__snapshots__/plotter.test.tsx.snap +863 -0
- package/src/widgets/__tests__/plotter.test.tsx +65 -0
- package/src/widgets/categorizer.tsx +1 -0
- package/src/widgets/graded-group-set.tsx +1 -0
- package/src/widgets/graded-group.tsx +1 -0
- package/src/widgets/interaction.tsx +1 -0
- package/src/widgets/label-image/marker.tsx +1 -0
- package/src/widgets/matcher.tsx +1 -0
- package/src/widgets/orderer.tsx +1 -0
- package/src/widgets/passage-ref-target.tsx +1 -0
- package/src/widgets/plotter.tsx +108 -98
- package/src/widgets/radio/base-radio.tsx +1 -0
- package/src/widgets/radio/choice-icon/choice-icon.tsx +1 -0
- package/src/widgets/sorter.tsx +1 -0
- package/src/widgets/transformer.tsx +1 -0
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
position: relative;
|
|
8
8
|
overflow: hidden;
|
|
9
9
|
|
|
10
|
-
> img,
|
|
10
|
+
> img,
|
|
11
|
+
> .unresponsive-svg-image {
|
|
11
12
|
position: absolute;
|
|
12
13
|
// Preserves background image alignment (if image is a graph,
|
|
13
14
|
// it can extend up and to the right to allow for axis labels)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {describe, it} from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
import {getRealImageUrl, isLabeledSVG} from "./util.graphie";
|
|
4
|
+
|
|
5
|
+
describe(".getRealImageUrl", () => {
|
|
6
|
+
it("when passed a graphie URL, it transforms the URL to an http url", () => {
|
|
7
|
+
const input =
|
|
8
|
+
"web+graphie://ka-perseus-graphie.s3.amazonaws.com/b4a76d480e49f89fddb973c88c53fc301ba0d927";
|
|
9
|
+
const result =
|
|
10
|
+
"https://ka-perseus-graphie.s3.amazonaws.com/b4a76d480e49f89fddb973c88c53fc301ba0d927.svg";
|
|
11
|
+
expect(getRealImageUrl(input)).toBe(result);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("when it's passed an HTTP URL, it returns the URL", () => {
|
|
15
|
+
const input =
|
|
16
|
+
"https://cdn.kastatic.org/ka-perseus-images/c633a1d04970211586b413985a7959876ca6c909.png";
|
|
17
|
+
expect(getRealImageUrl(input)).toBe(input);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe(".isLabeledSVG", () => {
|
|
22
|
+
it("returns true for web+graphie", () => {
|
|
23
|
+
expect(
|
|
24
|
+
isLabeledSVG(
|
|
25
|
+
"web+graphie://ka-perseus-graphie.s3.amazonaws.com/b4a76d480e49f89fddb973c88c53fc301ba0d927",
|
|
26
|
+
),
|
|
27
|
+
).toBeTruthy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns true for file+graphie", () => {
|
|
31
|
+
expect(
|
|
32
|
+
isLabeledSVG(
|
|
33
|
+
"file+graphie://b4a76d480e49f89fddb973c88c53fc301ba0d927",
|
|
34
|
+
),
|
|
35
|
+
).toBeTruthy();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns false for all other protocols", () => {
|
|
39
|
+
expect(
|
|
40
|
+
isLabeledSVG(
|
|
41
|
+
"https://cdn.kastatic.org/ka-perseus-images/c633a1d04970211586b413985a7959876ca6c909.png",
|
|
42
|
+
),
|
|
43
|
+
).toBeFalsy();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const svgLabelsRegex = /^web\+graphie:/;
|
|
2
|
+
// For offline exercises in the mobile app, we download the graphie data
|
|
3
|
+
// (svgs and localized data files) and serve them from the local file
|
|
4
|
+
// system (with file://). We replace urls that start with `web+graphie`
|
|
5
|
+
// in the perseus json with this `file+graphie` prefix to indicate that
|
|
6
|
+
// they should have the `file://` protocol instead of `https://`.
|
|
7
|
+
const svgLocalLabelsRegex = /^file\+graphie:/;
|
|
8
|
+
|
|
9
|
+
// Sometimes other components want to download the actual image e.g. to
|
|
10
|
+
// determine its size. Here, we transform an .svg-labels url into the
|
|
11
|
+
// correct image url, and leave normal image urls alone
|
|
12
|
+
export function getRealImageUrl(graphieUrl: string): string {
|
|
13
|
+
if (isLabeledSVG(graphieUrl)) {
|
|
14
|
+
return getSvgUrl(graphieUrl);
|
|
15
|
+
}
|
|
16
|
+
return graphieUrl;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isLabeledSVG(graphieUrl: string): boolean {
|
|
20
|
+
return (
|
|
21
|
+
svgLabelsRegex.test(graphieUrl) || svgLocalLabelsRegex.test(graphieUrl)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// For each graphie, there are two urls we need to download from. This gets
|
|
26
|
+
// the base url without the suffix, and `getSvgUrl` and `getDataUrl` apply
|
|
27
|
+
// appropriate suffixes to get the image and other data
|
|
28
|
+
export function getBaseUrl(graphieUrl: string): string {
|
|
29
|
+
return graphieUrl
|
|
30
|
+
.replace(svgLabelsRegex, "https:")
|
|
31
|
+
.replace(svgLocalLabelsRegex, "file:");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getSvgUrl(graphieUrl: string): string {
|
|
35
|
+
return getBaseUrl(graphieUrl) + ".svg";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getDataUrl(graphieUrl: string): string {
|
|
39
|
+
return getBaseUrl(graphieUrl) + "-data.json";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function getImageSizeModern(
|
|
43
|
+
url: string,
|
|
44
|
+
): Promise<[number, number]> {
|
|
45
|
+
const image = new Image();
|
|
46
|
+
|
|
47
|
+
return new Promise((resolve, reject) => {
|
|
48
|
+
// Handle the success case
|
|
49
|
+
image.onload = () => {
|
|
50
|
+
resolve([image.naturalWidth, image.naturalHeight]);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Handle the error case
|
|
54
|
+
image.onerror = reject;
|
|
55
|
+
|
|
56
|
+
// Kick off the loading
|
|
57
|
+
image.src = getRealImageUrl(url);
|
|
58
|
+
});
|
|
59
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -3,6 +3,7 @@ import _ from "underscore";
|
|
|
3
3
|
|
|
4
4
|
import {Errors} from "./logging/log";
|
|
5
5
|
import {PerseusError} from "./perseus-error";
|
|
6
|
+
import * as GraphieUtil from "./util.graphie";
|
|
6
7
|
import KhanAnswerTypes from "./util/answer-types";
|
|
7
8
|
|
|
8
9
|
import type {Range} from "./perseus-types";
|
|
@@ -51,14 +52,6 @@ type TouchHandlers = {
|
|
|
51
52
|
|
|
52
53
|
let supportsPassive = false;
|
|
53
54
|
|
|
54
|
-
const svgLabelsRegex = /^web\+graphie:/;
|
|
55
|
-
// For offline exercises in the mobile app, we download the graphie data
|
|
56
|
-
// (svgs and localized data files) and serve them from the local file
|
|
57
|
-
// system (with file://). We replace urls that start with `web+graphie`
|
|
58
|
-
// in the perseus json with this `file+graphie` prefix to indicate that
|
|
59
|
-
// they should have the `file://` protocol instead of `https://`.
|
|
60
|
-
const svgLocalLabelsRegex = /^file\+graphie:/;
|
|
61
|
-
|
|
62
55
|
const nestedMap = function <T, M>(
|
|
63
56
|
children: T | ReadonlyArray<T>,
|
|
64
57
|
func: (arg1: T) => M,
|
|
@@ -839,23 +832,6 @@ function captureScratchpadTouchStart(e: TouchEvent) {
|
|
|
839
832
|
e.stopPropagation();
|
|
840
833
|
}
|
|
841
834
|
|
|
842
|
-
async function getImageSizeModern(url: string): Promise<[number, number]> {
|
|
843
|
-
const image = new Image();
|
|
844
|
-
|
|
845
|
-
return new Promise((resolve, reject) => {
|
|
846
|
-
// Handle the success case
|
|
847
|
-
image.onload = () => {
|
|
848
|
-
resolve([image.naturalWidth, image.naturalHeight]);
|
|
849
|
-
};
|
|
850
|
-
|
|
851
|
-
// Handle the error case
|
|
852
|
-
image.onerror = reject;
|
|
853
|
-
|
|
854
|
-
// Kick off the loading
|
|
855
|
-
image.src = url;
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
|
|
859
835
|
function getImageSize(
|
|
860
836
|
url: string,
|
|
861
837
|
callback: (width: number, height: number) => void,
|
|
@@ -884,38 +860,7 @@ function getImageSize(
|
|
|
884
860
|
}
|
|
885
861
|
};
|
|
886
862
|
|
|
887
|
-
img.src = getRealImageUrl(url);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// Sometimes other components want to download the actual image e.g. to
|
|
891
|
-
// determine its size. Here, we transform an .svg-labels url into the
|
|
892
|
-
// correct image url, and leave normal image urls alone
|
|
893
|
-
function getRealImageUrl(url: string): string {
|
|
894
|
-
if (isLabeledSVG(url)) {
|
|
895
|
-
return getSvgUrl(url);
|
|
896
|
-
}
|
|
897
|
-
return url;
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
function isLabeledSVG(url: string): boolean {
|
|
901
|
-
return svgLabelsRegex.test(url) || svgLocalLabelsRegex.test(url);
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// For each svg+labels, there are two urls we need to download from. This gets
|
|
905
|
-
// the base url without the suffix, and `getSvgUrl` and `getDataUrl` apply
|
|
906
|
-
// appropriate suffixes to get the image and other data
|
|
907
|
-
function getBaseUrl(url: string): string {
|
|
908
|
-
return url
|
|
909
|
-
.replace(svgLabelsRegex, "https:")
|
|
910
|
-
.replace(svgLocalLabelsRegex, "file:");
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function getSvgUrl(url: string): string {
|
|
914
|
-
return getBaseUrl(url) + ".svg";
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
function getDataUrl(url: string): string {
|
|
918
|
-
return getBaseUrl(url) + "-data.json";
|
|
863
|
+
img.src = GraphieUtil.getRealImageUrl(url);
|
|
919
864
|
}
|
|
920
865
|
|
|
921
866
|
/**
|
|
@@ -1007,12 +952,12 @@ const Util = {
|
|
|
1007
952
|
supportsPassiveEvents,
|
|
1008
953
|
captureScratchpadTouchStart,
|
|
1009
954
|
getImageSize,
|
|
1010
|
-
getImageSizeModern,
|
|
1011
|
-
getRealImageUrl,
|
|
1012
|
-
isLabeledSVG,
|
|
1013
|
-
getBaseUrl,
|
|
1014
|
-
getSvgUrl,
|
|
1015
|
-
getDataUrl,
|
|
955
|
+
getImageSizeModern: GraphieUtil.getImageSizeModern,
|
|
956
|
+
getRealImageUrl: GraphieUtil.getRealImageUrl,
|
|
957
|
+
isLabeledSVG: GraphieUtil.isLabeledSVG,
|
|
958
|
+
getBaseUrl: GraphieUtil.getBaseUrl,
|
|
959
|
+
getSvgUrl: GraphieUtil.getSvgUrl,
|
|
960
|
+
getDataUrl: GraphieUtil.getDataUrl,
|
|
1016
961
|
textarea,
|
|
1017
962
|
unescapeMathMode,
|
|
1018
963
|
random,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {View} from "@khanacademy/wonder-blocks-core";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import {RendererWithDebugUI} from "../../../../../testing/renderer-with-debug-ui";
|
|
5
|
+
import {question1} from "../__testdata__/plotter.testdata";
|
|
6
|
+
|
|
7
|
+
import type {PerseusRenderer} from "../../perseus-types";
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
title: "Perseus/Widgets/Plotter",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type StoryArgs = Record<any, any>;
|
|
14
|
+
|
|
15
|
+
const Template = (props: {question: PerseusRenderer}): React.ReactElement => (
|
|
16
|
+
<View
|
|
17
|
+
style={{
|
|
18
|
+
paddingLeft: 20,
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<RendererWithDebugUI question={props.question} />
|
|
22
|
+
</View>
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export const Basic = (args: StoryArgs): React.ReactElement => {
|
|
26
|
+
return <Template question={question1} />;
|
|
27
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {PerseusRenderer} from "../../perseus-types";
|
|
2
|
+
|
|
3
|
+
export const question1: PerseusRenderer = {
|
|
4
|
+
content:
|
|
5
|
+
"The elementary school principal asked teachers to report the number of students absent in each grade during the past week.\n\n**Create a bar graph to show how many students were absent in each grade.**\n\nSchool grade | Number of absent students \n:- | :-: \n$1^{\\text{st}} \\text{ grade}$ | $15$ \n$2^{\\text{nd}} \\text{ grade}$ |$25$ \n$3^{\\text{rd}} \\text{ grade}$ | $5$ \n$4^{\\text{th}} \\text{ grade}$ | $10$ \n$5^{\\text{th}} \\text{ grade}$ | $10$ \n\n[[☃ plotter 1]]\n\n",
|
|
6
|
+
images: {},
|
|
7
|
+
widgets: {
|
|
8
|
+
// This is the actual problem from the server
|
|
9
|
+
// I don't know why the type is incorrect
|
|
10
|
+
// @ts-expect-error - TS2322
|
|
11
|
+
"plotter 1": {
|
|
12
|
+
alignment: "default",
|
|
13
|
+
graded: true,
|
|
14
|
+
options: {
|
|
15
|
+
categories: [
|
|
16
|
+
"$1^{\\text{st}} \\text{}$",
|
|
17
|
+
"$2^{\\text{nd}} \\text{}$",
|
|
18
|
+
"$3^{\\text{rd}} \\text{}$",
|
|
19
|
+
"$4^{\\text{th}} \\text{}$",
|
|
20
|
+
"$5^{\\text{th}} \\text{}$",
|
|
21
|
+
],
|
|
22
|
+
correct: [15, 25, 5, 10, 10],
|
|
23
|
+
labelInterval: 1,
|
|
24
|
+
labels: ["School grade", "Number of absent students"],
|
|
25
|
+
maxY: 30,
|
|
26
|
+
scaleY: 5,
|
|
27
|
+
snapsPerLine: 1,
|
|
28
|
+
starting: [0, 0, 0, 0, 0],
|
|
29
|
+
type: "bar",
|
|
30
|
+
},
|
|
31
|
+
static: false,
|
|
32
|
+
type: "plotter",
|
|
33
|
+
version: {major: 0, minor: 0},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
};
|