@khanacademy/perseus 0.0.1 → 0.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.
Files changed (503) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/dist/es/index.js +15 -1
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +11934 -0
  6. package/dist/index.js +15 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/strings.js +148 -1
  9. package/package.json +40 -4
  10. package/src/README.md +7 -0
  11. package/src/__stories__/item-renderer.stories.jsx +27 -0
  12. package/src/__stories__/server-item-renderer.stories.jsx +27 -0
  13. package/src/__testdata__/item-renderer_testdata.js +161 -0
  14. package/src/__testdata__/renderer_testdata.js +123 -0
  15. package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +233 -0
  16. package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +516 -0
  17. package/src/__tests__/__snapshots__/pure-markdown_test.js.snap +113 -0
  18. package/src/__tests__/__snapshots__/renderer_test.jsx.snap +576 -0
  19. package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +306 -0
  20. package/src/__tests__/a11y_test.jsx +304 -0
  21. package/src/__tests__/item-renderer_test.jsx +892 -0
  22. package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
  23. package/src/__tests__/mock-widget.jsx +35 -0
  24. package/src/__tests__/perseus-markdown_test.jsx +747 -0
  25. package/src/__tests__/pure-markdown_test.js +552 -0
  26. package/src/__tests__/renderability_test.jsx +481 -0
  27. package/src/__tests__/renderer-api_test.jsx +333 -0
  28. package/src/__tests__/renderer_test.jsx +1792 -0
  29. package/src/__tests__/server-item-renderer_test.jsx +516 -0
  30. package/src/__tests__/test-items/image-item.js +25 -0
  31. package/src/__tests__/test-items/input-number-1-item.js +27 -0
  32. package/src/__tests__/test-items/input-number-2-item.js +39 -0
  33. package/src/__tests__/test-items/table-item.js +30 -0
  34. package/src/__tests__/tex-wrangler_test.js +83 -0
  35. package/src/__tests__/util_test.js +41 -0
  36. package/src/a11y.js +46 -0
  37. package/src/all-widgets.js +9 -0
  38. package/src/article-renderer.jsx +215 -0
  39. package/src/asset-context.js +30 -0
  40. package/src/basic-widgets.js +13 -0
  41. package/src/components/__stories__/button-group.stories.jsx +61 -0
  42. package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
  43. package/src/components/__stories__/graph.stories.jsx +27 -0
  44. package/src/components/__stories__/graphie.stories.jsx +28 -0
  45. package/src/components/__stories__/hud.stories.jsx +36 -0
  46. package/src/components/__stories__/icon.stories.jsx +56 -0
  47. package/src/components/__stories__/image-loader.stories.jsx +60 -0
  48. package/src/components/__stories__/info-tip.stories.jsx +33 -0
  49. package/src/components/__stories__/inline-icon.stories.jsx +39 -0
  50. package/src/components/__stories__/input-with-examples.stories.jsx +51 -0
  51. package/src/components/__stories__/lint.stories.jsx +40 -0
  52. package/src/components/__stories__/math-input.stories.jsx +29 -0
  53. package/src/components/__stories__/math-output.stories.jsx +24 -0
  54. package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
  55. package/src/components/__stories__/number-input.stories.jsx +42 -0
  56. package/src/components/__stories__/possible-answers.stories.jsx +22 -0
  57. package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
  58. package/src/components/__stories__/range-input.stories.jsx +28 -0
  59. package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
  60. package/src/components/__stories__/sortable.stories.jsx +73 -0
  61. package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
  62. package/src/components/__stories__/svg-image.stories.jsx +60 -0
  63. package/src/components/__stories__/tex.stories.jsx +18 -0
  64. package/src/components/__stories__/text-buttons.stories.jsx +51 -0
  65. package/src/components/__stories__/text-input.stories.jsx +34 -0
  66. package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
  67. package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
  68. package/src/components/__stories__/zoomable.stories.jsx +40 -0
  69. package/src/components/__testdata__/sorter_testdata.js +20 -0
  70. package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
  71. package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
  72. package/src/components/__tests__/number-input_test.jsx +112 -0
  73. package/src/components/__tests__/sortable_test.jsx +234 -0
  74. package/src/components/__tests__/sorter_test.jsx +111 -0
  75. package/src/components/__tests__/zoomable_test.jsx +388 -0
  76. package/src/components/button-group.jsx +131 -0
  77. package/src/components/constants.js +15 -0
  78. package/src/components/fixed-to-responsive.jsx +163 -0
  79. package/src/components/graph.jsx +414 -0
  80. package/src/components/graphie-classes.jsx +148 -0
  81. package/src/components/graphie-movables.jsx +342 -0
  82. package/src/components/graphie.jsx +353 -0
  83. package/src/components/highlighting/highlightable-content.jsx +245 -0
  84. package/src/components/highlighting/highlights.js +208 -0
  85. package/src/components/highlighting/ranges.js +299 -0
  86. package/src/components/highlighting/types.js +73 -0
  87. package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
  88. package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
  89. package/src/components/highlighting/ui/highlight-tooltip.jsx +153 -0
  90. package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
  91. package/src/components/highlighting/ui/selection-tracker.jsx +195 -0
  92. package/src/components/highlighting/ui/types.js +21 -0
  93. package/src/components/highlighting/ui/util.js +166 -0
  94. package/src/components/highlighting/word-indexer.jsx +155 -0
  95. package/src/components/hud.jsx +179 -0
  96. package/src/components/icon.jsx +149 -0
  97. package/src/components/image-loader.jsx +175 -0
  98. package/src/components/info-tip/info-tip.jsx +149 -0
  99. package/src/components/info-tip.jsx +38 -0
  100. package/src/components/inline-icon.jsx +81 -0
  101. package/src/components/input-with-examples.jsx +231 -0
  102. package/src/components/lint.jsx +456 -0
  103. package/src/components/math-input.jsx +282 -0
  104. package/src/components/math-output.jsx +128 -0
  105. package/src/components/multi-button-group.jsx +137 -0
  106. package/src/components/new-tooltip/README.md +110 -0
  107. package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +437 -0
  108. package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +665 -0
  109. package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +70 -0
  110. package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +98 -0
  111. package/src/components/new-tooltip/__tests__/style-constants_test.js +8 -0
  112. package/src/components/new-tooltip/__tests__/util_test.js +47 -0
  113. package/src/components/new-tooltip/flexible-position.jsx +182 -0
  114. package/src/components/new-tooltip/new-tooltip.jsx +399 -0
  115. package/src/components/new-tooltip/style-constants.js +13 -0
  116. package/src/components/new-tooltip/tooltip-arrow.jsx +184 -0
  117. package/src/components/new-tooltip/tooltip-bubble.jsx +223 -0
  118. package/src/components/new-tooltip/tooltip-portal.jsx +101 -0
  119. package/src/components/new-tooltip/tooltip-positioner.jsx +529 -0
  120. package/src/components/new-tooltip/types.js +183 -0
  121. package/src/components/new-tooltip/util.js +27 -0
  122. package/src/components/number-input.jsx +253 -0
  123. package/src/components/possible-answers.jsx +36 -0
  124. package/src/components/prop-check-box.jsx +78 -0
  125. package/src/components/range-input.jsx +61 -0
  126. package/src/components/simple-keypad-input.jsx +91 -0
  127. package/src/components/sortable.jsx +872 -0
  128. package/src/components/stub-tag-editor.jsx +53 -0
  129. package/src/components/svg-image.jsx +762 -0
  130. package/src/components/tex-buttons.jsx +322 -0
  131. package/src/components/tex.jsx +70 -0
  132. package/src/components/text-input.jsx +82 -0
  133. package/src/components/text-list-editor.jsx +157 -0
  134. package/src/components/tooltip.jsx +395 -0
  135. package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
  136. package/src/components/visibility-observer/get-scroll-parent.js +45 -0
  137. package/src/components/visibility-observer/visibility-observer.js +109 -0
  138. package/src/components/visibility-observer/visibility-observer.jsx +123 -0
  139. package/src/components/zoomable-tex.jsx +70 -0
  140. package/src/components/zoomable.jsx +304 -0
  141. package/src/components.js +17 -0
  142. package/src/dependencies.js +22 -0
  143. package/src/error-boundary.jsx +48 -0
  144. package/src/extra-widgets.js +75 -0
  145. package/src/gorgon/README +40 -0
  146. package/src/gorgon/__tests__/matcher_test.js +497 -0
  147. package/src/gorgon/__tests__/rule_test.js +101 -0
  148. package/src/gorgon/__tests__/rules_test.js +487 -0
  149. package/src/gorgon/__tests__/selector-parser_test.js +52 -0
  150. package/src/gorgon/__tests__/tree-transformer_test.js +446 -0
  151. package/src/gorgon/gorgon.js +290 -0
  152. package/src/gorgon/proptypes.js +29 -0
  153. package/src/gorgon/rule.js +413 -0
  154. package/src/gorgon/rules/absolute-url.js +24 -0
  155. package/src/gorgon/rules/all-rules.js +72 -0
  156. package/src/gorgon/rules/blockquoted-math.js +10 -0
  157. package/src/gorgon/rules/blockquoted-widget.js +10 -0
  158. package/src/gorgon/rules/double-spacing-after-terminal.js +12 -0
  159. package/src/gorgon/rules/extra-content-spacing.js +12 -0
  160. package/src/gorgon/rules/heading-level-1.js +14 -0
  161. package/src/gorgon/rules/heading-level-skip.js +20 -0
  162. package/src/gorgon/rules/heading-sentence-case.js +11 -0
  163. package/src/gorgon/rules/heading-title-case.js +63 -0
  164. package/src/gorgon/rules/image-alt-text.js +21 -0
  165. package/src/gorgon/rules/image-in-table.js +10 -0
  166. package/src/gorgon/rules/image-spaces-around-urls.js +35 -0
  167. package/src/gorgon/rules/image-widget.js +50 -0
  168. package/src/gorgon/rules/link-click-here.js +11 -0
  169. package/src/gorgon/rules/lint-utils.js +48 -0
  170. package/src/gorgon/rules/long-paragraph.js +14 -0
  171. package/src/gorgon/rules/math-adjacent.js +10 -0
  172. package/src/gorgon/rules/math-align-extra-break.js +11 -0
  173. package/src/gorgon/rules/math-align-linebreaks.js +43 -0
  174. package/src/gorgon/rules/math-empty.js +10 -0
  175. package/src/gorgon/rules/math-font-size.js +12 -0
  176. package/src/gorgon/rules/math-frac.js +10 -0
  177. package/src/gorgon/rules/math-nested.js +11 -0
  178. package/src/gorgon/rules/math-starts-with-space.js +12 -0
  179. package/src/gorgon/rules/math-text-empty.js +10 -0
  180. package/src/gorgon/rules/math-without-dollars.js +14 -0
  181. package/src/gorgon/rules/nested-lists.js +11 -0
  182. package/src/gorgon/rules/profanity.js +10 -0
  183. package/src/gorgon/rules/table-missing-cells.js +20 -0
  184. package/src/gorgon/rules/unbalanced-code-delimiters.js +14 -0
  185. package/src/gorgon/rules/unescaped-dollar.js +10 -0
  186. package/src/gorgon/rules/widget-in-table.js +10 -0
  187. package/src/gorgon/selector.js +506 -0
  188. package/src/gorgon/tree-transformer.js +588 -0
  189. package/src/hint-renderer.jsx +159 -0
  190. package/src/hints-renderer.jsx +359 -0
  191. package/src/icon-paths.js +120 -0
  192. package/src/index.js +149 -2
  193. package/src/init.js +49 -0
  194. package/src/interaction-tracker.js +60 -0
  195. package/src/interactive2/__tests__/movable-mock.js +48 -0
  196. package/src/interactive2/__tests__/movable-point_test.js +146 -0
  197. package/src/interactive2/interactive-util.js +152 -0
  198. package/src/interactive2/movable-helper-methods.js +160 -0
  199. package/src/interactive2/movable-line-options.js +392 -0
  200. package/src/interactive2/movable-line.js +322 -0
  201. package/src/interactive2/movable-point-options.js +190 -0
  202. package/src/interactive2/movable-point.jsx +604 -0
  203. package/src/interactive2/movable-polygon-options.js +351 -0
  204. package/src/interactive2/movable-polygon.js +367 -0
  205. package/src/interactive2/movable.js +273 -0
  206. package/src/interactive2/objective_.js +79 -0
  207. package/src/interactive2/types.js +3 -0
  208. package/src/interactive2/wrapped-defaults.js +86 -0
  209. package/src/interactive2/wrapped-ellipse.js +87 -0
  210. package/src/interactive2/wrapped-line.js +109 -0
  211. package/src/interactive2/wrapped-path.js +47 -0
  212. package/src/interactive2.js +91 -0
  213. package/src/item-renderer.jsx +518 -0
  214. package/src/item-version.js +12 -0
  215. package/src/jipt-paragraphs.js +44 -0
  216. package/src/jquery.mobile.vmouse.js +545 -0
  217. package/src/keypad-context.js +33 -0
  218. package/src/loading-context.js +20 -0
  219. package/src/logging/log.js +107 -0
  220. package/src/logging/log_test.js +41 -0
  221. package/src/mixins/changeable.jsx +115 -0
  222. package/src/mixins/editor-jsonify.jsx +13 -0
  223. package/src/mixins/provide-keypad.jsx +131 -0
  224. package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
  225. package/src/mixins/widget-prop-blacklist.js +36 -0
  226. package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
  227. package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
  228. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +994 -0
  229. package/src/multi-items/__tests__/items_test.js +245 -0
  230. package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
  231. package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
  232. package/src/multi-items/__tests__/shapes_test.js +44 -0
  233. package/src/multi-items/__tests__/trees_test.js +316 -0
  234. package/src/multi-items/item-types.js +55 -0
  235. package/src/multi-items/items.js +176 -0
  236. package/src/multi-items/multi-renderer.jsx +542 -0
  237. package/src/multi-items/prop-type-builders.js +105 -0
  238. package/src/multi-items/shape-types.js +47 -0
  239. package/src/multi-items/shapes.js +53 -0
  240. package/src/multi-items/tree-types.js +42 -0
  241. package/src/multi-items/trees.js +345 -0
  242. package/src/multi-items.js +76 -0
  243. package/src/not-gorgon.js +129 -0
  244. package/src/perseus-api.jsx +270 -0
  245. package/src/perseus-env.js +26 -0
  246. package/src/perseus-error.js +19 -0
  247. package/src/perseus-markdown.jsx +434 -0
  248. package/src/perseus-types.js +1597 -0
  249. package/src/pure-markdown.js +346 -0
  250. package/src/question-paragraph.jsx +30 -0
  251. package/src/renderability.jsx +108 -0
  252. package/src/renderer.jsx +1931 -0
  253. package/src/server-item-renderer.jsx +467 -0
  254. package/src/sigfigs.jsx +492 -0
  255. package/src/styles/articles.less +748 -0
  256. package/src/styles/constants.js +119 -0
  257. package/src/styles/global-constants.js +350 -0
  258. package/src/styles/global-styles.js +307 -0
  259. package/src/styles/khan-exercise.css +1304 -0
  260. package/src/styles/media-queries.js +48 -0
  261. package/src/styles/perseus-renderer.less +1008 -0
  262. package/src/styles/shared.js +132 -0
  263. package/src/styles/util.less +47 -0
  264. package/src/styles/variables.boostrap.less +301 -0
  265. package/src/styles/variables.less +68 -0
  266. package/src/styles/variables.shared.less +356 -0
  267. package/src/styles/widgets/categorizer.less +39 -0
  268. package/src/styles/widgets/definition.less +86 -0
  269. package/src/styles/widgets/dropdown.less +3 -0
  270. package/src/styles/widgets/expression.less +147 -0
  271. package/src/styles/widgets/graded-group.less +36 -0
  272. package/src/styles/widgets/grapher.less +11 -0
  273. package/src/styles/widgets/group.less +19 -0
  274. package/src/styles/widgets/image.less +29 -0
  275. package/src/styles/widgets/interactive-graph.less +61 -0
  276. package/src/styles/widgets/label-image.less +41 -0
  277. package/src/styles/widgets/matcher.less +8 -0
  278. package/src/styles/widgets/matrix.less +120 -0
  279. package/src/styles/widgets/measurer.less +20 -0
  280. package/src/styles/widgets/orderer.less +97 -0
  281. package/src/styles/widgets/passage.less +151 -0
  282. package/src/styles/widgets/plotter.less +33 -0
  283. package/src/styles/widgets/radio.less +52 -0
  284. package/src/styles/widgets/reaction-diagram.less +35 -0
  285. package/src/styles/widgets/sequence.less +5 -0
  286. package/src/styles/widgets/sortable.less +127 -0
  287. package/src/styles/widgets/table.less +56 -0
  288. package/src/styles/widgets/transformer.less +28 -0
  289. package/src/styles/widgets/unit.less +14 -0
  290. package/src/tex-wrangler.js +176 -0
  291. package/src/traversal.jsx +141 -0
  292. package/src/types.js +538 -0
  293. package/src/util/a11y.js +24 -0
  294. package/src/util/answer-types.js +862 -0
  295. package/src/util/answer-types_test.js +115 -0
  296. package/src/util/colors.js +94 -0
  297. package/src/util/fix-passage-refs.jsx +122 -0
  298. package/src/util/graph-utils.js +16 -0
  299. package/src/util/graphie.js +1585 -0
  300. package/src/util/interactive.js +4071 -0
  301. package/src/util/katex-preprocess.js +16 -0
  302. package/src/util/math.js +296 -0
  303. package/src/util/mobile-native-utils.js +14 -0
  304. package/src/util/mobile-native-utils_test.js +24 -0
  305. package/src/util/register-all-widgets-for-testing.js +12 -0
  306. package/src/util/scroll-utils.js +162 -0
  307. package/src/util/sizing-utils.js +46 -0
  308. package/src/util/tex.js +203 -0
  309. package/src/util/url-utils.js +36 -0
  310. package/src/util.js +990 -0
  311. package/src/version.js +11 -0
  312. package/src/widget-container.jsx +174 -0
  313. package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
  314. package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
  315. package/src/widgets/__stories__/definition.stories.jsx +38 -0
  316. package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
  317. package/src/widgets/__stories__/explanation.stories.jsx +19 -0
  318. package/src/widgets/__stories__/expression.stories.jsx +26 -0
  319. package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
  320. package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
  321. package/src/widgets/__stories__/grapher.stories.jsx +47 -0
  322. package/src/widgets/__stories__/group.stories.jsx +15 -0
  323. package/src/widgets/__stories__/iframe.stories.jsx +15 -0
  324. package/src/widgets/__stories__/image.stories.jsx +62 -0
  325. package/src/widgets/__stories__/input-number.stories.jsx +90 -0
  326. package/src/widgets/__stories__/interaction.stories.jsx +15 -0
  327. package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
  328. package/src/widgets/__stories__/matcher.stories.jsx +15 -0
  329. package/src/widgets/__stories__/matrix.stories.jsx +15 -0
  330. package/src/widgets/__stories__/number-line.stories.jsx +15 -0
  331. package/src/widgets/__stories__/numeric-input.stories.jsx +19 -0
  332. package/src/widgets/__stories__/orderer.stories.jsx +15 -0
  333. package/src/widgets/__stories__/passage-ref.stories.jsx +19 -0
  334. package/src/widgets/__stories__/passage.stories.jsx +19 -0
  335. package/src/widgets/__stories__/radio.stories.jsx +82 -0
  336. package/src/widgets/__stories__/sorter.stories.jsx +15 -0
  337. package/src/widgets/__stories__/transformer.stories.jsx +15 -0
  338. package/src/widgets/__stories__/video.stories.jsx +15 -0
  339. package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
  340. package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
  341. package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
  342. package/src/widgets/__testdata__/explanation_testdata.js +56 -0
  343. package/src/widgets/__testdata__/expression_testdata.js +95 -0
  344. package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
  345. package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
  346. package/src/widgets/__testdata__/grapher_testdata.js +376 -0
  347. package/src/widgets/__testdata__/group_testdata.js +156 -0
  348. package/src/widgets/__testdata__/iframe_testdata.js +36 -0
  349. package/src/widgets/__testdata__/image_testdata.js +38 -0
  350. package/src/widgets/__testdata__/input-number_testdata.js +73 -0
  351. package/src/widgets/__testdata__/interaction_testdata.js +264 -0
  352. package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
  353. package/src/widgets/__testdata__/matcher_testdata.js +34 -0
  354. package/src/widgets/__testdata__/matrix_testdata.js +29 -0
  355. package/src/widgets/__testdata__/number-line_testdata.js +31 -0
  356. package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
  357. package/src/widgets/__testdata__/orderer_testdata.js +58 -0
  358. package/src/widgets/__testdata__/passage-ref_testdata.js +41 -0
  359. package/src/widgets/__testdata__/passage_testdata.js +71 -0
  360. package/src/widgets/__testdata__/radio_testdata.js +210 -0
  361. package/src/widgets/__testdata__/sorter_testdata.js +20 -0
  362. package/src/widgets/__testdata__/transformer_testdata.js +109 -0
  363. package/src/widgets/__testdata__/video_testdata.js +22 -0
  364. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +928 -0
  365. package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
  366. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +99 -0
  367. package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +143 -0
  368. package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
  369. package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
  370. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +754 -0
  371. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +310 -0
  372. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +863 -0
  373. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2880 -0
  374. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1154 -0
  375. package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
  376. package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +388 -0
  377. package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
  378. package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
  379. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2915 -0
  380. package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
  381. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +450 -0
  382. package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
  383. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +930 -0
  384. package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
  385. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
  386. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
  387. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +7107 -0
  388. package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +163 -0
  389. package/src/widgets/__tests__/categorizer_test.jsx +205 -0
  390. package/src/widgets/__tests__/cs-program_test.jsx +45 -0
  391. package/src/widgets/__tests__/definition_test.js +152 -0
  392. package/src/widgets/__tests__/dropdown_test.jsx +106 -0
  393. package/src/widgets/__tests__/explanation_test.js +199 -0
  394. package/src/widgets/__tests__/expression_test.jsx +439 -0
  395. package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +33 -0
  396. package/src/widgets/__tests__/graded-group-set_test.jsx +270 -0
  397. package/src/widgets/__tests__/graded-group_test.jsx +233 -0
  398. package/src/widgets/__tests__/grapher.cypress.jsx +486 -0
  399. package/src/widgets/__tests__/grapher_test.jsx +35 -0
  400. package/src/widgets/__tests__/group_test.jsx +512 -0
  401. package/src/widgets/__tests__/iframe_test.jsx +47 -0
  402. package/src/widgets/__tests__/image_test.jsx +65 -0
  403. package/src/widgets/__tests__/input-number_test.jsx +360 -0
  404. package/src/widgets/__tests__/interaction_test.jsx +33 -0
  405. package/src/widgets/__tests__/interactive-graph.cypress.jsx +697 -0
  406. package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
  407. package/src/widgets/__tests__/matcher_test.jsx +157 -0
  408. package/src/widgets/__tests__/matrix_test.jsx +84 -0
  409. package/src/widgets/__tests__/number-line_test.jsx +81 -0
  410. package/src/widgets/__tests__/numeric-input_test.jsx +574 -0
  411. package/src/widgets/__tests__/orderer_test.jsx +75 -0
  412. package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
  413. package/src/widgets/__tests__/passage_test.jsx +133 -0
  414. package/src/widgets/__tests__/radio/base-radio_test.jsx +82 -0
  415. package/src/widgets/__tests__/radio_test.jsx +651 -0
  416. package/src/widgets/__tests__/renderQuestion.jsx +65 -0
  417. package/src/widgets/__tests__/unit_test.jsx +144 -0
  418. package/src/widgets/__tests__/video_test.jsx +55 -0
  419. package/src/widgets/categorizer.jsx +338 -0
  420. package/src/widgets/cs-program.jsx +228 -0
  421. package/src/widgets/definition.jsx +141 -0
  422. package/src/widgets/dropdown.jsx +143 -0
  423. package/src/widgets/explanation.jsx +315 -0
  424. package/src/widgets/expression.jsx +654 -0
  425. package/src/widgets/graded-group-answer-bar.jsx +152 -0
  426. package/src/widgets/graded-group-set.jsx +285 -0
  427. package/src/widgets/graded-group.jsx +448 -0
  428. package/src/widgets/grapher/util.jsx +819 -0
  429. package/src/widgets/grapher.jsx +658 -0
  430. package/src/widgets/group.jsx +213 -0
  431. package/src/widgets/iframe.jsx +199 -0
  432. package/src/widgets/image.jsx +309 -0
  433. package/src/widgets/input-number.jsx +410 -0
  434. package/src/widgets/interaction/arrow-picker.jsx +39 -0
  435. package/src/widgets/interaction/color-picker.jsx +69 -0
  436. package/src/widgets/interaction/dash-picker.jsx +46 -0
  437. package/src/widgets/interaction/element-container.jsx +97 -0
  438. package/src/widgets/interaction.jsx +742 -0
  439. package/src/widgets/interactive-graph.jsx +2821 -0
  440. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
  441. package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
  442. package/src/widgets/label-image/answer-choices.jsx +311 -0
  443. package/src/widgets/label-image/marker.jsx +365 -0
  444. package/src/widgets/label-image/types.js +33 -0
  445. package/src/widgets/label-image.jsx +841 -0
  446. package/src/widgets/label-image_test.js +840 -0
  447. package/src/widgets/lights-puzzle.jsx +356 -0
  448. package/src/widgets/matcher.jsx +271 -0
  449. package/src/widgets/matrix.jsx +615 -0
  450. package/src/widgets/measurer.jsx +215 -0
  451. package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
  452. package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
  453. package/src/widgets/molecule/molecule-drawing.jsx +175 -0
  454. package/src/widgets/molecule/molecule-layout.jsx +367 -0
  455. package/src/widgets/molecule/smiles-parser.jsx +300 -0
  456. package/src/widgets/molecule.jsx +172 -0
  457. package/src/widgets/number-line.jsx +858 -0
  458. package/src/widgets/numeric-input.jsx +622 -0
  459. package/src/widgets/orderer.jsx +753 -0
  460. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +382 -0
  461. package/src/widgets/passage/get-line-height-for-node.js +10 -0
  462. package/src/widgets/passage/passage-markdown.jsx +416 -0
  463. package/src/widgets/passage-ref-target.jsx +70 -0
  464. package/src/widgets/passage-ref.jsx +198 -0
  465. package/src/widgets/passage.jsx +601 -0
  466. package/src/widgets/plotter.jsx +1198 -0
  467. package/src/widgets/radio/__stories__/base-radio.stories.jsx +40 -0
  468. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +157 -0
  469. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +26 -0
  470. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +64 -0
  471. package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +59 -0
  472. package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +59 -0
  473. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +38 -0
  474. package/src/widgets/radio/__stories__/option-status.stories.jsx +118 -0
  475. package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +28 -0
  476. package/src/widgets/radio/base-radio.jsx +642 -0
  477. package/src/widgets/radio/choice-icon.jsx +468 -0
  478. package/src/widgets/radio/choice-none-above.jsx +54 -0
  479. package/src/widgets/radio/choice.jsx +692 -0
  480. package/src/widgets/radio/choice.jsx.fixture.js +64 -0
  481. package/src/widgets/radio/cross-out-button.jsx +136 -0
  482. package/src/widgets/radio/cross-out-menu-wrapper.jsx +492 -0
  483. package/src/widgets/radio/cross-out-tooltip.jsx +129 -0
  484. package/src/widgets/radio/focus-ring.jsx +51 -0
  485. package/src/widgets/radio/option-status.jsx +89 -0
  486. package/src/widgets/radio/toggleable-radio-button.jsx +133 -0
  487. package/src/widgets/radio/util.js +32 -0
  488. package/src/widgets/radio/util_test.js +19 -0
  489. package/src/widgets/radio/widget.jsx +470 -0
  490. package/src/widgets/radio.jsx +156 -0
  491. package/src/widgets/reaction-diagram.jsx +136 -0
  492. package/src/widgets/sequence.jsx +140 -0
  493. package/src/widgets/simulator.jsx +808 -0
  494. package/src/widgets/sorter.jsx +146 -0
  495. package/src/widgets/table.jsx +395 -0
  496. package/src/widgets/transformer.jsx +2870 -0
  497. package/src/widgets/unit.jsx +371 -0
  498. package/src/widgets/video-transcript-link.jsx +84 -0
  499. package/src/widgets/video-transcript-link_test.js +211 -0
  500. package/src/widgets/video.jsx +131 -0
  501. package/src/widgets.js +431 -0
  502. package/src/zoom.js +539 -0
  503. package/src/__tests__/index.test.js +0 -14
