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