@khanacademy/perseus 18.0.1 → 19.1.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.
- package/CHANGELOG.md +36 -0
- package/dist/es/index.css +209 -128
- package/dist/es/index.js +1026 -3516
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +209 -128
- package/dist/index.d.ts +3 -3
- package/dist/index.js +917 -3409
- package/dist/index.js.map +1 -1
- package/dist/mixins/changeable.d.ts +1 -1
- package/dist/perseus-api.d.ts +1 -0
- package/dist/perseus-types.d.ts +2 -109
- package/dist/renderer.d.ts +2 -4
- package/dist/server-item-renderer.d.ts +1 -0
- package/dist/strings.js +0 -29
- package/dist/types.d.ts +4 -4
- package/dist/util/geometry.d.ts +2 -0
- package/dist/util/graphie.d.ts +107 -1
- package/dist/util/math.d.ts +2 -0
- package/dist/util/parse-perseus-json.d.ts +3 -3
- package/dist/widgets/expression.d.ts +8 -2
- package/dist/widgets/label-image.d.ts +2 -0
- package/dist/widgets.d.ts +1 -1
- package/package.json +3 -4
- package/src/__testdata__/renderer.testdata.ts +152 -0
- package/src/__tests__/renderer.test.tsx +17 -0
- package/src/__tests__/widgets.test.ts +0 -1
- package/src/components/math-input.tsx +4 -6
- package/src/extra-widgets.ts +0 -2
- package/src/index.ts +8 -4
- package/src/mixins/changeable.ts +1 -1
- package/src/perseus-types.ts +1 -154
- package/src/renderer.tsx +14 -8
- package/src/styles/perseus-renderer.less +0 -1
- package/src/types.ts +9 -5
- package/src/util/geometry.test.ts +75 -0
- package/src/util/geometry.ts +19 -0
- package/src/util/graphie.test.ts +1291 -0
- package/src/util/graphie.ts +832 -784
- package/src/util/interactive.ts +23 -51
- package/src/util/math.test.ts +11 -0
- package/src/util/math.ts +8 -0
- package/src/util/parse-perseus-json.test.ts +3 -3
- package/src/util/parse-perseus-json.ts +4 -4
- package/src/util/register-all-widgets-for-testing.ts +1 -3
- package/src/widgets/__tests__/__snapshots__/input-number.test.ts.snap +4 -2
- package/src/widgets/__tests__/__snapshots__/numeric-input.test.ts.snap +4 -2
- package/src/widgets/__tests__/expression-mobile.test.tsx +8 -5
- package/src/widgets/__tests__/renderQuestion.tsx +11 -3
- package/src/widgets/expression.tsx +1 -23
- package/src/widgets/label-image.tsx +0 -1
- package/src/widgets.ts +1 -9
- package/tsconfig-build.tsbuildinfo +1 -1
- package/dist/widgets/transformer.d.ts +0 -94
- package/src/styles/widgets/transformer.less +0 -29
- package/src/widgets/__stories__/transformer.stories.tsx +0 -14
- package/src/widgets/__testdata__/transformer.testdata.ts +0 -107
- package/src/widgets/transformer.tsx +0 -2936
package/src/util/graphie.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/* eslint-disable @babel/no-invalid-this */
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
point as kpoint,
|
|
4
|
+
vector as kvector,
|
|
5
|
+
number as knumber,
|
|
6
|
+
} from "@khanacademy/kmath";
|
|
7
|
+
import {entries} from "@khanacademy/wonder-stuff-core";
|
|
3
8
|
import $ from "jquery";
|
|
4
9
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
5
10
|
import Raphael from "raphael";
|
|
6
|
-
import _ from "underscore";
|
|
7
11
|
|
|
8
12
|
// Minify Raphael ourselves because IE8 has a problem with the 1.5.2 minified
|
|
9
13
|
// release
|
|
@@ -23,7 +27,7 @@ import type {Coord} from "../interactive2/types";
|
|
|
23
27
|
|
|
24
28
|
const {processMath} = Tex;
|
|
25
29
|
|
|
26
|
-
export function polar(r: number | Coord, th: number) {
|
|
30
|
+
export function polar(r: number | Coord, th: number): Coord {
|
|
27
31
|
if (typeof r === "number") {
|
|
28
32
|
r = [r, r];
|
|
29
33
|
}
|
|
@@ -31,64 +35,62 @@ export function polar(r: number | Coord, th: number) {
|
|
|
31
35
|
return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
|
|
32
36
|
}
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
return "z";
|
|
44
|
-
}
|
|
45
|
-
return (i === 0 ? "M" : "L") + point[0] + " " + point[1];
|
|
46
|
-
}).join("");
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
getDistance: function (point1, point2) {
|
|
50
|
-
return kpoint.distanceToPoint(point1, point2);
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Return the difference between two sets of coordinates
|
|
55
|
-
*/
|
|
56
|
-
coordDiff: function (startCoord, endCoord) {
|
|
57
|
-
return _.map(endCoord, function (val, i) {
|
|
58
|
-
return endCoord[i] - startCoord[i];
|
|
59
|
-
});
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Round the given coordinates to a given snap value
|
|
64
|
-
* (e.g., nearest 0.2 increment)
|
|
65
|
-
*/
|
|
66
|
-
snapCoord: function (coord, snap) {
|
|
67
|
-
return _.map(coord, function (val, i) {
|
|
68
|
-
return KhanMath.roundToNearest(snap[i], val);
|
|
69
|
-
});
|
|
70
|
-
},
|
|
38
|
+
interface RaphaelElement {
|
|
39
|
+
type: string;
|
|
40
|
+
attrs: Record<string, any>;
|
|
41
|
+
node: {
|
|
42
|
+
style: {
|
|
43
|
+
shapeRendering: "crispEdges" | "geometricPrecision";
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
}
|
|
71
47
|
|
|
72
|
-
|
|
73
|
-
findAngle: function (point1, point2, vertex) {
|
|
74
|
-
if (vertex === undefined) {
|
|
75
|
-
const x = point1[0] - point2[0];
|
|
76
|
-
const y = point1[1] - point2[1];
|
|
77
|
-
if (!x && !y) {
|
|
78
|
-
return 0;
|
|
79
|
-
}
|
|
80
|
-
return (180 + (Math.atan2(-y, -x) * 180) / Math.PI + 360) % 360;
|
|
81
|
-
}
|
|
82
|
-
return (
|
|
83
|
-
GraphUtils.findAngle(point1, vertex) -
|
|
84
|
-
GraphUtils.findAngle(point2, vertex)
|
|
85
|
-
);
|
|
86
|
-
},
|
|
48
|
+
type PositionedShape = {wrapper: HTMLDivElement; visibleShape: RaphaelElement};
|
|
87
49
|
|
|
88
|
-
|
|
50
|
+
export type StyleParams = {
|
|
51
|
+
fill?: string;
|
|
52
|
+
labelDistance?: number;
|
|
53
|
+
opacity?: number;
|
|
54
|
+
step?: Coord;
|
|
55
|
+
stroke?: string;
|
|
56
|
+
strokeWidth?: number;
|
|
89
57
|
};
|
|
90
58
|
|
|
91
|
-
|
|
59
|
+
type LabelPosition =
|
|
60
|
+
| "center"
|
|
61
|
+
| "above"
|
|
62
|
+
| "below"
|
|
63
|
+
| "right"
|
|
64
|
+
| "left"
|
|
65
|
+
| "above right"
|
|
66
|
+
| "above left"
|
|
67
|
+
| "below right"
|
|
68
|
+
| "below left";
|
|
69
|
+
|
|
70
|
+
interface LabelMethod {
|
|
71
|
+
(point: Coord, text: string, position: LabelPosition): any;
|
|
72
|
+
(
|
|
73
|
+
point: Coord,
|
|
74
|
+
text: string,
|
|
75
|
+
position: LabelPosition,
|
|
76
|
+
renderTex: boolean,
|
|
77
|
+
): any;
|
|
78
|
+
(
|
|
79
|
+
point: Coord,
|
|
80
|
+
text: string,
|
|
81
|
+
position: LabelPosition,
|
|
82
|
+
style: StyleParams,
|
|
83
|
+
): any;
|
|
84
|
+
(
|
|
85
|
+
point: Coord,
|
|
86
|
+
text: string,
|
|
87
|
+
position: LabelPosition,
|
|
88
|
+
renderTex: boolean,
|
|
89
|
+
style: StyleParams,
|
|
90
|
+
): any;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class Graphie {
|
|
92
94
|
el: HTMLElement;
|
|
93
95
|
#bounds?: GraphBounds;
|
|
94
96
|
#drawingTransform?: DrawingTransform;
|
|
@@ -121,7 +123,7 @@ class Graphie {
|
|
|
121
123
|
init(options: {
|
|
122
124
|
range?: [Interval, Interval];
|
|
123
125
|
scale?: number | Coord;
|
|
124
|
-
isMobile
|
|
126
|
+
isMobile?: boolean;
|
|
125
127
|
}) {
|
|
126
128
|
let scale = options.scale || [40, 40];
|
|
127
129
|
scale = typeof scale === "number" ? [scale, scale] : scale;
|
|
@@ -156,7 +158,7 @@ class Graphie {
|
|
|
156
158
|
this.xpixels = w;
|
|
157
159
|
this.ypixels = h;
|
|
158
160
|
|
|
159
|
-
this.isMobile = options.isMobile;
|
|
161
|
+
this.isMobile = options.isMobile ?? false;
|
|
160
162
|
|
|
161
163
|
return this;
|
|
162
164
|
}
|
|
@@ -173,14 +175,37 @@ class Graphie {
|
|
|
173
175
|
// - labelStep: [a, b] or number (relative to tick steps)
|
|
174
176
|
// - yLabelFormat: fn to format label string for y-axis
|
|
175
177
|
// - xLabelFormat: fn to format label string for x-axis
|
|
176
|
-
graphInit(options:
|
|
178
|
+
graphInit(options: {
|
|
179
|
+
range?: [Interval, Interval] | Coord;
|
|
180
|
+
grid?: boolean;
|
|
181
|
+
gridRange?: [Interval, Interval] | Coord;
|
|
182
|
+
scale: Coord | number;
|
|
183
|
+
axes?: true;
|
|
184
|
+
axisArrows?: "<->" | "->" | true | "";
|
|
185
|
+
axisOpacity?: number;
|
|
186
|
+
axisCenter?: Coord;
|
|
187
|
+
axisLabels?: [string, string] | false;
|
|
188
|
+
gridOpacity?: number;
|
|
189
|
+
gridStep?: Coord | number;
|
|
190
|
+
ticks?: boolean;
|
|
191
|
+
tickStep?: Coord | number;
|
|
192
|
+
tickLen?: Coord | number;
|
|
193
|
+
tickOpacity?: number;
|
|
194
|
+
labels?: boolean;
|
|
195
|
+
labelStep?: number;
|
|
196
|
+
labelOpacity?: number;
|
|
197
|
+
labelFormat?: (a: number) => string;
|
|
198
|
+
yLabelFormat?: (y: number) => string;
|
|
199
|
+
xLabelFormat?: (x: number) => string;
|
|
200
|
+
unityLabels?: boolean | [boolean, boolean];
|
|
201
|
+
isMobile?: boolean;
|
|
202
|
+
}) {
|
|
177
203
|
options = options || {};
|
|
178
204
|
|
|
179
|
-
|
|
205
|
+
for (const [prop, val] of entries(options)) {
|
|
180
206
|
// allow options to be specified by a single number for shorthand if
|
|
181
207
|
// the horizontal and vertical components are the same
|
|
182
208
|
if (
|
|
183
|
-
// @ts-expect-error - TS2339 - Property 'match' does not exist on type 'string | number | symbol'.
|
|
184
209
|
!prop.match(/.*Opacity$/) &&
|
|
185
210
|
prop !== "range" &&
|
|
186
211
|
typeof val === "number"
|
|
@@ -190,32 +215,21 @@ class Graphie {
|
|
|
190
215
|
|
|
191
216
|
// allow symmetric ranges to be specified by the absolute values
|
|
192
217
|
if (prop === "range" || prop === "gridRange") {
|
|
193
|
-
|
|
194
|
-
// but don't mandate symmetric ranges
|
|
195
|
-
if (val[0].constructor !== Array) {
|
|
196
|
-
options[prop] = [
|
|
197
|
-
[-val[0], val[0]],
|
|
198
|
-
[-val[1], val[1]],
|
|
199
|
-
];
|
|
200
|
-
}
|
|
201
|
-
} else if (typeof val === "number") {
|
|
202
|
-
options[prop] = [
|
|
203
|
-
[-val, val],
|
|
204
|
-
[-val, val],
|
|
205
|
-
];
|
|
206
|
-
}
|
|
218
|
+
options[prop] = normalizeRange(options[prop]);
|
|
207
219
|
}
|
|
208
|
-
}
|
|
220
|
+
}
|
|
209
221
|
|
|
210
|
-
const range =
|
|
211
|
-
[
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
222
|
+
const range = normalizeRange(
|
|
223
|
+
options.range || [
|
|
224
|
+
[-10, 10],
|
|
225
|
+
[-10, 10],
|
|
226
|
+
],
|
|
227
|
+
);
|
|
228
|
+
const gridRange = normalizeRange(options.gridRange || range);
|
|
215
229
|
const scale = options.scale || [20, 20];
|
|
216
230
|
const grid = options.grid != null ? options.grid : true;
|
|
217
231
|
const gridOpacity = options.gridOpacity || 0.1;
|
|
218
|
-
const gridStep = options.gridStep || [1, 1];
|
|
232
|
+
const gridStep = toPair(options.gridStep || [1, 1]);
|
|
219
233
|
const axes = options.axes != null ? options.axes : true;
|
|
220
234
|
const axisArrows = options.axisArrows || "";
|
|
221
235
|
const axisOpacity = options.axisOpacity || 1.0;
|
|
@@ -251,7 +265,7 @@ class Graphie {
|
|
|
251
265
|
],
|
|
252
266
|
] as [Interval, Interval];
|
|
253
267
|
|
|
254
|
-
if (!
|
|
268
|
+
if (!Array.isArray(unityLabels)) {
|
|
255
269
|
unityLabels = [unityLabels, unityLabels];
|
|
256
270
|
}
|
|
257
271
|
|
|
@@ -366,7 +380,7 @@ class Graphie {
|
|
|
366
380
|
opacity: options.isMobile ? 1 : tickOpacity,
|
|
367
381
|
strokeWidth: 1,
|
|
368
382
|
},
|
|
369
|
-
|
|
383
|
+
() => {
|
|
370
384
|
// horizontal axis
|
|
371
385
|
let step = gridStep[0] * tickStep[0];
|
|
372
386
|
let len = tickLen[0] / scale[1];
|
|
@@ -424,7 +438,6 @@ class Graphie {
|
|
|
424
438
|
y += step
|
|
425
439
|
) {
|
|
426
440
|
if (y < stop || !axisArrows) {
|
|
427
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
428
441
|
this.line(
|
|
429
442
|
[-len + axisCenter[0], y],
|
|
430
443
|
[
|
|
@@ -443,7 +456,6 @@ class Graphie {
|
|
|
443
456
|
y -= step
|
|
444
457
|
) {
|
|
445
458
|
if (y > start || !axisArrows) {
|
|
446
|
-
// @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
|
|
447
459
|
this.line(
|
|
448
460
|
[-len + axisCenter[0], y],
|
|
449
461
|
[
|
|
@@ -552,7 +564,7 @@ class Graphie {
|
|
|
552
564
|
}
|
|
553
565
|
}
|
|
554
566
|
|
|
555
|
-
drawingTransform(): DrawingTransform {
|
|
567
|
+
private drawingTransform(): DrawingTransform {
|
|
556
568
|
if (this.#drawingTransform == null) {
|
|
557
569
|
throw new Error(
|
|
558
570
|
"Can't get drawingTransform of an uninitialized Graphie",
|
|
@@ -561,7 +573,7 @@ class Graphie {
|
|
|
561
573
|
return this.#drawingTransform;
|
|
562
574
|
}
|
|
563
575
|
|
|
564
|
-
bounds(): GraphBounds {
|
|
576
|
+
private bounds(): GraphBounds {
|
|
565
577
|
if (this.#bounds == null) {
|
|
566
578
|
throw new Error("Can't get bounds of an uninitialized Graphie");
|
|
567
579
|
}
|
|
@@ -573,39 +585,517 @@ class Graphie {
|
|
|
573
585
|
|
|
574
586
|
if (typeof fn === "function") {
|
|
575
587
|
const oldStyle = this.currentStyle;
|
|
576
|
-
this.currentStyle =
|
|
588
|
+
this.currentStyle = {...this.currentStyle, ...processed};
|
|
577
589
|
const result = fn.call(this);
|
|
578
590
|
this.currentStyle = oldStyle;
|
|
579
591
|
return result;
|
|
580
592
|
}
|
|
581
|
-
|
|
593
|
+
Object.assign(this.currentStyle, processed);
|
|
582
594
|
}
|
|
583
595
|
|
|
584
|
-
|
|
596
|
+
grid(xr: Interval, yr: Interval, style?: StyleParams): unknown {
|
|
597
|
+
return this.withStyle(style, () => {
|
|
598
|
+
const step: any = this.currentStyle.step || [1, 1];
|
|
599
|
+
const set = this.raphael.set();
|
|
600
|
+
|
|
601
|
+
let x = step[0] * Math.ceil(xr[0] / step[0]);
|
|
602
|
+
for (; x <= xr[1]; x += step[0]) {
|
|
603
|
+
set.push(this.line([x, yr[0]], [x, yr[1]]));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
let y = step[1] * Math.ceil(yr[0] / step[1]);
|
|
607
|
+
for (; y <= yr[1]; y += step[1]) {
|
|
608
|
+
set.push(this.line([xr[0], y], [xr[1], y]));
|
|
609
|
+
}
|
|
585
610
|
|
|
586
|
-
|
|
611
|
+
return set;
|
|
612
|
+
});
|
|
613
|
+
}
|
|
587
614
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
615
|
+
arc(
|
|
616
|
+
center: Coord,
|
|
617
|
+
radius: Coord,
|
|
618
|
+
startAngle: number,
|
|
619
|
+
endAngle: number,
|
|
620
|
+
sector: boolean,
|
|
621
|
+
style?: StyleParams,
|
|
622
|
+
): RaphaelElement {
|
|
623
|
+
return this.withStyle(style, () => {
|
|
624
|
+
startAngle = ((startAngle % 360) + 360) % 360;
|
|
625
|
+
endAngle = ((endAngle % 360) + 360) % 360;
|
|
626
|
+
|
|
627
|
+
const cent = this.scalePoint(center);
|
|
628
|
+
const radii = this.scaleVector(radius);
|
|
629
|
+
const startVector = polar(radius, startAngle);
|
|
630
|
+
const endVector = polar(radius, endAngle);
|
|
631
|
+
|
|
632
|
+
// We round the coordinates to make testing easier, because trig
|
|
633
|
+
// operations return unround numbers.
|
|
634
|
+
// TODO(benchristel): move rounding to scalePoint?
|
|
635
|
+
const round = (x) => knumber.round(x, 6);
|
|
636
|
+
const startPoint = this.scalePoint([
|
|
637
|
+
round(center[0] + startVector[0]),
|
|
638
|
+
round(center[1] + startVector[1]),
|
|
639
|
+
]);
|
|
640
|
+
const endPoint = this.scalePoint([
|
|
641
|
+
round(center[0] + endVector[0]),
|
|
642
|
+
round(center[1] + endVector[1]),
|
|
643
|
+
]);
|
|
644
|
+
|
|
645
|
+
const largeAngle =
|
|
646
|
+
(((endAngle - startAngle) % 360) + 360) % 360 > 180;
|
|
647
|
+
|
|
648
|
+
return this.raphael.path(
|
|
649
|
+
"M" +
|
|
650
|
+
startPoint.join(" ") +
|
|
651
|
+
"A" +
|
|
652
|
+
radii.join(" ") +
|
|
653
|
+
" 0 " + // ellipse rotation
|
|
654
|
+
(largeAngle ? 1 : 0) +
|
|
655
|
+
" 0 " + // sweep flag
|
|
656
|
+
endPoint.join(" ") +
|
|
657
|
+
(sector ? "L" + cent.join(" ") + "z" : ""),
|
|
658
|
+
);
|
|
659
|
+
});
|
|
660
|
+
}
|
|
591
661
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
662
|
+
circle(center: Coord, radius: number, style?: StyleParams) {
|
|
663
|
+
return this.withStyle(style, () =>
|
|
664
|
+
this.raphael.ellipse(
|
|
665
|
+
...this.scalePoint(center),
|
|
666
|
+
...this.scaleVector([radius, radius]),
|
|
667
|
+
),
|
|
668
|
+
);
|
|
669
|
+
}
|
|
595
670
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
671
|
+
// (x, y) is coordinate of bottom left corner
|
|
672
|
+
rect(x, y, width, height, style?: StyleParams) {
|
|
673
|
+
return this.withStyle(style, () => {
|
|
674
|
+
// Raphael needs (x, y) to be coordinate of upper left corner
|
|
675
|
+
const corner = this.scalePoint([x, y + height]);
|
|
676
|
+
const dims = this.scaleVector([width, height]);
|
|
677
|
+
const elem = this.raphael.rect(...corner.concat(dims));
|
|
678
|
+
|
|
679
|
+
if (this.isMobile) {
|
|
680
|
+
elem.node.style.shapeRendering = "crispEdges";
|
|
600
681
|
}
|
|
601
|
-
|
|
602
|
-
return
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
682
|
+
|
|
683
|
+
return elem;
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
ellipse(center, radii, style?: StyleParams) {
|
|
688
|
+
return this.withStyle(style, () =>
|
|
689
|
+
this.raphael.ellipse(
|
|
690
|
+
...this.scalePoint(center).concat(this.scaleVector(radii)),
|
|
691
|
+
),
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
fixedEllipse(
|
|
696
|
+
center: number | Coord,
|
|
697
|
+
// Different type than Coord, this is radiusX, radiusY
|
|
698
|
+
radii: number | [number, number],
|
|
699
|
+
maxScale: number,
|
|
700
|
+
padding: number,
|
|
701
|
+
style?: StyleParams,
|
|
702
|
+
): PositionedShape {
|
|
703
|
+
return this.withStyle(style, () => {
|
|
704
|
+
// Scale point and radius
|
|
705
|
+
const scaledPoint = this.scalePoint(center);
|
|
706
|
+
const scaledRadii = this.scaleVector(radii);
|
|
707
|
+
|
|
708
|
+
const width = 2 * scaledRadii[0] * maxScale + padding;
|
|
709
|
+
const height = 2 * scaledRadii[1] * maxScale + padding;
|
|
710
|
+
|
|
711
|
+
// Calculate absolute left, top
|
|
712
|
+
const left = scaledPoint[0] - width / 2;
|
|
713
|
+
const top = scaledPoint[1] - height / 2;
|
|
714
|
+
|
|
715
|
+
// Wrap in <div>
|
|
716
|
+
const wrapper = document.createElement("div");
|
|
717
|
+
$(wrapper).css({
|
|
718
|
+
position: "absolute",
|
|
719
|
+
width: width + "px",
|
|
720
|
+
height: height + "px",
|
|
721
|
+
left: left + "px",
|
|
722
|
+
top: top + "px",
|
|
723
|
+
});
|
|
724
|
+
// wrapper.setAttribute("data-graphie-type", "ellipse");
|
|
725
|
+
|
|
726
|
+
// Create Raphael canvas
|
|
727
|
+
const localRaphael = Raphael(wrapper, width, height);
|
|
728
|
+
const visibleShape = localRaphael.ellipse(
|
|
729
|
+
width / 2,
|
|
730
|
+
height / 2,
|
|
731
|
+
scaledRadii[0],
|
|
732
|
+
scaledRadii[1],
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
wrapper: wrapper,
|
|
737
|
+
visibleShape: visibleShape,
|
|
738
|
+
};
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
private unstyledPath(points: Coord[]): RaphaelElement {
|
|
743
|
+
const p = this.raphael.path(this.svgPath(points));
|
|
744
|
+
p.graphiePath = points;
|
|
745
|
+
return p;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
path(points: Coord[], style?: StyleParams): RaphaelElement {
|
|
749
|
+
return this.withStyle(style, () => {
|
|
750
|
+
return this.unstyledPath(points);
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// fixedPath is a stub that gets overwritten with a function from
|
|
755
|
+
// drawingTools in createGraphie
|
|
756
|
+
fixedPath(
|
|
757
|
+
points: Coord[],
|
|
758
|
+
center: Coord | null,
|
|
759
|
+
createPath: (scaledPoints: Coord[]) => string,
|
|
760
|
+
): PositionedShape {
|
|
761
|
+
points = points.map(this.scalePoint);
|
|
762
|
+
center = center ? this.scalePoint(center) : null;
|
|
763
|
+
createPath = createPath || this.svgPath;
|
|
764
|
+
|
|
765
|
+
// Compute bounding box
|
|
766
|
+
const xs = points.map((p) => p[0]);
|
|
767
|
+
const ys = points.map((p) => p[1]);
|
|
768
|
+
const pathLeft = Math.min(...xs);
|
|
769
|
+
const pathRight = Math.max(...xs);
|
|
770
|
+
const pathTop = Math.min(...ys);
|
|
771
|
+
const pathBottom = Math.max(...ys);
|
|
772
|
+
|
|
773
|
+
// Apply padding to line
|
|
774
|
+
const padding: Coord = [4, 4];
|
|
775
|
+
|
|
776
|
+
// Calculate and apply additional offset
|
|
777
|
+
const topLeftOfBoundingBox: Coord = [pathLeft, pathTop];
|
|
778
|
+
|
|
779
|
+
// Apply padding and offset to points to convert from
|
|
780
|
+
// canvas coordinates to pixel coordinates relative to bounding box
|
|
781
|
+
points = points.map(function (point) {
|
|
782
|
+
return kvector.add(
|
|
783
|
+
kvector.subtract(point, topLeftOfBoundingBox),
|
|
784
|
+
kvector.scale(padding, 0.5),
|
|
785
|
+
);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
// Calculate <div> dimensions
|
|
789
|
+
const width = pathRight - pathLeft + padding[0];
|
|
790
|
+
const height = pathBottom - pathTop + padding[1];
|
|
791
|
+
const left = topLeftOfBoundingBox[0] - padding[0] / 2;
|
|
792
|
+
const top = topLeftOfBoundingBox[1] - padding[1] / 2;
|
|
793
|
+
|
|
794
|
+
// Create <div>
|
|
795
|
+
const wrapper = document.createElement("div");
|
|
796
|
+
$(wrapper).css({
|
|
797
|
+
position: "absolute",
|
|
798
|
+
width: width + "px",
|
|
799
|
+
height: height + "px",
|
|
800
|
+
left: left + "px",
|
|
801
|
+
top: top + "px",
|
|
802
|
+
// If user specified a center, set it
|
|
803
|
+
// NOTE(kevinb): jQuery doesn't like that `transformOrigin `could be `null`
|
|
804
|
+
// so we cast to `any` here.
|
|
805
|
+
transformOrigin: center
|
|
806
|
+
? width / 2 +
|
|
807
|
+
center[0] +
|
|
808
|
+
"px " +
|
|
809
|
+
(height / 2 + center[1]) +
|
|
810
|
+
"px"
|
|
811
|
+
: null,
|
|
812
|
+
} as any);
|
|
813
|
+
|
|
814
|
+
// Create Raphael canvas
|
|
815
|
+
const localRaphael = Raphael(wrapper, width, height);
|
|
816
|
+
|
|
817
|
+
// Calculate path
|
|
818
|
+
const visibleShape = localRaphael.path(createPath(points));
|
|
819
|
+
|
|
820
|
+
return {
|
|
821
|
+
wrapper: wrapper,
|
|
822
|
+
visibleShape: visibleShape,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
scaledPath(points: Coord[], style?: StyleParams): RaphaelElement {
|
|
827
|
+
return this.withStyle(style, () => {
|
|
828
|
+
const p = this.raphael.path(
|
|
829
|
+
this.svgPath(points, /* alreadyScaled */ true),
|
|
607
830
|
);
|
|
608
|
-
|
|
831
|
+
p.graphiePath = points;
|
|
832
|
+
return p;
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
line(start: Coord, end: Coord, style?: StyleParams): RaphaelElement {
|
|
837
|
+
return this.withStyle(style, () => {
|
|
838
|
+
const l = this.unstyledPath([start, end]);
|
|
839
|
+
|
|
840
|
+
if (this.isMobile) {
|
|
841
|
+
l.node.style.shapeRendering = "crispEdges";
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return l;
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
parabola(
|
|
849
|
+
a: number,
|
|
850
|
+
b: number,
|
|
851
|
+
c: number,
|
|
852
|
+
style?: StyleParams,
|
|
853
|
+
): RaphaelElement {
|
|
854
|
+
return this.withStyle(style, () =>
|
|
855
|
+
this.raphael.path(this.svgParabolaPath(a, b, c)),
|
|
856
|
+
);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
fixedLine(start: Coord, end: Coord, thickness: number): PositionedShape {
|
|
860
|
+
// Apply padding to line
|
|
861
|
+
const padding: Coord = [thickness, thickness];
|
|
862
|
+
|
|
863
|
+
// Scale points to get values in pixels
|
|
864
|
+
start = this.scalePoint(start);
|
|
865
|
+
end = this.scalePoint(end);
|
|
866
|
+
|
|
867
|
+
// Calculate and apply additional offset
|
|
868
|
+
const extraOffset: Coord = [
|
|
869
|
+
Math.min(start[0], end[0]),
|
|
870
|
+
Math.min(start[1], end[1]),
|
|
871
|
+
];
|
|
872
|
+
|
|
873
|
+
// Apply padding and offset to start, end points
|
|
874
|
+
start = kvector.add(
|
|
875
|
+
kvector.subtract(start, extraOffset),
|
|
876
|
+
kvector.scale(padding, 0.5),
|
|
877
|
+
);
|
|
878
|
+
end = kvector.add(
|
|
879
|
+
kvector.subtract(end, extraOffset),
|
|
880
|
+
kvector.scale(padding, 0.5),
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
// Calculate <div> dimensions
|
|
884
|
+
const left = extraOffset[0] - padding[0] / 2;
|
|
885
|
+
const top = extraOffset[1] - padding[1] / 2;
|
|
886
|
+
const width = Math.abs(start[0] - end[0]) + padding[0];
|
|
887
|
+
const height = Math.abs(start[1] - end[1]) + padding[1];
|
|
888
|
+
|
|
889
|
+
// Create <div>
|
|
890
|
+
const wrapper = document.createElement("div");
|
|
891
|
+
$(wrapper).css({
|
|
892
|
+
position: "absolute",
|
|
893
|
+
width: width + "px",
|
|
894
|
+
height: height + "px",
|
|
895
|
+
left: left + "px",
|
|
896
|
+
top: top + "px",
|
|
897
|
+
// Outsiders should feel like the line's 'origin' (i.e., for
|
|
898
|
+
// rotation) is the starting point
|
|
899
|
+
transformOrigin: start[0] + "px " + start[1] + "px",
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
// Create Raphael canvas
|
|
903
|
+
const localRaphael = Raphael(wrapper, width, height);
|
|
904
|
+
|
|
905
|
+
// Calculate path
|
|
906
|
+
const path =
|
|
907
|
+
"M" + start[0] + " " + start[1] + " " + "L" + end[0] + " " + end[1];
|
|
908
|
+
const visibleShape = localRaphael.path(path);
|
|
909
|
+
visibleShape.graphiePath = [start, end];
|
|
910
|
+
|
|
911
|
+
return {
|
|
912
|
+
wrapper: wrapper,
|
|
913
|
+
visibleShape: visibleShape,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
sinusoid(
|
|
918
|
+
a: number,
|
|
919
|
+
b: number,
|
|
920
|
+
c: number,
|
|
921
|
+
d: number,
|
|
922
|
+
style?: StyleParams,
|
|
923
|
+
): RaphaelElement {
|
|
924
|
+
return this.withStyle(style, () =>
|
|
925
|
+
// Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
|
|
926
|
+
this.raphael.path(this.svgSinusoidPath(a, b, c, d)),
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
label: LabelMethod = (
|
|
931
|
+
point: Coord,
|
|
932
|
+
text: string,
|
|
933
|
+
direction:
|
|
934
|
+
| "center"
|
|
935
|
+
| "above"
|
|
936
|
+
| "below"
|
|
937
|
+
| "right"
|
|
938
|
+
| "left"
|
|
939
|
+
| "above right"
|
|
940
|
+
| "above left"
|
|
941
|
+
| "below right"
|
|
942
|
+
| "below left",
|
|
943
|
+
arg4?: boolean | StyleParams,
|
|
944
|
+
arg5?: StyleParams,
|
|
945
|
+
): any => {
|
|
946
|
+
const style = typeof arg4 === "object" ? arg4 : arg5;
|
|
947
|
+
const latex = typeof arg4 === "boolean" ? arg4 : true;
|
|
948
|
+
return this.withStyle(style, () => {
|
|
949
|
+
const $span = $("<span>").addClass("graphie-label");
|
|
950
|
+
|
|
951
|
+
const pad = this.currentStyle["label-distance"];
|
|
952
|
+
|
|
953
|
+
$span
|
|
954
|
+
.css({
|
|
955
|
+
position: "absolute",
|
|
956
|
+
padding: (pad != null ? pad : 7) + "px",
|
|
957
|
+
color: "black",
|
|
958
|
+
})
|
|
959
|
+
.data("labelDirection", direction)
|
|
960
|
+
.appendTo(this.el);
|
|
961
|
+
|
|
962
|
+
// @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
|
|
963
|
+
$span.setPosition = (point) => {
|
|
964
|
+
const scaledPoint = this.scalePoint(point);
|
|
965
|
+
$span.css({
|
|
966
|
+
left: scaledPoint[0],
|
|
967
|
+
top: scaledPoint[1],
|
|
968
|
+
});
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
// @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
|
|
972
|
+
$span.setPosition(point);
|
|
973
|
+
|
|
974
|
+
const span = $span[0];
|
|
975
|
+
|
|
976
|
+
// @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
|
|
977
|
+
$span.processMath = function (math, force) {
|
|
978
|
+
processMath(span, math, force, function () {
|
|
979
|
+
const width = span.scrollWidth;
|
|
980
|
+
const height = span.scrollHeight;
|
|
981
|
+
setLabelMargins(span, [width, height]);
|
|
982
|
+
});
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
|
|
986
|
+
$span.processText = function (text: any) {
|
|
987
|
+
$span.html(text);
|
|
988
|
+
const width = span.scrollWidth;
|
|
989
|
+
const height = span.scrollHeight;
|
|
990
|
+
setLabelMargins(span, [width, height]);
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
if (latex) {
|
|
994
|
+
// @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
|
|
995
|
+
$span.processMath(text, /* force */ false);
|
|
996
|
+
} else {
|
|
997
|
+
// @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
|
|
998
|
+
$span.processText(text);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return $span;
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
plotParametric(
|
|
1006
|
+
fn: (t: number) => Coord,
|
|
1007
|
+
range: Interval,
|
|
1008
|
+
style?: StyleParams,
|
|
1009
|
+
): RaphaelElement {
|
|
1010
|
+
return this.withStyle(style, () => {
|
|
1011
|
+
// We truncate to 500,000, since anything bigger causes
|
|
1012
|
+
// overflow in the firefox svg renderer. This is safe
|
|
1013
|
+
// since 500,000 is outside the viewport anyway. We
|
|
1014
|
+
// write these functions the way we do to handle undefined.
|
|
1015
|
+
const clip = (xy) => {
|
|
1016
|
+
if (Math.abs(xy[1]) > 500000) {
|
|
1017
|
+
return [xy[0], Math.min(Math.max(xy[1], -500000), 500000)];
|
|
1018
|
+
}
|
|
1019
|
+
return xy;
|
|
1020
|
+
};
|
|
1021
|
+
const clippedFn = (x) => clip(fn(x));
|
|
1022
|
+
|
|
1023
|
+
const min = range[0];
|
|
1024
|
+
const max = range[1];
|
|
1025
|
+
let step = (max - min) / (this.currentStyle["plot-points"] || 800);
|
|
1026
|
+
if (step === 0) {
|
|
1027
|
+
step = 1;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const paths = this.raphael.set();
|
|
1031
|
+
let points: Coord[] = [];
|
|
1032
|
+
let lastY = clippedFn(min)[1];
|
|
1033
|
+
|
|
1034
|
+
for (let t = min; t <= max; t += step) {
|
|
1035
|
+
const point = clippedFn(t);
|
|
1036
|
+
const y = point[1];
|
|
1037
|
+
|
|
1038
|
+
// Find points where it flips
|
|
1039
|
+
if (
|
|
1040
|
+
// if there is an asymptote here, meaning that the graph
|
|
1041
|
+
// switches signs and has a large difference
|
|
1042
|
+
(y > 0 !== lastY > 0 &&
|
|
1043
|
+
Math.abs(y - lastY) >
|
|
1044
|
+
2 * this.drawingTransform().pixelsPerUnitY()) ||
|
|
1045
|
+
// or the function is undefined
|
|
1046
|
+
isNaN(y)
|
|
1047
|
+
) {
|
|
1048
|
+
// split the path at this point, and draw it
|
|
1049
|
+
paths.push(this.unstyledPath(points));
|
|
1050
|
+
// restart the path, excluding this point
|
|
1051
|
+
points = [];
|
|
1052
|
+
} else {
|
|
1053
|
+
// otherwise, just add the point to the path
|
|
1054
|
+
points.push(point);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
lastY = y;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
paths.push(this.unstyledPath(points));
|
|
1061
|
+
|
|
1062
|
+
return paths;
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
plot(
|
|
1067
|
+
fn: (x: number) => number,
|
|
1068
|
+
range: Interval,
|
|
1069
|
+
style?: StyleParams,
|
|
1070
|
+
): RaphaelElement {
|
|
1071
|
+
return this.withStyle(style, () => {
|
|
1072
|
+
const min = range[0];
|
|
1073
|
+
const max = range[1];
|
|
1074
|
+
if (!this.currentStyle["plot-points"]) {
|
|
1075
|
+
this.currentStyle["plot-points"] =
|
|
1076
|
+
2 * (max - min) * this.drawingTransform().pixelsPerUnitX();
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const parametricFn = (x): Coord => [x, fn(x)];
|
|
1080
|
+
return this.plotParametric(parametricFn, range);
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
svgPath = (points: (Coord | true)[], alreadyScaled?: boolean) => {
|
|
1085
|
+
return points
|
|
1086
|
+
.map((point, i) => {
|
|
1087
|
+
if (point === true) {
|
|
1088
|
+
return "z";
|
|
1089
|
+
}
|
|
1090
|
+
const scaled = alreadyScaled ? point : this.scalePoint(point);
|
|
1091
|
+
return (
|
|
1092
|
+
(i === 0 ? "M" : "L") +
|
|
1093
|
+
KhanMath.bound(scaled[0]) +
|
|
1094
|
+
" " +
|
|
1095
|
+
KhanMath.bound(scaled[1])
|
|
1096
|
+
);
|
|
1097
|
+
})
|
|
1098
|
+
.join("");
|
|
609
1099
|
};
|
|
610
1100
|
|
|
611
1101
|
svgParabolaPath = (a: any, b: any, c: any) => {
|
|
@@ -615,11 +1105,10 @@ class Graphie {
|
|
|
615
1105
|
|
|
616
1106
|
// If points are collinear, plot a line instead
|
|
617
1107
|
if (a === 0) {
|
|
618
|
-
const points = [
|
|
1108
|
+
const points: Coord[] = [
|
|
619
1109
|
[this.bounds().xMin, computeParabola(this.bounds().xMin)],
|
|
620
1110
|
[this.bounds().xMax, computeParabola(this.bounds().xMax)],
|
|
621
1111
|
];
|
|
622
|
-
// @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
|
|
623
1112
|
return this.svgPath(points);
|
|
624
1113
|
}
|
|
625
1114
|
|
|
@@ -639,17 +1128,16 @@ class Graphie {
|
|
|
639
1128
|
const point = [xPoint, computeParabola(xPoint)];
|
|
640
1129
|
|
|
641
1130
|
// Calculate SVG 'control' point, defined by spec
|
|
642
|
-
const control = [vertex[0], vertex[1] - (point[1] - vertex[1])];
|
|
1131
|
+
const control: Coord = [vertex[0], vertex[1] - (point[1] - vertex[1])];
|
|
643
1132
|
|
|
644
1133
|
// Calculate mirror points across parabola's axis of symmetry
|
|
645
1134
|
const dx = Math.abs(vertex[0] - point[0]);
|
|
646
|
-
const left = [vertex[0] - dx, point[1]];
|
|
647
|
-
const right = [vertex[0] + dx, point[1]];
|
|
1135
|
+
const left: Coord = [vertex[0] - dx, point[1]];
|
|
1136
|
+
const right: Coord = [vertex[0] + dx, point[1]];
|
|
648
1137
|
|
|
649
1138
|
// Scale and bound
|
|
650
|
-
|
|
651
|
-
const
|
|
652
|
-
const values = _.map(_.flatten(points), KhanMath.bound);
|
|
1139
|
+
const points = [left, control, right].map(this.scalePoint);
|
|
1140
|
+
const values = points.flat().map(KhanMath.bound);
|
|
653
1141
|
return (
|
|
654
1142
|
"M" +
|
|
655
1143
|
values[0] +
|
|
@@ -666,7 +1154,7 @@ class Graphie {
|
|
|
666
1154
|
);
|
|
667
1155
|
};
|
|
668
1156
|
|
|
669
|
-
svgSinusoidPath = (a, b, c:
|
|
1157
|
+
svgSinusoidPath = (a: number, b: number, c: number, d: number) => {
|
|
670
1158
|
// Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
|
|
671
1159
|
const quarterPeriod = Math.abs(Math.PI / (2 * b));
|
|
672
1160
|
|
|
@@ -699,9 +1187,8 @@ class Graphie {
|
|
|
699
1187
|
computeSine(x1),
|
|
700
1188
|
];
|
|
701
1189
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
return _.map(_.zip(xCoords, yCoords), this.scalePoint);
|
|
1190
|
+
const points = kvector.zip(xCoords, yCoords);
|
|
1191
|
+
return points.map(this.scalePoint);
|
|
705
1192
|
};
|
|
706
1193
|
|
|
707
1194
|
// How many quarter-periods do we need to span the graph?
|
|
@@ -752,10 +1239,101 @@ class Graphie {
|
|
|
752
1239
|
return path;
|
|
753
1240
|
};
|
|
754
1241
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1242
|
+
private withStyle<T>(style: StyleParams | undefined, fn: () => T): T {
|
|
1243
|
+
const oldStyle = this.currentStyle;
|
|
1244
|
+
this.currentStyle = {
|
|
1245
|
+
...this.currentStyle,
|
|
1246
|
+
...this.processAttributes(style),
|
|
1247
|
+
};
|
|
1248
|
+
const result = this.postprocessDrawingResult(fn());
|
|
1249
|
+
this.currentStyle = oldStyle;
|
|
1250
|
+
return result;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
private postprocessDrawingResult(result: any): any {
|
|
1254
|
+
// Bad heuristic for recognizing Raphael elements and sets
|
|
1255
|
+
const type = result.constructor.prototype;
|
|
1256
|
+
if (type === Raphael.el || type === Raphael.st) {
|
|
1257
|
+
result.attr(this.currentStyle);
|
|
1258
|
+
|
|
1259
|
+
if (this.currentStyle.arrows) {
|
|
1260
|
+
result = this.addArrowheads(result);
|
|
1261
|
+
}
|
|
1262
|
+
} else if (result instanceof $) {
|
|
1263
|
+
// We assume that if it's not a Raphael element/set, it
|
|
1264
|
+
// does not contain SVG.
|
|
1265
|
+
// @ts-expect-error - TS2339 - Property 'css' does not exist on type '{}'.
|
|
1266
|
+
result.css({
|
|
1267
|
+
...this.currentStyle,
|
|
1268
|
+
...SVG_SPECIFIC_STYLE_MASK,
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
return result;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
private addArrowheads(path: any) {
|
|
1276
|
+
const type = path.constructor.prototype;
|
|
1277
|
+
|
|
1278
|
+
if (type === Raphael.el) {
|
|
1279
|
+
if (
|
|
1280
|
+
path.type === "path" &&
|
|
1281
|
+
typeof path.arrowheadsDrawn === "undefined"
|
|
1282
|
+
) {
|
|
1283
|
+
const w = path.attr("stroke-width");
|
|
1284
|
+
const s = 0.6 + 0.4 * w;
|
|
1285
|
+
const l = path.getTotalLength();
|
|
1286
|
+
const set = this.raphael.set();
|
|
1287
|
+
const head = this.raphael.path(
|
|
1288
|
+
this.isMobile
|
|
1289
|
+
? "M-4,4 C-4,4 -0.25,0 -0.25,0 C-0.25,0 -4,-4 -4,-4"
|
|
1290
|
+
: "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4",
|
|
1291
|
+
);
|
|
1292
|
+
const end = path.getPointAtLength(l - 0.4);
|
|
1293
|
+
const almostTheEnd = path.getPointAtLength(l - 0.75 * s);
|
|
1294
|
+
const angle =
|
|
1295
|
+
(Math.atan2(
|
|
1296
|
+
end.y - almostTheEnd.y,
|
|
1297
|
+
end.x - almostTheEnd.x,
|
|
1298
|
+
) *
|
|
1299
|
+
180) /
|
|
1300
|
+
Math.PI;
|
|
1301
|
+
const attrs = path.attr();
|
|
1302
|
+
delete attrs.path;
|
|
1303
|
+
|
|
1304
|
+
let subpath = path.getSubpath(0, l - 0.75 * s);
|
|
1305
|
+
subpath = this.raphael.path(subpath).attr(attrs);
|
|
1306
|
+
subpath.arrowheadsDrawn = true;
|
|
1307
|
+
path.remove();
|
|
1308
|
+
|
|
1309
|
+
// For some unknown reason 0 doesn't work for the rotation
|
|
1310
|
+
// origin so we use a tiny number.
|
|
1311
|
+
head.rotate(angle, this.isMobile ? 1e-5 : 0.75, 0)
|
|
1312
|
+
.scale(s, s, 0.75, 0)
|
|
1313
|
+
.translate(almostTheEnd.x, almostTheEnd.y)
|
|
1314
|
+
.attr(attrs)
|
|
1315
|
+
.attr({
|
|
1316
|
+
"stroke-linejoin": "round",
|
|
1317
|
+
"stroke-linecap": "round",
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
head.arrowheadsDrawn = true;
|
|
1321
|
+
set.push(subpath);
|
|
1322
|
+
set.push(head);
|
|
1323
|
+
return set;
|
|
1324
|
+
}
|
|
1325
|
+
} else if (type === Raphael.st) {
|
|
1326
|
+
for (let i = 0, l = path.items.length; i < l; i++) {
|
|
1327
|
+
this.addArrowheads(path.items[i]);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return path;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
scalePoint = (point: number | Coord): Coord => {
|
|
1334
|
+
return this.drawingTransform().scalePoint(point);
|
|
1335
|
+
};
|
|
1336
|
+
|
|
759
1337
|
scaleVector = (point: number | Coord) => {
|
|
760
1338
|
return this.drawingTransform().scaleVector(point);
|
|
761
1339
|
};
|
|
@@ -768,7 +1346,7 @@ class Graphie {
|
|
|
768
1346
|
return this.drawingTransform().unscaleVector(point);
|
|
769
1347
|
};
|
|
770
1348
|
|
|
771
|
-
processAttributes(attrs: any) {
|
|
1349
|
+
private processAttributes(attrs: any) {
|
|
772
1350
|
const transformers = {
|
|
773
1351
|
scale: (scale) => {
|
|
774
1352
|
this.drawingTransform().setScale(scale);
|
|
@@ -805,14 +1383,13 @@ class Graphie {
|
|
|
805
1383
|
} as const;
|
|
806
1384
|
|
|
807
1385
|
const processed: Record<string, any> = {};
|
|
808
|
-
|
|
1386
|
+
Object.entries(attrs || {}).forEach(function ([key, value]) {
|
|
809
1387
|
const transformer = transformers[key];
|
|
810
1388
|
|
|
811
1389
|
if (typeof transformer === "function") {
|
|
812
|
-
|
|
1390
|
+
Object.assign(processed, transformer(value));
|
|
813
1391
|
} else {
|
|
814
|
-
const dasherized = key
|
|
815
|
-
// @ts-expect-error - TS2339 - Property 'replace' does not exist on type 'string | number | symbol'.
|
|
1392
|
+
const dasherized = String(key)
|
|
816
1393
|
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
|
|
817
1394
|
.replace(/([a-z\d])([A-Z])/g, "$1-$2")
|
|
818
1395
|
.toLowerCase();
|
|
@@ -824,8 +1401,6 @@ class Graphie {
|
|
|
824
1401
|
}
|
|
825
1402
|
}
|
|
826
1403
|
|
|
827
|
-
GraphUtils.Graphie = Graphie;
|
|
828
|
-
|
|
829
1404
|
const labelDirections = {
|
|
830
1405
|
center: [-0.5, -0.5],
|
|
831
1406
|
above: [-0.5, -1.0],
|
|
@@ -838,689 +1413,162 @@ const labelDirections = {
|
|
|
838
1413
|
"above left": [-1.0, -1.0],
|
|
839
1414
|
} as const;
|
|
840
1415
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
[width, height] = [1, 1];
|
|
866
|
-
Log.log("Label size was 0x0 in graphie.js; using 1x1 instead");
|
|
867
|
-
}
|
|
868
|
-
$span.css("visibility", "");
|
|
869
|
-
|
|
870
|
-
if (typeof direction === "number") {
|
|
871
|
-
const x = Math.cos(direction);
|
|
872
|
-
const y = Math.sin(direction);
|
|
873
|
-
|
|
874
|
-
const scale = Math.min(
|
|
875
|
-
width / 2 / Math.abs(x),
|
|
876
|
-
height / 2 / Math.abs(y),
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
$span.css({
|
|
880
|
-
marginLeft: -width / 2 + x * scale,
|
|
881
|
-
marginTop: -height / 2 - y * scale,
|
|
882
|
-
});
|
|
1416
|
+
// A range for a Graphie can be specified in one of three forms:
|
|
1417
|
+
// - A pair of intervals, [[xMin, xMax], [yMin, yMax]]. This is the
|
|
1418
|
+
// "normalized" form.
|
|
1419
|
+
// - A pair of magnitudes for the x and y ranges, respectively. E.g. [5, 10]
|
|
1420
|
+
// is equivalent to [[-5, 5], [-10, 10]].
|
|
1421
|
+
// - A single magnitude to be used for both x and y. E.g. 10 is equivalent to
|
|
1422
|
+
// [[-10, 10], [-10, 10]]
|
|
1423
|
+
type RangeSpecifier = [Interval, Interval] | Coord | number;
|
|
1424
|
+
|
|
1425
|
+
// Overload normalizeRange to specify that it only returns undefined if its
|
|
1426
|
+
// argument is undefined.
|
|
1427
|
+
export function normalizeRange(range: RangeSpecifier): [Interval, Interval];
|
|
1428
|
+
export function normalizeRange(
|
|
1429
|
+
range: RangeSpecifier | undefined,
|
|
1430
|
+
): [Interval, Interval] | undefined;
|
|
1431
|
+
|
|
1432
|
+
// normalizeRange converts a RangeSpecifier into a pair of Intervals that give
|
|
1433
|
+
// the x and y ranges, respectively
|
|
1434
|
+
export function normalizeRange(
|
|
1435
|
+
range: RangeSpecifier | undefined,
|
|
1436
|
+
): [Interval, Interval] | undefined {
|
|
1437
|
+
function normalizeInterval(magnitude: number | Interval): Interval {
|
|
1438
|
+
if (typeof magnitude === "number") {
|
|
1439
|
+
return [-magnitude, magnitude];
|
|
883
1440
|
} else {
|
|
884
|
-
|
|
885
|
-
$span.css({
|
|
886
|
-
marginLeft: Math.round(width * multipliers[0]),
|
|
887
|
-
marginTop: Math.round(height * multipliers[1]),
|
|
888
|
-
});
|
|
1441
|
+
return magnitude;
|
|
889
1442
|
}
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
// `svgPath` is independent of graphie range, so we export it independently
|
|
893
|
-
GraphUtils.svgPath = thisGraphie.svgPath;
|
|
894
|
-
|
|
895
|
-
const addArrowheads = function arrows(path: any) {
|
|
896
|
-
const type = path.constructor.prototype;
|
|
897
|
-
|
|
898
|
-
if (type === Raphael.el) {
|
|
899
|
-
if (
|
|
900
|
-
path.type === "path" &&
|
|
901
|
-
typeof path.arrowheadsDrawn === "undefined"
|
|
902
|
-
) {
|
|
903
|
-
const w = path.attr("stroke-width");
|
|
904
|
-
const s = 0.6 + 0.4 * w;
|
|
905
|
-
const l = path.getTotalLength();
|
|
906
|
-
const set = thisGraphie.raphael.set();
|
|
907
|
-
const head = thisGraphie.raphael.path(
|
|
908
|
-
thisGraphie.isMobile
|
|
909
|
-
? "M-4,4 C-4,4 -0.25,0 -0.25,0 C-0.25,0 -4,-4 -4,-4"
|
|
910
|
-
: "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4",
|
|
911
|
-
);
|
|
912
|
-
const end = path.getPointAtLength(l - 0.4);
|
|
913
|
-
const almostTheEnd = path.getPointAtLength(l - 0.75 * s);
|
|
914
|
-
const angle =
|
|
915
|
-
(Math.atan2(
|
|
916
|
-
end.y - almostTheEnd.y,
|
|
917
|
-
end.x - almostTheEnd.x,
|
|
918
|
-
) *
|
|
919
|
-
180) /
|
|
920
|
-
Math.PI;
|
|
921
|
-
const attrs = path.attr();
|
|
922
|
-
delete attrs.path;
|
|
923
|
-
|
|
924
|
-
let subpath = path.getSubpath(0, l - 0.75 * s);
|
|
925
|
-
subpath = thisGraphie.raphael.path(subpath).attr(attrs);
|
|
926
|
-
subpath.arrowheadsDrawn = true;
|
|
927
|
-
path.remove();
|
|
928
|
-
|
|
929
|
-
// For some unknown reason 0 doesn't work for the rotation
|
|
930
|
-
// origin so we use a tiny number.
|
|
931
|
-
head.rotate(angle, thisGraphie.isMobile ? 1e-5 : 0.75, 0)
|
|
932
|
-
.scale(s, s, 0.75, 0)
|
|
933
|
-
.translate(almostTheEnd.x, almostTheEnd.y)
|
|
934
|
-
.attr(attrs)
|
|
935
|
-
.attr({
|
|
936
|
-
"stroke-linejoin": "round",
|
|
937
|
-
"stroke-linecap": "round",
|
|
938
|
-
});
|
|
939
|
-
|
|
940
|
-
head.arrowheadsDrawn = true;
|
|
941
|
-
set.push(subpath);
|
|
942
|
-
set.push(head);
|
|
943
|
-
return set;
|
|
944
|
-
}
|
|
945
|
-
} else if (type === Raphael.st) {
|
|
946
|
-
for (let i = 0, l = path.items.length; i < l; i++) {
|
|
947
|
-
arrows(path.items[i]);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
return path;
|
|
951
|
-
};
|
|
952
|
-
|
|
953
|
-
function circle(center, radius) {
|
|
954
|
-
return thisGraphie.raphael.ellipse(
|
|
955
|
-
...thisGraphie
|
|
956
|
-
.scalePoint(center)
|
|
957
|
-
.concat(thisGraphie.scaleVector([radius, radius])),
|
|
958
|
-
);
|
|
959
1443
|
}
|
|
960
1444
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (thisGraphie.isMobile) {
|
|
969
|
-
elem.node.style.shapeRendering = "crispEdges";
|
|
1445
|
+
function getXAndYRanges(
|
|
1446
|
+
range: RangeSpecifier,
|
|
1447
|
+
): Coord | [Interval, Interval] {
|
|
1448
|
+
if (Array.isArray(range)) {
|
|
1449
|
+
return range;
|
|
1450
|
+
} else {
|
|
1451
|
+
return [range, range];
|
|
970
1452
|
}
|
|
971
|
-
|
|
972
|
-
return elem;
|
|
973
1453
|
}
|
|
974
1454
|
|
|
975
|
-
|
|
976
|
-
return
|
|
977
|
-
...thisGraphie
|
|
978
|
-
.scalePoint(center)
|
|
979
|
-
.concat(thisGraphie.scaleVector(radii)),
|
|
980
|
-
);
|
|
1455
|
+
if (range == null) {
|
|
1456
|
+
return range;
|
|
981
1457
|
}
|
|
982
1458
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
radii: number | [number, number],
|
|
987
|
-
maxScale: number,
|
|
988
|
-
padding: number,
|
|
989
|
-
) {
|
|
990
|
-
// Scale point and radius
|
|
991
|
-
const scaledPoint = thisGraphie.scalePoint(center);
|
|
992
|
-
const scaledRadii = thisGraphie.scaleVector(radii);
|
|
993
|
-
|
|
994
|
-
const width = 2 * scaledRadii[0] * maxScale + padding;
|
|
995
|
-
const height = 2 * scaledRadii[1] * maxScale + padding;
|
|
996
|
-
|
|
997
|
-
// Calculate absolute left, top
|
|
998
|
-
const left = scaledPoint[0] - width / 2;
|
|
999
|
-
const top = scaledPoint[1] - height / 2;
|
|
1000
|
-
|
|
1001
|
-
// Wrap in <div>
|
|
1002
|
-
const wrapper = document.createElement("div");
|
|
1003
|
-
$(wrapper).css({
|
|
1004
|
-
position: "absolute",
|
|
1005
|
-
width: width + "px",
|
|
1006
|
-
height: height + "px",
|
|
1007
|
-
left: left + "px",
|
|
1008
|
-
top: top + "px",
|
|
1009
|
-
});
|
|
1010
|
-
// wrapper.setAttribute("data-graphie-type", "ellipse");
|
|
1011
|
-
|
|
1012
|
-
// Create Raphael canvas
|
|
1013
|
-
const localRaphael = Raphael(wrapper, width, height);
|
|
1014
|
-
const visibleShape = localRaphael.ellipse(
|
|
1015
|
-
width / 2,
|
|
1016
|
-
height / 2,
|
|
1017
|
-
scaledRadii[0],
|
|
1018
|
-
scaledRadii[1],
|
|
1019
|
-
);
|
|
1020
|
-
|
|
1021
|
-
return {
|
|
1022
|
-
wrapper: wrapper,
|
|
1023
|
-
visibleShape: visibleShape,
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
function arc(center, radius, startAngle, endAngle, sector) {
|
|
1028
|
-
startAngle = ((startAngle % 360) + 360) % 360;
|
|
1029
|
-
endAngle = ((endAngle % 360) + 360) % 360;
|
|
1030
|
-
|
|
1031
|
-
const cent = thisGraphie.scalePoint(center);
|
|
1032
|
-
const radii = thisGraphie.scaleVector(radius);
|
|
1033
|
-
const startVector = polar(radius, startAngle);
|
|
1034
|
-
const endVector = polar(radius, endAngle);
|
|
1035
|
-
|
|
1036
|
-
const startPoint = thisGraphie.scalePoint([
|
|
1037
|
-
center[0] + startVector[0],
|
|
1038
|
-
center[1] + startVector[1],
|
|
1039
|
-
]);
|
|
1040
|
-
const endPoint = thisGraphie.scalePoint([
|
|
1041
|
-
center[0] + endVector[0],
|
|
1042
|
-
center[1] + endVector[1],
|
|
1043
|
-
]);
|
|
1044
|
-
|
|
1045
|
-
const largeAngle = (((endAngle - startAngle) % 360) + 360) % 360 > 180;
|
|
1046
|
-
|
|
1047
|
-
return thisGraphie.raphael.path(
|
|
1048
|
-
"M" +
|
|
1049
|
-
startPoint.join(" ") +
|
|
1050
|
-
"A" +
|
|
1051
|
-
radii.join(" ") +
|
|
1052
|
-
" 0 " + // ellipse rotation
|
|
1053
|
-
(largeAngle ? 1 : 0) +
|
|
1054
|
-
" 0 " + // sweep flag
|
|
1055
|
-
endPoint.join(" ") +
|
|
1056
|
-
(sector ? "L" + cent.join(" ") + "z" : ""),
|
|
1057
|
-
);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
function path(points) {
|
|
1061
|
-
// @ts-expect-error - TS2554 - Expected 2 arguments, but got 1.
|
|
1062
|
-
const p = thisGraphie.raphael.path(thisGraphie.svgPath(points));
|
|
1063
|
-
p.graphiePath = points;
|
|
1064
|
-
|
|
1065
|
-
return p;
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
function fixedPath(points, center, createPath) {
|
|
1069
|
-
points = _.map(points, thisGraphie.scalePoint);
|
|
1070
|
-
center = center ? thisGraphie.scalePoint(center) : null;
|
|
1071
|
-
createPath = createPath || thisGraphie.svgPath;
|
|
1072
|
-
|
|
1073
|
-
const pathLeft = _.min(_.pluck(points, 0));
|
|
1074
|
-
const pathRight = _.max(_.pluck(points, 0));
|
|
1075
|
-
const pathTop = _.min(_.pluck(points, 1));
|
|
1076
|
-
const pathBottom = _.max(_.pluck(points, 1));
|
|
1077
|
-
|
|
1078
|
-
// Apply padding to line
|
|
1079
|
-
const padding = [4, 4];
|
|
1080
|
-
|
|
1081
|
-
// Calculate and apply additional offset
|
|
1082
|
-
const extraOffset = [pathLeft, pathTop];
|
|
1083
|
-
|
|
1084
|
-
// Apply padding and offset to points
|
|
1085
|
-
points = _.map(points, function (point) {
|
|
1086
|
-
return kvector.add(
|
|
1087
|
-
kvector.subtract(point, extraOffset),
|
|
1088
|
-
kvector.scale(padding, 0.5),
|
|
1089
|
-
);
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
// Calculate <div> dimensions
|
|
1093
|
-
const width = pathRight - pathLeft + padding[0];
|
|
1094
|
-
const height = pathBottom - pathTop + padding[1];
|
|
1095
|
-
const left = extraOffset[0] - padding[0] / 2;
|
|
1096
|
-
const top = extraOffset[1] - padding[1] / 2;
|
|
1097
|
-
|
|
1098
|
-
// Create <div>
|
|
1099
|
-
const wrapper = document.createElement("div");
|
|
1100
|
-
$(wrapper).css({
|
|
1101
|
-
position: "absolute",
|
|
1102
|
-
width: width + "px",
|
|
1103
|
-
height: height + "px",
|
|
1104
|
-
left: left + "px",
|
|
1105
|
-
top: top + "px",
|
|
1106
|
-
// If user specified a center, set it
|
|
1107
|
-
// NOTE(kevinb): jQuery doesn't like that `transformOrigin `could be `null`
|
|
1108
|
-
// so we cast to `any` here.
|
|
1109
|
-
transformOrigin: center
|
|
1110
|
-
? width / 2 +
|
|
1111
|
-
center[0] +
|
|
1112
|
-
"px " +
|
|
1113
|
-
(height / 2 + center[1]) +
|
|
1114
|
-
"px"
|
|
1115
|
-
: null,
|
|
1116
|
-
} as any);
|
|
1117
|
-
|
|
1118
|
-
// Create Raphael canvas
|
|
1119
|
-
const localRaphael = Raphael(wrapper, width, height);
|
|
1120
|
-
|
|
1121
|
-
// Calculate path
|
|
1122
|
-
const visibleShape = localRaphael.path(createPath(points));
|
|
1123
|
-
|
|
1124
|
-
return {
|
|
1125
|
-
wrapper: wrapper,
|
|
1126
|
-
visibleShape: visibleShape,
|
|
1127
|
-
};
|
|
1128
|
-
}
|
|
1459
|
+
const [xRange, yRange] = getXAndYRanges(range);
|
|
1460
|
+
return [normalizeInterval(xRange), normalizeInterval(yRange)];
|
|
1461
|
+
}
|
|
1129
1462
|
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
);
|
|
1134
|
-
p.graphiePath = points;
|
|
1135
|
-
return p;
|
|
1463
|
+
function toPair(x: number | [number, number]): [number, number] {
|
|
1464
|
+
if (Array.isArray(x)) {
|
|
1465
|
+
return x;
|
|
1136
1466
|
}
|
|
1467
|
+
return [x, x];
|
|
1468
|
+
}
|
|
1137
1469
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1470
|
+
/**
|
|
1471
|
+
* Safari applies some SVG-specific styles to things that are not SVGs, so we
|
|
1472
|
+
* need to exclude those styles from things that are not SVGs.
|
|
1473
|
+
*
|
|
1474
|
+
* To see this behavior in action, open https://codepen.io/anon/pen/zENEoa in
|
|
1475
|
+
* Safari.
|
|
1476
|
+
*
|
|
1477
|
+
* Usage `{...someStyles, ...SVG_SPECIFIC_STYLE_MASK}`
|
|
1478
|
+
*/
|
|
1479
|
+
const SVG_SPECIFIC_STYLE_MASK = {
|
|
1480
|
+
"stroke-width": null,
|
|
1481
|
+
} as const;
|
|
1147
1482
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1483
|
+
const setLabelMargins = function (span: HTMLElement, size: Coord): void {
|
|
1484
|
+
const $span = $(span);
|
|
1485
|
+
const direction = $span.data("labelDirection");
|
|
1486
|
+
let [width, height] = size;
|
|
1487
|
+
// This can happen when a span
|
|
1488
|
+
// is invisible but we still want to update the CSS. At worst, we will
|
|
1489
|
+
// be off by a few pixels instead of in a different position entirely.
|
|
1490
|
+
if (width === 0 && height === 0) {
|
|
1491
|
+
[width, height] = [1, 1];
|
|
1492
|
+
Log.log("Label size was 0x0 in graphie.js; using 1x1 instead");
|
|
1151
1493
|
}
|
|
1494
|
+
$span.css("visibility", "");
|
|
1152
1495
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
const
|
|
1156
|
-
|
|
1157
|
-
// Scale points to get values in pixels
|
|
1158
|
-
start = thisGraphie.scalePoint(start);
|
|
1159
|
-
end = thisGraphie.scalePoint(end);
|
|
1496
|
+
if (typeof direction === "number") {
|
|
1497
|
+
const x = Math.cos(direction);
|
|
1498
|
+
const y = Math.sin(direction);
|
|
1160
1499
|
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
Math.
|
|
1164
|
-
Math.min(start[1], end[1]),
|
|
1165
|
-
];
|
|
1166
|
-
|
|
1167
|
-
// Apply padding and offset to start, end points
|
|
1168
|
-
start = kvector.add(
|
|
1169
|
-
kvector.subtract(start, extraOffset),
|
|
1170
|
-
kvector.scale(padding, 0.5),
|
|
1500
|
+
const scale = Math.min(
|
|
1501
|
+
width / 2 / Math.abs(x),
|
|
1502
|
+
height / 2 / Math.abs(y),
|
|
1171
1503
|
);
|
|
1172
|
-
end = kvector.add(
|
|
1173
|
-
kvector.subtract(end, extraOffset),
|
|
1174
|
-
kvector.scale(padding, 0.5),
|
|
1175
|
-
);
|
|
1176
|
-
|
|
1177
|
-
// Calculate <div> dimensions
|
|
1178
|
-
const left = extraOffset[0] - padding[0] / 2;
|
|
1179
|
-
const top = extraOffset[1] - padding[1] / 2;
|
|
1180
|
-
const width = Math.abs(start[0] - end[0]) + padding[0];
|
|
1181
|
-
const height = Math.abs(start[1] - end[1]) + padding[1];
|
|
1182
1504
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
// rotation) is the starting point
|
|
1193
|
-
transformOrigin: start[0] + "px " + start[1] + "px",
|
|
1505
|
+
$span.css({
|
|
1506
|
+
marginLeft: -width / 2 + x * scale,
|
|
1507
|
+
marginTop: -height / 2 - y * scale,
|
|
1508
|
+
});
|
|
1509
|
+
} else {
|
|
1510
|
+
const multipliers = labelDirections[direction || "center"];
|
|
1511
|
+
$span.css({
|
|
1512
|
+
marginLeft: Math.round(width * multipliers[0]),
|
|
1513
|
+
marginTop: Math.round(height * multipliers[1]),
|
|
1194
1514
|
});
|
|
1195
|
-
|
|
1196
|
-
// Create Raphael canvas
|
|
1197
|
-
const localRaphael = Raphael(wrapper, width, height);
|
|
1198
|
-
|
|
1199
|
-
// Calculate path
|
|
1200
|
-
const path =
|
|
1201
|
-
"M" + start[0] + " " + start[1] + " " + "L" + end[0] + " " + end[1];
|
|
1202
|
-
const visibleShape = localRaphael.path(path);
|
|
1203
|
-
visibleShape.graphiePath = [start, end];
|
|
1204
|
-
|
|
1205
|
-
return {
|
|
1206
|
-
wrapper: wrapper,
|
|
1207
|
-
visibleShape: visibleShape,
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
function sinusoid(a, b, c, d) {
|
|
1212
|
-
// Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
|
|
1213
|
-
return thisGraphie.raphael.path(
|
|
1214
|
-
thisGraphie.svgSinusoidPath(a, b, c, d),
|
|
1215
|
-
);
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
function grid(xr, yr) {
|
|
1219
|
-
const step: any = thisGraphie.currentStyle.step || [1, 1];
|
|
1220
|
-
const set = thisGraphie.raphael.set();
|
|
1221
|
-
|
|
1222
|
-
let x = step[0] * Math.ceil(xr[0] / step[0]);
|
|
1223
|
-
for (; x <= xr[1]; x += step[0]) {
|
|
1224
|
-
set.push(line([x, yr[0]], [x, yr[1]]));
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
let y = step[1] * Math.ceil(yr[0] / step[1]);
|
|
1228
|
-
for (; y <= yr[1]; y += step[1]) {
|
|
1229
|
-
set.push(line([xr[0], y], [xr[1], y]));
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
return set;
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
function label(point, text, direction, latex) {
|
|
1236
|
-
latex = typeof latex === "undefined" || latex;
|
|
1237
|
-
|
|
1238
|
-
const $span = $("<span>").addClass("graphie-label");
|
|
1239
|
-
|
|
1240
|
-
const pad = thisGraphie.currentStyle["label-distance"];
|
|
1241
|
-
|
|
1242
|
-
$span
|
|
1243
|
-
.css(
|
|
1244
|
-
$.extend(
|
|
1245
|
-
{},
|
|
1246
|
-
{
|
|
1247
|
-
position: "absolute",
|
|
1248
|
-
padding: (pad != null ? pad : 7) + "px",
|
|
1249
|
-
color: "black",
|
|
1250
|
-
},
|
|
1251
|
-
),
|
|
1252
|
-
)
|
|
1253
|
-
.data("labelDirection", direction)
|
|
1254
|
-
.appendTo(thisGraphie.el);
|
|
1255
|
-
|
|
1256
|
-
// @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
|
|
1257
|
-
$span.setPosition = function (point) {
|
|
1258
|
-
const scaledPoint = thisGraphie.scalePoint(point);
|
|
1259
|
-
$span.css({
|
|
1260
|
-
left: scaledPoint[0],
|
|
1261
|
-
top: scaledPoint[1],
|
|
1262
|
-
});
|
|
1263
|
-
};
|
|
1264
|
-
|
|
1265
|
-
// @ts-expect-error - TS2339 - Property 'setPosition' does not exist on type 'JQuery<HTMLElement>'.
|
|
1266
|
-
$span.setPosition(point);
|
|
1267
|
-
|
|
1268
|
-
const span = $span[0];
|
|
1269
|
-
|
|
1270
|
-
// @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
|
|
1271
|
-
$span.processMath = function (math, force) {
|
|
1272
|
-
processMath(span, math, force, function () {
|
|
1273
|
-
const width = span.scrollWidth;
|
|
1274
|
-
const height = span.scrollHeight;
|
|
1275
|
-
setLabelMargins(span, [width, height]);
|
|
1276
|
-
});
|
|
1277
|
-
};
|
|
1278
|
-
|
|
1279
|
-
// @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
|
|
1280
|
-
$span.processText = function (text: any) {
|
|
1281
|
-
$span.html(text);
|
|
1282
|
-
const width = span.scrollWidth;
|
|
1283
|
-
const height = span.scrollHeight;
|
|
1284
|
-
setLabelMargins(span, [width, height]);
|
|
1285
|
-
};
|
|
1286
|
-
|
|
1287
|
-
if (latex) {
|
|
1288
|
-
// @ts-expect-error - TS2339 - Property 'processMath' does not exist on type 'JQuery<HTMLElement>'.
|
|
1289
|
-
$span.processMath(text, /* force */ false);
|
|
1290
|
-
} else {
|
|
1291
|
-
// @ts-expect-error - TS2339 - Property 'processText' does not exist on type 'JQuery<HTMLElement>'.
|
|
1292
|
-
$span.processText(text);
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
return $span;
|
|
1296
1515
|
}
|
|
1516
|
+
};
|
|
1297
1517
|
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
range,
|
|
1301
|
-
shade,
|
|
1302
|
-
fn2: (t: number) => Coord = (t) => [t, 0],
|
|
1303
|
-
) {
|
|
1304
|
-
// Note: fn2 should only be set if 'shade' is true, as it denotes
|
|
1305
|
-
// the function between which fn should have its area shaded.
|
|
1306
|
-
// In general, plotParametric shouldn't be used to shade the area
|
|
1307
|
-
// between two arbitrary parametrics functions over an interval,
|
|
1308
|
-
// as the method assumes that fn and fn2 are both of the form
|
|
1309
|
-
// fn(t) = (t, fn'(t)) for some initial fn'.
|
|
1310
|
-
|
|
1311
|
-
// We truncate to 500,000, since anything bigger causes
|
|
1312
|
-
// overflow in the firefox svg renderer. This is safe
|
|
1313
|
-
// since 500,000 is outside the viewport anyway. We
|
|
1314
|
-
// write these functions the way we do to handle undefined.
|
|
1315
|
-
const clipper = (xy) => {
|
|
1316
|
-
if (Math.abs(xy[1]) > 500000) {
|
|
1317
|
-
return [xy[0], Math.min(Math.max(xy[1], -500000), 500000)];
|
|
1318
|
-
}
|
|
1319
|
-
return xy;
|
|
1320
|
-
};
|
|
1321
|
-
const clippedFn = (x) => clipper(fn(x));
|
|
1322
|
-
const clippedFn2 = (x: number) => clipper(fn2(x));
|
|
1518
|
+
const GraphUtils: any = {
|
|
1519
|
+
Graphie,
|
|
1323
1520
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
if (!thisGraphie.currentStyle.strokeLinecap) {
|
|
1328
|
-
thisGraphie.currentStyle.strokeLinecap = "round";
|
|
1329
|
-
}
|
|
1521
|
+
createGraphie: function (el: any): Graphie {
|
|
1522
|
+
return new Graphie(el);
|
|
1523
|
+
},
|
|
1330
1524
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
step = 1;
|
|
1525
|
+
unscaledSvgPath: function (points) {
|
|
1526
|
+
// If this is an empty closed path, return "" instead of "z", which
|
|
1527
|
+
// would give an error
|
|
1528
|
+
if (points[0] === true) {
|
|
1529
|
+
return "";
|
|
1337
1530
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
let lastFlip = min;
|
|
1344
|
-
for (let t = min; t <= max; t += step) {
|
|
1345
|
-
const top = clippedFn(t);
|
|
1346
|
-
const bottom = clippedFn2(t);
|
|
1347
|
-
const diff = GraphUtils.coordDiff(top, bottom);
|
|
1348
|
-
|
|
1349
|
-
// Find points where it flips
|
|
1350
|
-
// Create path that sketches area between the two functions
|
|
1351
|
-
if (
|
|
1352
|
-
// if there is an asymptote here, meaning that the graph
|
|
1353
|
-
// switches signs and has a large difference
|
|
1354
|
-
(diff[1] < 0 !== lastDiff[1] < 0 &&
|
|
1355
|
-
Math.abs(diff[1] - lastDiff[1]) >
|
|
1356
|
-
2 * thisGraphie.drawingTransform().pixelsPerUnitY()) ||
|
|
1357
|
-
// or the function is undefined
|
|
1358
|
-
isNaN(diff[1])
|
|
1359
|
-
) {
|
|
1360
|
-
// split the path at this point, and draw it
|
|
1361
|
-
if (shade) {
|
|
1362
|
-
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1363
|
-
points.push(top);
|
|
1364
|
-
|
|
1365
|
-
// backtrack to draw paired function
|
|
1366
|
-
for (let u = t - step; u >= lastFlip; u -= step) {
|
|
1367
|
-
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1368
|
-
points.push(clippedFn2(u));
|
|
1369
|
-
}
|
|
1370
|
-
lastFlip = t;
|
|
1371
|
-
}
|
|
1372
|
-
paths.push(path(points));
|
|
1373
|
-
// restart the path, excluding this point
|
|
1374
|
-
points = [];
|
|
1375
|
-
if (shade) {
|
|
1376
|
-
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1377
|
-
points.push(top);
|
|
1531
|
+
return points
|
|
1532
|
+
.map(function (point, i) {
|
|
1533
|
+
if (point === true) {
|
|
1534
|
+
return "z";
|
|
1378
1535
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
lastDiff = diff;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
if (shade) {
|
|
1389
|
-
// backtrack to draw paired function
|
|
1390
|
-
for (let u = max - step; u >= lastFlip; u -= step) {
|
|
1391
|
-
// @ts-expect-error - TS2345 - Argument of type 'any' is not assignable to parameter of type 'never'.
|
|
1392
|
-
points.push(clippedFn2(u));
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
paths.push(path(points));
|
|
1536
|
+
return (i === 0 ? "M" : "L") + point[0] + " " + point[1];
|
|
1537
|
+
})
|
|
1538
|
+
.join("");
|
|
1539
|
+
},
|
|
1396
1540
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1541
|
+
getDistance: function (point1, point2) {
|
|
1542
|
+
return kpoint.distanceToPoint(point1, point2);
|
|
1543
|
+
},
|
|
1399
1544
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1545
|
+
/**
|
|
1546
|
+
* Round the given coordinates to a given snap value
|
|
1547
|
+
* (e.g., nearest 0.2 increment)
|
|
1548
|
+
*/
|
|
1549
|
+
snapCoord: function (coord, snap) {
|
|
1550
|
+
return coord.map(function (val, i) {
|
|
1551
|
+
return KhanMath.roundToNearest(snap[i], val);
|
|
1552
|
+
});
|
|
1553
|
+
},
|
|
1409
1554
|
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
}
|
|
1418
|
-
return plotParametric(
|
|
1419
|
-
function (y) {
|
|
1420
|
-
return [fn(y), y];
|
|
1421
|
-
},
|
|
1422
|
-
range,
|
|
1423
|
-
shade,
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
if (fn2) {
|
|
1427
|
-
if (shade) {
|
|
1428
|
-
return plotParametric(
|
|
1429
|
-
function (x) {
|
|
1430
|
-
return [x, fn(x)];
|
|
1431
|
-
},
|
|
1432
|
-
range,
|
|
1433
|
-
shade,
|
|
1434
|
-
function (x) {
|
|
1435
|
-
return [x, fn2(x)];
|
|
1436
|
-
},
|
|
1437
|
-
);
|
|
1555
|
+
// Find the angle in degrees between two or three points
|
|
1556
|
+
findAngle: function (point1, point2, vertex) {
|
|
1557
|
+
if (vertex === undefined) {
|
|
1558
|
+
const x = point1[0] - point2[0];
|
|
1559
|
+
const y = point1[1] - point2[1];
|
|
1560
|
+
if (!x && !y) {
|
|
1561
|
+
return 0;
|
|
1438
1562
|
}
|
|
1439
|
-
|
|
1440
|
-
"fn2 should only be set when 'shade' is True.",
|
|
1441
|
-
Errors.Internal,
|
|
1442
|
-
);
|
|
1563
|
+
return (180 + (Math.atan2(-y, -x) * 180) / Math.PI + 360) % 360;
|
|
1443
1564
|
}
|
|
1444
|
-
return
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
},
|
|
1448
|
-
range,
|
|
1449
|
-
shade,
|
|
1565
|
+
return (
|
|
1566
|
+
GraphUtils.findAngle(point1, vertex) -
|
|
1567
|
+
GraphUtils.findAngle(point2, vertex)
|
|
1450
1568
|
);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
const drawingTools = {
|
|
1454
|
-
circle,
|
|
1455
|
-
rect,
|
|
1456
|
-
ellipse,
|
|
1457
|
-
fixedEllipse,
|
|
1458
|
-
arc,
|
|
1459
|
-
path,
|
|
1460
|
-
fixedPath,
|
|
1461
|
-
scaledPath,
|
|
1462
|
-
line,
|
|
1463
|
-
parabola,
|
|
1464
|
-
fixedLine,
|
|
1465
|
-
sinusoid,
|
|
1466
|
-
grid,
|
|
1467
|
-
label,
|
|
1468
|
-
plotParametric,
|
|
1469
|
-
plot,
|
|
1470
|
-
};
|
|
1471
|
-
|
|
1472
|
-
function graphify(drawingFn: any): any {
|
|
1473
|
-
return function (...args) {
|
|
1474
|
-
const last = args[args.length - 1];
|
|
1475
|
-
const oldStyle = thisGraphie.currentStyle;
|
|
1476
|
-
let result;
|
|
1477
|
-
|
|
1478
|
-
// The last argument is probably trying to change the style
|
|
1479
|
-
if (typeof last === "object" && !_.isArray(last)) {
|
|
1480
|
-
thisGraphie.currentStyle = {
|
|
1481
|
-
...thisGraphie.currentStyle,
|
|
1482
|
-
...thisGraphie.processAttributes(last),
|
|
1483
|
-
};
|
|
1484
|
-
|
|
1485
|
-
const rest = [].slice.call(args, 0, args.length - 1);
|
|
1486
|
-
result = drawingFn(...rest);
|
|
1487
|
-
} else {
|
|
1488
|
-
thisGraphie.currentStyle = $.extend(
|
|
1489
|
-
{},
|
|
1490
|
-
thisGraphie.currentStyle,
|
|
1491
|
-
);
|
|
1492
|
-
|
|
1493
|
-
result = drawingFn(...args);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
// Bad heuristic for recognizing Raphael elements and sets
|
|
1497
|
-
const type = result.constructor.prototype;
|
|
1498
|
-
if (type === Raphael.el || type === Raphael.st) {
|
|
1499
|
-
result.attr(thisGraphie.currentStyle);
|
|
1500
|
-
|
|
1501
|
-
if (thisGraphie.currentStyle.arrows) {
|
|
1502
|
-
result = addArrowheads(result);
|
|
1503
|
-
}
|
|
1504
|
-
} else if (result instanceof $) {
|
|
1505
|
-
// We assume that if it's not a Raphael element/set, it
|
|
1506
|
-
// does not contain SVG.
|
|
1507
|
-
// @ts-expect-error - TS2339 - Property 'css' does not exist on type '{}'.
|
|
1508
|
-
result.css({
|
|
1509
|
-
...thisGraphie.currentStyle,
|
|
1510
|
-
...SVG_SPECIFIC_STYLE_MASK,
|
|
1511
|
-
});
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
thisGraphie.currentStyle = oldStyle;
|
|
1515
|
-
return result;
|
|
1516
|
-
};
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
$.each(drawingTools, function (name) {
|
|
1520
|
-
thisGraphie[name] = graphify(drawingTools[name]);
|
|
1521
|
-
});
|
|
1569
|
+
},
|
|
1522
1570
|
|
|
1523
|
-
|
|
1571
|
+
graphs: {},
|
|
1524
1572
|
};
|
|
1525
1573
|
|
|
1526
1574
|
export default GraphUtils;
|