├── .babelrc ├── .circleci └── config.yml ├── .eslintrc ├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .npmignore ├── .percy.yml ├── .prettierrc ├── .storybook ├── main.js ├── preview.js └── webpack.config.js ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── THEMING.md ├── accessTokens.tpl.js ├── babel.config.json ├── config └── CSSStub.js ├── dev ├── App.js ├── customConfigTest.js ├── dataSources.js ├── favicon.ico ├── index.html ├── index.js ├── mocks.json ├── mocks │ └── aggregate.json ├── percy │ ├── bar.json │ ├── box.json │ ├── funnel.json │ ├── funnelarea.json │ ├── geoTest.json │ ├── histogram.json │ ├── histogram2d.json │ ├── index.js │ ├── panelTest.json │ ├── pie.json │ ├── sankey.json │ ├── sunburst.json │ ├── violin.json │ └── waterfall.json └── styles.css ├── examples ├── README.md ├── components.png ├── custom │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ ├── CustomEditor.js │ │ ├── index.css │ │ └── index.js ├── demo │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ ├── Nav.js │ │ ├── dataSources.js │ │ ├── index.css │ │ └── index.js ├── editor.gif ├── redux │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ └── src │ │ ├── App.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.css │ │ ├── index.js │ │ └── reducer.js └── simple │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ └── src │ ├── App.js │ ├── index.css │ └── index.js ├── package.json ├── postcss.config.js ├── scripts ├── combineTranslationKeys.js ├── findTranslationKeys.js ├── makeArrows.js └── translationKeys │ ├── combined-translation-keys.txt │ └── translation-keys.txt ├── src ├── DefaultEditor.js ├── EditorControls.js ├── PlotlyEditor.js ├── __stories__ │ ├── index.stories.js │ └── stories.css ├── __tests__ │ └── syntax-test.js ├── components │ ├── PanelMenuWrapper.js │ ├── containers │ │ ├── AnnotationAccordion.js │ │ ├── AxesFold.js │ │ ├── FoldEmpty.js │ │ ├── ImageAccordion.js │ │ ├── MapboxLayersAccordion.js │ │ ├── MenuPanel.js │ │ ├── Modal.js │ │ ├── ModalBox.js │ │ ├── ModalProvider.js │ │ ├── PanelEmpty.js │ │ ├── PanelHeader.js │ │ ├── PlotlyFold.js │ │ ├── PlotlyPanel.js │ │ ├── PlotlySection.js │ │ ├── RangeSelectorAccordion.js │ │ ├── ShapeAccordion.js │ │ ├── SingleSidebarItem.js │ │ ├── SliderAccordion.js │ │ ├── SubplotAccordion.js │ │ ├── TraceAccordion.js │ │ ├── TraceMarkerSection.js │ │ ├── TraceRequiredPanel.js │ │ ├── TransformAccordion.js │ │ ├── UpdateMenuAccordion.js │ │ ├── __tests__ │ │ │ ├── AnnotationAccordion-test.js │ │ │ ├── ConnectedContainersVisibility-test.js │ │ │ ├── Fold-test.js │ │ │ ├── Layout-test.js │ │ │ ├── PlotlySection-test.js │ │ │ ├── TraceAccordion-test.js │ │ │ └── UnconnectedContainersVisibility-test.js │ │ ├── derived.js │ │ └── index.js │ ├── fields │ │ ├── ArrowSelector.js │ │ ├── AxesCreator.js │ │ ├── AxesSelector.js │ │ ├── AxisInterval.js │ │ ├── AxisRangeValue.js │ │ ├── ColorArrayPicker.js │ │ ├── ColorPicker.js │ │ ├── ColorscalePicker.js │ │ ├── ColorwayPicker.js │ │ ├── DataSelector.js │ │ ├── DateTimePicker.js │ │ ├── Dropdown.js │ │ ├── DropdownCustom.js │ │ ├── Dropzone.js │ │ ├── DualNumeric.js │ │ ├── ErrorBars.js │ │ ├── Field.js │ │ ├── FilterOperation.js │ │ ├── Flaglist.js │ │ ├── FontSelector.js │ │ ├── GroupCreator.js │ │ ├── HoverLabelNameLength.js │ │ ├── Info.js │ │ ├── LineSelectors.js │ │ ├── LocationSelector.js │ │ ├── MarkerColor.js │ │ ├── MarkerSize.js │ │ ├── MultiColorPicker.js │ │ ├── Numeric.js │ │ ├── NumericOrDate.js │ │ ├── PieColorscalePicker.js │ │ ├── Radio.js │ │ ├── RectanglePositioner.js │ │ ├── SubplotCreator.js │ │ ├── SymbolSelector.js │ │ ├── Text.js │ │ ├── TextEditor.js │ │ ├── TextPosition.js │ │ ├── TraceSelector.js │ │ ├── UpdateMenuButtons.js │ │ ├── VisibilitySelect.js │ │ ├── __tests__ │ │ │ ├── AnnotationRef-test.js │ │ │ ├── ArrowSelector-test.js │ │ │ ├── DataSelector-test.js │ │ │ ├── Radio-test.js │ │ │ └── TraceSelector-test.js │ │ ├── derived.js │ │ └── index.js │ ├── index.js │ ├── sidebar │ │ ├── SidebarGroup.js │ │ └── SidebarItem.js │ └── widgets │ │ ├── Button.js │ │ ├── CheckboxGroup.js │ │ ├── ColorPicker.js │ │ ├── ColorscalePicker.js │ │ ├── DateTimePicker.js │ │ ├── Dropdown.js │ │ ├── Dropzone.js │ │ ├── EditableText.js │ │ ├── FlaglistCheckboxGroup.js │ │ ├── Logo.js │ │ ├── NumericInput.js │ │ ├── RadioBlocks.js │ │ ├── SymbolSelector.js │ │ ├── TextArea.js │ │ ├── TextInput.js │ │ ├── TraceTypeSelector.js │ │ ├── index.js │ │ └── text_editors │ │ ├── HTML.js │ │ ├── LaTeX.js │ │ ├── MultiFormat.js │ │ ├── RichText │ │ ├── DraftCommands.js │ │ ├── LinkDecorator.js │ │ ├── LinkEditor.js │ │ ├── StyleButton.js │ │ ├── StyleButtonGroup.js │ │ ├── configuration.js │ │ ├── debounce.js │ │ ├── decoratorStrategies.js │ │ ├── getSelectionCoordinates.js │ │ └── index.js │ │ └── convertFormats.js ├── default_panels │ ├── GraphCreatePanel.js │ ├── GraphSubplotsPanel.js │ ├── GraphTransformsPanel.js │ ├── StyleAxesPanel.js │ ├── StyleColorbarsPanel.js │ ├── StyleImagesPanel.js │ ├── StyleLayoutPanel.js │ ├── StyleLegendPanel.js │ ├── StyleMapsPanel.js │ ├── StyleNotesPanel.js │ ├── StyleShapesPanel.js │ ├── StyleSlidersPanel.js │ ├── StyleTracesPanel.js │ ├── StyleUpdateMenusPanel.js │ └── index.js ├── index.js ├── lib │ ├── __tests__ │ │ ├── connectAnnotationToLayout-test.js │ │ ├── connectLayoutToPlot-test.js │ │ ├── connectTraceToPlot-test.js │ │ ├── dereference-test.js │ │ ├── maybeAdjustSrc-test.js │ │ ├── maybeTransposeData-test.js │ │ ├── multiValued-test.js │ │ ├── nestedContainerConnections-test.js │ │ ├── sortMenu-test.js │ │ ├── transpose-test.js │ │ ├── unpackPlotProps-test.js │ │ └── walkObject-test.js │ ├── bem.js │ ├── computeTraceOptionsFromSchema.js │ ├── connectAggregationToTransform.js │ ├── connectAnnotationToLayout.js │ ├── connectAxesToLayout.js │ ├── connectCartesianSubplotToLayout.js │ ├── connectImageToLayout.js │ ├── connectLayersToMapbox.js │ ├── connectLayoutToPlot.js │ ├── connectNonCartesianSubplotToLayout.js │ ├── connectRangeSelectorToAxis.js │ ├── connectShapeToLayout.js │ ├── connectSliderToLayout.js │ ├── connectToContainer.js │ ├── connectTraceToPlot.js │ ├── connectTransformToTrace.js │ ├── connectUpdateMenuToLayout.js │ ├── constants.js │ ├── customTraceType.js │ ├── dereference.js │ ├── getAllAxes.js │ ├── index.js │ ├── localize.js │ ├── multiValues.js │ ├── sortMenu.js │ ├── strings.js │ ├── striptags.js │ ├── test-utils.js │ ├── traceTypes.js │ ├── unpackPlotProps.js │ └── walkObject.js ├── locales │ ├── en.js │ ├── index.js │ └── xx.js ├── shame.js └── styles │ ├── _helpers.scss │ ├── _mixins.scss │ ├── _movement.scss │ ├── components │ ├── _main.scss │ ├── containers │ │ ├── _fold.scss │ │ ├── _info.scss │ │ ├── _main.scss │ │ ├── _menupanel.scss │ │ ├── _modal.scss │ │ ├── _modalbox.scss │ │ ├── _panel.scss │ │ ├── _section.scss │ │ └── _tabs.scss │ ├── fields │ │ ├── _field.scss │ │ ├── _main.scss │ │ └── _symbolselector.scss │ ├── sidebar │ │ └── _main.scss │ └── widgets │ │ ├── _button.scss │ │ ├── _checkbox.scss │ │ ├── _colorpicker.scss │ │ ├── _colorscalepicker.scss │ │ ├── _datetimepicker.scss │ │ ├── _dropdown.scss │ │ ├── _dropzone.scss │ │ ├── _main.scss │ │ ├── _microtip.scss │ │ ├── _numeric-input.scss │ │ ├── _radio-block.scss │ │ ├── _rangeslider.scss │ │ ├── _text-editor.scss │ │ ├── _text-input.scss │ │ └── _trace-type-selector.scss │ ├── main.scss │ └── variables │ ├── _colors.scss │ ├── _defaults.scss │ ├── _layout.scss │ ├── _main.scss │ ├── _theme.scss │ ├── _typography.scss │ └── _units.scss └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/react", "@babel/env"], 3 | "plugins": [ 4 | "@babel/plugin-proposal-object-rest-spread", 5 | [ 6 | "module-resolver", 7 | { 8 | "root": ["./"], 9 | "alias": { 10 | "components": "./src/components", 11 | "lib": "./src/lib", 12 | "styles": "./src/styles" 13 | } 14 | } 15 | ] 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | # specify the version you desire here 6 | - image: cimg/node:20.6.0-browsers 7 | 8 | working_directory: ~/react-chart-editor 9 | 10 | steps: 11 | - checkout 12 | - restore_cache: 13 | keys: 14 | - v2-dependencies-{{ checksum "package.json" }} 15 | - v2-dependencies- 16 | 17 | - run: yarn install 18 | 19 | - save_cache: 20 | paths: 21 | - node_modules 22 | key: v2-dependencies-{{ checksum "package.json" }} 23 | 24 | - run: yarn test 25 | - run: yarn test:percy 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | example/**/* linguist-documentation 2 | example/src/assets/codemirror.css linguist-vendored 3 | example/src/assets/plotly-basic.min.js linguist-vendored 4 | example/src/assets/plotly.min.js linguist-vendored 5 | build/* linguist-generated 6 | lib/* linguist-generated 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://plot.ly/products/consulting-and-oem/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | npm-debug.log* 4 | *.sublime* 5 | 6 | .* 7 | !.storybook 8 | !.gitignore 9 | !.gitattributes 10 | !.npmignore 11 | !.eslintrc 12 | !.eslintignore 13 | !.percy.yml 14 | !.circleci 15 | 16 | example/dist 17 | 18 | lib 19 | !src/lib 20 | 21 | accessTokens.js 22 | yarn.lock 23 | yarn-error.log 24 | package-lock.json 25 | 26 | storybook-static 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Almost all .gitignore content 2 | 3 | npm-debug.log* 4 | *.sublime* 5 | accessTokens.js 6 | yarn.lock 7 | yarn-error.log 8 | package-lock.json 9 | storybook-static 10 | 11 | 12 | # Additionally: 13 | 14 | test 15 | src 16 | build 17 | examples 18 | 19 | .ackrc 20 | .agignore 21 | .circleci 22 | .github 23 | .storybook 24 | .babelrc 25 | .eslintrc 26 | .gitattributes 27 | .percy.yml 28 | .prettierrc 29 | .git 30 | babel.config.json 31 | postcss.config.js 32 | renovate.json 33 | 34 | npm-debug.log 35 | -------------------------------------------------------------------------------- /.percy.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | snapshot: 3 | widths: 4 | - 500 5 | minHeight: 600 6 | percyCSS: "" 7 | discovery: 8 | allowedHostnames: [] 9 | disallowedHostnames: [] 10 | networkIdleTimeout: 100 11 | upload: 12 | files: "**/*.{png,jpg,jpeg}" 13 | ignore: "" 14 | stripExtensions: false 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "bracketSpacing": false, 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../src/__stories__/*.stories.js'], 3 | core: { 4 | disableTelemetry: true, // Disables telemetry 5 | }, 6 | framework: { 7 | name: '@storybook/react-webpack5', 8 | options: {}, 9 | }, 10 | docs: { 11 | autodocs: false, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | layout: 'fullscreen', 3 | }; 4 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = async ({config}) => { 4 | config.module.rules.push({ 5 | test: /\.scss$/, 6 | use: ['style-loader', 'css-loader', 'sass-loader'], 7 | include: path.resolve(__dirname, '../'), 8 | }); 9 | return config; 10 | }; 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2024 Plotly Technologies Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /accessTokens.tpl.js: -------------------------------------------------------------------------------- 1 | // To use Satellite Maps in the Editor, Mapbox access tokens are required. 2 | // see https://www.mapbox.com/help/how-access-tokens-work/ 3 | 4 | const ACCESS_TOKENS = { 5 | MAPBOX: '', 6 | }; 7 | 8 | export default ACCESS_TOKENS; 9 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "development": {"comments": false, "minified": true}, 4 | "production": {"comments": true, "minified": false} 5 | }, 6 | "presets": [ 7 | [ 8 | "@babel/preset-react", 9 | { 10 | "runtime": "automatic" 11 | } 12 | ], 13 | "@babel/env" 14 | ], 15 | "plugins": [ 16 | "react-hot-loader/babel", 17 | "@babel/plugin-proposal-object-rest-spread", 18 | [ 19 | "module-resolver", 20 | { 21 | "root": ["./"], 22 | "alias": { 23 | "components": "./src/components", 24 | "lib": "./src/lib", 25 | "styles": "./src/styles" 26 | } 27 | } 28 | ] 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /config/CSSStub.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /dev/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/react-chart-editor/b3891d801646cb48e93aca0c5bd3271f0a571ae8/dev/favicon.ico -------------------------------------------------------------------------------- /dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Editor Dev 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dev/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './styles.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /dev/mocks/aggregate.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "scatter", 5 | "x": [ 6 | "Moe", 7 | "Larry", 8 | "Curly", 9 | "Moe", 10 | "Larry", 11 | "Curly", 12 | "Moe", 13 | "Larry", 14 | "Curly", 15 | "Moe", 16 | "Larry", 17 | "Curly" 18 | ], 19 | "y": [1, 6, 2, 8, 2, 9, 4, 5, 1, 5, 2, 8], 20 | "mode": "markers", 21 | "transforms": [ 22 | { 23 | "enabled" :true, 24 | "type": "aggregate", 25 | "groups": [ 26 | "Moe", 27 | "Larry", 28 | "Curly", 29 | "Moe", 30 | "Larry", 31 | "Curly", 32 | "Moe", 33 | "Larry", 34 | "Curly", 35 | "Moe", 36 | "Larry", 37 | "Curly" 38 | ], 39 | "aggregations": [{"target": "y", "func": "avg", "enabled": true}] 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /dev/percy/bar.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "bar", 5 | "mode": "markers", 6 | "uid": "040729", 7 | "x": [ 8 | 1, 9 | 2, 10 | 3 11 | ], 12 | "xsrc": "x1", 13 | "error_x": { 14 | "visible": true, 15 | "symmetric": true 16 | }, 17 | "error_y": { 18 | "visible": true, 19 | "symmetric": false 20 | } 21 | } 22 | ], 23 | "layout": { 24 | "xaxis": { 25 | "type": "linear", 26 | "range": [ 27 | 0, 28 | 115.78947368421052 29 | ], 30 | "autorange": true 31 | }, 32 | "yaxis": { 33 | "range": [ 34 | -0.5, 35 | 5.815789473684211 36 | ], 37 | "autorange": true 38 | }, 39 | "autosize": true, 40 | "barmode": "group", 41 | "barnorm": "percent", 42 | "bargroupgap": 0.28 43 | }, 44 | "frames": [] 45 | } 46 | -------------------------------------------------------------------------------- /dev/percy/box.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "box", 5 | "mode": "markers", 6 | "uid": "43d2ef", 7 | "boxpoints": "all", 8 | "x": [ 9 | 1, 10 | 2, 11 | 3 12 | ], 13 | "xsrc": "x1", 14 | "boxmean": true, 15 | "notched": true 16 | } 17 | ], 18 | "layout": { 19 | "xaxis": { 20 | "type": "linear", 21 | "range": [ 22 | 0.7222222222222222, 23 | 6.277777777777778 24 | ], 25 | "autorange": true 26 | }, 27 | "yaxis": { 28 | "type": "category", 29 | "range": [ 30 | -0.696, 31 | 0.5 32 | ], 33 | "autorange": true 34 | }, 35 | "autosize": true, 36 | "boxmode": "overlay" 37 | }, 38 | "frames": [] 39 | } 40 | -------------------------------------------------------------------------------- /dev/percy/funnel.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "funnel", 5 | "y": ["Lead", "Pipeline", "Proposal", "Negotiation", "Closed (Won)"], 6 | "x": [ 610, 432, 231, 103, 54 ] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /dev/percy/funnelarea.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "funnelarea", 5 | "labels": ["Lead", "Pipeline", "Proposal", "Negotiation", "Closed (Won)"], 6 | "values": [ 610, 432, 231, 103, 54 ] 7 | } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /dev/percy/geoTest.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "choropleth", 5 | "mode": "markers", 6 | "stackgroup": null, 7 | "locations": [ 8 | "Angola", 9 | "Albania", 10 | "Armenia" 11 | ], 12 | "locationssrc": "x1", 13 | "locationmode": "country names", 14 | "z": [ 15 | 1.01, 16 | 1.66, 17 | 3.5 18 | ], 19 | "zsrc": "x1", 20 | "zauto": false, 21 | "hovertemplate": "" 22 | }, 23 | { 24 | "type": "scattermapbox", 25 | "lat": [ 26 | 1, 27 | 2, 28 | 3 29 | ], 30 | "latsrc": "x1", 31 | "lon": [ 32 | 1, 33 | 2, 34 | 3 35 | ], 36 | "lonsrc": "x1", 37 | "mode": "markers+lines+text", 38 | "connectgaps": false, 39 | "fill": "toself" 40 | }, 41 | { 42 | "type": "scattergeo", 43 | "lat": [ 44 | 16.99, 45 | 10.34, 46 | 21.01 47 | ], 48 | "latsrc": "x1", 49 | "lon": [ 50 | 16.99, 51 | 10.34, 52 | 21.01 53 | ], 54 | "lonsrc": "x1", 55 | "geo": "geo2", 56 | "mode": "markers+lines+text" 57 | } 58 | ], 59 | "layout": { 60 | "xaxis": { 61 | "range": [ 62 | 0.8304469606674613, 63 | 3.679553039332539 64 | ], 65 | "autorange": true, 66 | "type": "linear" 67 | }, 68 | "yaxis": { 69 | "range": [ 70 | -0.12629852378348821, 71 | 2.1262985237834884 72 | ], 73 | "autorange": true 74 | }, 75 | "autosize": true, 76 | "geo": { 77 | "showcountries": true, 78 | "showocean": true, 79 | "showland": true, 80 | "showlakes": true, 81 | "showrivers": true 82 | }, 83 | "geo2": { 84 | "showcountries": true, 85 | "showrivers": true, 86 | "showlakes": true, 87 | "showland": true, 88 | "showocean": true 89 | }, 90 | "mapbox": { 91 | "style": "basic" 92 | } 93 | }, 94 | "frames": [] 95 | } 96 | -------------------------------------------------------------------------------- /dev/percy/histogram.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "histogram", 5 | "mode": "markers", 6 | "uid": "dcb598", 7 | "x": [ 8 | 1, 9 | 2, 10 | 3 11 | ], 12 | "xsrc": "x1", 13 | "autobinx": true, 14 | "xbins": { 15 | "start": -0.5, 16 | "end": 7.5, 17 | "size": 2 18 | }, 19 | "y": [ 20 | 2, 21 | 3, 22 | 4 23 | ], 24 | "ysrc": "y1", 25 | "error_x": { 26 | "visible": true, 27 | "symmetric": true 28 | }, 29 | "error_y": { 30 | "visible": true, 31 | "symmetric": false 32 | }, 33 | "orientation": "v", 34 | "autobiny": true, 35 | "ybins": { 36 | "start": -0.5, 37 | "end": 7.5, 38 | "size": 2 39 | } 40 | } 41 | ], 42 | "layout": { 43 | "plot_bgcolor": "rgb(255, 0, 0)", 44 | "xaxis": { 45 | "range": [ 46 | 0, 47 | 2.3157894736842106 48 | ], 49 | "autorange": true, 50 | "type": "linear" 51 | }, 52 | "yaxis": { 53 | "range": [ 54 | -0.5, 55 | 7.552631578947369 56 | ], 57 | "autorange": true, 58 | "type": "linear" 59 | }, 60 | "autosize": true 61 | }, 62 | "frames": [] 63 | } 64 | -------------------------------------------------------------------------------- /dev/percy/histogram2d.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "histogram2d", 5 | "mode": "markers", 6 | "uid": "7cb332", 7 | "x": [ 8 | 1, 9 | 2, 10 | 3 11 | ], 12 | "xsrc": "x1", 13 | "y": [ 14 | 2, 15 | 3, 16 | 4 17 | ], 18 | "ysrc": "y1", 19 | "autobinx": false, 20 | "xbins": { 21 | "start": 0.5, 22 | "end": 9.5, 23 | "size": 5, 24 | "_dataSpan": 5 25 | }, 26 | "autobiny": true, 27 | "ybins": { 28 | "start": -0.5, 29 | "end": 9.5, 30 | "size": 10, 31 | "_dataSpan": 5 32 | }, 33 | "zmin": 2, 34 | "zmax": 4, 35 | "zauto": true, 36 | "cumulative": { 37 | "enabled": true 38 | } 39 | } 40 | ], 41 | "layout": { 42 | "xaxis": { 43 | "type": "linear", 44 | "range": [ 45 | -0.5, 46 | 9.5 47 | ], 48 | "autorange": true 49 | }, 50 | "yaxis": { 51 | "type": "linear", 52 | "range": [ 53 | -0.5, 54 | 9.5 55 | ], 56 | "autorange": true 57 | }, 58 | "autosize": true 59 | }, 60 | "frames": [] 61 | } 62 | -------------------------------------------------------------------------------- /dev/percy/index.js: -------------------------------------------------------------------------------- 1 | export {default as panelTest} from './panelTest.json'; 2 | export {default as histogram} from './histogram.json'; 3 | export {default as histogram2d} from './histogram2d.json'; 4 | export {default as pie} from './pie.json'; 5 | export {default as violin} from './violin.json'; 6 | export {default as bar} from './bar.json'; 7 | export {default as box} from './box.json'; 8 | export {default as waterfall} from './waterfall.json'; 9 | export {default as sunburst} from './sunburst.json'; 10 | export {default as sankey} from './sankey.json'; 11 | export {default as funnel} from './funnel.json'; 12 | export {default as funnelarea} from './funnelarea.json'; 13 | export {default as geoTest} from './geoTest.json'; 14 | -------------------------------------------------------------------------------- /dev/percy/pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "pie", 5 | "mode": "markers", 6 | "uid": "3e343a", 7 | "labels": [ 8 | 1, 9 | 2, 10 | 3 11 | ], 12 | "labelssrc": "x1", 13 | "values": [ 14 | 1, 15 | 2, 16 | 3 17 | ], 18 | "valuessrc": "x1", 19 | "text": [ 20 | 1, 21 | 2, 22 | 3 23 | ], 24 | "textsrc": "x1" 25 | } 26 | ], 27 | "layout": { 28 | "xaxis": { 29 | "range": [ 30 | 0.5699530516431925, 31 | 6.917370892018779 32 | ], 33 | "autorange": true, 34 | "type": "linear" 35 | }, 36 | "yaxis": { 37 | "range": [ 38 | -0.3055555555555556, 39 | 5.805555555555555 40 | ], 41 | "autorange": true 42 | }, 43 | "autosize": true, 44 | "hovermode": "closest" 45 | }, 46 | "frames": [] 47 | } 48 | -------------------------------------------------------------------------------- /dev/percy/sankey.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "sankey", 5 | "mode": "markers", 6 | "link": { 7 | "value": [ 8 | 124.729, 9 | 0.597, 10 | 26.862 11 | ], 12 | "valuesrc": "x1", 13 | "source": [ 14 | 0, 15 | 1, 16 | 1 17 | ], 18 | "sourcesrc": "x1", 19 | "target": [ 20 | 1, 21 | 2, 22 | 3 23 | ], 24 | "targetsrc": "x1" 25 | }, 26 | "node": { 27 | "label": [ 28 | "A", 29 | "B", 30 | "C" 31 | ], 32 | "labelsrc": "x1" 33 | } 34 | } 35 | ], 36 | "layout": { 37 | "xaxis": { 38 | "range": [ 39 | -1, 40 | 6 41 | ], 42 | "autorange": true 43 | }, 44 | "yaxis": { 45 | "range": [ 46 | -1, 47 | 4 48 | ], 49 | "autorange": true 50 | }, 51 | "autosize": true 52 | }, 53 | "frames": [] 54 | } 55 | -------------------------------------------------------------------------------- /dev/percy/violin.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "violin", 5 | "mode": "markers", 6 | "uid": "91db56", 7 | "box": { 8 | "visible": true 9 | }, 10 | "meanline": { 11 | "visible": true 12 | }, 13 | "bandwidth": 0, 14 | "x": [ 15 | 1, 16 | 2, 17 | 3 18 | ], 19 | "xsrc": "x1" 20 | } 21 | ], 22 | "layout": { 23 | "xaxis": {}, 24 | "yaxis": {}, 25 | "autosize": true 26 | }, 27 | "frames": [] 28 | } 29 | -------------------------------------------------------------------------------- /dev/percy/waterfall.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "type": "waterfall", 5 | "mode": "markers", 6 | "decreasing": { 7 | "line": { 8 | "color": "#7f7f7f" 9 | } 10 | }, 11 | "increasing": { 12 | "line": { 13 | "color": "#17becf" 14 | } 15 | }, 16 | "x": [ 17 | 1, 18 | 2, 19 | 3 20 | ], 21 | "xsrc": "x1", 22 | "connector": { 23 | "line": { 24 | "width": 4, 25 | "color": "#7f7f7f" 26 | }, 27 | "visible": true, 28 | "mode": "between" 29 | }, 30 | "totals": { 31 | "marker": { 32 | "line": { 33 | "width": 2 34 | } 35 | } 36 | }, 37 | "textposition": "auto", 38 | "textfont": { 39 | "family": "sans-serif" 40 | }, 41 | "measure": [ 42 | "absolute", 43 | "relative", 44 | "total" 45 | ], 46 | "measuresrc": "y2" 47 | } 48 | ], 49 | "layout": { 50 | "xaxis": { 51 | "range": [ 52 | 0, 53 | 6.315789473684211 54 | ], 55 | "autorange": true, 56 | "type": "linear" 57 | }, 58 | "yaxis": { 59 | "range": [ 60 | -0.5, 61 | 2.5 62 | ], 63 | "autorange": true 64 | }, 65 | "autosize": true 66 | }, 67 | "frames": [] 68 | } 69 | -------------------------------------------------------------------------------- /dev/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .app { 8 | height: 100vh; 9 | max-height: 100vh; 10 | } 11 | 12 | .mocks * { 13 | z-index: 100; 14 | } 15 | 16 | .devbtn { 17 | margin: 10px; 18 | background: white; 19 | } 20 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # `react-chart-editor` examples 2 | 3 | * [Simple `react-chart-editor` example](simple): `DefaultEditor`, synchronous data, top-level component state management 4 | * [Demo `react-chart-editor` example](demo): `DefaultEditor`, top-level component state management, navbar to load mocks (same as dev app but no hot-reloading) 5 | * [Custom `react-chart-editor` example](custom): `CustomEditor`, synchronous data, top-level component state management 6 | * [Redux `react-chart-editor` example](redux): `DefaultEditor`, synchronous data, Redux state management 7 | -------------------------------------------------------------------------------- /examples/components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/react-chart-editor/b3891d801646cb48e93aca0c5bd3271f0a571ae8/examples/components.png -------------------------------------------------------------------------------- /examples/custom/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | yarn.lock 24 | package-lock.json 25 | -------------------------------------------------------------------------------- /examples/custom/README.md: -------------------------------------------------------------------------------- 1 | # Custom `react-chart-editor` example 2 | 3 | This example built with [`create-react-app`](https://github.com/facebookincubator/create-react-app) uses a [customized editor](src/CustomEditor.js), with synchronously-loaded data and a top-level component for state, so as to demo the customizability of this component. 4 | 5 | The custom editor shows minimal examples of various built-in elements, as well as how to extend elements to create customized behaviour. 6 | 7 | ## Quick start 8 | 9 | ``` 10 | npm install 11 | npm start 12 | ``` 13 | -------------------------------------------------------------------------------- /examples/custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "plotly.js": "^1.35.0", 7 | "react": "^16.2.0", 8 | "react-dom": "^16.2.0", 9 | "react-chart-editor": "latest", 10 | "react-scripts": "1.0.17" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/custom/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/react-chart-editor/b3891d801646cb48e93aca0c5bd3271f0a571ae8/examples/custom/public/favicon.ico -------------------------------------------------------------------------------- /examples/custom/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Custom editor example 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/custom/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/custom/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import plotly from 'plotly.js/dist/plotly'; 3 | import PlotlyEditor from 'react-chart-editor'; 4 | import CustomEditor from './CustomEditor'; 5 | import 'react-chart-editor/lib/react-chart-editor.css'; 6 | 7 | const dataSources = { 8 | col1: [1, 2, 3], // eslint-disable-line no-magic-numbers 9 | col2: [4, 3, 2], // eslint-disable-line no-magic-numbers 10 | col3: [17, 13, 9], // eslint-disable-line no-magic-numbers 11 | }; 12 | const dataSourceOptions = Object.keys(dataSources).map((name) => ({ 13 | value: name, 14 | label: name, 15 | })); 16 | 17 | const config = {editable: true}; 18 | 19 | class App extends Component { 20 | constructor() { 21 | super(); 22 | this.state = { 23 | data: [ 24 | { 25 | type: 'scatter', 26 | x: dataSources.col1, 27 | y: dataSources.col2, 28 | marker: {color: dataSources.col3}, 29 | }, 30 | ], 31 | layout: {}, 32 | frames: [], 33 | }; 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 | this.setState({data, layout, frames})} 48 | useResizeHandler 49 | debug 50 | advancedTraceTypeSelector 51 | > 52 | 53 | 54 |
55 | ); 56 | } 57 | } 58 | 59 | export default App; 60 | -------------------------------------------------------------------------------- /examples/custom/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .app { 8 | height: 100vh; 9 | max-height: 100vh; 10 | } 11 | -------------------------------------------------------------------------------- /examples/custom/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /examples/demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | yarn.lock 24 | package-lock.json 25 | -------------------------------------------------------------------------------- /examples/demo/README.md: -------------------------------------------------------------------------------- 1 | # Simple `react-chart-editor` example 2 | 3 | This example built with [`create-react-app`](https://github.com/facebookincubator/create-react-app) uses the `DefaultEditor`, with synchronously-loaded data and a top-level component for state, so as to demo the basic functionality of this component. 4 | 5 | ## Quick start 6 | 7 | ``` 8 | npm install 9 | npm start 10 | ``` 11 | -------------------------------------------------------------------------------- /examples/demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "version": "0.2.0", 4 | "private": true, 5 | "dependencies": { 6 | "plotly.js": "2.16.4", 7 | "react": "16.14.0", 8 | "react-chart-editor": "latest", 9 | "react-dom": "16.14.0", 10 | "react-scripts": "5.0.1" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "DISABLE_ESLINT_PLUGIN=true react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject", 17 | "predeploy": "npm run build", 18 | "deploy": "gh-pages -d build" 19 | }, 20 | "homepage": "http://plotly.github.io/react-chart-editor", 21 | "devDependencies": { 22 | "gh-pages": "^5.0.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/react-chart-editor/b3891d801646cb48e93aca0c5bd3271f0a571ae8/examples/demo/public/favicon.ico -------------------------------------------------------------------------------- /examples/demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Demo App 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/demo/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/demo/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import plotly from 'plotly.js/dist/plotly'; 3 | import PlotlyEditor from 'react-chart-editor'; 4 | import 'react-chart-editor/lib/react-chart-editor.css'; 5 | import Nav from './Nav'; 6 | import dataSources from './dataSources'; 7 | 8 | const dataSourceOptions = Object.keys(dataSources).map((name) => ({ 9 | value: name, 10 | label: name, 11 | })); 12 | 13 | const config = {editable: true}; 14 | 15 | class App extends Component { 16 | constructor() { 17 | super(); 18 | 19 | this.state = { 20 | data: [], 21 | layout: {}, 22 | frames: [], 23 | currentMockIndex: -1, 24 | mocks: [], 25 | }; 26 | 27 | this.loadMock = this.loadMock.bind(this); 28 | } 29 | 30 | UNSAFE_componentWillMount() { 31 | fetch('https://api.github.com/repos/plotly/plotly.js/contents/test/image/mocks') 32 | .then((response) => response.json()) 33 | .then((mocks) => this.setState({mocks})); 34 | } 35 | 36 | loadMock(mockIndex) { 37 | const mock = this.state.mocks[mockIndex]; 38 | fetch(mock.url, { 39 | headers: new Headers({Accept: 'application/vnd.github.v3.raw'}), 40 | }) 41 | .then((response) => response.json()) 42 | .then((figure) => { 43 | this.setState({ 44 | currentMockIndex: mockIndex, 45 | data: figure.data, 46 | layout: figure.layout, 47 | frames: figure.frames, 48 | }); 49 | }); 50 | } 51 | 52 | render() { 53 | return ( 54 |
55 | this.setState({data, layout, frames})} 64 | useResizeHandler 65 | debug 66 | advancedTraceTypeSelector 67 | /> 68 |
74 | ); 75 | } 76 | } 77 | 78 | export default App; 79 | -------------------------------------------------------------------------------- /examples/demo/src/Nav.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React from 'react'; 3 | import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'; 4 | 5 | const Nav = ({mocks, currentMockIndex, loadMock}) => ( 6 |
7 | Select mock: 8 |
9 | ({ 19 | label: item.name, 20 | value: i, 21 | }))} 22 | value={currentMockIndex} 23 | onChange={(option) => loadMock(option)} 24 | /> 25 |
26 |
27 | ); 28 | 29 | Nav.propTypes = { 30 | currentMockIndex: PropTypes.number, 31 | loadMock: PropTypes.func, 32 | mocks: PropTypes.array, 33 | }; 34 | 35 | export default Nav; 36 | -------------------------------------------------------------------------------- /examples/demo/src/dataSources.js: -------------------------------------------------------------------------------- 1 | ../../../dev/dataSources.js -------------------------------------------------------------------------------- /examples/demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .app { 8 | height: calc(100vh - 50px); 9 | max-height: calc(100vh - 50px); 10 | } 11 | 12 | .mock-nav { 13 | height: 50px; 14 | width: 100%; 15 | background-color: #506784; 16 | display: inline-flex; 17 | color: white; 18 | } 19 | 20 | .mock-nav__label { 21 | line-height: 50px; 22 | padding-left: 10px; 23 | } 24 | 25 | .mock-nav__select { 26 | width: 300px; 27 | margin-left: 20px; 28 | margin-right: 20px; 29 | margin-top: 7px; 30 | } 31 | 32 | .open-top .Select__menu { 33 | top: auto; 34 | bottom: 100%; 35 | border-top-right-radius: 4px; 36 | border-top-left-radius: 4px; 37 | border-bottom-right-radius: unset; 38 | border-bottom-left-radius: unset; 39 | } 40 | .open-top .Select__option { 41 | color: hsl(0, 0%, 50%); 42 | } 43 | .open-top .Select__option:first-child { 44 | border-top-right-radius: 4px; 45 | border-top-left-radius: 4px; 46 | } 47 | .open-top .Select__option:last-child { 48 | border-bottom-right-radius: unset; 49 | border-bottom-left-radius: unset; 50 | } 51 | .open-top .Select.is-open > .Select__option { 52 | border-top-right-radius: unset; 53 | border-top-left-radius: unset; 54 | border-bottom-right-radius: 4px; 55 | border-bottom-left-radius: 4px; 56 | } 57 | -------------------------------------------------------------------------------- /examples/demo/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /examples/editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/react-chart-editor/b3891d801646cb48e93aca0c5bd3271f0a571ae8/examples/editor.gif -------------------------------------------------------------------------------- /examples/redux/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | yarn.lock 24 | package-lock.json 25 | -------------------------------------------------------------------------------- /examples/redux/README.md: -------------------------------------------------------------------------------- 1 | # Redux `react-chart-editor` example 2 | 3 | This example built with [`create-react-app`](https://github.com/facebookincubator/create-react-app) uses the `DefaultEditor`, with synchronously-loaded data and a [Redux](https://redux.js.org) for state manage, so as to demo the Redux machinery required to. 4 | 5 | ## Quick start 6 | 7 | ``` 8 | npm install 9 | npm start 10 | ``` 11 | -------------------------------------------------------------------------------- /examples/redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "plotly.js": "^1.35.0", 7 | "prop-types": "^15.6.0", 8 | "react": "^16.2.0", 9 | "react-dom": "^16.2.0", 10 | "react-chart-editor": "latest", 11 | "react-redux": "^5.0.6", 12 | "react-scripts": "1.0.17", 13 | "redux": "^3.7.2" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/redux/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/react-chart-editor/b3891d801646cb48e93aca0c5bd3271f0a571ae8/examples/redux/public/favicon.ico -------------------------------------------------------------------------------- /examples/redux/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Redux App 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/redux/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/redux/src/App.js: -------------------------------------------------------------------------------- 1 | import 'react-chart-editor/lib/react-chart-editor.css'; 2 | import PlotlyEditor from 'react-chart-editor'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import plotly from 'plotly.js/dist/plotly'; 6 | import {bindActionCreators} from 'redux'; 7 | import {connect} from 'react-redux'; 8 | import * as actions from './actions'; 9 | 10 | const dataSources = { 11 | col1: [1, 2, 3], // eslint-disable-line no-magic-numbers 12 | col2: [4, 3, 2], // eslint-disable-line no-magic-numbers 13 | col3: [17, 13, 9], // eslint-disable-line no-magic-numbers 14 | }; 15 | const dataSourceOptions = Object.keys(dataSources).map((name) => ({ 16 | value: name, 17 | label: name, 18 | })); 19 | 20 | const config = {editable: true}; 21 | 22 | class App extends Component { 23 | constructor(props) { 24 | super(props); 25 | const {actions} = props; 26 | 27 | actions.sourcesUpdate(dataSources); 28 | actions.dataSourceOptionsUpdate(dataSourceOptions); 29 | } 30 | 31 | render() { 32 | const {actions, dataSources, dataSourceOptions, data, layout, frames} = this.props; 33 | 34 | return ( 35 |
36 | 49 |
50 | ); 51 | } 52 | } 53 | 54 | App.propTypes = { 55 | actions: PropTypes.object, 56 | dataSourceOptions: PropTypes.array, 57 | dataSources: PropTypes.object, 58 | data: PropTypes.array, 59 | layout: PropTypes.object, 60 | frames: PropTypes.array, 61 | }; 62 | 63 | const mapStateToProps = (state) => ({ 64 | dataSourceOptions: state.dataSourceOptions, 65 | dataSources: state.dataSources, 66 | data: state.data, 67 | layout: state.layout, 68 | frames: state.frames, 69 | }); 70 | 71 | const mapDispatchToProps = (dispatch) => ({ 72 | actions: bindActionCreators(actions, dispatch), 73 | }); 74 | 75 | export default connect(mapStateToProps, mapDispatchToProps)(App); 76 | -------------------------------------------------------------------------------- /examples/redux/src/actions.js: -------------------------------------------------------------------------------- 1 | import ACTIONS from './constants'; 2 | 3 | export function sourcesUpdate(payload) { 4 | return { 5 | type: ACTIONS.UPDATE_SOURCES, 6 | payload, 7 | }; 8 | } 9 | 10 | export function dataSourceOptionsUpdate(payload) { 11 | return { 12 | type: ACTIONS.UPDATE_SOURCE_OPTIONS, 13 | payload, 14 | }; 15 | } 16 | 17 | export function editorUpdate(data, layout, frames) { 18 | return { 19 | type: ACTIONS.EDITOR_UPDATE, 20 | payload: {data, layout, frames}, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /examples/redux/src/constants.js: -------------------------------------------------------------------------------- 1 | const ACTIONS = { 2 | EDITOR_UPDATE: 'EDITOR_UPDATE', 3 | INITIALIZE_PLOT: 'INITIALIZE_PLOT', 4 | PLOT_UPDATE: 'PLOT_UPDATE', 5 | UPDATE_SOURCES: 'UPDATE_SOURCES', 6 | UPDATE_SOURCE_OPTIONS: 'UPDATE_SOURCE_OPTIONS', 7 | }; 8 | 9 | export default ACTIONS; 10 | -------------------------------------------------------------------------------- /examples/redux/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .app { 8 | height: 100vh; 9 | max-height: 100vh; 10 | } 11 | -------------------------------------------------------------------------------- /examples/redux/src/index.js: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import App from './App'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import reducer from './reducer'; 6 | import {Provider} from 'react-redux'; 7 | import {createStore} from 'redux'; 8 | 9 | const store = createStore(reducer); 10 | 11 | ReactDOM.render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /examples/redux/src/reducer.js: -------------------------------------------------------------------------------- 1 | import ACTIONS from './constants'; 2 | 3 | const initialState = { 4 | dataSources: {}, 5 | dataSourceOptions: [], 6 | data: [], 7 | layout: {}, 8 | frames: [], 9 | }; 10 | 11 | export default (state = initialState, action) => { 12 | switch (action.type) { 13 | case ACTIONS.UPDATE_SOURCES: 14 | return {...state, dataSources: action.payload}; 15 | case ACTIONS.UPDATE_SOURCE_OPTIONS: 16 | return {...state, dataSourceOptions: action.payload}; 17 | case ACTIONS.EDITOR_UPDATE: 18 | return { 19 | ...state, 20 | data: action.payload.data, 21 | layout: action.payload.layout, 22 | frames: action.payload.frames, 23 | }; 24 | default: 25 | return state; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | yarn.lock 24 | package-lock.json 25 | -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | # Simple `react-chart-editor` example 2 | 3 | This example built with [`create-react-app`](https://github.com/facebookincubator/create-react-app) uses the `DefaultEditor`, with synchronously-loaded data and a top-level component for state, so as to demo the basic functionality of this component. 4 | 5 | ## Quick start 6 | 7 | ``` 8 | npm install 9 | npm start 10 | ``` 11 | -------------------------------------------------------------------------------- /examples/simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "plotly.js": "^1.35.0", 7 | "react": "^16.2.0", 8 | "react-dom": "^16.2.0", 9 | "react-chart-editor": "latest", 10 | "react-scripts": "1.0.17" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test --env=jsdom", 16 | "eject": "react-scripts eject" 17 | }, 18 | "homepage": "http://plotly.github.io/react-chart-editor", 19 | "devDependencies": { 20 | "gh-pages": "^1.1.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/simple/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plotly/react-chart-editor/b3891d801646cb48e93aca0c5bd3271f0a571ae8/examples/simple/public/favicon.ico -------------------------------------------------------------------------------- /examples/simple/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | Simple App 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/simple/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/simple/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import plotly from 'plotly.js/dist/plotly'; 3 | import PlotlyEditor from 'react-chart-editor'; 4 | import 'react-chart-editor/lib/react-chart-editor.css'; 5 | 6 | const dataSources = { 7 | col1: [1, 2, 3], // eslint-disable-line no-magic-numbers 8 | col2: [4, 3, 2], // eslint-disable-line no-magic-numbers 9 | col3: [17, 13, 9], // eslint-disable-line no-magic-numbers 10 | }; 11 | 12 | const dataSourceOptions = Object.keys(dataSources).map((name) => ({ 13 | value: name, 14 | label: name, 15 | })); 16 | 17 | const config = {editable: true}; 18 | 19 | class App extends Component { 20 | constructor() { 21 | super(); 22 | this.state = {data: [], layout: {}, frames: []}; 23 | } 24 | 25 | render() { 26 | return ( 27 |
28 | this.setState({data, layout, frames})} 37 | useResizeHandler 38 | debug 39 | advancedTraceTypeSelector 40 | /> 41 |
42 | ); 43 | } 44 | } 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /examples/simple/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | 7 | .app { 8 | height: 100vh; 9 | max-height: 100vh; 10 | width: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /examples/simple/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('postcss-import'), 4 | require('postcss-preset-env')({ 5 | features: {'nesting-rules': false}, 6 | }), 7 | require('postcss-combine-duplicated-selectors'), 8 | require('autoprefixer'), 9 | require('cssnano')({ 10 | preset: 'default', 11 | }), 12 | // ...(process.env.NODE_ENV === 'production' ? {cssnano: {preset: 'default'}} : {}), 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /scripts/combineTranslationKeys.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | // generalize so we can use this script in other es6 repos 5 | // so you can call: 6 | // combineTranslationKeys ... 7 | 8 | const pathToCombinedTranslationKeys = path.join( 9 | __dirname, 10 | './translationKeys/combined-translation-keys.txt' 11 | ); 12 | 13 | const plotlyJS = path.join( 14 | __dirname, 15 | '../node_modules/plotly.js/dist/translation-keys.txt' 16 | ); 17 | 18 | const editor = path.join(__dirname, './translationKeys/translation-keys.txt'); 19 | 20 | const argvLen = process.argv.length; 21 | const minHasPaths = 4; 22 | 23 | const hasPaths = argvLen >= minHasPaths; 24 | 25 | const inputPaths = hasPaths 26 | ? process.argv.slice(2, argvLen - 1) 27 | : [plotlyJS, editor]; 28 | 29 | const outputPath = hasPaths 30 | ? process.argv[argvLen - 1] 31 | : pathToCombinedTranslationKeys; 32 | 33 | combineTranslationKeys(); 34 | 35 | function combineTranslationKeys() { 36 | const dict = {}; 37 | let maxLen = 0; 38 | 39 | inputPaths.map(relPath => path.resolve(relPath)).forEach(inputPath => { 40 | const lines = fs.readFileSync(inputPath, 'utf-8').split(/\r?\n/); 41 | 42 | const repository = getRepository(inputPath); 43 | 44 | lines.forEach(line => { 45 | const splitString = line.split(/\/\//); 46 | const stringToTranslate = splitString[0].trim(); 47 | const source = splitString[1].trim(); 48 | maxLen = Math.max(maxLen, stringToTranslate.length); 49 | 50 | if (!dict[stringToTranslate]) { 51 | dict[stringToTranslate] = ' // ' + repository + ': ' + source; 52 | } else { 53 | dict[stringToTranslate] += ` && ${repository}: ${source}`; 54 | } 55 | }); 56 | }); 57 | 58 | const strings = Object.keys(dict) 59 | .sort() 60 | .map(k => k + spaces(maxLen - k.length) + dict[k]) 61 | .join('\n'); 62 | 63 | fs.writeFileSync(outputPath, strings); 64 | console.log(`combined translation keys were written to: ${outputPath}`); 65 | } 66 | 67 | function getRepository(inputPath) { 68 | const dir = path.dirname(inputPath); 69 | if (fs.existsSync(path.join(dir, 'package.json'))) { 70 | return path.basename(dir); 71 | } 72 | return getRepository(dir); 73 | } 74 | 75 | function spaces(len) { 76 | let out = ''; 77 | for (let i = 0; i < len; i++) { 78 | out += ' '; 79 | } 80 | return out; 81 | } 82 | 83 | process.on('exit', function(code) { 84 | if (code === 1) { 85 | throw new Error('combineTranslationKeys failed.'); 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /scripts/makeArrows.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | // generalize so we can use this script in other repos 5 | // so you can call: 6 | // makeArrows 7 | 8 | const pathToCombinedTranslationKeys = process.argv[2] || path.join( 9 | __dirname, 10 | './translationKeys/combined-translation-keys.txt' 11 | ); 12 | 13 | const pathToArrowsOut = process.argv[3] || path.join( 14 | __dirname, 15 | '../src/locales/xx.js' 16 | ); 17 | 18 | const wordRE = /^[A-Za-z]+$/; 19 | const maxLineLen = 80; 20 | 21 | function makeArrows() { 22 | const lines = fs 23 | .readFileSync(pathToCombinedTranslationKeys, 'utf-8') 24 | .split(/\r?\n/); 25 | const entries = lines 26 | .map(line => { 27 | const key = line.split(/\/\//)[0].trim(); 28 | const quoteChar = key.indexOf("'") === -1 ? "'" : '"'; 29 | 30 | if (key.indexOf('"') !== -1) { 31 | throw new Error('double quotes are not supported, key: ' + key); 32 | } 33 | 34 | const maybeQuoteKey = wordRE.test(key) 35 | ? key 36 | : quoteChar + key + quoteChar; 37 | const arrowStr = arrowPad(getArrowLen(key)); 38 | 39 | const quotedVal = quoteChar + arrowStr + key + arrowStr + quoteChar + ','; 40 | const singleLine = ' ' + maybeQuoteKey + ': ' + quotedVal; 41 | 42 | if (singleLine.length <= maxLineLen) { 43 | return singleLine; 44 | } 45 | 46 | return ' ' + maybeQuoteKey + ':\n ' + quotedVal; 47 | }) 48 | .join('\n'); 49 | 50 | const head = 'export default {'; 51 | const tail = '};\n'; 52 | 53 | fs.writeFileSync(pathToArrowsOut, [head, entries, tail].join('\n')); 54 | console.log('arrows mock translation written to: ' + pathToArrowsOut); 55 | } 56 | 57 | // inferred from the arrow file Greg provided 58 | const arrowFactor = 5.7; 59 | function getArrowLen(key) { 60 | return Math.max(1, Math.round(key.length / arrowFactor)); 61 | } 62 | 63 | function arrowPad(n) { 64 | let out = ''; 65 | for (let i = 0; i < n; i++) { 66 | out += '⇚'; 67 | } 68 | return out; 69 | } 70 | 71 | makeArrows(); 72 | 73 | process.on('exit', function(code) { 74 | if (code === 1) { 75 | throw new Error('makeArrows failed.'); 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /src/__stories__/stories.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Dosis:500,700|Open+Sans:400,400i,600,700'); 2 | 3 | .editor_controls__wrapper > div { 4 | position: relative !important; 5 | } 6 | -------------------------------------------------------------------------------- /src/__tests__/syntax-test.js: -------------------------------------------------------------------------------- 1 | // check for for focus and exclude jasmine blocks 2 | import fs from 'fs'; 3 | import glob from 'glob'; 4 | 5 | const BLACK_LIST = ['fdescribe', 'fit', 'xdescribe', 'xit', 'it\\.only', 'describe\\.only']; 6 | const REGEXS = BLACK_LIST.map((token) => new RegExp(`^\\s*${token}\\(.*`)); 7 | 8 | describe('Syntax and test validation', () => { 9 | describe(`ensures ${BLACK_LIST} is not present in tests`, () => { 10 | const files = glob.sync('!(node_modules|examples)/**/__tests__/*.js'); 11 | files.forEach((file) => 12 | it(`checks ${file} for test checks`, () => { 13 | const code = fs.readFileSync(file, {encoding: 'utf-8'}); 14 | code.split('\n').forEach((line) => { 15 | expect(REGEXS.some((re) => re.test(line))).toBe(false); 16 | }); 17 | }) 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/containers/AnnotationAccordion.js: -------------------------------------------------------------------------------- 1 | import PlotlyFold from './PlotlyFold'; 2 | import {LayoutPanel} from './derived'; 3 | import {PanelMessage} from './PanelEmpty'; 4 | import PropTypes from 'prop-types'; 5 | import React, {Component} from 'react'; 6 | import {connectAnnotationToLayout, getParsedTemplateString} from 'lib'; 7 | 8 | const AnnotationFold = connectAnnotationToLayout(PlotlyFold); 9 | 10 | class AnnotationAccordion extends Component { 11 | render() { 12 | const { 13 | layout: {annotations = [], meta = []}, 14 | localize: _, 15 | } = this.context; 16 | const {canAdd, children, canReorder} = this.props; 17 | 18 | const content = 19 | annotations.length && 20 | annotations.map((ann, i) => { 21 | return ( 22 | 28 | {children} 29 | 30 | ); 31 | }); 32 | 33 | const addAction = { 34 | label: _('Annotation'), 35 | handler: ({layout, updateContainer}) => { 36 | let annotationIndex; 37 | if (Array.isArray(layout.annotations)) { 38 | annotationIndex = layout.annotations.length; 39 | } else { 40 | annotationIndex = 0; 41 | } 42 | 43 | const key = `annotations[${annotationIndex}]`; 44 | const value = {text: _('new text')}; 45 | 46 | if (updateContainer) { 47 | updateContainer({[key]: value}); 48 | } 49 | }, 50 | }; 51 | 52 | return ( 53 | 54 | {content ? ( 55 | content 56 | ) : ( 57 | 58 |

59 | {_( 60 | 'Annotations are text and arrows you can use to point out specific parts of your figure.' 61 | )} 62 |

63 |

{_('Click on the + button above to add an annotation.')}

64 |
65 | )} 66 |
67 | ); 68 | } 69 | } 70 | 71 | AnnotationAccordion.contextTypes = { 72 | layout: PropTypes.object, 73 | localize: PropTypes.func, 74 | }; 75 | 76 | AnnotationAccordion.propTypes = { 77 | children: PropTypes.node, 78 | canAdd: PropTypes.bool, 79 | canReorder: PropTypes.bool, 80 | }; 81 | 82 | export default AnnotationAccordion; 83 | -------------------------------------------------------------------------------- /src/components/containers/AxesFold.js: -------------------------------------------------------------------------------- 1 | import AxesSelector from '../fields/AxesSelector'; 2 | import PlotlyFold from './PlotlyFold'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectAxesToLayout} from 'lib'; 6 | 7 | class AxesFold extends Component { 8 | render() { 9 | const {children, options} = this.props; 10 | return options.length && children ? ( 11 | 12 | {options.length === 1 ? null : } 13 | {children} 14 | 15 | ) : null; 16 | } 17 | } 18 | 19 | AxesFold.propTypes = { 20 | children: PropTypes.any, 21 | options: PropTypes.array, 22 | }; 23 | 24 | AxesFold.plotly_editor_traits = {foldable: true}; 25 | 26 | export default connectAxesToLayout(AxesFold); 27 | -------------------------------------------------------------------------------- /src/components/containers/FoldEmpty.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | 4 | export default class FoldEmpty extends Component { 5 | render() { 6 | const {children, icon: Icon, messagePrimary, messageSecondary} = this.props; 7 | 8 | return ( 9 |
10 | {Icon ? ( 11 |
12 | 13 |
14 | ) : null} 15 | {messagePrimary ? ( 16 |
{messagePrimary}
17 | ) : null} 18 | {messageSecondary ? ( 19 |
{messageSecondary}
20 | ) : null} 21 | {children ? children : null} 22 |
23 | ); 24 | } 25 | } 26 | 27 | FoldEmpty.propTypes = { 28 | messagePrimary: PropTypes.string, 29 | messageSecondary: PropTypes.string, 30 | children: PropTypes.node, 31 | icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), 32 | }; 33 | -------------------------------------------------------------------------------- /src/components/containers/ImageAccordion.js: -------------------------------------------------------------------------------- 1 | import PlotlyFold from './PlotlyFold'; 2 | import {LayoutPanel} from './derived'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectImageToLayout} from 'lib'; 6 | import {PanelMessage} from './PanelEmpty'; 7 | 8 | const ImageFold = connectImageToLayout(PlotlyFold); 9 | 10 | class ImageAccordion extends Component { 11 | render() { 12 | const { 13 | layout: {images = []}, 14 | localize: _, 15 | } = this.context; 16 | const {canAdd, children, canReorder} = this.props; 17 | 18 | const content = 19 | images.length && 20 | images.map((img, i) => ( 21 | 22 | {children} 23 | 24 | )); 25 | 26 | const addAction = { 27 | label: _('Image'), 28 | handler: ({layout, updateContainer}) => { 29 | let imageIndex; 30 | if (Array.isArray(layout.images)) { 31 | imageIndex = layout.images.length; 32 | } else { 33 | imageIndex = 0; 34 | } 35 | 36 | const key = `images[${imageIndex}]`; 37 | const value = { 38 | sizex: 0.1, 39 | sizey: 0.1, 40 | x: 0.5, 41 | y: 0.5, 42 | }; 43 | 44 | if (updateContainer) { 45 | updateContainer({[key]: value}); 46 | } 47 | }, 48 | }; 49 | 50 | return ( 51 | 52 | {content ? ( 53 | content 54 | ) : ( 55 | 56 |

57 | {_( 58 | 'Embed images in your figure to make the data more readable or to brand your content.' 59 | )} 60 |

61 |

{_('Click on the + button above to add an image.')}

62 |
63 | )} 64 |
65 | ); 66 | } 67 | } 68 | 69 | ImageAccordion.contextTypes = { 70 | layout: PropTypes.object, 71 | localize: PropTypes.func, 72 | }; 73 | 74 | ImageAccordion.propTypes = { 75 | children: PropTypes.node, 76 | canAdd: PropTypes.bool, 77 | canReorder: PropTypes.bool, 78 | }; 79 | 80 | export default ImageAccordion; 81 | -------------------------------------------------------------------------------- /src/components/containers/MapboxLayersAccordion.js: -------------------------------------------------------------------------------- 1 | import PlotlyFold from './PlotlyFold'; 2 | import PlotlyPanel from './PlotlyPanel'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectLayersToMapbox, getParsedTemplateString} from 'lib'; 6 | 7 | const MapboxLayersFold = connectLayersToMapbox(PlotlyFold); 8 | 9 | class MapboxLayersAccordion extends Component { 10 | render() { 11 | const { 12 | fullContainer: {layers = []}, 13 | localize: _, 14 | layout: meta, 15 | } = this.context; 16 | const {children} = this.props; 17 | 18 | const content = 19 | layers.length && 20 | layers.map((layer, i) => ( 21 | 27 | {children} 28 | 29 | )); 30 | 31 | const addAction = { 32 | label: _('Layer'), 33 | handler: (context) => { 34 | const {fullContainer, updateContainer} = context; 35 | if (updateContainer) { 36 | const mapboxLayerIndex = Array.isArray(fullContainer.layers) 37 | ? fullContainer.layers.length 38 | : 0; 39 | 40 | updateContainer({ 41 | [`layers[${mapboxLayerIndex}]`]: { 42 | name: `Layer ${mapboxLayerIndex}`, 43 | sourcetype: 'raster', 44 | below: 'traces', 45 | }, 46 | }); 47 | } 48 | }, 49 | }; 50 | 51 | return ( 52 | 53 | {content ? content : null} 54 | 55 | ); 56 | } 57 | } 58 | 59 | MapboxLayersAccordion.contextTypes = { 60 | fullContainer: PropTypes.object, 61 | localize: PropTypes.func, 62 | layout: PropTypes.object, 63 | }; 64 | 65 | MapboxLayersAccordion.propTypes = { 66 | children: PropTypes.node, 67 | }; 68 | 69 | MapboxLayersAccordion.plotly_editor_traits = { 70 | no_visibility_forcing: true, 71 | }; 72 | 73 | export default MapboxLayersAccordion; 74 | -------------------------------------------------------------------------------- /src/components/containers/MenuPanel.js: -------------------------------------------------------------------------------- 1 | import ModalBox from './ModalBox'; 2 | import PropTypes from 'prop-types'; 3 | import React, {Component} from 'react'; 4 | import classnames from 'classnames'; 5 | import {QuestionIcon, CogIcon} from 'plotly-icons'; 6 | 7 | export default class MenuPanel extends Component { 8 | constructor() { 9 | super(); 10 | this.state = {isOpen: false}; 11 | 12 | this.togglePanel = this.togglePanel.bind(this); 13 | } 14 | 15 | getIcon() { 16 | const {question, icon: Icon} = this.props; 17 | if (question) { 18 | return { 19 | icon: , 20 | spanClass: `menupanel__icon-span menupanel__icon-span--question`, 21 | }; 22 | } 23 | if (Icon) { 24 | return { 25 | icon: , 26 | spanClass: `menupanel__icon-span`, 27 | }; 28 | } 29 | return { 30 | icon: , 31 | spanClass: 'menupanel__icon-span menupanel__icon-span--cog', 32 | }; 33 | } 34 | 35 | togglePanel() { 36 | this.setState({isOpen: !this.state.isOpen}); 37 | } 38 | 39 | render() { 40 | const {show, ownline, label, children} = this.props; 41 | const isOpen = show || this.state.isOpen; 42 | 43 | const containerClass = classnames('menupanel', { 44 | 'menupanel--ownline': ownline, 45 | }); 46 | 47 | const {icon, spanClass} = this.getIcon(); 48 | 49 | return ( 50 |
51 |
52 |
{label}
53 |
54 | {icon} 55 |
56 |
57 | {isOpen && {children}} 58 |
59 | ); 60 | } 61 | } 62 | 63 | MenuPanel.propTypes = { 64 | children: PropTypes.node, 65 | icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), 66 | label: PropTypes.string, 67 | ownline: PropTypes.bool, 68 | question: PropTypes.bool, 69 | show: PropTypes.bool, 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/containers/Modal.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {CloseIcon} from 'plotly-icons'; 4 | 5 | const ModalHeader = ({title, handleClose}) => ( 6 |
7 | {title ?
{title}
: null} 8 | {handleClose ? ( 9 |
handleClose() : null}> 10 | 11 |
12 | ) : null} 13 |
14 | ); 15 | 16 | const ModalContent = ({children}) =>
{children}
; 17 | 18 | class Modal extends Component { 19 | constructor(props) { 20 | super(props); 21 | this.escFunction = this.escFunction.bind(this); 22 | } 23 | 24 | escFunction(event) { 25 | const escKeyCode = 27; 26 | if (event.keyCode === escKeyCode) { 27 | this.context.handleClose(); 28 | } 29 | } 30 | 31 | componentDidMount() { 32 | document.addEventListener('keydown', this.escFunction, false); 33 | } 34 | 35 | componentWillUnmount() { 36 | document.removeEventListener('keydown', this.escFunction, false); 37 | } 38 | 39 | render() { 40 | const {children, title} = this.props; 41 | let classes = 'modal'; 42 | if (this.context.isAnimatingOut) { 43 | classes += ' modal--animate-out'; 44 | } 45 | return ( 46 |
47 |
48 | this.context.handleClose()} /> 49 | {children} 50 |
51 |
this.context.handleClose()} /> 52 |
53 | ); 54 | } 55 | } 56 | 57 | ModalHeader.propTypes = { 58 | title: PropTypes.node, 59 | handleClose: PropTypes.func.isRequired, 60 | }; 61 | 62 | ModalContent.propTypes = { 63 | children: PropTypes.node.isRequired, 64 | }; 65 | 66 | Modal.propTypes = { 67 | children: PropTypes.node.isRequired, 68 | title: PropTypes.node, 69 | }; 70 | 71 | Modal.contextTypes = { 72 | handleClose: PropTypes.func, 73 | isAnimatingOut: PropTypes.bool, 74 | }; 75 | 76 | export default Modal; 77 | 78 | export {ModalHeader, ModalContent}; 79 | -------------------------------------------------------------------------------- /src/components/containers/ModalBox.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | 5 | export default class ModalBox extends Component { 6 | render() { 7 | const {backgroundDark, children, onClose, relative} = this.props; 8 | const modalboxClass = classnames('modalbox', { 9 | 'modalbox--dark': backgroundDark, 10 | 'modalbox--relative': relative, 11 | }); 12 | return ( 13 |
14 |
15 |
{children}
16 |
17 | ); 18 | } 19 | } 20 | 21 | ModalBox.propTypes = { 22 | backgroundDark: PropTypes.bool, 23 | relative: PropTypes.bool, 24 | children: PropTypes.node, 25 | onClose: PropTypes.func, 26 | }; 27 | -------------------------------------------------------------------------------- /src/components/containers/ModalProvider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class ModalProvider extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.state = { 8 | component: null, 9 | componentProps: {}, 10 | open: false, 11 | isAnimatingOut: false, 12 | }; 13 | } 14 | 15 | componentDidUpdate() { 16 | const body = document.body; 17 | const {open} = this.state; 18 | 19 | // Toggle scroll on document body if modal is open 20 | const hasClass = body.classList.contains('no-scroll'); 21 | 22 | if (open && !hasClass) { 23 | body.classList.add('no-scroll'); 24 | } 25 | if (!open && hasClass) { 26 | body.classList.remove('no-scroll'); 27 | } 28 | } 29 | 30 | openModal(component, componentProps) { 31 | const {localize: _} = this.context; 32 | if (!component) { 33 | throw Error(_('You need to provide a component for the modal to open!')); 34 | } 35 | const {open} = this.state; 36 | 37 | if (!open) { 38 | this.setState({ 39 | component: component, 40 | componentProps: componentProps, 41 | open: true, 42 | }); 43 | } 44 | } 45 | 46 | closeModal() { 47 | const {open} = this.state; 48 | if (open) { 49 | this.setState({ 50 | open: false, 51 | component: null, 52 | }); 53 | } 54 | } 55 | handleClose() { 56 | this.setState({isAnimatingOut: true}); 57 | const animationDuration = 600; 58 | setTimeout(() => { 59 | this.setState({isAnimatingOut: false}); 60 | this.closeModal(); 61 | }, animationDuration); 62 | } 63 | 64 | getChildContext() { 65 | return { 66 | openModal: (c, p) => this.openModal(c, p), 67 | closeModal: () => this.closeModal(), 68 | handleClose: () => this.handleClose(), 69 | isAnimatingOut: this.state.isAnimatingOut, 70 | }; 71 | } 72 | 73 | render() { 74 | const {component: Component, componentProps, isAnimatingOut} = this.state; 75 | return ( 76 | <> 77 | {this.props.children} 78 | {this.state.open ? : null} 79 | 80 | ); 81 | } 82 | } 83 | 84 | ModalProvider.propTypes = { 85 | children: PropTypes.node, 86 | }; 87 | ModalProvider.contextTypes = { 88 | localize: PropTypes.func, 89 | }; 90 | ModalProvider.childContextTypes = { 91 | openModal: PropTypes.func, 92 | closeModal: PropTypes.func, 93 | handleClose: PropTypes.func, 94 | isAnimatingOut: PropTypes.bool, 95 | }; 96 | 97 | export default ModalProvider; 98 | -------------------------------------------------------------------------------- /src/components/containers/PanelEmpty.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | import {ChartLineIcon} from 'plotly-icons'; 4 | import {bem} from 'lib'; 5 | 6 | export class PanelMessage extends Component { 7 | render() { 8 | const {children, icon: Icon} = this.props; 9 | const heading = this.props.heading || ''; 10 | 11 | return ( 12 |
13 | {Boolean(Icon) && ( 14 |
15 | 16 |
17 | )} 18 | {Boolean(heading) &&
{heading}
} 19 |
{children}
20 |
21 | ); 22 | } 23 | } 24 | 25 | PanelMessage.defaultProps = { 26 | icon: ChartLineIcon, 27 | }; 28 | 29 | PanelMessage.propTypes = { 30 | heading: PropTypes.string, 31 | children: PropTypes.node, 32 | icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), 33 | }; 34 | 35 | class PanelEmpty extends Component { 36 | render() { 37 | return ( 38 |
39 | 40 |
41 | ); 42 | } 43 | } 44 | 45 | PanelEmpty.propTypes = { 46 | heading: PropTypes.string, 47 | children: PropTypes.node, 48 | icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), 49 | }; 50 | 51 | export default PanelEmpty; 52 | -------------------------------------------------------------------------------- /src/components/containers/RangeSelectorAccordion.js: -------------------------------------------------------------------------------- 1 | import PlotlyFold from './PlotlyFold'; 2 | import PlotlyPanel from './PlotlyPanel'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectRangeSelectorToAxis, getParsedTemplateString} from 'lib'; 6 | 7 | const RangeSelectorFold = connectRangeSelectorToAxis(PlotlyFold); 8 | 9 | class RangeSelectorAccordion extends Component { 10 | render() { 11 | if ( 12 | !this.context.fullContainer || 13 | !this.context.fullContainer.rangeselector || 14 | !this.context.fullContainer.rangeselector.visible || 15 | // next line checks for "all" case 16 | this.context.fullContainer._axisGroup === 0 17 | ) { 18 | return null; 19 | } 20 | 21 | const { 22 | fullContainer: { 23 | rangeselector: {buttons = []}, 24 | }, 25 | localize: _, 26 | layout: meta, 27 | } = this.context; 28 | const {children} = this.props; 29 | 30 | const content = 31 | buttons.length && 32 | buttons.map((btn, i) => ( 33 | 39 | {children} 40 | 41 | )); 42 | 43 | const addAction = { 44 | label: _('Button'), 45 | handler: (context) => { 46 | const {fullContainer, updateContainer} = context; 47 | if (updateContainer) { 48 | const rangeselectorIndex = Array.isArray(fullContainer.rangeselector.buttons) 49 | ? fullContainer.rangeselector.buttons.length 50 | : 0; 51 | 52 | updateContainer({ 53 | [`rangeselector.buttons[${rangeselectorIndex}]`]: {}, 54 | }); 55 | } 56 | }, 57 | }; 58 | 59 | return {content ? content : null}; 60 | } 61 | } 62 | 63 | RangeSelectorAccordion.contextTypes = { 64 | fullContainer: PropTypes.object, 65 | localize: PropTypes.func, 66 | layout: PropTypes.object, 67 | }; 68 | 69 | RangeSelectorAccordion.propTypes = { 70 | children: PropTypes.node, 71 | }; 72 | 73 | RangeSelectorAccordion.plotly_editor_traits = { 74 | no_visibility_forcing: true, 75 | }; 76 | 77 | export default RangeSelectorAccordion; 78 | -------------------------------------------------------------------------------- /src/components/containers/ShapeAccordion.js: -------------------------------------------------------------------------------- 1 | import PlotlyFold from './PlotlyFold'; 2 | import {LayoutPanel} from './derived'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectShapeToLayout} from 'lib'; 6 | import {COLORS} from 'lib/constants'; 7 | import {PanelMessage} from './PanelEmpty'; 8 | 9 | const ShapeFold = connectShapeToLayout(PlotlyFold); 10 | 11 | class ShapeAccordion extends Component { 12 | render() { 13 | const { 14 | layout: {shapes = []}, 15 | localize: _, 16 | } = this.context; 17 | const {canAdd, children, canReorder} = this.props; 18 | 19 | const content = 20 | shapes.length && 21 | shapes.map((shp, i) => ( 22 | 23 | {children} 24 | 25 | )); 26 | 27 | const addAction = { 28 | label: _('Shape'), 29 | handler: ({layout, updateContainer}) => { 30 | let shapeIndex; 31 | if (Array.isArray(layout.shapes)) { 32 | shapeIndex = layout.shapes.length; 33 | } else { 34 | shapeIndex = 0; 35 | } 36 | 37 | const key = `shapes[${shapeIndex}]`; 38 | const value = { 39 | line: {color: COLORS.charcoal}, 40 | fillcolor: COLORS.middleGray, 41 | opacity: 0.3, 42 | }; 43 | 44 | if (updateContainer) { 45 | updateContainer({[key]: value}); 46 | } 47 | }, 48 | }; 49 | 50 | return ( 51 | 52 | {content ? ( 53 | content 54 | ) : ( 55 | 56 |

57 | {_( 58 | 'Add shapes to a figure to highlight points or periods in time, thresholds, or areas of interest.' 59 | )} 60 |

61 |

{_('Click on the + button above to add a shape.')}

62 |
63 | )} 64 |
65 | ); 66 | } 67 | } 68 | 69 | ShapeAccordion.contextTypes = { 70 | layout: PropTypes.object, 71 | localize: PropTypes.func, 72 | }; 73 | 74 | ShapeAccordion.propTypes = { 75 | children: PropTypes.node, 76 | canAdd: PropTypes.bool, 77 | canReorder: PropTypes.bool, 78 | }; 79 | 80 | export default ShapeAccordion; 81 | -------------------------------------------------------------------------------- /src/components/containers/SingleSidebarItem.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | 4 | export default class SingleSidebarItem extends Component { 5 | render() { 6 | return this.props.children ? ( 7 |
{this.props.children}
8 | ) : null; 9 | } 10 | } 11 | 12 | SingleSidebarItem.plotly_editor_traits = {sidebar_element: true}; 13 | 14 | SingleSidebarItem.propTypes = { 15 | children: PropTypes.any, 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/containers/SliderAccordion.js: -------------------------------------------------------------------------------- 1 | import PlotlyFold from './PlotlyFold'; 2 | import TraceRequiredPanel from './TraceRequiredPanel'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectSliderToLayout} from 'lib'; 6 | 7 | const SliderFold = connectSliderToLayout(PlotlyFold); 8 | 9 | class SliderAccordion extends Component { 10 | render() { 11 | const { 12 | layout: {sliders = []}, 13 | localize: _, 14 | } = this.context; 15 | const {children} = this.props; 16 | 17 | const content = 18 | sliders.length > 0 && 19 | sliders.map((sli, i) => ( 20 | 21 | {children} 22 | 23 | )); 24 | 25 | return {content ? content : null}; 26 | } 27 | } 28 | 29 | SliderAccordion.contextTypes = { 30 | layout: PropTypes.object, 31 | localize: PropTypes.func, 32 | }; 33 | 34 | SliderAccordion.propTypes = { 35 | children: PropTypes.node, 36 | }; 37 | 38 | export default SliderAccordion; 39 | -------------------------------------------------------------------------------- /src/components/containers/TraceMarkerSection.js: -------------------------------------------------------------------------------- 1 | import PlotlySection from './PlotlySection'; 2 | import React, {Component} from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | class TraceMarkerSection extends Component { 6 | constructor(props, context) { 7 | super(props, context); 8 | this.setLocals(context); 9 | } 10 | 11 | UNSAFE_componentWillReceiveProps(nextProps, nextContext) { 12 | this.setLocals(nextContext); 13 | } 14 | 15 | setLocals(context) { 16 | const _ = this.context.localize; 17 | const traceType = context.fullContainer.type; 18 | if (['bar', 'histogram', 'funnel', 'waterfall'].includes(traceType)) { 19 | this.name = _('Bars'); 20 | } else if (['funnelarea', 'pie', 'sunburst', 'treemap'].includes(traceType)) { 21 | this.name = _('Segments'); 22 | } else { 23 | this.name = _('Points'); 24 | } 25 | } 26 | 27 | render() { 28 | return {this.props.children}; 29 | } 30 | } 31 | 32 | TraceMarkerSection.propTypes = { 33 | children: PropTypes.node, 34 | name: PropTypes.string, 35 | }; 36 | 37 | TraceMarkerSection.contextTypes = { 38 | fullContainer: PropTypes.object, 39 | localize: PropTypes.func, 40 | }; 41 | 42 | export default TraceMarkerSection; 43 | -------------------------------------------------------------------------------- /src/components/containers/TraceRequiredPanel.js: -------------------------------------------------------------------------------- 1 | import PanelEmpty from './PanelEmpty'; 2 | import PropTypes from 'prop-types'; 3 | import React, {Component} from 'react'; 4 | import {LayoutPanel} from './derived'; 5 | 6 | class TraceRequiredPanel extends Component { 7 | hasTrace() { 8 | return this.context.fullData.filter((trace) => trace.visible).length > 0; 9 | } 10 | 11 | render() { 12 | const {localize: _} = this.context; 13 | const {children, ...rest} = this.props; 14 | 15 | if (!this.props.visible) { 16 | return null; 17 | } 18 | 19 | return this.hasTrace() ? ( 20 | {children} 21 | ) : ( 22 | 23 |

24 | {_('Go to the ')} 25 | this.context.setPanel('Structure', 'Traces')}>{_('Traces')} 26 | {_(' panel under Structure to define traces.')} 27 |

28 |
29 | ); 30 | } 31 | } 32 | 33 | TraceRequiredPanel.propTypes = { 34 | children: PropTypes.node, 35 | visible: PropTypes.bool, 36 | }; 37 | 38 | TraceRequiredPanel.defaultProps = { 39 | visible: true, 40 | }; 41 | 42 | TraceRequiredPanel.contextTypes = { 43 | fullData: PropTypes.array, 44 | localize: PropTypes.func, 45 | setPanel: PropTypes.func, 46 | }; 47 | 48 | export default TraceRequiredPanel; 49 | -------------------------------------------------------------------------------- /src/components/containers/UpdateMenuAccordion.js: -------------------------------------------------------------------------------- 1 | import PlotlyFold from './PlotlyFold'; 2 | import TraceRequiredPanel from './TraceRequiredPanel'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectUpdateMenuToLayout} from 'lib'; 6 | 7 | const UpdateMenuFold = connectUpdateMenuToLayout(PlotlyFold); 8 | 9 | class UpdateMenuAccordion extends Component { 10 | render() { 11 | const { 12 | fullLayout: {updatemenus = []}, 13 | localize: _, 14 | } = this.context; 15 | const {children} = this.props; 16 | 17 | const content = 18 | updatemenus.length > 0 && 19 | updatemenus.map((upd, i) => { 20 | const localizedType = { 21 | dropdown: _('Dropdown'), 22 | buttons: _('Buttons'), 23 | }; 24 | const menuType = localizedType[upd.type] || localizedType.dropdown; 25 | const activeBtn = upd.buttons.filter((b) => b._index === upd.active)[0]; 26 | const foldName = menuType + (activeBtn ? ': ' + activeBtn.label : ''); 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | ); 33 | }); 34 | 35 | return {content ? content : null}; 36 | } 37 | } 38 | 39 | UpdateMenuAccordion.contextTypes = { 40 | fullLayout: PropTypes.object, 41 | localize: PropTypes.func, 42 | }; 43 | 44 | UpdateMenuAccordion.propTypes = { 45 | children: PropTypes.node, 46 | }; 47 | 48 | export default UpdateMenuAccordion; 49 | -------------------------------------------------------------------------------- /src/components/containers/__tests__/AnnotationAccordion-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {AnnotationAccordion, PlotlyPanel, PlotlyFold} from '..'; 3 | import {Numeric} from '../../fields'; 4 | import {TestEditor, fixtures, mount} from 'lib/test-utils'; 5 | import {connectLayoutToPlot} from 'lib'; 6 | 7 | const LayoutPanel = connectLayoutToPlot(PlotlyPanel); 8 | 9 | describe('', () => { 10 | it('generates annotation PlotlyFolds with name == text', () => { 11 | const fixture = fixtures.scatter({ 12 | layout: {annotations: [{text: 'hodor'}, {text: 'rodoh'}]}, 13 | }); 14 | 15 | const folds = mount( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ).find(PlotlyFold); 24 | 25 | expect(folds.length).toBe(2); 26 | expect(folds.at(0).prop('name')).toBe('hodor'); 27 | expect(folds.at(1).prop('name')).toBe('rodoh'); 28 | }); 29 | 30 | it('can add annotations', () => { 31 | const fixture = fixtures.scatter(); 32 | const beforeUpdateLayout = jest.fn(); 33 | const editor = mount( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | 43 | editor.find('button.js-add-button').simulate('click'); 44 | 45 | const payload = beforeUpdateLayout.mock.calls[0][0]; 46 | expect(payload.update).toEqual({'annotations[0]': {text: 'new text'}}); 47 | }); 48 | 49 | it('can delete annotations', () => { 50 | const fixture = fixtures.scatter({ 51 | layout: {annotations: [{text: 'hodor'}, {text: 'rodoh'}]}, 52 | }); 53 | const beforeDeleteAnnotation = jest.fn(); 54 | const editor = mount( 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | editor.find('.js-fold__delete').at(0).simulate('click'); 64 | 65 | const update = beforeDeleteAnnotation.mock.calls[0][0]; 66 | expect(update.annotationIndex).toBe(0); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/components/containers/__tests__/Fold-test.js: -------------------------------------------------------------------------------- 1 | import {Numeric} from '../../fields'; 2 | import {PlotlyFold, PlotlyPanel} from '..'; 3 | import React from 'react'; 4 | import {TestEditor, fixtures, mount} from 'lib/test-utils'; 5 | import {connectTraceToPlot} from 'lib'; 6 | 7 | const TraceFold = connectTraceToPlot(PlotlyFold); 8 | 9 | describe('', () => { 10 | it('shows deleteContainer button when deleteContainer function present and canDelete is true', () => { 11 | const withoutDelete = mount( 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ).find('.js-fold__delete'); 20 | expect(withoutDelete.exists()).toBe(false); 21 | 22 | const withDelete = mount( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ).find('.js-fold__delete'); 31 | expect(withDelete.exists()).toBe(true); 32 | }); 33 | 34 | it('calls deleteContainer when function present and canDelete is true', () => { 35 | const beforeDeleteTrace = jest.fn(); 36 | mount( 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ) 45 | .find('.js-fold__delete') 46 | .simulate('click'); 47 | 48 | const payload = beforeDeleteTrace.mock.calls[0][0]; 49 | expect(payload).toEqual({ 50 | axesToBeGarbageCollected: [], 51 | subplotToBeGarbageCollected: null, 52 | traceIndexes: [0], 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/components/containers/__tests__/Layout-test.js: -------------------------------------------------------------------------------- 1 | import {Numeric} from '../../fields'; 2 | import {PlotlyFold, PlotlyPanel, PlotlySection} from '..'; 3 | import NumericInput from '../../widgets/NumericInput'; 4 | import React from 'react'; 5 | import {TestEditor, fixtures} from 'lib/test-utils'; 6 | import {connectLayoutToPlot} from 'lib'; 7 | import {mount} from 'enzyme'; 8 | 9 | const Layouts = [PlotlyPanel, PlotlyFold, PlotlySection].map(connectLayoutToPlot); 10 | const Editor = (props) => ; 11 | 12 | Layouts.forEach((Layout) => { 13 | describe(`<${Layout.displayName}>`, () => { 14 | it(`wraps container with fullValue pointing to gd._fullLayout`, () => { 15 | const wrapper = mount( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | .find('[attr="height"]') 25 | .find(NumericInput); 26 | expect(wrapper.prop('value')).toBe(100); 27 | }); 28 | 29 | it(`sends updates to gd._layout`, () => { 30 | const beforeUpdateLayout = jest.fn(); 31 | const wrapper = mount( 32 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | .find('[attr="height"]') 44 | .find(NumericInput); 45 | 46 | const heightUpdate = 200; 47 | wrapper.prop('onChange')(heightUpdate); 48 | const payload = beforeUpdateLayout.mock.calls[0][0]; 49 | expect(payload).toEqual({update: {height: heightUpdate}}); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/containers/__tests__/TraceAccordion-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {TraceAccordion, PlotlyFold, LayoutPanel} from '..'; 3 | import {TextEditor} from '../../fields'; 4 | import {TestEditor, fixtures, mount} from 'lib/test-utils'; 5 | 6 | describe('', () => { 7 | it('generates trace PlotlyFolds with name == text', () => { 8 | const fixture = fixtures.scatter({data: [{name: 'hodor'}]}); 9 | 10 | const folds = mount( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ).find(PlotlyFold); 19 | 20 | expect(folds.at(0).prop('name')).toBe('hodor'); 21 | }); 22 | 23 | it('can add traces', () => { 24 | const fixture = fixtures.scatter({data: [{name: 'hodor'}]}); 25 | const beforeAddTrace = jest.fn(); 26 | const editor = mount( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | 36 | editor.find('button.js-add-button').simulate('click'); 37 | 38 | expect(beforeAddTrace).toBeCalled(); 39 | }); 40 | 41 | it('can delete traces', () => { 42 | const fixture = fixtures.scatter({data: [{name: 'hodor'}]}); 43 | const beforeDeleteTrace = jest.fn(); 44 | const editor = mount( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | 54 | editor.find('.js-fold__delete').at(0).simulate('click'); 55 | 56 | expect(beforeDeleteTrace).toBeCalled(); 57 | const update = beforeDeleteTrace.mock.calls[0][0]; 58 | expect(update.traceIndexes[0]).toBe(0); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/components/containers/derived.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PlotlyPanel from './PlotlyPanel'; 3 | import PlotlySection from './PlotlySection'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import {connectLayoutToPlot, containerConnectedContextTypes} from 'lib'; 7 | 8 | const LayoutPanel = connectLayoutToPlot(PlotlyPanel); 9 | const LayoutSection = connectLayoutToPlot(PlotlySection); 10 | 11 | const TraceTypeSection = (props, context) => { 12 | const {fullContainer, fullData} = context; 13 | const {mode, traceTypes} = props; 14 | 15 | const ifConnectedToTrace = 16 | mode === 'trace' && fullContainer && traceTypes.includes(fullContainer.type); 17 | 18 | const ifConnectedToLayout = 19 | mode === 'layout' && fullData && fullData.some((t) => traceTypes.includes(t.type)); 20 | 21 | if (ifConnectedToTrace || ifConnectedToLayout) { 22 | return ; 23 | } 24 | 25 | return null; 26 | }; 27 | 28 | TraceTypeSection.contextTypes = containerConnectedContextTypes; 29 | TraceTypeSection.propTypes = { 30 | children: PropTypes.node, 31 | name: PropTypes.string, 32 | traceTypes: PropTypes.array, 33 | mode: PropTypes.string, 34 | }; 35 | 36 | TraceTypeSection.defaultProps = { 37 | traceTypes: [], 38 | mode: 'layout', 39 | }; 40 | 41 | export {LayoutPanel, LayoutSection, TraceTypeSection}; 42 | -------------------------------------------------------------------------------- /src/components/containers/index.js: -------------------------------------------------------------------------------- 1 | import AnnotationAccordion from './AnnotationAccordion'; 2 | import ShapeAccordion from './ShapeAccordion'; 3 | import SliderAccordion from './SliderAccordion'; 4 | import ImageAccordion from './ImageAccordion'; 5 | import UpdateMenuAccordion from './UpdateMenuAccordion'; 6 | import RangeSelectorAccordion from './RangeSelectorAccordion'; 7 | import MapboxLayersAccordion from './MapboxLayersAccordion'; 8 | import AxesFold from './AxesFold'; 9 | import PlotlyFold, {Fold} from './PlotlyFold'; 10 | import MenuPanel from './MenuPanel'; 11 | import PlotlyPanel, {Panel} from './PlotlyPanel'; 12 | import PlotlySection, {Section} from './PlotlySection'; 13 | import PanelEmpty, {PanelMessage} from './PanelEmpty'; 14 | import SubplotAccordion from './SubplotAccordion'; 15 | import TraceAccordion from './TraceAccordion'; 16 | import TransformAccordion from './TransformAccordion'; 17 | import TraceMarkerSection from './TraceMarkerSection'; 18 | import TraceRequiredPanel from './TraceRequiredPanel'; 19 | import SingleSidebarItem from './SingleSidebarItem'; 20 | import ModalProvider from './ModalProvider'; 21 | import Modal from './Modal'; 22 | 23 | export * from './derived'; 24 | export { 25 | AnnotationAccordion, 26 | ShapeAccordion, 27 | SliderAccordion, 28 | ImageAccordion, 29 | UpdateMenuAccordion, 30 | RangeSelectorAccordion, 31 | MapboxLayersAccordion, 32 | MenuPanel, 33 | PlotlyFold, 34 | Fold, 35 | PlotlyPanel, 36 | Panel, 37 | PanelEmpty, 38 | PlotlySection, 39 | Section, 40 | SubplotAccordion, 41 | TraceAccordion, 42 | TransformAccordion, 43 | TraceMarkerSection, 44 | TraceRequiredPanel, 45 | AxesFold, 46 | SingleSidebarItem, 47 | Modal, 48 | ModalProvider, 49 | PanelMessage, 50 | }; 51 | -------------------------------------------------------------------------------- /src/components/fields/ArrowSelector.js: -------------------------------------------------------------------------------- 1 | import Dropdown from './Dropdown'; 2 | import React from 'react'; 3 | import ARROW_PATHS from 'plotly.js/src/components/annotations/arrow_paths'; 4 | 5 | const ARROW_OPTIONS = ARROW_PATHS.map(({path}, index) => { 6 | const label = ( 7 | 8 | 17 | 22 | 23 | ); 24 | 25 | return { 26 | label, 27 | value: index, 28 | key: 'arrow' + index, 29 | }; 30 | }); 31 | 32 | const ArrowSelector = (props) => { 33 | return ; 34 | }; 35 | 36 | ArrowSelector.propTypes = { 37 | ...Dropdown.propTypes, 38 | }; 39 | 40 | ArrowSelector.defaultProps = { 41 | clearable: false, 42 | }; 43 | 44 | export default ArrowSelector; 45 | -------------------------------------------------------------------------------- /src/components/fields/AxesSelector.js: -------------------------------------------------------------------------------- 1 | import Field from './Field'; 2 | import PropTypes from 'prop-types'; 3 | import Dropdown from '../widgets/Dropdown'; 4 | import RadioBlocks from '../widgets/RadioBlocks'; 5 | import React, {Component} from 'react'; 6 | import {getParsedTemplateString} from 'lib'; 7 | 8 | class AxesSelector extends Component { 9 | constructor(props, context) { 10 | super(props, context); 11 | const {localize: _} = context; 12 | 13 | if (!context.axesTargetHandler) { 14 | throw new Error(_('AxesSelector must be nested within a connectAxesToPlot component')); 15 | } 16 | } 17 | 18 | render() { 19 | const {axesTargetHandler, axesTarget, fullLayout, localize: _} = this.context; 20 | const {axesOptions} = this.props; 21 | const maxCharsThatFitInRadio = 27; 22 | const maxOptions = axesOptions.length > 4; // eslint-disable-line 23 | 24 | const multipleSublots = 25 | fullLayout && 26 | fullLayout._subplots && 27 | Object.values(fullLayout._subplots).some((s) => s.length > 1); 28 | 29 | const options = multipleSublots 30 | ? axesOptions.map((option) => 31 | option.value === 'allaxes' 32 | ? option 33 | : { 34 | label: getParsedTemplateString(option.title, { 35 | meta: fullLayout.meta, 36 | }), 37 | value: option.value, 38 | } 39 | ) 40 | : axesOptions; 41 | 42 | const totalCharsInOptions = 43 | (options && options.map((o) => o.label).reduce((acc, o) => acc + o.length, 0)) || 0; 44 | 45 | return maxOptions || totalCharsInOptions >= maxCharsThatFitInRadio ? ( 46 | 47 | 53 | 54 | ) : ( 55 | 56 | 61 | 62 | ); 63 | } 64 | } 65 | 66 | AxesSelector.contextTypes = { 67 | axesTargetHandler: PropTypes.func, 68 | axesTarget: PropTypes.string, 69 | fullLayout: PropTypes.object, 70 | localize: PropTypes.func, 71 | }; 72 | 73 | AxesSelector.propTypes = { 74 | axesOptions: PropTypes.array, 75 | }; 76 | 77 | export default AxesSelector; 78 | -------------------------------------------------------------------------------- /src/components/fields/AxisRangeValue.js: -------------------------------------------------------------------------------- 1 | import Field from './Field'; 2 | import {UnconnectedNumeric} from './Numeric'; 3 | import {UnconnectedDateTimePicker} from './DateTimePicker'; 4 | import PropTypes from 'prop-types'; 5 | import React, {Component} from 'react'; 6 | import {connectToContainer} from 'lib'; 7 | import Info from './Info'; 8 | import {MULTI_VALUED} from 'lib/constants'; 9 | 10 | export class UnconnectedAxisRangeValue extends Component { 11 | render() { 12 | // only when all axes have the type date, can we output an UnconnectedDateTimePicker 13 | if (this.props.fullContainer && this.props.fullContainer.type === 'date') { 14 | return ; 15 | } 16 | // If its multivalued, it can be multivalued for different reasons: 17 | // - the range is different, but same type 18 | // - the type is different (i.e. date + number axes) 19 | // If we're in the case of a mixed axis type (i.e. date + number) case, 20 | // There's going to be a this.props.fullContainer.type, but it's going to be MULTIVALUED 21 | if (this.props.multiValued && this.props.fullContainer.type === MULTI_VALUED) { 22 | return ; 23 | } 24 | 25 | // For cases that the range is numeric, but does not have the same number 26 | // Or numeric and has the same number 27 | return ; 28 | } 29 | } 30 | 31 | UnconnectedAxisRangeValue.propTypes = { 32 | defaultValue: PropTypes.any, 33 | fullValue: PropTypes.any, 34 | min: PropTypes.number, 35 | max: PropTypes.number, 36 | multiValued: PropTypes.bool, 37 | hideArrows: PropTypes.bool, 38 | showSlider: PropTypes.bool, 39 | step: PropTypes.number, 40 | fullContainer: PropTypes.object, 41 | updatePlot: PropTypes.func, 42 | ...Field.propTypes, 43 | }; 44 | 45 | export default connectToContainer(UnconnectedAxisRangeValue); 46 | -------------------------------------------------------------------------------- /src/components/fields/ColorPicker.js: -------------------------------------------------------------------------------- 1 | import ColorPickerWidget from '../widgets/ColorPicker'; 2 | import Field from './Field'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | export class UnconnectedColorPicker extends Component { 8 | constructor(props, context) { 9 | super(props, context); 10 | this.state = { 11 | empty: !this.props.fullValue && this.props.handleEmpty, 12 | }; 13 | } 14 | 15 | render() { 16 | const {localize: _} = this.context; 17 | 18 | if (this.state.empty) { 19 | return ( 20 | 21 |
22 | {_('This color is computed from other parts of the figure but you can')}{' '} 23 | { 25 | this.setState({empty: false}); 26 | this.props.updatePlot(this.props.defaultColor); 27 | }} 28 | > 29 | {_('override it')} 30 | 31 | . 32 |
33 |
34 | ); 35 | } 36 | 37 | return ( 38 | 39 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | UnconnectedColorPicker.propTypes = { 49 | fullValue: PropTypes.any, 50 | updatePlot: PropTypes.func, 51 | handleEmpty: PropTypes.bool, 52 | defaultColor: PropTypes.string, 53 | ...Field.propTypes, 54 | }; 55 | 56 | UnconnectedColorPicker.contextTypes = { 57 | localize: PropTypes.func, 58 | }; 59 | 60 | UnconnectedColorPicker.displayName = 'UnconnectedColorPicker'; 61 | 62 | export default connectToContainer(UnconnectedColorPicker); 63 | -------------------------------------------------------------------------------- /src/components/fields/ColorscalePicker.js: -------------------------------------------------------------------------------- 1 | import ColorscalePickerWidget from '../widgets/ColorscalePicker'; 2 | import Field from './Field'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | import {EDITOR_ACTIONS} from 'lib/constants'; 7 | 8 | export class UnconnectedColorscalePicker extends Component { 9 | constructor() { 10 | super(); 11 | this.onUpdate = this.onUpdate.bind(this); 12 | } 13 | 14 | onUpdate(colorscale, colorscaleType) { 15 | if (Array.isArray(colorscale)) { 16 | this.props.updatePlot( 17 | colorscale.map((c, i) => { 18 | let step = i / (colorscale.length - 1); 19 | if (i === 0) { 20 | step = 0; 21 | } 22 | return [step, c]; 23 | }), 24 | colorscaleType 25 | ); 26 | this.context.onUpdate({ 27 | type: EDITOR_ACTIONS.UPDATE_TRACES, 28 | payload: { 29 | update: {autocolorscale: false}, 30 | traceIndexes: [this.props.fullContainer.index], 31 | }, 32 | }); 33 | } 34 | } 35 | 36 | render() { 37 | const {fullValue} = this.props; 38 | const colorscale = Array.isArray(fullValue) ? fullValue.map((v) => v[1]) : null; 39 | 40 | return ( 41 | 42 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | UnconnectedColorscalePicker.propTypes = { 54 | labelWidth: PropTypes.number, 55 | fullValue: PropTypes.any, 56 | fullContainer: PropTypes.object, 57 | updatePlot: PropTypes.func, 58 | initialCategory: PropTypes.string, 59 | ...Field.propTypes, 60 | }; 61 | 62 | UnconnectedColorscalePicker.contextTypes = { 63 | container: PropTypes.object, 64 | graphDiv: PropTypes.object, 65 | onUpdate: PropTypes.func, 66 | }; 67 | 68 | UnconnectedColorscalePicker.displayName = 'UnconnectedColorscalePicker'; 69 | 70 | export default connectToContainer(UnconnectedColorscalePicker); 71 | -------------------------------------------------------------------------------- /src/components/fields/ColorwayPicker.js: -------------------------------------------------------------------------------- 1 | import ColorscalePickerWidget from '../widgets/ColorscalePicker'; 2 | import Field from './Field'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | class UnconnectedColorwayPicker extends Component { 8 | render() { 9 | return ( 10 | 11 | 17 | 18 | ); 19 | } 20 | } 21 | 22 | UnconnectedColorwayPicker.propTypes = { 23 | fullValue: PropTypes.any, 24 | updatePlot: PropTypes.func, 25 | ...Field.propTypes, 26 | }; 27 | 28 | UnconnectedColorwayPicker.displayName = 'UnconnectedColorwayPicker'; 29 | 30 | export default connectToContainer(UnconnectedColorwayPicker); 31 | -------------------------------------------------------------------------------- /src/components/fields/DateTimePicker.js: -------------------------------------------------------------------------------- 1 | import Field from './Field'; 2 | import Picker from '../widgets/DateTimePicker'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | export class UnconnectedDateTimePicker extends Component { 8 | render() { 9 | return ( 10 | 11 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | UnconnectedDateTimePicker.propTypes = { 22 | fullValue: PropTypes.string, 23 | updatePlot: PropTypes.func, 24 | placeholder: PropTypes.string, 25 | ...Field.propTypes, 26 | }; 27 | 28 | UnconnectedDateTimePicker.displayName = 'UnconnectedDateTimePicker'; 29 | 30 | export default connectToContainer(UnconnectedDateTimePicker); 31 | -------------------------------------------------------------------------------- /src/components/fields/Dropdown.js: -------------------------------------------------------------------------------- 1 | import DropdownWidget from '../widgets/Dropdown'; 2 | import Field from './Field'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | export class UnconnectedDropdown extends Component { 8 | render() { 9 | let placeholder; 10 | if (this.props.multiValued) { 11 | placeholder = this.props.fullValue; 12 | } 13 | 14 | return ( 15 | 16 | 26 | 27 | ); 28 | } 29 | } 30 | 31 | UnconnectedDropdown.propTypes = { 32 | backgroundDark: PropTypes.bool, 33 | components: PropTypes.object, 34 | clearable: PropTypes.bool, 35 | fullValue: PropTypes.any, 36 | options: PropTypes.array.isRequired, 37 | updatePlot: PropTypes.func, 38 | disabled: PropTypes.bool, 39 | ...Field.propTypes, 40 | }; 41 | 42 | UnconnectedDropdown.displayName = 'UnconnectedDropdown'; 43 | 44 | export default connectToContainer(UnconnectedDropdown); 45 | -------------------------------------------------------------------------------- /src/components/fields/Dropzone.js: -------------------------------------------------------------------------------- 1 | import Drop from '../widgets/Dropzone'; 2 | import Field from './Field'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | export class UnconnectedDropzone extends Component { 8 | render() { 9 | return ( 10 | 11 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | UnconnectedDropzone.propTypes = { 22 | value: PropTypes.any, 23 | onUpdate: PropTypes.func, 24 | ...Field.propTypes, 25 | }; 26 | 27 | UnconnectedDropzone.displayName = 'UnconnectedDropzone'; 28 | 29 | function modifyPlotProps(props, context, plotProps) { 30 | if (context.container.type === 'choroplethmapbox' || context.container.type === 'choropleth') { 31 | plotProps.isVisible = true; 32 | } 33 | } 34 | 35 | export default connectToContainer(UnconnectedDropzone, {modifyPlotProps}); 36 | -------------------------------------------------------------------------------- /src/components/fields/Flaglist.js: -------------------------------------------------------------------------------- 1 | import Field from './Field'; 2 | import FlaglistCheckboxGroup from '../widgets/FlaglistCheckboxGroup'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | export class UnconnectedFlaglist extends Component { 8 | render() { 9 | return ( 10 | 11 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | UnconnectedFlaglist.propTypes = { 22 | fullValue: PropTypes.any, 23 | options: PropTypes.array.isRequired, 24 | updatePlot: PropTypes.func, 25 | ...Field.propTypes, 26 | }; 27 | 28 | UnconnectedFlaglist.displayName = 'UnconnectedFlaglist'; 29 | 30 | export default connectToContainer(UnconnectedFlaglist); 31 | -------------------------------------------------------------------------------- /src/components/fields/FontSelector.js: -------------------------------------------------------------------------------- 1 | import Dropdown from './Dropdown'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const FontSelector = (props, context) => ( 6 | ({ 9 | label: {label}, 10 | value, 11 | }))} 12 | /> 13 | ); 14 | 15 | FontSelector.propTypes = { 16 | ...Dropdown.propTypes, 17 | }; 18 | 19 | FontSelector.defaultProps = {clearable: false}; 20 | 21 | FontSelector.contextTypes = { 22 | fontOptions: PropTypes.array, 23 | }; 24 | 25 | export default FontSelector; 26 | -------------------------------------------------------------------------------- /src/components/fields/Info.js: -------------------------------------------------------------------------------- 1 | import Field from './Field'; 2 | import React, {Component} from 'react'; 3 | 4 | export default class Info extends Component { 5 | render() { 6 | return ( 7 | 8 |
9 | {this.props.children} 10 |
11 |
12 | ); 13 | } 14 | } 15 | 16 | Info.plotly_editor_traits = { 17 | no_visibility_forcing: true, 18 | }; 19 | 20 | Info.propTypes = { 21 | ...Field.propTypes, 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/fields/Numeric.js: -------------------------------------------------------------------------------- 1 | import Field from './Field'; 2 | import NumericInput from '../widgets/NumericInput'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | export class UnconnectedNumeric extends Component { 8 | render() { 9 | let fullValue = this.props.fullValue; 10 | let placeholder; 11 | if (this.props.multiValued) { 12 | placeholder = fullValue; 13 | fullValue = ''; 14 | } 15 | 16 | return ( 17 | 18 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | UnconnectedNumeric.propTypes = { 37 | defaultValue: PropTypes.any, 38 | fullValue: PropTypes.any, 39 | min: PropTypes.number, 40 | max: PropTypes.number, 41 | multiValued: PropTypes.bool, 42 | hideArrows: PropTypes.bool, 43 | showSlider: PropTypes.bool, 44 | step: PropTypes.number, 45 | stepmode: PropTypes.string, 46 | updatePlot: PropTypes.func, 47 | ...Field.propTypes, 48 | }; 49 | 50 | UnconnectedNumeric.displayName = 'UnconnectedNumeric'; 51 | 52 | export default connectToContainer(UnconnectedNumeric); 53 | -------------------------------------------------------------------------------- /src/components/fields/NumericOrDate.js: -------------------------------------------------------------------------------- 1 | import Field from './Field'; 2 | import {UnconnectedNumeric} from './Numeric'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | import {isDateTime} from 'plotly.js/src/lib'; 7 | import {isJSDate} from 'plotly.js/src/lib/dates'; 8 | import {UnconnectedDateTimePicker} from './DateTimePicker'; 9 | 10 | export class UnconnectedNumericOrDate extends Component { 11 | render() { 12 | const date = typeof this.props.fullValue === 'string' && this.props.fullValue.split(' ')[0]; 13 | const fullValueIsDate = 14 | typeof this.props.fullValue === 'string' && date && (isDateTime(date) || isJSDate(date)); 15 | 16 | return fullValueIsDate ? ( 17 | 18 | ) : ( 19 | 20 | ); 21 | } 22 | } 23 | 24 | UnconnectedNumericOrDate.propTypes = { 25 | defaultValue: PropTypes.any, 26 | fullValue: PropTypes.any, 27 | min: PropTypes.number, 28 | max: PropTypes.number, 29 | multiValued: PropTypes.bool, 30 | hideArrows: PropTypes.bool, 31 | showSlider: PropTypes.bool, 32 | step: PropTypes.number, 33 | fullContainer: PropTypes.object, 34 | updatePlot: PropTypes.func, 35 | ...Field.propTypes, 36 | }; 37 | 38 | UnconnectedNumericOrDate.displayName = 'UnconnectedNumericOrDate'; 39 | 40 | export default connectToContainer(UnconnectedNumericOrDate); 41 | -------------------------------------------------------------------------------- /src/components/fields/PieColorscalePicker.js: -------------------------------------------------------------------------------- 1 | import ColorscalePickerWidget from '../widgets/ColorscalePicker'; 2 | import Field from './Field'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer, adjustColorscale} from 'lib'; 6 | 7 | class UnconnectedPieColorscalePicker extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.onUpdate = this.onUpdate.bind(this); 11 | } 12 | 13 | onUpdate(colorscale, colorscaleType) { 14 | if (Array.isArray(colorscale)) { 15 | const numPieSlices = this.context.graphDiv.calcdata[0].length + 1; 16 | const adjustedColorscale = adjustColorscale(colorscale, numPieSlices, colorscaleType, { 17 | repeat: true, 18 | }); 19 | this.props.updatePlot(adjustedColorscale); 20 | } 21 | } 22 | 23 | render() { 24 | const {fullValue} = this.props; 25 | const colorscale = Array.isArray(fullValue) ? fullValue : null; 26 | 27 | return ( 28 | 29 | 34 | 35 | ); 36 | } 37 | } 38 | 39 | UnconnectedPieColorscalePicker.propTypes = { 40 | fullValue: PropTypes.any, 41 | updatePlot: PropTypes.func, 42 | ...Field.propTypes, 43 | }; 44 | 45 | UnconnectedPieColorscalePicker.contextTypes = { 46 | container: PropTypes.object, 47 | graphDiv: PropTypes.object, 48 | }; 49 | 50 | UnconnectedPieColorscalePicker.displayName = 'UnconnectedPieColorscalePicker'; 51 | 52 | export default connectToContainer(UnconnectedPieColorscalePicker, { 53 | modifyPlotProps: (props, context, plotProps) => { 54 | if ( 55 | context && 56 | context.container && 57 | context.graphDiv && 58 | (!plotProps.fullValue || 59 | (Array.isArray(plotProps.fullValue) && !plotProps.fullValue.length)) && 60 | context.graphDiv.calcdata 61 | ) { 62 | plotProps.fullValue = context.graphDiv.calcdata[0].map((d) => d.color); 63 | } 64 | 65 | if (context.traceIndexes.length > 1) { 66 | plotProps.isVisible = false; 67 | } 68 | }, 69 | }); 70 | -------------------------------------------------------------------------------- /src/components/fields/Radio.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | import RadioBlocks from '../widgets/RadioBlocks'; 4 | import Field from './Field'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | export class UnconnectedRadio extends Component { 8 | render() { 9 | return ( 10 | 11 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | UnconnectedRadio.propTypes = { 22 | center: PropTypes.bool, 23 | fullValue: PropTypes.any, 24 | options: PropTypes.array.isRequired, 25 | updatePlot: PropTypes.func, 26 | ...Field.propTypes, 27 | }; 28 | 29 | // for better appearance overrides {center: false} 30 | // default prop. This can be overridden manually using props for . 31 | UnconnectedRadio.defaultProps = { 32 | center: true, 33 | }; 34 | 35 | UnconnectedRadio.displayName = 'UnconnectedRadio'; 36 | 37 | export default connectToContainer(UnconnectedRadio); 38 | -------------------------------------------------------------------------------- /src/components/fields/Text.js: -------------------------------------------------------------------------------- 1 | import Field from './Field'; 2 | import TextInput from '../widgets/TextInput'; 3 | import PropTypes from 'prop-types'; 4 | import React, {Component} from 'react'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | export class UnconnectedText extends Component { 8 | render() { 9 | let fullValue = this.props.fullValue; 10 | let placeholder; 11 | if (this.props.multiValued) { 12 | placeholder = fullValue; 13 | fullValue = ''; 14 | } 15 | 16 | return ( 17 | 18 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | UnconnectedText.propTypes = { 31 | defaultValue: PropTypes.any, 32 | fullValue: PropTypes.any, 33 | multiValued: PropTypes.bool, 34 | updatePlot: PropTypes.func, 35 | onChange: PropTypes.func, 36 | ...Field.propTypes, 37 | }; 38 | 39 | UnconnectedText.displayName = 'UnconnectedText'; 40 | 41 | export default connectToContainer(UnconnectedText); 42 | -------------------------------------------------------------------------------- /src/components/fields/UpdateMenuButtons.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | import {Dropdown, TextEditor} from '../index'; 4 | import Field from './Field'; 5 | import {connectToContainer} from 'lib'; 6 | 7 | class UpdateMenuButtons extends Component { 8 | constructor(props, context) { 9 | super(props, context); 10 | this.state = { 11 | currentButtonIndex: 0, 12 | }; 13 | } 14 | 15 | renderDropdown() { 16 | const _ = this.context.localize; 17 | const options = this.props.fullValue.map((button, index) => { 18 | return {label: _('Button') + ` ${index + 1}`, value: index}; 19 | }); 20 | return ( 21 | this.setState({currentButtonIndex: index})} 26 | clearable={false} 27 | fullValue={this.state.currentButtonIndex} 28 | /> 29 | ); 30 | } 31 | 32 | render() { 33 | return ( 34 | 35 | {this.renderDropdown()} 36 | 37 | 38 | ); 39 | } 40 | } 41 | 42 | UpdateMenuButtons.propTypes = { 43 | attr: PropTypes.string, 44 | fullValue: PropTypes.array, 45 | updatePlot: PropTypes.func, 46 | }; 47 | 48 | UpdateMenuButtons.contextTypes = { 49 | localize: PropTypes.func, 50 | }; 51 | 52 | export default connectToContainer(UpdateMenuButtons); 53 | -------------------------------------------------------------------------------- /src/components/fields/__tests__/ArrowSelector-test.js: -------------------------------------------------------------------------------- 1 | import ArrowSelector from '../ArrowSelector'; 2 | import Dropdown from '../Dropdown'; 3 | import React from 'react'; 4 | import {shallow} from 'lib/test-utils'; 5 | 6 | describe('', () => { 7 | // test mostly an insurance policy against plotly.js changing arrow_paths on us. 8 | it('pulls arrow_paths from plotly.js and sets as options', () => { 9 | const minNumberOfArrowsExpected = 4; 10 | const options = shallow() 11 | .find(Dropdown) 12 | .prop('options'); 13 | expect(options.length > minNumberOfArrowsExpected).toBe(true); 14 | 15 | // make sure path info is being destructured 16 | const innerPath = options[3].label.props.children[1]; 17 | expect(innerPath.type).toBe('path'); 18 | expect(innerPath.props.d.startsWith('M-')).toBe(true); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/fields/__tests__/Radio-test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Field from '../Field'; 3 | import Radio from '../Radio'; 4 | import {PlotlySection} from '../../containers'; 5 | import {TestEditor, fixtures, plotly} from 'lib/test-utils'; 6 | import {connectTraceToPlot} from 'lib'; 7 | import {mount} from 'enzyme'; 8 | 9 | const Trace = connectTraceToPlot(PlotlySection); 10 | 11 | describe('', () => { 12 | it('enables centering by default', () => { 13 | const wrapper = mount( 14 | 15 | 16 | 24 | 25 | 26 | ).find(Field); 27 | 28 | expect(wrapper.prop('center')).toBe(true); 29 | }); 30 | 31 | it('permits centering to be disabled', () => { 32 | const wrapper = mount( 33 | 34 | 35 | 44 | 45 | 46 | ).find(Field); 47 | 48 | expect(wrapper.prop('center')).toBe(false); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import {Button} from './widgets'; 2 | import PanelMenuWrapper from './PanelMenuWrapper'; 3 | 4 | export {Button, PanelMenuWrapper}; 5 | 6 | export * from './fields'; 7 | export * from './containers'; 8 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarGroup.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | import {bem} from 'lib'; 4 | import {AngleRightIcon} from 'plotly-icons'; 5 | import SidebarItem from './SidebarItem'; 6 | 7 | export default class SidebarGroup extends Component { 8 | constructor(props) { 9 | super(props); 10 | 11 | this.state = { 12 | expanded: this.props.group === this.props.selectedGroup, 13 | }; 14 | 15 | this.toggleExpanded = this.toggleExpanded.bind(this); 16 | this.onChangeGroup = this.onChangeGroup.bind(this); 17 | this.renderSubItem = this.renderSubItem.bind(this); 18 | } 19 | 20 | toggleExpanded() { 21 | this.setState({expanded: !this.state.expanded}); 22 | } 23 | 24 | onChangeGroup(panel) { 25 | this.props.onChangeGroup(this.props.group, panel); 26 | } 27 | 28 | renderSubItem(panel, i) { 29 | const isActive = 30 | this.props.selectedPanel === panel && this.props.group === this.props.selectedGroup; 31 | 32 | return ( 33 | this.onChangeGroup(panel)} 37 | label={panel} 38 | /> 39 | ); 40 | } 41 | 42 | render() { 43 | const {group, panels, selectedGroup} = this.props; 44 | const {expanded} = this.state; 45 | return ( 46 |
52 |
53 |
54 | 55 |
56 |
{group}
57 |
58 | {expanded && panels.map(this.renderSubItem)} 59 |
60 | ); 61 | } 62 | } 63 | 64 | SidebarGroup.propTypes = { 65 | group: PropTypes.string, 66 | onChangeGroup: PropTypes.func, 67 | panels: PropTypes.array, 68 | selectedGroup: PropTypes.string, 69 | selectedPanel: PropTypes.string, 70 | }; 71 | -------------------------------------------------------------------------------- /src/components/sidebar/SidebarItem.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | import {bem} from 'lib'; 4 | 5 | export default class SidebarItem extends Component { 6 | render() { 7 | const {onClick, label, active} = this.props; 8 | return ( 9 |
10 |
11 |
{label}
12 |
13 |
14 | ); 15 | } 16 | } 17 | 18 | SidebarItem.propTypes = { 19 | active: PropTypes.bool, 20 | label: PropTypes.string, 21 | onClick: PropTypes.func, 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/widgets/Button.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | import {bem} from 'lib'; 4 | 5 | class Button extends Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | const {children, className, icon, label, variant, ...rest} = this.props; 12 | 13 | let classes = `button`; 14 | 15 | if (variant) { 16 | classes += ` button--${variant}`; 17 | } else { 18 | classes += ` button--default`; 19 | } 20 | 21 | if (className) { 22 | classes += ` ${className}`; 23 | } 24 | 25 | const Icon = icon ?
{icon}
: null; 26 | 27 | return ( 28 | 34 | ); 35 | } 36 | } 37 | 38 | Button.propTypes = { 39 | children: PropTypes.node, 40 | className: PropTypes.any, 41 | icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), 42 | label: PropTypes.any, 43 | variant: PropTypes.string, 44 | }; 45 | 46 | export default Button; 47 | -------------------------------------------------------------------------------- /src/components/widgets/CheckboxGroup.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import {CheckIcon} from 'plotly-icons'; 5 | 6 | class CheckboxGroup extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = {options: this.props.options}; 10 | this.handleChange = this.handleChange.bind(this); 11 | } 12 | 13 | UNSAFE_componentWillReceiveProps(nextProps) { 14 | this.setState({options: nextProps.options}); 15 | } 16 | 17 | handleChange(i) { 18 | var newOptions = this.props.options.slice(); 19 | newOptions[i] = Object.assign(newOptions[i], { 20 | checked: !newOptions[i].checked, 21 | }); 22 | this.props.onChange(newOptions); 23 | } 24 | 25 | renderOptions() { 26 | return this.state.options.map((option, i) => { 27 | const checkClass = classnames(['checkbox__check', 'icon'], { 28 | 'checkbox__check--checked': option.checked, 29 | }); 30 | 31 | const itemClass = classnames('checkbox__item', { 32 | 'checkbox__item--vertical': this.props.orientation === 'vertical', 33 | 'checkbox__item--horizontal': this.props.orientation === 'horizontal', 34 | }); 35 | 36 | return ( 37 |
38 |
this.handleChange(i)} 41 | > 42 | {option.checked && ( 43 |
44 | 45 |
46 | )} 47 |
48 |
this.handleChange(i)}> 49 | {option.label} 50 |
51 |
52 | ); 53 | }); 54 | } 55 | 56 | render() { 57 | const boxClass = classnames('checkbox__group', this.props.className, { 58 | checkbox__group_horizontal: this.props.orientation === 'horizontal', 59 | }); 60 | 61 | return
{this.renderOptions()}
; 62 | } 63 | } 64 | 65 | CheckboxGroup.propTypes = { 66 | options: PropTypes.arrayOf( 67 | PropTypes.shape({ 68 | label: PropTypes.string.isRequired, 69 | value: PropTypes.string.isRequired, 70 | checked: PropTypes.bool.isRequired, 71 | }) 72 | ).isRequired, 73 | onChange: PropTypes.func, 74 | className: PropTypes.string, 75 | orientation: PropTypes.string, 76 | }; 77 | 78 | CheckboxGroup.defaultProps = { 79 | className: '', 80 | }; 81 | 82 | export default CheckboxGroup; 83 | -------------------------------------------------------------------------------- /src/components/widgets/Logo.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | import React, {Component} from 'react'; 3 | 4 | export default class Logo extends Component { 5 | render() { 6 | const {link, src} = this.props; 7 | const image = ; 8 | return link ? {image} : image; 9 | } 10 | } 11 | 12 | Logo.plotly_editor_traits = {sidebar_element: true}; 13 | 14 | Logo.propTypes = { 15 | src: PropTypes.string, 16 | link: PropTypes.string, 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/widgets/RadioBlocks.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | 5 | class RadioBlocks extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = {activeOption: this.props.activeOption}; 9 | this.handleChange = this.handleChange.bind(this); 10 | this.renderOption = this.renderOption.bind(this); 11 | } 12 | 13 | UNSAFE_componentWillReceiveProps(nextProps) { 14 | // Reset the value to the graph's actual value 15 | if (nextProps.activeOption !== this.state.activeOption) { 16 | this.setState({ 17 | activeOption: nextProps.activeOption, 18 | }); 19 | } 20 | } 21 | 22 | handleChange(newValue) { 23 | this.setState({activeOption: newValue}); 24 | this.props.onOptionChange(newValue); 25 | } 26 | 27 | renderOption(optionName) { 28 | const {label, value, icon: Icon} = optionName; 29 | const defaultActive = this.state.activeOption === value; 30 | 31 | const optionClass = classnames('radio-block__option', { 32 | 'radio-block__option--active': defaultActive, 33 | }); 34 | 35 | return ( 36 |
this.handleChange(value)}> 37 | {Icon ? : null} 38 | {label ? {label} : null} 39 |
40 | ); 41 | } 42 | 43 | render() { 44 | const optionList = this.props.options.map(this.renderOption); 45 | 46 | const groupClass = classnames('radio-block', 'radio-block__group', { 47 | 'radio-block__group--center': this.props.alignment === 'center', 48 | }); 49 | 50 | return
{optionList}
; 51 | } 52 | } 53 | 54 | RadioBlocks.propTypes = { 55 | options: PropTypes.arrayOf( 56 | PropTypes.shape({ 57 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]).isRequired, 58 | label: PropTypes.string, 59 | icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), 60 | disabled: PropTypes.bool, 61 | }) 62 | ), 63 | onOptionChange: PropTypes.func.isRequired, 64 | activeOption: PropTypes.oneOfType([PropTypes.string, PropTypes.bool, PropTypes.number]), 65 | radioClassName: PropTypes.string, 66 | 67 | // One of right, left, center 68 | alignment: PropTypes.string, 69 | }; 70 | 71 | export default RadioBlocks; 72 | -------------------------------------------------------------------------------- /src/components/widgets/TextArea.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | export default class TextArea extends Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | value: this.props.value, 10 | }; 11 | 12 | this.onChange = this.onChange.bind(this); 13 | } 14 | 15 | UNSAFE_componentWillReceiveProps(nextProps) { 16 | // Reset the value to the graph's actual value 17 | if (nextProps.value !== this.state.value) { 18 | this.setState({ 19 | value: nextProps.value, 20 | }); 21 | } 22 | } 23 | 24 | onChange(e) { 25 | const newValue = e.target.value; 26 | this.setState({value: newValue}); 27 | this.props.onChange(newValue); 28 | } 29 | 30 | render() { 31 | return ( 32 | 33 |