@khanacademy/perseus 19.1.3 → 19.1.4

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.
@@ -18,7 +18,7 @@ declare const InteractiveUtil: {
18
18
  /**
19
19
  * Convert all function-or-array arguments to arrays of functions
20
20
  */
21
- readonly normalizeOptions: (arrayOptionNames: ReadonlyArray<string>, options: Record<any, any>) => Record<any, any>;
21
+ readonly normalizeOptions: (options: Record<any, any>) => Record<any, any>;
22
22
  /**
23
23
  * Get the correct vendor-prefixed `transform`.
24
24
  */
@@ -1,2 +1,21 @@
1
- declare const Movable: (graphie: any, options: any) => void;
1
+ import type { Coord } from "./types";
2
+ import type { Graphie } from "../util/graphie";
3
+ export interface State {
4
+ added?: boolean;
5
+ isHovering?: boolean;
6
+ isMouseOver?: boolean;
7
+ isDragging?: boolean;
8
+ mouseTarget?: unknown | null;
9
+ cursor: unknown | null;
10
+ id: string;
11
+ add: (() => void)[];
12
+ modify: (() => void)[];
13
+ draw: (() => void)[];
14
+ remove: (() => void)[];
15
+ onMoveStart: ((position: Coord) => void)[];
16
+ onMove: ((end: Coord, start: Coord) => void)[];
17
+ onMoveEnd: ((end: Coord, start: Coord) => void)[];
18
+ onClick: ((position: Coord, start: Coord) => void)[];
19
+ }
20
+ declare const Movable: (graphie: Graphie, options: any) => void;
2
21
  export default Movable;
@@ -42,7 +42,7 @@ export declare class Graphie {
42
42
  ypixels?: number;
43
43
  constructor(el: Element);
44
44
  init(options: {
45
- range?: [Interval, Interval];
45
+ range: [Interval, Interval];
46
46
  scale?: number | Coord;
47
47
  isMobile?: boolean;
48
48
  }): this;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Core Perseus API (includes renderers and widgets)",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "19.1.3",
6
+ "version": "19.1.4",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -24,7 +24,7 @@
24
24
  "dependencies": {
25
25
  "@khanacademy/kas": "^0.3.7",
26
26
  "@khanacademy/kmath": "^0.1.10",
27
- "@khanacademy/math-input": "^17.0.2",
27
+ "@khanacademy/math-input": "^17.0.3",
28
28
  "@khanacademy/perseus-core": "1.4.1",
29
29
  "@khanacademy/perseus-linter": "^0.3.10",
30
30
  "@khanacademy/pure-markdown": "^0.2.13",
@@ -4,10 +4,11 @@
4
4
 
5
5
  import _ from "underscore";
6
6
 
7
+ import {Graphie} from "../../util/graphie";
7
8
  import Movable from "../movable";
8
9
 
9
10
  const createMock = function (): any {
10
- const movable = new Movable(null, {
11
+ const movable = new Movable(new Graphie(document.createElement("div")), {
11
12
  mouseTarget: null,
12
13
  });
13
14
  movable.modify = function (options: any) {
@@ -0,0 +1,157 @@
1
+ /* eslint-disable testing-library/prefer-user-event */
2
+ import {fireEvent} from "@testing-library/react";
3
+
4
+ import {Graphie} from "../../util/graphie";
5
+ import Movable from "../movable";
6
+
7
+ // Yay for side-effect imports!
8
+ // eslint-disable-next-line import/no-unassigned-import
9
+ import "../../util/interactive";
10
+
11
+ describe("Movable", () => {
12
+ it("should be constructable", () => {
13
+ const movable = new Movable(
14
+ new Graphie(document.createElement("div")),
15
+ {},
16
+ );
17
+
18
+ expect(movable).toBeDefined();
19
+ });
20
+
21
+ it("should have accessor properties", () => {
22
+ const movable = new Movable(
23
+ new Graphie(document.createElement("div")),
24
+ {},
25
+ );
26
+
27
+ expect(typeof movable.isHovering).toBe("function");
28
+ expect(typeof movable.cursor).toBe("function");
29
+ expect(typeof movable.added).toBe("function");
30
+ expect(typeof movable.isHovering).toBe("function");
31
+ expect(typeof movable.isMouseOver).toBe("function");
32
+ expect(typeof movable.isDragging).toBe("function");
33
+ expect(typeof movable.mouseTarget).toBe("function");
34
+ });
35
+
36
+ test("cloneState()", () => {
37
+ const movable = new Movable(
38
+ new Graphie(document.createElement("div")),
39
+ {},
40
+ );
41
+
42
+ expect(movable.cloneState()).toEqual(
43
+ expect.objectContaining({
44
+ id: expect.any(String),
45
+ add: [],
46
+ added: true,
47
+ cursor: null,
48
+ draw: [],
49
+ isDragging: false,
50
+ isHovering: false,
51
+ isMouseOver: false,
52
+ modify: [],
53
+ mouseTarget: null,
54
+ onClick: [],
55
+ onMove: [],
56
+ onMoveEnd: [],
57
+ onMoveStart: [],
58
+ remove: [],
59
+ }),
60
+ );
61
+ });
62
+
63
+ test("cloneState() should copy state", () => {
64
+ const movable = new Movable(
65
+ new Graphie(document.createElement("div")),
66
+ {},
67
+ );
68
+
69
+ expect(movable.cloneState()).not.toBe(movable.state);
70
+ });
71
+
72
+ test("modify() resets movable to initial state when no params given", () => {
73
+ const movable = new Movable(
74
+ new Graphie(document.createElement("div")),
75
+ {},
76
+ );
77
+
78
+ const state = movable.cloneState();
79
+ movable.state.added = false;
80
+
81
+ // It seems that all usages of this function in our production code
82
+ // pass the original state + mutations to modify(). However, it _does_
83
+ // work to pass no arguments so we'll keep this Characterization test
84
+ // here for now.
85
+ movable.modify();
86
+
87
+ expect(movable.cloneState()).toEqual(state);
88
+ });
89
+
90
+ test("modify() retains original ID", () => {
91
+ const movable = new Movable(
92
+ new Graphie(document.createElement("div")),
93
+ {},
94
+ );
95
+ const id = movable.state.id;
96
+
97
+ movable.modify({added: true});
98
+
99
+ expect(movable.state.id).toBe(id);
100
+ });
101
+
102
+ test("grab() fires onMoveStart", () => {
103
+ const graphie = new Graphie(document.createElement("div"));
104
+ graphie.init({
105
+ range: [
106
+ [-10, 10],
107
+ [-10, 10],
108
+ ],
109
+ });
110
+ const onMoveStart = jest.fn();
111
+ const movable = new Movable(graphie, {onMoveStart});
112
+
113
+ movable.grab([0, 0]);
114
+
115
+ expect(onMoveStart).toHaveBeenCalledWith([0, 0], [0, 0]);
116
+ });
117
+
118
+ test("grab() fires onMove when mouse moves", () => {
119
+ const graphie = new Graphie(document.createElement("div"));
120
+ graphie.init({
121
+ range: [
122
+ [-10, 10],
123
+ [-10, 10],
124
+ ],
125
+ scale: [10, 10],
126
+ });
127
+ const onMove = jest.fn();
128
+ const movable = new Movable(graphie, {onMove});
129
+
130
+ movable.grab([0, 0]);
131
+ fireEvent.mouseMove(document, {clientX: 120, clientY: 80});
132
+
133
+ expect(onMove).toHaveBeenCalledWith([2, 2], [0, 0]);
134
+ });
135
+
136
+ test("grab() fires onMoveEnd when mouse up", () => {
137
+ const dummyGraphie: any = {};
138
+ const onMoveEnd = jest.fn();
139
+ const movable = new Movable(dummyGraphie, {onMoveEnd});
140
+
141
+ movable.grab([0, 0]);
142
+ fireEvent.mouseUp(document);
143
+
144
+ expect(onMoveEnd).toHaveBeenCalledWith([0, 0], [0, 0]);
145
+ });
146
+
147
+ test("grab() fires onClick if still hovering when mouse up", () => {
148
+ const dummyGraphie: any = {};
149
+ const onClick = jest.fn();
150
+ const movable = new Movable(dummyGraphie, {onClick});
151
+
152
+ movable.grab([0, 0]);
153
+ fireEvent.mouseUp(document);
154
+
155
+ expect(onClick).toHaveBeenCalledWith([0, 0], [0, 0]);
156
+ });
157
+ });
@@ -0,0 +1,93 @@
1
+ import InteractiveUtil from "./interactive-util";
2
+
3
+ describe("normalizeOptions", () => {
4
+ it("wraps single functions in arrays", () => {
5
+ const options = {
6
+ add: () => {},
7
+ modify: () => {},
8
+ draw: () => {},
9
+ remove: () => {},
10
+ constraints: () => {},
11
+ onMoveStart: () => {},
12
+ onMove: () => {},
13
+ onMoveEnd: () => {},
14
+ onClick: () => {},
15
+ };
16
+ expect(InteractiveUtil.normalizeOptions(options)).toEqual({
17
+ add: [options.add],
18
+ modify: [options.modify],
19
+ draw: [options.draw],
20
+ remove: [options.remove],
21
+ constraints: [options.constraints],
22
+ onMoveStart: [options.onMoveStart],
23
+ onMove: [options.onMove],
24
+ onMoveEnd: [options.onMoveEnd],
25
+ onClick: [options.onClick],
26
+ });
27
+ });
28
+
29
+ it("tolerates missing properties", () => {
30
+ const options = {
31
+ add: () => {},
32
+ };
33
+ expect(InteractiveUtil.normalizeOptions(options)).toEqual({
34
+ add: [options.add],
35
+ });
36
+ });
37
+
38
+ it("ignores extra properties", () => {
39
+ const options = {
40
+ foo: () => {},
41
+ };
42
+ expect(InteractiveUtil.normalizeOptions(options)).toEqual({
43
+ foo: options.foo,
44
+ });
45
+ });
46
+
47
+ it("does not modify its argument", () => {
48
+ const options = {
49
+ add: () => {},
50
+ };
51
+ const optionsCopy = {...options};
52
+ InteractiveUtil.normalizeOptions(options);
53
+ expect(options).toEqual(optionsCopy);
54
+ });
55
+
56
+ it("leaves existing arrays of functions alone", () => {
57
+ const options = {
58
+ add: [() => {}],
59
+ modify: [() => {}],
60
+ draw: [() => {}],
61
+ remove: [() => {}],
62
+ onMoveStart: [() => {}],
63
+ onMove: [() => {}],
64
+ onMoveEnd: [() => {}],
65
+ onClick: [() => {}],
66
+ };
67
+ expect(InteractiveUtil.normalizeOptions(options)).toEqual(options);
68
+ });
69
+
70
+ it("flattens arrays", () => {
71
+ const dummyFunction = () => {};
72
+ const options = {
73
+ add: [[dummyFunction]],
74
+ modify: [[dummyFunction]],
75
+ draw: [[dummyFunction]],
76
+ remove: [[dummyFunction]],
77
+ onMoveStart: [[dummyFunction]],
78
+ onMove: [[dummyFunction]],
79
+ onMoveEnd: [[dummyFunction]],
80
+ onClick: [[dummyFunction]],
81
+ };
82
+ expect(InteractiveUtil.normalizeOptions(options)).toEqual({
83
+ add: [dummyFunction],
84
+ modify: [dummyFunction],
85
+ draw: [dummyFunction],
86
+ remove: [dummyFunction],
87
+ onMoveStart: [dummyFunction],
88
+ onMove: [dummyFunction],
89
+ onMoveEnd: [dummyFunction],
90
+ onClick: [dummyFunction],
91
+ });
92
+ });
93
+ });
@@ -52,6 +52,18 @@ function computeCanUse3dTransform(): boolean {
52
52
  return !!el.style[prefix];
53
53
  }
54
54
 
55
+ const FUNCTION_ARRAY_OPTIONS = [
56
+ "add",
57
+ "modify",
58
+ "draw",
59
+ "remove",
60
+ "constraints",
61
+ "onMoveStart",
62
+ "onMove",
63
+ "onMoveEnd",
64
+ "onClick",
65
+ ];
66
+
55
67
  const InteractiveUtil = {
56
68
  assert: function (isTrue: boolean, message?: string) {
57
69
  if (!isTrue) {
@@ -106,24 +118,16 @@ const InteractiveUtil = {
106
118
  /**
107
119
  * Convert all function-or-array arguments to arrays of functions
108
120
  */
109
- normalizeOptions: function (
110
- arrayOptionNames: ReadonlyArray<string>,
111
- options: Record<any, any>,
112
- ): Record<any, any> {
113
- // TODO(jack): Having to clone here is annoying; this
114
- // function should really just modify this.state in place
115
- // (and maybe be a function on MovableHelperMethods to get access
116
- // to this.state), which would also be nicer because we could
117
- // normalizeOptions once in this.modify
118
- const result = _.clone(options);
119
- _.each(arrayOptionNames, function (eventName) {
120
- const funcOrArray = options[eventName];
121
+ normalizeOptions: function (options: Record<any, any>): Record<any, any> {
122
+ const result = {...options};
123
+ FUNCTION_ARRAY_OPTIONS.forEach(function (eventName) {
121
124
  // Only propagate values which were set; not present values
122
125
  // shouldn't be added to options because we'd like them to
123
126
  // fall through to defaults
124
- if (funcOrArray !== undefined) {
125
- const funcArray = InteractiveUtil.arrayify(funcOrArray);
126
- result[eventName] = funcArray;
127
+ if (options[eventName] !== undefined) {
128
+ result[eventName] = InteractiveUtil.arrayify(
129
+ options[eventName],
130
+ );
127
131
  }
128
132
  });
129
133
  return result;
@@ -27,8 +27,13 @@ const MovableHelperMethods: any = {
27
27
  /**
28
28
  * Fire an onSomething type event to all functions in listeners
29
29
  */
30
- _fireEvent: function (listeners, currentValue, previousValue) {
31
- _.invoke(listeners, "call", this, currentValue, previousValue);
30
+ _fireEvent: function <F extends (...args: any[]) => any>(
31
+ listeners: F[],
32
+ ...args: Parameters<F>
33
+ ) {
34
+ for (const listener of listeners) {
35
+ listener.call(this, ...args);
36
+ }
32
37
  },
33
38
 
34
39
  /**
@@ -14,16 +14,6 @@ import WrappedLine from "./wrapped-line";
14
14
  const assert = InteractiveUtil.assert;
15
15
  const normalizeOptions = InteractiveUtil.normalizeOptions;
16
16
 
17
- const FUNCTION_ARRAY_OPTIONS = [
18
- "add",
19
- "draw",
20
- "remove",
21
- "onMoveStart",
22
- "constraints",
23
- "onMove",
24
- "onMoveEnd",
25
- ];
26
-
27
17
  // Default "props" and "state". Both are added to this.state and
28
18
  // receive magic getter methods (this.cursor() etc).
29
19
  // However, properties in DEFAULT_PROPS are updated on `modify()`,
@@ -87,7 +77,6 @@ _.extend(MovableLine.prototype, {
87
77
  id: this.state.id,
88
78
  },
89
79
  normalizeOptions(
90
- FUNCTION_ARRAY_OPTIONS,
91
80
  // Defaults are copied from MovableLineOptions.*.standard
92
81
  // These defaults are set here instead of DEFAULT_PROPS/STATE
93
82
  // because they:
@@ -121,7 +110,7 @@ _.extend(MovableLine.prototype, {
121
110
  const graphie = this.graphie;
122
111
  const state = (self.state = _.extend(
123
112
  self.state,
124
- normalizeOptions(FUNCTION_ARRAY_OPTIONS, options),
113
+ normalizeOptions(options),
125
114
  ));
126
115
 
127
116
  // Default things inside the state.normalStyle object, because
@@ -69,10 +69,6 @@ const normalizeOptions = InteractiveUtil.normalizeOptions;
69
69
 
70
70
  const {processMath} = Tex;
71
71
 
72
- // State parameters that should be converted into an array of
73
- // functions
74
- const FUNCTION_ARRAY_OPTIONS = _.keys(MovablePointOptions);
75
-
76
72
  // Default "props" and "state". Both are added to this.state and
77
73
  // receive magic getter methods (this.coord() etc).
78
74
  // However, properties in DEFAULT_PROPS are updated on `modify()`,
@@ -134,7 +130,6 @@ _.extend(MovablePoint.prototype, {
134
130
  id: this.state.id,
135
131
  },
136
132
  normalizeOptions(
137
- FUNCTION_ARRAY_OPTIONS,
138
133
  // Defaults are copied from MovablePointOptions.*.standard
139
134
  // These defaults are set here instead of DEFAULT_PROPS/STATE
140
135
  // because they:
@@ -216,10 +211,7 @@ _.extend(MovablePoint.prototype, {
216
211
  update: function (options) {
217
212
  const self = this;
218
213
  const graphie = self.graphie;
219
- const state = _.extend(
220
- self.state,
221
- normalizeOptions(FUNCTION_ARRAY_OPTIONS, options),
222
- );
214
+ const state = _.extend(self.state, normalizeOptions(options));
223
215
 
224
216
  assert(kpoint.is(state.coord));
225
217
 
@@ -15,10 +15,6 @@ import objective_ from "./objective_";
15
15
  const assert = InteractiveUtil.assert;
16
16
  const normalizeOptions = InteractiveUtil.normalizeOptions;
17
17
 
18
- // State parameters that should be converted into an array of
19
- // functions
20
- const FUNCTION_ARRAY_OPTIONS = _.keys(MovablePolygonOptions);
21
-
22
18
  // Default "props" and "state". Both are added to this.state and
23
19
  // receive magic getter methods (this.points() etc).
24
20
  // However, properties in DEFAULT_PROPS are updated on `modify()`,
@@ -89,7 +85,6 @@ _.extend(MovablePolygon.prototype, {
89
85
  id: this.state.id,
90
86
  },
91
87
  normalizeOptions(
92
- FUNCTION_ARRAY_OPTIONS,
93
88
  // Defaults are copied from MovablePolygonOptions.*.standard
94
89
  // These defaults are set here instead of DEFAULT_PROPS/STATE
95
90
  // because they:
@@ -124,10 +119,7 @@ _.extend(MovablePolygon.prototype, {
124
119
  update: function (options) {
125
120
  const self = this;
126
121
  const graphie = self.graphie;
127
- const state = _.extend(
128
- self.state,
129
- normalizeOptions(FUNCTION_ARRAY_OPTIONS, options),
130
- );
122
+ const state = _.extend(self.state, normalizeOptions(options));
131
123
 
132
124
  // Default things inside the state.normalStyle object, because
133
125
  // _.extend is not deep.
@@ -15,23 +15,13 @@ import _ from "underscore";
15
15
 
16
16
  import InteractiveUtil from "./interactive-util";
17
17
 
18
+ import type {Coord} from "./types";
19
+ import type {Graphie} from "../util/graphie";
20
+
18
21
  const normalizeOptions = InteractiveUtil.normalizeOptions;
19
22
 
20
23
  const assert = InteractiveUtil.assert;
21
24
 
22
- // state parameters that should be converted into an array of
23
- // functions
24
- const FUNCTION_ARRAY_OPTIONS = [
25
- "add",
26
- "modify",
27
- "draw",
28
- "remove",
29
- "onMoveStart",
30
- "onMove",
31
- "onMoveEnd",
32
- "onClick",
33
- ];
34
-
35
25
  // Default "props" and "state". Both are added to this.state and
36
26
  // receive magic getter methods (this.isHovering() etc).
37
27
  // However, properties in DEFAULT_PROPS are updated on `modify()`,
@@ -49,10 +39,30 @@ const DEFAULT_STATE = {
49
39
  mouseTarget: null,
50
40
  } as const;
51
41
 
52
- const Movable = function (graphie: any, options: any): void {
42
+ export interface State {
43
+ added?: boolean;
44
+ isHovering?: boolean;
45
+ isMouseOver?: boolean;
46
+ isDragging?: boolean;
47
+ // TODO(benchristel): improve types
48
+ mouseTarget?: unknown | null;
49
+ // TODO(benchristel): improve types
50
+ cursor: unknown | null;
51
+ id: string;
52
+ add: (() => void)[];
53
+ modify: (() => void)[];
54
+ draw: (() => void)[];
55
+ remove: (() => void)[];
56
+ onMoveStart: ((position: Coord) => void)[];
57
+ onMove: ((end: Coord, start: Coord) => void)[];
58
+ onMoveEnd: ((end: Coord, start: Coord) => void)[];
59
+ onClick: ((position: Coord, start: Coord) => void)[];
60
+ }
61
+
62
+ const Movable = function (graphie: Graphie, options: any): void {
53
63
  // @ts-expect-error - TS2683 - 'this' implicitly has type 'any' because it does not have a type annotation.
54
64
  _.extend(this, {
55
- graphie: graphie,
65
+ graphie,
56
66
  state: {
57
67
  // Set here because this must be unique for each instance
58
68
  id: _.uniqueId("movable"),
@@ -71,28 +81,26 @@ InteractiveUtil.createGettersFor(
71
81
  InteractiveUtil.addMovableHelperMethodsTo(Movable);
72
82
 
73
83
  _.extend(Movable.prototype, {
74
- cloneState: function () {
75
- return _.clone(this.state);
84
+ cloneState: function (): State {
85
+ return {...this.state};
76
86
  },
77
87
 
78
- _createDefaultState: function () {
79
- return _.extend(
80
- {
81
- id: this.state.id,
82
- add: [],
83
- modify: [],
84
- draw: [],
85
- remove: [],
86
- onMoveStart: [],
87
- onMove: [],
88
- onMoveEnd: [],
89
- onClick: [],
90
-
91
- // We only update props here, because we want things on state to
92
- // be persistent, and updated appropriately in modify()
93
- },
94
- DEFAULT_PROPS,
95
- );
88
+ _createDefaultState: function (): State {
89
+ return {
90
+ id: this.state.id,
91
+ add: [],
92
+ modify: [],
93
+ draw: [],
94
+ remove: [],
95
+ onMoveStart: [],
96
+ onMove: [],
97
+ onMoveEnd: [],
98
+ onClick: [],
99
+
100
+ // We only update props here, because we want things on state to
101
+ // be persistent, and updated appropriately in modify()
102
+ ...DEFAULT_PROPS,
103
+ };
96
104
  },
97
105
 
98
106
  /**
@@ -101,18 +109,18 @@ _.extend(Movable.prototype, {
101
109
  *
102
110
  * Analogous to React.js's replaceProps
103
111
  */
104
- modify: function (options) {
105
- this.update(_.extend({}, this._createDefaultState(), options));
112
+ modify: function (options: Partial<State>) {
113
+ this.update({...this._createDefaultState(), ...options});
106
114
  },
107
115
 
108
116
  /**
109
117
  * Simulates a mouse grab event on the movable object.
110
118
  */
111
- grab: function (coord) {
119
+ grab: function (coord: Coord) {
112
120
  assert(kpoint.is(coord));
113
121
  const self = this;
114
122
  const graphie = self.graphie;
115
- const state = self.state;
123
+ const state: State = self.state;
116
124
 
117
125
  state.isHovering = true;
118
126
  state.isDragging = true;
@@ -154,15 +162,12 @@ _.extend(Movable.prototype, {
154
162
  *
155
163
  * Analogous to React.js's setProps
156
164
  */
157
- update: function (options) {
165
+ update: function (options: State) {
158
166
  const self = this;
159
167
  const graphie = self.graphie;
160
168
 
161
169
  const prevState = self.cloneState();
162
- const state = _.extend(
163
- self.state,
164
- normalizeOptions(FUNCTION_ARRAY_OPTIONS, options),
165
- );
170
+ const state = _.extend(self.state, normalizeOptions(options));
166
171
 
167
172
  // the invisible shape in front of the point that gets mouse events
168
173
  if (state.mouseTarget && !prevState.mouseTarget) {
@@ -121,13 +121,15 @@ export class Graphie {
121
121
  }
122
122
 
123
123
  init(options: {
124
- range?: [Interval, Interval];
124
+ range: [Interval, Interval];
125
125
  scale?: number | Coord;
126
126
  isMobile?: boolean;
127
127
  }) {
128
128
  let scale = options.scale || [40, 40];
129
129
  scale = typeof scale === "number" ? [scale, scale] : scale;
130
130
 
131
+ // TODO(jeremy): This can be removed once we're confident that callers
132
+ // obey the Typescript types.
131
133
  if (options.range == null) {
132
134
  throw new PerseusError(
133
135
  "range should be specified in graph init",