@@ -0,0 +1,1585 @@
1
+ /* eslint-disable @babel/no-invalid-this */
2
+ // @flow
3
+ import $ from "jquery";
4
+ import {point as kpoint, vector as kvector} from "kmath";
5
+ import Raphael from "raphael";
6
+ import _ from "underscore";
7
+
8
+ // Minify Raphael ourselves because IE8 has a problem with the 1.5.2 minified
9
+ // release
10
+ // http://groups.google.com/group/raphaeljs/browse_thread/thread/c34c75ad8d431544
11
+
12
+ import {Errors, Log} from "../logging/log.js";
13
+ import {PerseusError} from "../perseus-error.js";
14
+
15
+ import KhanColors from "./colors.js";
16
+ import KhanMath from "./math.js";
17
+ import Tex from "./tex.js";
18
+
19
+ import type {Coord} from "../interactive2/types";
20
+
21
+ const {processMath} = Tex;
22
+
23
+ /* Convert cartesian coordinates [x, y] to polar coordinates [r,
24
+ * theta], with theta in degrees, or in radians if angleInRadians is
25
+ * specified.
26
+ */
27
+ function cartToPolar(coord, angleInRadians) {
28
+ const r = Math.sqrt(Math.pow(coord[0], 2) + Math.pow(coord[1], 2));
29
+ let theta = Math.atan2(coord[1], coord[0]);
30
+ // convert angle range from [-pi, pi] to [0, 2pi]
31
+ if (theta < 0) {
32
+ theta += 2 * Math.PI;
33
+ }
34
+ if (!angleInRadians) {
35
+ theta = (theta * 180) / Math.PI;
36
+ }
37
+ return [r, theta];
38
+ }
39
+
40
+ function polar(r, th) {
41
+ if (typeof r === "number") {
42
+ r = [r, r];
43
+ }
44
+ th = (th * Math.PI) / 180;
45
+ return [r[0] * Math.cos(th), r[1] * Math.sin(th)];
46
+ }
47
+
48
+ const GraphUtils: $FlowFixMe = {
49
+ unscaledSvgPath: function (points) {
50
+ // If this is an empty closed path, return "" instead of "z", which
51
+ // would give an error
52
+ if (points[0] === true) {
53
+ return "";
54
+ }
55
+ return $.map(points, function (point, i) {
56
+ if (point === true) {
57
+ return "z";
58
+ }
59
+ return (i === 0 ? "M" : "L") + point[0] + " " + point[1];
60
+ }).join("");
61
+ },
62
+
63
+ getDistance: function (point1, point2) {
64
+ return kpoint.distanceToPoint(point1, point2);
65
+ },
66
+
67
+ /**
68
+ * Return the difference between two sets of coordinates
69
+ */
70
+ coordDiff: function (startCoord, endCoord) {
71
+ return _.map(endCoord, function (val, i) {
72
+ return endCoord[i] - startCoord[i];
73
+ });
74
+ },
75
+
76
+ /**
77
+ * Round the given coordinates to a given snap value
78
+ * (e.g., nearest 0.2 increment)
79
+ */
80
+ snapCoord: function (coord, snap) {
81
+ return _.map(coord, function (val, i) {
82
+ return KhanMath.roundToNearest(snap[i], val);
83
+ });
84
+ },
85
+
86
+ // Find the angle in degrees between two or three points
87
+ findAngle: function (point1, point2, vertex) {
88
+ if (vertex === undefined) {
89
+ const x = point1[0] - point2[0];
90
+ const y = point1[1] - point2[1];
91
+ if (!x && !y) {
92
+ return 0;
93
+ }
94
+ return (180 + (Math.atan2(-y, -x) * 180) / Math.PI + 360) % 360;
95
+ }
96
+ return (
97
+ GraphUtils.findAngle(point1, vertex) -
98
+ GraphUtils.findAngle(point2, vertex)
99
+ );
100
+ },
101
+
102
+ graphs: {},
103
+ };
104
+
105
+ const Graphie = (GraphUtils.Graphie = function () {});
106
+
107
+ _.extend(Graphie.prototype, {
108
+ cartToPolar: cartToPolar,
109
+ polar: polar,
110
+ });
111
+
112
+ const labelDirections = {
113
+ center: [-0.5, -0.5],
114
+ above: [-0.5, -1.0],
115
+ "above right": [0.0, -1.0],
116
+ right: [0.0, -0.5],
117
+ "below right": [0.0, 0.0],
118
+ below: [-0.5, 0.0],
119
+ "below left": [-1.0, 0.0],
120
+ left: [-1.0, -0.5],
121
+ "above left": [-1.0, -1.0],
122
+ };
123
+
124
+ /**
125
+ * Safari applies some SVG-specific styles to things that are not SVGs, so we
126
+ * need to exclude those styles from things that are not SVGs.
127
+ *
128
+ * To see this behavior in action, open https://codepen.io/anon/pen/zENEoa in
129
+ * Safari.
130
+ *
131
+ * Usage `$.extend({}, someStyles, SVG_SPECIFIC_STYLE_MASK)`
132
+ */
133
+ const SVG_SPECIFIC_STYLE_MASK = {
134
+ "stroke-width": null,
135
+ };
136
+
137
+ GraphUtils.createGraphie = function (el) {
138
+ let xScale = 40;
139
+ let yScale = 40;
140
+ let xRange;
141
+ let yRange;
142
+
143
+ $(el).css("position", "relative");
144
+ const raphael = Raphael(el);
145
+
146
+ // For a sometimes-reproducible IE8 bug; doesn't affect SVG browsers at all
147
+ $(el).children("div").css("position", "absolute");
148
+
149
+ // Set up some reasonable defaults
150
+ let currentStyle: $FlowFixMe = {
151
+ "stroke-width": 2,
152
+ fill: "none",
153
+ };
154
+
155
+ const scaleVector = function (point: number | Coord) {
156
+ if (typeof point === "number") {
157
+ return scaleVector([point, point]);
158
+ }
159
+
160
+ const x = point[0];
161
+ const y = point[1];
162
+ return [x * xScale, y * yScale];
163
+ };
164
+
165
+ const scalePoint = function scalePoint(point: number | Coord): Coord {
166
+ if (typeof point === "number") {
167
+ return scalePoint([point, point]);
168
+ }
169
+
170
+ const x = point[0];
171
+ const y = point[1];
172
+ return [(x - xRange[0]) * xScale, (yRange[1] - y) * yScale];
173
+ };
174
+
175
+ const unscalePoint = function (point) {
176
+ if (typeof point === "number") {
177
+ return unscalePoint([point, point]);
178
+ }
179
+
180
+ const x = point[0];
181
+ const y = point[1];
182
+ return [x / xScale + xRange[0], yRange[1] - y / yScale];
183
+ };
184
+
185
+ const unscaleVector = function (point) {
186
+ if (typeof point === "number") {
187
+ return unscaleVector([point, point]);
188
+ }
189
+
190
+ return [point[0] / xScale, point[1] / yScale];
191
+ };
192
+
193
+ const setLabelMargins = function (span, size) {
194
+ const $span = $(span);
195
+ const direction = $span.data("labelDirection");
196
+ let [width, height] = size;
197
+ // This can happen when a span
198
+ // is invisible but we still want to update the CSS. At worst, we will
199
+ // be off by a few pixels instead of in a different position entirely.
200
+ if (width === 0 && height === 0) {
201
+ [width, height] = [1, 1];
202
+ Log.log("Label size was 0x0 in graphie.js; using 1x1 instead");
203
+ }
204
+ $span.css("visibility", "");
205
+
206
+ if (typeof direction === "number") {
207
+ const x = Math.cos(direction);
208
+ const y = Math.sin(direction);
209
+
210
+ const scale = Math.min(
211
+ width / 2 / Math.abs(x),
212
+ height / 2 / Math.abs(y),
213
+ );
214
+
215
+ $span.css({
216
+ marginLeft: -width / 2 + x * scale,
217
+ marginTop: -height / 2 - y * scale,
218
+ });
219
+ } else {
220
+ const multipliers = labelDirections[direction || "center"];
221
+ $span.css({
222
+ marginLeft: Math.round(width * multipliers[0]),
223
+ marginTop: Math.round(height * multipliers[1]),
224
+ });
225
+ }
226
+ };
227
+
228
+ const svgPath = function (points, alreadyScaled) {
229
+ return $.map(points, function (point, i) {
230
+ if (point === true) {
231
+ return "z";
232
+ }
233
+ const scaled = alreadyScaled ? point : scalePoint(point);
234
+ return (
235
+ (i === 0 ? "M" : "L") +
236
+ KhanMath.bound(scaled[0]) +
237
+ " " +
238
+ KhanMath.bound(scaled[1])
239
+ );
240
+ }).join("");
241
+ };
242
+
243
+ const svgParabolaPath = function (a, b, c) {
244
+ const computeParabola = function (x) {
245
+ return (a * x + b) * x + c;
246
+ };
247
+
248
+ // If points are collinear, plot a line instead
249
+ if (a === 0) {
250
+ const points = _.map(xRange, function (x) {
251
+ return [x, computeParabola(x)];
252
+ });
253
+ return svgPath(points);
254
+ }
255
+
256
+ // Calculate x coordinates of points on parabola
257
+ const xVertex = -b / (2 * a);
258
+ const distToEdge = Math.max(
259
+ Math.abs(xVertex - xRange[0]),
260
+ Math.abs(xVertex - xRange[1]),
261
+ );
262
+
263
+ // To guarantee that drawn parabola to spans the viewport, use a point
264
+ // on the edge of the graph furtherest from the vertex
265
+ const xPoint = xVertex + distToEdge;
266
+
267
+ // Compute parabola and other point on the curve
268
+ const vertex = [xVertex, computeParabola(xVertex)];
269
+ const point = [xPoint, computeParabola(xPoint)];
270
+
271
+ // Calculate SVG 'control' point, defined by spec
272
+ const control = [vertex[0], vertex[1] - (point[1] - vertex[1])];
273
+
274
+ // Calculate mirror points across parabola's axis of symmetry
275
+ const dx = Math.abs(vertex[0] - point[0]);
276
+ const left = [vertex[0] - dx, point[1]];
277
+ const right = [vertex[0] + dx, point[1]];
278
+
279
+ // Scale and bound
280
+ const points = _.map([left, control, right], scalePoint);
281
+ const values = _.map(_.flatten(points), KhanMath.bound);
282
+ return (
283
+ "M" +
284
+ values[0] +
285
+ "," +
286
+ values[1] +
287
+ " Q" +
288
+ values[2] +
289
+ "," +
290
+ values[3] +
291
+ " " +
292
+ values[4] +
293
+ "," +
294
+ values[5]
295
+ );
296
+ };
297
+
298
+ const svgSinusoidPath = function (a, b, c, d) {
299
+ // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
300
+ const quarterPeriod = Math.abs(Math.PI / (2 * b));
301
+
302
+ const computeSine = function (x) {
303
+ return a * Math.sin(b * x - c) + d;
304
+ };
305
+
306
+ const computeDerivative = function (x) {
307
+ return a * b * Math.cos(c - b * x);
308
+ };
309
+
310
+ const coordsForOffset = function (initial, i) {
311
+ // Return the cubic coordinates (including the two anchor and two
312
+ // control points) for the ith portion of the sinusoid.
313
+ const x0 = initial + quarterPeriod * i;
314
+ const x1 = x0 + quarterPeriod;
315
+
316
+ // Interpolate using derivative technique
317
+ // See: http://stackoverflow.com/questions/13932704/how-to-draw-sine-waves-with-svg-js
318
+ const xCoords = [
319
+ x0,
320
+ (x0 * 2) / 3 + (x1 * 1) / 3,
321
+ (x0 * 1) / 3 + (x1 * 2) / 3,
322
+ x1,
323
+ ];
324
+ const yCoords = [
325
+ computeSine(x0),
326
+ computeSine(x0) + (computeDerivative(x0) * (x1 - x0)) / 3,
327
+ computeSine(x1) - (computeDerivative(x1) * (x1 - x0)) / 3,
328
+ computeSine(x1),
329
+ ];
330
+
331
+ // Zip and scale
332
+ return _.map(_.zip(xCoords, yCoords), scalePoint);
333
+ };
334
+
335
+ // How many quarter-periods do we need to span the graph?
336
+ const extent = xRange[1] - xRange[0];
337
+ const numQuarterPeriods = Math.ceil(extent / quarterPeriod) + 1;
338
+
339
+ // Find starting coordinate: first anchor point curve left of xRange[0]
340
+ let initial = c / b;
341
+ const distToEdge = initial - xRange[0];
342
+ initial -= quarterPeriod * Math.ceil(distToEdge / quarterPeriod);
343
+
344
+ // First portion of path is special-case, requiring move-to ('M')
345
+ let coords = coordsForOffset(initial, 0);
346
+ let path =
347
+ "M" +
348
+ coords[0][0] +
349
+ "," +
350
+ coords[0][1] +
351
+ " C" +
352
+ coords[1][0] +
353
+ "," +
354
+ coords[1][1] +
355
+ " " +
356
+ coords[2][0] +
357
+ "," +
358
+ coords[2][1] +
359
+ " " +
360
+ coords[3][0] +
361
+ "," +
362
+ coords[3][1];
363
+ for (let i = 1; i < numQuarterPeriods; i++) {
364
+ coords = coordsForOffset(initial, i);
365
+ path +=
366
+ " C" +
367
+ coords[1][0] +
368
+ "," +
369
+ coords[1][1] +
370
+ " " +
371
+ coords[2][0] +
372
+ "," +
373
+ coords[2][1] +
374
+ " " +
375
+ coords[3][0] +
376
+ "," +
377
+ coords[3][1];
378
+ }
379
+
380
+ return path;
381
+ };
382
+
383
+ // `svgPath` is independent of graphie range, so we export it independently
384
+ GraphUtils.svgPath = svgPath;
385
+
386
+ const processAttributes = function (attrs) {
387
+ const transformers = {
388
+ scale: function (scale) {
389
+ if (typeof scale === "number") {
390
+ scale = [scale, scale];
391
+ }
392
+
393
+ xScale = scale[0];
394
+ yScale = scale[1];
395
+
396
+ // Update the canvas size
397
+ raphael.setSize(
398
+ (xRange[1] - xRange[0]) * xScale,
399
+ (yRange[1] - yRange[0]) * yScale,
400
+ );
401
+ },
402
+
403
+ clipRect: function (pair) {
404
+ const point = pair[0];
405
+ const size = pair[1];
406
+ point[1] += size[1]; // because our coordinates are flipped
407
+
408
+ return {
409
+ "clip-rect": scalePoint(point)
410
+ .concat(scaleVector(size))
411
+ .join(" "),
412
+ };
413
+ },
414
+
415
+ strokeWidth: function (val) {
416
+ return {"stroke-width": parseFloat(val)};
417
+ },
418
+
419
+ rx: function (val) {
420
+ return {rx: scaleVector([val, 0])[0]};
421
+ },
422
+
423
+ ry: function (val) {
424
+ return {ry: scaleVector([0, val])[1]};
425
+ },
426
+
427
+ r: function (val) {
428
+ const scaled = scaleVector([val, val]);
429
+ return {rx: scaled[0], ry: scaled[1]};
430
+ },
431
+ };
432
+
433
+ const processed = {};
434
+ $.each(attrs || {}, function (key, value) {
435
+ const transformer = transformers[key];
436
+
437
+ if (typeof transformer === "function") {
438
+ $.extend(processed, transformer(value));
439
+ } else {
440
+ const dasherized = key
441
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2")
442
+ .replace(/([a-z\d])([A-Z])/g, "$1-$2")
443
+ .toLowerCase();
444
+ processed[dasherized] = value;
445
+ }
446
+ });
447
+
448
+ return processed;
449
+ };
450
+
451
+ const addArrowheads = function arrows(path) {
452
+ const type = path.constructor.prototype;
453
+
454
+ if (type === Raphael.el) {
455
+ if (
456
+ path.type === "path" &&
457
+ typeof path.arrowheadsDrawn === "undefined"
458
+ ) {
459
+ const w = path.attr("stroke-width");
460
+ const s = 0.6 + 0.4 * w;
461
+ const l = path.getTotalLength();
462
+ const set = raphael.set();
463
+ const head = raphael.path(
464
+ graphie.isMobile
465
+ ? "M-4,4 C-4,4 -0.25,0 -0.25,0 C-0.25,0 -4,-4 -4,-4"
466
+ : "M-3 4 C-2.75 2.5 0 0.25 0.75 0C0 -0.25 -2.75 -2.5 -3 -4",
467
+ );
468
+ const end = path.getPointAtLength(l - 0.4);
469
+ const almostTheEnd = path.getPointAtLength(l - 0.75 * s);
470
+ const angle =
471
+ (Math.atan2(
472
+ end.y - almostTheEnd.y,
473
+ end.x - almostTheEnd.x,
474
+ ) *
475
+ 180) /
476
+ Math.PI;
477
+ const attrs = path.attr();
478
+ delete attrs.path;
479
+
480
+ let subpath = path.getSubpath(0, l - 0.75 * s);
481
+ subpath = raphael.path(subpath).attr(attrs);
482
+ subpath.arrowheadsDrawn = true;
483
+ path.remove();
484
+
485
+ // For some unknown reason 0 doesn't work for the rotation
486
+ // origin so we use a tiny number.
487
+ head.rotate(angle, graphie.isMobile ? 1e-5 : 0.75, 0)
488
+ .scale(s, s, 0.75, 0)
489
+ .translate(almostTheEnd.x, almostTheEnd.y)
490
+ .attr(attrs)
491
+ .attr({
492
+ "stroke-linejoin": "round",
493
+ "stroke-linecap": "round",
494
+ });
495
+
496
+ head.arrowheadsDrawn = true;
497
+ set.push(subpath);
498
+ set.push(head);
499
+ return set;
500
+ }
501
+ } else if (type === Raphael.st) {
502
+ for (let i = 0, l = path.items.length; i < l; i++) {
503
+ arrows(path.items[i]);
504
+ }
505
+ }
506
+ return path;
507
+ };
508
+
509
+ const drawingTools: $FlowFixMe = {
510
+ circle: function (center, radius) {
511
+ return raphael.ellipse(
512
+ ...scalePoint(center).concat(scaleVector([radius, radius])),
513
+ );
514
+ },
515
+
516
+ // (x, y) is coordinate of bottom left corner
517
+ rect: function (x, y, width, height) {
518
+ // Raphael needs (x, y) to be coordinate of upper left corner
519
+ const corner = scalePoint([x, y + height]);
520
+ const dims = scaleVector([width, height]);
521
+ const elem = raphael.rect(...corner.concat(dims));
522
+
523
+ if (graphie.isMobile) {
524
+ elem.node.style.shapeRendering = "crispEdges";
525
+ }
526
+
527
+ return elem;
528
+ },
529
+
530
+ ellipse: function (center, radii) {
531
+ return raphael.ellipse(
532
+ ...scalePoint(center).concat(scaleVector(radii)),
533
+ );
534
+ },
535
+
536
+ fixedEllipse: function (
537
+ center: number | Coord,
538
+ // Different type than Coord, this is radiusX, radiusY
539
+ radii: number | [number, number],
540
+ maxScale: number,
541
+ padding: number,
542
+ ) {
543
+ // Scale point and radius
544
+ const scaledPoint = scalePoint(center);
545
+ const scaledRadii = scaleVector(radii);
546
+
547
+ const width = 2 * scaledRadii[0] * maxScale + padding;
548
+ const height = 2 * scaledRadii[1] * maxScale + padding;
549
+
550
+ // Calculate absolute left, top
551
+ const left = scaledPoint[0] - width / 2;
552
+ const top = scaledPoint[1] - height / 2;
553
+
554
+ // Wrap in <div>
555
+ const wrapper = document.createElement("div");
556
+ $(wrapper).css({
557
+ position: "absolute",
558
+ width: width + "px",
559
+ height: height + "px",
560
+ left: left + "px",
561
+ top: top + "px",
562
+ });
563
+ // wrapper.setAttribute("data-graphie-type", "ellipse");
564
+
565
+ // Create Raphael canvas
566
+ const localRaphael = Raphael(wrapper, width, height);
567
+ const visibleShape = localRaphael.ellipse(
568
+ width / 2,
569
+ height / 2,
570
+ scaledRadii[0],
571
+ scaledRadii[1],
572
+ );
573
+
574
+ return {
575
+ wrapper: wrapper,
576
+ visibleShape: visibleShape,
577
+ };
578
+ },
579
+
580
+ arc: function (center, radius, startAngle, endAngle, sector) {
581
+ startAngle = ((startAngle % 360) + 360) % 360;
582
+ endAngle = ((endAngle % 360) + 360) % 360;
583
+
584
+ const cent = scalePoint(center);
585
+ const radii = scaleVector(radius);
586
+ const startVector = polar(radius, startAngle);
587
+ const endVector = polar(radius, endAngle);
588
+
589
+ const startPoint = scalePoint([
590
+ center[0] + startVector[0],
591
+ center[1] + startVector[1],
592
+ ]);
593
+ const endPoint = scalePoint([
594
+ center[0] + endVector[0],
595
+ center[1] + endVector[1],
596
+ ]);
597
+
598
+ const largeAngle =
599
+ (((endAngle - startAngle) % 360) + 360) % 360 > 180;
600
+
601
+ return raphael.path(
602
+ "M" +
603
+ startPoint.join(" ") +
604
+ "A" +
605
+ radii.join(" ") +
606
+ " 0 " + // ellipse rotation
607
+ (largeAngle ? 1 : 0) +
608
+ " 0 " + // sweep flag
609
+ endPoint.join(" ") +
610
+ (sector ? "L" + cent.join(" ") + "z" : ""),
611
+ );
612
+ },
613
+
614
+ path: function (points) {
615
+ const p = raphael.path(svgPath(points));
616
+ p.graphiePath = points;
617
+
618
+ return p;
619
+ },
620
+
621
+ fixedPath: function (points, center, createPath) {
622
+ points = _.map(points, scalePoint);
623
+ center = center ? scalePoint(center) : null;
624
+ createPath = createPath || svgPath;
625
+
626
+ const pathLeft = _.min(_.pluck(points, 0));
627
+ const pathRight = _.max(_.pluck(points, 0));
628
+ const pathTop = _.min(_.pluck(points, 1));
629
+ const pathBottom = _.max(_.pluck(points, 1));
630
+
631
+ // Apply padding to line
632
+ const padding = [4, 4];
633
+
634
+ // Calculate and apply additional offset
635
+ const extraOffset = [pathLeft, pathTop];
636
+
637
+ // Apply padding and offset to points
638
+ points = _.map(points, function (point) {
639
+ return kvector.add(
640
+ kvector.subtract(point, extraOffset),
641
+ kvector.scale(padding, 0.5),
642
+ );
643
+ });
644
+
645
+ // Calculate <div> dimensions
646
+ const width = pathRight - pathLeft + padding[0];
647
+ const height = pathBottom - pathTop + padding[1];
648
+ const left = extraOffset[0] - padding[0] / 2;
649
+ const top = extraOffset[1] - padding[1] / 2;
650
+
651
+ // Create <div>
652
+ const wrapper = document.createElement("div");
653
+ $(wrapper).css({
654
+ position: "absolute",
655
+ width: width + "px",
656
+ height: height + "px",
657
+ left: left + "px",
658
+ top: top + "px",
659
+ // If user specified a center, set it
660
+ transformOrigin: center
661
+ ? width / 2 +
662
+ center[0] +
663
+ "px " +
664
+ (height / 2 + center[1]) +
665
+ "px"
666
+ : null,
667
+ });
668
+
669
+ // Create Raphael canvas
670
+ const localRaphael = Raphael(wrapper, width, height);
671
+
672
+ // Calculate path
673
+ const visibleShape = localRaphael.path(createPath(points));
674
+
675
+ return {
676
+ wrapper: wrapper,
677
+ visibleShape: visibleShape,
678
+ };
679
+ },
680
+
681
+ scaledPath: function (points) {
682
+ const p = raphael.path(svgPath(points, /* alreadyScaled */ true));
683
+ p.graphiePath = points;
684
+ return p;
685
+ },
686
+
687
+ line: function (start, end) {
688
+ const l = this.path([start, end]);
689
+
690
+ if (graphie.isMobile) {
691
+ l.node.style.shapeRendering = "crispEdges";
692
+ }
693
+
694
+ return l;
695
+ },
696
+
697
+ parabola: function (a, b, c) {
698
+ // Plot a parabola of the form: f(x) = (a * x + b) * x + c
699
+ return raphael.path(svgParabolaPath(a, b, c));
700
+ },
701
+
702
+ fixedLine: function (start, end, thickness) {
703
+ // Apply padding to line
704
+ const padding = [thickness, thickness];
705
+
706
+ // Scale points to get values in pixels
707
+ start = scalePoint(start);
708
+ end = scalePoint(end);
709
+
710
+ // Calculate and apply additional offset
711
+ const extraOffset = [
712
+ Math.min(start[0], end[0]),
713
+ Math.min(start[1], end[1]),
714
+ ];
715
+
716
+ // Apply padding and offset to start, end points
717
+ start = kvector.add(
718
+ kvector.subtract(start, extraOffset),
719
+ kvector.scale(padding, 0.5),
720
+ );
721
+ end = kvector.add(
722
+ kvector.subtract(end, extraOffset),
723
+ kvector.scale(padding, 0.5),
724
+ );
725
+
726
+ // Calculate <div> dimensions
727
+ const left = extraOffset[0] - padding[0] / 2;
728
+ const top = extraOffset[1] - padding[1] / 2;
729
+ const width = Math.abs(start[0] - end[0]) + padding[0];
730
+ const height = Math.abs(start[1] - end[1]) + padding[1];
731
+
732
+ // Create <div>
733
+ const wrapper = document.createElement("div");
734
+ $(wrapper).css({
735
+ position: "absolute",
736
+ width: width + "px",
737
+ height: height + "px",
738
+ left: left + "px",
739
+ top: top + "px",
740
+ // Outsiders should feel like the line's 'origin' (i.e., for
741
+ // rotation) is the starting point
742
+ transformOrigin: start[0] + "px " + start[1] + "px",
743
+ });
744
+
745
+ // Create Raphael canvas
746
+ const localRaphael = Raphael(wrapper, width, height);
747
+
748
+ // Calculate path
749
+ const path =
750
+ "M" +
751
+ start[0] +
752
+ " " +
753
+ start[1] +
754
+ " " +
755
+ "L" +
756
+ end[0] +
757
+ " " +
758
+ end[1];
759
+ const visibleShape = localRaphael.path(path);
760
+ visibleShape.graphiePath = [start, end];
761
+
762
+ return {
763
+ wrapper: wrapper,
764
+ visibleShape: visibleShape,
765
+ };
766
+ },
767
+
768
+ sinusoid: function (a, b, c, d) {
769
+ // Plot a sinusoid of the form: f(x) = a * sin(b * x - c) + d
770
+ return raphael.path(svgSinusoidPath(a, b, c, d));
771
+ },
772
+
773
+ grid: function (xr, yr) {
774
+ const step: $FlowFixMe = currentStyle.step || [1, 1];
775
+ const set = raphael.set();
776
+
777
+ let x = step[0] * Math.ceil(xr[0] / step[0]);
778
+ for (; x <= xr[1]; x += step[0]) {
779
+ set.push(this.line([x, yr[0]], [x, yr[1]]));
780
+ }
781
+
782
+ let y = step[1] * Math.ceil(yr[0] / step[1]);
783
+ for (; y <= yr[1]; y += step[1]) {
784
+ set.push(this.line([xr[0], y], [xr[1], y]));
785
+ }
786
+
787
+ return set;
788
+ },
789
+
790
+ label: function (point, text, direction, latex) {
791
+ latex = typeof latex === "undefined" || latex;
792
+
793
+ const $span = $("<span>").addClass("graphie-label");
794
+
795
+ const pad = currentStyle["label-distance"];
796
+
797
+ $span
798
+ .css(
799
+ $.extend(
800
+ {},
801
+ {
802
+ position: "absolute",
803
+ padding: (pad != null ? pad : 7) + "px",
804
+ color: "black",
805
+ },
806
+ ),
807
+ )
808
+ .data("labelDirection", direction)
809
+ .appendTo(el);
810
+
811
+ $span.setPosition = function (point) {
812
+ const scaledPoint = scalePoint(point);
813
+ $span.css({
814
+ left: scaledPoint[0],
815
+ top: scaledPoint[1],
816
+ });
817
+ };
818
+
819
+ $span.setPosition(point);
820
+
821
+ const span = $span[0];
822
+
823
+ $span.processMath = function (math, force) {
824
+ processMath(span, math, force, function () {
825
+ const width = span.scrollWidth;
826
+ const height = span.scrollHeight;
827
+ setLabelMargins(span, [width, height]);
828
+ });
829
+ };
830
+
831
+ $span.processText = function (text) {
832
+ $span.html(text);
833
+ const width = span.scrollWidth;
834
+ const height = span.scrollHeight;
835
+ setLabelMargins(span, [width, height]);
836
+ };
837
+
838
+ if (latex) {
839
+ $span.processMath(text, /* force */ false);
840
+ } else {
841
+ $span.processText(text);
842
+ }
843
+
844
+ return $span;
845
+ },
846
+
847
+ plotParametric: function (fn, range, shade, fn2) {
848
+ // Note: fn2 should only be set if 'shade' is true, as it denotes
849
+ // the function between which fn should have its area shaded.
850
+ // In general, plotParametric shouldn't be used to shade the area
851
+ // between two arbitrary parametrics functions over an interval,
852
+ // as the method assumes that fn and fn2 are both of the form
853
+ // fn(t) = (t, fn'(t)) for some initial fn'.
854
+ fn2 =
855
+ fn2 ||
856
+ function (t) {
857
+ return [t, 0];
858
+ };
859
+
860
+ // We truncate to 500,000, since anything bigger causes
861
+ // overflow in the firefox svg renderer. This is safe
862
+ // since 500,000 is outside the viewport anyway. We
863
+ // write these functions the way we do to handle undefined.
864
+ const clipper = (xy) => {
865
+ if (Math.abs(xy[1]) > 500000) {
866
+ return [xy[0], Math.min(Math.max(xy[1], -500000), 500000)];
867
+ }
868
+ return xy;
869
+ };
870
+ const clippedFn = (x) => clipper(fn(x));
871
+ const clippedFn2 = (x) => clipper(fn2(x));
872
+
873
+ if (!currentStyle.strokeLinejoin) {
874
+ currentStyle.strokeLinejoin = "round";
875
+ }
876
+ if (!currentStyle.strokeLinecap) {
877
+ currentStyle.strokeLinecap = "round";
878
+ }
879
+
880
+ const min = range[0];
881
+ const max = range[1];
882
+ let step = (max - min) / (currentStyle["plot-points"] || 800);
883
+ if (step === 0) {
884
+ step = 1;
885
+ }
886
+
887
+ const paths = raphael.set();
888
+ let points = [];
889
+ let lastDiff = GraphUtils.coordDiff(
890
+ clippedFn(min),
891
+ clippedFn2(min),
892
+ );
893
+
894
+ let lastFlip = min;
895
+ for (let t = min; t <= max; t += step) {
896
+ const top = clippedFn(t);
897
+ const bottom = clippedFn2(t);
898
+ const diff = GraphUtils.coordDiff(top, bottom);
899
+
900
+ // Find points where it flips
901
+ // Create path that sketches area between the two functions
902
+ if (
903
+ // if there is an asymptote here, meaning that the graph
904
+ // switches signs and has a large difference
905
+ (diff[1] < 0 !== lastDiff[1] < 0 &&
906
+ Math.abs(diff[1] - lastDiff[1]) > 2 * yScale) ||
907
+ // or the function is undefined
908
+ isNaN(diff[1])
909
+ ) {
910
+ // split the path at this point, and draw it
911
+ if (shade) {
912
+ points.push(top);
913
+
914
+ // backtrack to draw paired function
915
+ for (let u = t - step; u >= lastFlip; u -= step) {
916
+ points.push(clippedFn2(u));
917
+ }
918
+ lastFlip = t;
919
+ }
920
+ paths.push(this.path(points));
921
+ // restart the path, excluding this point
922
+ points = [];
923
+ if (shade) {
924
+ points.push(top);
925
+ }
926
+ } else {
927
+ // otherwise, just add the point to the path
928
+ points.push(top);
929
+ }
930
+
931
+ lastDiff = diff;
932
+ }
933
+
934
+ if (shade) {
935
+ // backtrack to draw paired function
936
+ for (let u = max - step; u >= lastFlip; u -= step) {
937
+ points.push(clippedFn2(u));
938
+ }
939
+ }
940
+ paths.push(this.path(points));
941
+
942
+ return paths;
943
+ },
944
+
945
+ plotPolar: function (fn, range) {
946
+ const min = range[0];
947
+ const max = range[1];
948
+
949
+ // There is probably a better heuristic for this
950
+ if (!currentStyle["plot-points"]) {
951
+ currentStyle["plot-points"] = 2 * (max - min) * xScale;
952
+ }
953
+
954
+ return this.plotParametric(function (th) {
955
+ return polar(fn(th), (th * 180) / Math.PI);
956
+ }, range);
957
+ },
958
+
959
+ plot: function (fn, range, swapAxes, shade, fn2) {
960
+ const min = range[0];
961
+ const max = range[1];
962
+ if (!currentStyle["plot-points"]) {
963
+ currentStyle["plot-points"] = 2 * (max - min) * xScale;
964
+ }
965
+
966
+ if (swapAxes) {
967
+ if (fn2) {
968
+ // TODO(charlie): support swapped axis area shading
969
+ throw new PerseusError(
970
+ "Can't shade area between functions with swapped axes.",
971
+ Errors.Internal,
972
+ );
973
+ }
974
+ return this.plotParametric(
975
+ function (y) {
976
+ return [fn(y), y];
977
+ },
978
+ range,
979
+ shade,
980
+ );
981
+ }
982
+ if (fn2) {
983
+ if (shade) {
984
+ return this.plotParametric(
985
+ function (x) {
986
+ return [x, fn(x)];
987
+ },
988
+ range,
989
+ shade,
990
+ function (x) {
991
+ return [x, fn2(x)];
992
+ },
993
+ );
994
+ }
995
+ throw new PerseusError(
996
+ "fn2 should only be set when 'shade' is True.",
997
+ Errors.Internal,
998
+ );
999
+ }
1000
+ return this.plotParametric(
1001
+ function (x) {
1002
+ return [x, fn(x)];
1003
+ },
1004
+ range,
1005
+ shade,
1006
+ );
1007
+ },
1008
+
1009
+ /**
1010
+ * Given a piecewise function, return a Raphael set of paths that
1011
+ * can be used to draw the function, e.g. using style().
1012
+ * Calls plotParametric.
1013
+ *
1014
+ * @param {[]} fnArray array of functions which when called
1015
+ * with a parameter i return the value of
1016
+ * the function at i
1017
+ * @param {[]} rangeArray array of ranges over which the
1018
+ * corresponding functions are defined
1019
+ * @return {Raphael set}
1020
+ */
1021
+ plotPiecewise: function (fnArray, rangeArray) {
1022
+ const paths = raphael.set();
1023
+ const self = this;
1024
+ _.times(fnArray.length, function (i) {
1025
+ const fn = fnArray[i];
1026
+ const range = rangeArray[i];
1027
+ const fnPaths = self.plotParametric(function (x) {
1028
+ return [x, fn(x)];
1029
+ }, range);
1030
+ _.each(fnPaths, function (fnPath) {
1031
+ paths.push(fnPath);
1032
+ });
1033
+ });
1034
+
1035
+ return paths;
1036
+ },
1037
+
1038
+ /**
1039
+ * Given an array of coordinates of the form [x, y], create and
1040
+ * return a Raphael set of Raphael circle objects at those
1041
+ * coordinates
1042
+ *
1043
+ * @param {Array of arrays} endpointArray
1044
+ * @return {Raphael set}
1045
+ */
1046
+ plotEndpointCircles: function (endpointArray) {
1047
+ const circles = raphael.set();
1048
+ const self = this;
1049
+
1050
+ _.each(endpointArray, function (coord, i) {
1051
+ circles.push(self.circle(coord, 0.15));
1052
+ });
1053
+
1054
+ return circles;
1055
+ },
1056
+
1057
+ plotAsymptotes: function (fn, range) {
1058
+ const min = range[0];
1059
+ const max = range[1];
1060
+ const step = (max - min) / (currentStyle["plot-points"] || 800);
1061
+
1062
+ const asymptotes = raphael.set();
1063
+ let lastVal = fn(min);
1064
+
1065
+ for (let t = min; t <= max; t += step) {
1066
+ const funcVal = fn(t);
1067
+
1068
+ if (
1069
+ funcVal < 0 !== lastVal < 0 &&
1070
+ Math.abs(funcVal - lastVal) > 2 * yScale
1071
+ ) {
1072
+ asymptotes.push(this.line([t, yScale], [t, -yScale]));
1073
+ }
1074
+
1075
+ lastVal = funcVal;
1076
+ }
1077
+
1078
+ return asymptotes;
1079
+ },
1080
+ };
1081
+
1082
+ // $FlowFixMe[invalid-constructor]
1083
+ const graphie = new Graphie();
1084
+ _.extend(graphie, {
1085
+ raphael: raphael,
1086
+
1087
+ init: function (options) {
1088
+ let scale = options.scale || [40, 40];
1089
+ scale = typeof scale === "number" ? [scale, scale] : scale;
1090
+
1091
+ xScale = scale[0];
1092
+ yScale = scale[1];
1093
+
1094
+ if (options.range == null) {
1095
+ throw new PerseusError(
1096
+ "range should be specified in graph init",
1097
+ Errors.Internal,
1098
+ );
1099
+ }
1100
+
1101
+ xRange = options.range[0];
1102
+ yRange = options.range[1];
1103
+
1104
+ const w = (xRange[1] - xRange[0]) * xScale;
1105
+ const h = (yRange[1] - yRange[0]) * yScale;
1106
+ raphael.setSize(w, h);
1107
+
1108
+ $(el).css({
1109
+ width: w,
1110
+ height: h,
1111
+ });
1112
+
1113
+ this.range = options.range;
1114
+ this.scale = scale;
1115
+ this.dimensions = [w, h];
1116
+ this.xpixels = w;
1117
+ this.ypixels = h;
1118
+
1119
+ this.isMobile = options.isMobile;
1120
+
1121
+ return this;
1122
+ },
1123
+
1124
+ style: function (attrs, fn) {
1125
+ const processed = processAttributes(attrs);
1126
+
1127
+ if (typeof fn === "function") {
1128
+ const oldStyle = currentStyle;
1129
+ currentStyle = $.extend({}, currentStyle, processed);
1130
+ const result = fn.call(graphie);
1131
+ currentStyle = oldStyle;
1132
+ return result;
1133
+ }
1134
+ $.extend(currentStyle, processed);
1135
+ },
1136
+
1137
+ scalePoint: scalePoint,
1138
+ scaleVector: scaleVector,
1139
+
1140
+ unscalePoint: unscalePoint,
1141
+ unscaleVector: unscaleVector,
1142
+
1143
+ // Custom SVG path functions that are dependent on graphie range
1144
+ // `svgPath`, while independent of range, is exported for consistency
1145
+ svgPath: svgPath,
1146
+ svgParabolaPath: svgParabolaPath,
1147
+ svgSinusoidPath: svgSinusoidPath,
1148
+ });
1149
+
1150
+ $.each(drawingTools, function (name) {
1151
+ graphie[name] = function () {
1152
+ const last = arguments[arguments.length - 1];
1153
+ const oldStyle = currentStyle;
1154
+ let result;
1155
+
1156
+ // The last argument is probably trying to change the style
1157
+ if (typeof last === "object" && !_.isArray(last)) {
1158
+ currentStyle = {
1159
+ ...currentStyle,
1160
+ ...processAttributes(last),
1161
+ };
1162
+
1163
+ // $FlowFixMe[method-unbinding]
1164
+ const rest = [].slice.call(arguments, 0, arguments.length - 1);
1165
+ result = drawingTools[name](...rest);
1166
+ } else {
1167
+ currentStyle = $.extend({}, currentStyle);
1168
+
1169
+ result = drawingTools[name](...arguments);
1170
+ }
1171
+
1172
+ // Bad heuristic for recognizing Raphael elements and sets
1173
+ const type = result.constructor.prototype;
1174
+ if (type === Raphael.el || type === Raphael.st) {
1175
+ result.attr(currentStyle);
1176
+
1177
+ if (currentStyle.arrows) {
1178
+ result = addArrowheads(result);
1179
+ }
1180
+ } else if (result instanceof $) {
1181
+ // We assume that if it's not a Raphael element/set, it
1182
+ // does not contain SVG.
1183
+ result.css({...currentStyle, ...SVG_SPECIFIC_STYLE_MASK});
1184
+ }
1185
+
1186
+ currentStyle = oldStyle;
1187
+ return result;
1188
+ };
1189
+ });
1190
+
1191
+ // Initializes graphie settings for a graph and draws the basic graph
1192
+ // features (axes, grid, tick marks, and axis labels)
1193
+ // Options expected are:
1194
+ // - range: [[a, b], [c, d]] or [a, b]
1195
+ // - scale: [a, b] or number
1196
+ // - gridOpacity: number (0 - 1)
1197
+ // - gridStep: [a, b] or number (relative to units)
1198
+ // - tickStep: [a, b] or number (relative to grid steps)
1199
+ // - tickLen: [a, b] or number (in pixels)
1200
+ // - labelStep: [a, b] or number (relative to tick steps)
1201
+ // - yLabelFormat: fn to format label string for y-axis
1202
+ // - xLabelFormat: fn to format label string for x-axis
1203
+ // - smartLabelPositioning: true or false to ignore minus sign
1204
+ graphie.graphInit = function (options) {
1205
+ options = options || {};
1206
+
1207
+ $.each(options, function (prop, val: $FlowFixMe) {
1208
+ // allow options to be specified by a single number for shorthand if
1209
+ // the horizontal and vertical components are the same
1210
+ if (
1211
+ !prop.match(/.*Opacity$/) &&
1212
+ prop !== "range" &&
1213
+ typeof val === "number"
1214
+ ) {
1215
+ options[prop] = [val, val];
1216
+ }
1217
+
1218
+ // allow symmetric ranges to be specified by the absolute values
1219
+ if (prop === "range" || prop === "gridRange") {
1220
+ if (val.constructor === Array) {
1221
+ // but don't mandate symmetric ranges
1222
+ if (val[0].constructor !== Array) {
1223
+ options[prop] = [
1224
+ [-val[0], val[0]],
1225
+ [-val[1], val[1]],
1226
+ ];
1227
+ }
1228
+ } else if (typeof val === "number") {
1229
+ options[prop] = [
1230
+ [-val, val],
1231
+ [-val, val],
1232
+ ];
1233
+ }
1234
+ }
1235
+ });
1236
+
1237
+ const range = options.range || [
1238
+ [-10, 10],
1239
+ [-10, 10],
1240
+ ];
1241
+ const gridRange = options.gridRange || options.range;
1242
+ const scale = options.scale || [20, 20];
1243
+ const grid = options.grid != null ? options.grid : true;
1244
+ const gridOpacity = options.gridOpacity || 0.1;
1245
+ const gridStep = options.gridStep || [1, 1];
1246
+ const axes = options.axes != null ? options.axes : true;
1247
+ const axisArrows = options.axisArrows || "";
1248
+ const axisOpacity = options.axisOpacity || 1.0;
1249
+ const axisCenter = options.axisCenter || [
1250
+ Math.min(Math.max(range[0][0], 0), range[0][1]),
1251
+ Math.min(Math.max(range[1][0], 0), range[1][1]),
1252
+ ];
1253
+ const axisLabels =
1254
+ options.axisLabels != null ? options.axisLabels : false;
1255
+ const ticks = options.ticks != null ? options.ticks : true;
1256
+ const tickStep = options.tickStep || [2, 2];
1257
+ const tickLen = options.tickLen || [5, 5];
1258
+ const tickOpacity = options.tickOpacity || 1.0;
1259
+ const labels = options.labels || options.labelStep || false;
1260
+ const labelStep = options.labelStep || [1, 1];
1261
+ const labelOpacity = options.labelOpacity || 1.0;
1262
+ let unityLabels = options.unityLabels || false;
1263
+ const labelFormat =
1264
+ options.labelFormat ||
1265
+ function (a) {
1266
+ return a;
1267
+ };
1268
+ let xLabelFormat = options.xLabelFormat || labelFormat;
1269
+ let yLabelFormat = options.yLabelFormat || labelFormat;
1270
+ const smartLabelPositioning =
1271
+ options.smartLabelPositioning != null
1272
+ ? options.smartLabelPositioning
1273
+ : true;
1274
+ const realRange = [
1275
+ [
1276
+ range[0][0] - (range[0][0] > 0 ? 1 : 0),
1277
+ range[0][1] + (range[0][1] < 0 ? 1 : 0),
1278
+ ],
1279
+ [
1280
+ range[1][0] - (range[1][0] > 0 ? 1 : 0),
1281
+ range[1][1] + (range[1][1] < 0 ? 1 : 0),
1282
+ ],
1283
+ ];
1284
+
1285
+ if (!_.isArray(unityLabels)) {
1286
+ unityLabels = [unityLabels, unityLabels];
1287
+ }
1288
+
1289
+ if (smartLabelPositioning) {
1290
+ const minusIgnorer = function (lf) {
1291
+ return function (a) {
1292
+ return (lf(a) + "").replace(/-(\d)/g, "\\llap{-}$1");
1293
+ };
1294
+ };
1295
+
1296
+ xLabelFormat = minusIgnorer(xLabelFormat);
1297
+ yLabelFormat = minusIgnorer(yLabelFormat);
1298
+ }
1299
+
1300
+ this.init({
1301
+ range: realRange,
1302
+ scale: scale,
1303
+ isMobile: options.isMobile,
1304
+ });
1305
+
1306
+ // draw grid
1307
+ if (grid) {
1308
+ this.grid(gridRange[0], gridRange[1], {
1309
+ stroke: options.isMobile ? KhanColors.GRAY_C : "#000000",
1310
+ opacity: options.isMobile ? 1 : gridOpacity,
1311
+ step: gridStep,
1312
+ strokeWidth: options.isMobile ? 1 : 2,
1313
+ });
1314
+ }
1315
+
1316
+ // draw axes
1317
+ if (axes) {
1318
+ // this is a slight hack until <-> arrowheads work
1319
+ if (axisArrows === "<->" || axisArrows === true) {
1320
+ this.style(
1321
+ {
1322
+ stroke: options.isMobile
1323
+ ? KhanColors.GRAY_G
1324
+ : "#000000",
1325
+ opacity: options.isMobile ? 1 : axisOpacity,
1326
+ strokeWidth: options.isMobile ? 1 : 2,
1327
+ arrows: "->",
1328
+ },
1329
+ function () {
1330
+ if (range[1][0] < 0 && range[1][1] > 0) {
1331
+ this.path([
1332
+ axisCenter,
1333
+ [gridRange[0][0], axisCenter[1]],
1334
+ ]);
1335
+ this.path([
1336
+ axisCenter,
1337
+ [gridRange[0][1], axisCenter[1]],
1338
+ ]);
1339
+ }
1340
+ if (range[0][0] < 0 && range[0][1] > 0) {
1341
+ this.path([
1342
+ axisCenter,
1343
+ [axisCenter[0], gridRange[1][0]],
1344
+ ]);
1345
+ this.path([
1346
+ axisCenter,
1347
+ [axisCenter[0], gridRange[1][1]],
1348
+ ]);
1349
+ }
1350
+ },
1351
+ );
1352
+
1353
+ // also, we don't support "<-" arrows yet, but why you
1354
+ // would want that on your graph is beyond me.
1355
+ } else if (axisArrows === "->" || axisArrows === "") {
1356
+ this.style(
1357
+ {
1358
+ stroke: "#000000",
1359
+ opacity: axisOpacity,
1360
+ strokeWidth: 2,
1361
+ arrows: axisArrows,
1362
+ },
1363
+ function () {
1364
+ this.path([
1365
+ [gridRange[0][0], axisCenter[1]],
1366
+ [gridRange[0][1], axisCenter[1]],
1367
+ ]);
1368
+ this.path([
1369
+ [axisCenter[0], gridRange[1][0]],
1370
+ [axisCenter[0], gridRange[1][1]],
1371
+ ]);
1372
+ },
1373
+ );
1374
+ }
1375
+
1376
+ if (axisLabels && axisLabels.length === 2) {
1377
+ this.label(
1378
+ [gridRange[0][1], axisCenter[1]],
1379
+ axisLabels[0],
1380
+ "right",
1381
+ );
1382
+ this.label(
1383
+ [axisCenter[0], gridRange[1][1]],
1384
+ axisLabels[1],
1385
+ "above",
1386
+ );
1387
+ }
1388
+ }
1389
+
1390
+ // draw tick marks
1391
+ if (ticks) {
1392
+ const halfWidthTicks = options.isMobile;
1393
+ this.style(
1394
+ {
1395
+ stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
1396
+ opacity: options.isMobile ? 1 : tickOpacity,
1397
+ strokeWidth: 1,
1398
+ },
1399
+ function () {
1400
+ // horizontal axis
1401
+ let step = gridStep[0] * tickStep[0];
1402
+ let len = tickLen[0] / scale[1];
1403
+ let start = gridRange[0][0];
1404
+ let stop = gridRange[0][1];
1405
+
1406
+ if (range[1][0] < 0 && range[1][1] > 0) {
1407
+ for (
1408
+ let x = step + axisCenter[0];
1409
+ x <= stop;
1410
+ x += step
1411
+ ) {
1412
+ if (x < stop || !axisArrows) {
1413
+ this.line(
1414
+ [x, -len + axisCenter[1]],
1415
+ [
1416
+ x,
1417
+ halfWidthTicks
1418
+ ? 0
1419
+ : len + axisCenter[1],
1420
+ ],
1421
+ );
1422
+ }
1423
+ }
1424
+
1425
+ for (
1426
+ let x = -step + axisCenter[0];
1427
+ x >= start;
1428
+ x -= step
1429
+ ) {
1430
+ if (x > start || !axisArrows) {
1431
+ this.line(
1432
+ [x, -len + axisCenter[1]],
1433
+ [
1434
+ x,
1435
+ halfWidthTicks
1436
+ ? 0
1437
+ : len + axisCenter[1],
1438
+ ],
1439
+ );
1440
+ }
1441
+ }
1442
+ }
1443
+
1444
+ // vertical axis
1445
+ step = gridStep[1] * tickStep[1];
1446
+ len = tickLen[1] / scale[0];
1447
+ start = gridRange[1][0];
1448
+ stop = gridRange[1][1];
1449
+
1450
+ if (range[0][0] < 0 && range[0][1] > 0) {
1451
+ for (
1452
+ let y = step + axisCenter[1];
1453
+ y <= stop;
1454
+ y += step
1455
+ ) {
1456
+ if (y < stop || !axisArrows) {
1457
+ this.line(
1458
+ [-len + axisCenter[0], y],
1459
+ [
1460
+ halfWidthTicks
1461
+ ? 0
1462
+ : len + axisCenter[0],
1463
+ y,
1464
+ ],
1465
+ );
1466
+ }
1467
+ }
1468
+
1469
+ for (
1470
+ let y = -step + axisCenter[1];
1471
+ y >= start;
1472
+ y -= step
1473
+ ) {
1474
+ if (y > start || !axisArrows) {
1475
+ this.line(
1476
+ [-len + axisCenter[0], y],
1477
+ [
1478
+ halfWidthTicks
1479
+ ? 0
1480
+ : len + axisCenter[0],
1481
+ y,
1482
+ ],
1483
+ );
1484
+ }
1485
+ }
1486
+ }
1487
+ },
1488
+ );
1489
+ }
1490
+
1491
+ // draw axis labels
1492
+ if (labels) {
1493
+ this.style(
1494
+ {
1495
+ stroke: options.isMobile ? KhanColors.GRAY_G : "#000000",
1496
+ opacity: options.isMobile ? 1 : labelOpacity,
1497
+ },
1498
+ function () {
1499
+ // horizontal axis
1500
+ let step = gridStep[0] * tickStep[0] * labelStep[0];
1501
+ let start = gridRange[0][0];
1502
+ let stop = gridRange[0][1];
1503
+ const xAxisPosition = axisCenter[0] < 0 ? "above" : "below";
1504
+ const yAxisPosition = axisCenter[0] < 0 ? "right" : "left";
1505
+ const xShowZero =
1506
+ axisCenter[0] === 0 && axisCenter[1] !== 0;
1507
+ const yShowZero =
1508
+ axisCenter[0] !== 0 && axisCenter[1] === 0;
1509
+ const axisOffCenter =
1510
+ axisCenter[0] !== 0 || axisCenter[1] !== 0;
1511
+ const showUnityX = unityLabels[0] || axisOffCenter;
1512
+ const showUnityY = unityLabels[1] || axisOffCenter;
1513
+
1514
+ // positive x-axis
1515
+ for (
1516
+ let x = (xShowZero ? 0 : step) + axisCenter[0];
1517
+ x <= stop;
1518
+ x += step
1519
+ ) {
1520
+ if (x < stop || !axisArrows) {
1521
+ this.label(
1522
+ [x, axisCenter[1]],
1523
+ xLabelFormat(x),
1524
+ xAxisPosition,
1525
+ );
1526
+ }
1527
+ }
1528
+
1529
+ // negative x-axis
1530
+ for (
1531
+ let x = -step * (showUnityX ? 1 : 2) + axisCenter[0];
1532
+ x >= start;
1533
+ x -= step
1534
+ ) {
1535
+ if (x > start || !axisArrows) {
1536
+ this.label(
1537
+ [x, axisCenter[1]],
1538
+ xLabelFormat(x),
1539
+ xAxisPosition,
1540
+ );
1541
+ }
1542
+ }
1543
+
1544
+ step = gridStep[1] * tickStep[1] * labelStep[1];
1545
+ start = gridRange[1][0];
1546
+ stop = gridRange[1][1];
1547
+
1548
+ // positive y-axis
1549
+ for (
1550
+ let y = (yShowZero ? 0 : step) + axisCenter[1];
1551
+ y <= stop;
1552
+ y += step
1553
+ ) {
1554
+ if (y < stop || !axisArrows) {
1555
+ this.label(
1556
+ [axisCenter[0], y],
1557
+ yLabelFormat(y),
1558
+ yAxisPosition,
1559
+ );
1560
+ }
1561
+ }
1562
+
1563
+ // negative y-axis
1564
+ for (
1565
+ let y = -step * (showUnityY ? 1 : 2) + axisCenter[1];
1566
+ y >= start;
1567
+ y -= step
1568
+ ) {
1569
+ if (y > start || !axisArrows) {
1570
+ this.label(
1571
+ [axisCenter[0], y],
1572
+ yLabelFormat(y),
1573
+ yAxisPosition,
1574
+ );
1575
+ }
1576
+ }
1577
+ },
1578
+ );
1579
+ }
1580
+ };
1581
+
1582
+ return graphie;
1583
+ };
1584
+
1585
+ export default GraphUtils;