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 |
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 |
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 |
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 |
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 |
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 |
41 |
42 | );
43 | }
44 | }
45 |
46 | TextArea.propTypes = {
47 | value: PropTypes.string.isRequired,
48 | onChange: PropTypes.func.isRequired,
49 | placeholder: PropTypes.string.isRequired,
50 | visibleRows: PropTypes.number,
51 | areaWidth: PropTypes.number,
52 | textareaClass: PropTypes.string,
53 | };
54 |
55 | TextArea.defaultProps = {
56 | visibleRows: 10,
57 | areaWidth: 30,
58 | };
59 |
--------------------------------------------------------------------------------
/src/components/widgets/TextInput.js:
--------------------------------------------------------------------------------
1 | import EditableText from './EditableText';
2 | import React, {Component} from 'react';
3 | import PropTypes from 'prop-types';
4 |
5 | export default class TextInput extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {value: props.value};
10 | }
11 |
12 | UNSAFE_componentWillReceiveProps(nextProps) {
13 | if (nextProps.value !== this.state.value) {
14 | this.setState({value: nextProps.value});
15 | }
16 | }
17 |
18 | render() {
19 | return (
20 | {
26 | if (this.props.onChange) {
27 | this.props.onChange(value);
28 | }
29 | this.setState({value});
30 | }}
31 | onUpdate={this.props.onUpdate}
32 | />
33 | );
34 | }
35 | }
36 |
37 | TextInput.propTypes = {
38 | defaultValue: PropTypes.any,
39 | editableClassName: PropTypes.string,
40 | onUpdate: PropTypes.func.isRequired,
41 | onChange: PropTypes.func,
42 | placeholder: PropTypes.string,
43 | value: PropTypes.any,
44 | };
45 |
--------------------------------------------------------------------------------
/src/components/widgets/index.js:
--------------------------------------------------------------------------------
1 | import Button from './Button';
2 | import RadioBlocks from './RadioBlocks';
3 | import DateTimePicker from './DateTimePicker';
4 | import TraceTypeSelector, {TraceTypeSelectorButton} from './TraceTypeSelector';
5 |
6 | export {Button, RadioBlocks, TraceTypeSelector, TraceTypeSelectorButton, DateTimePicker};
7 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/HTML.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import TextArea from '../TextArea';
4 |
5 | class HTML extends TextArea {
6 | render() {
7 | const {className} = this.props;
8 | const editorClassNames = className ? className : 'text-editor__html';
9 | return (
10 |
16 | );
17 | }
18 | }
19 |
20 | HTML.propTypes = {
21 | className: PropTypes.string,
22 | };
23 |
24 | HTML.defaultProps = {
25 | placeholder: '',
26 | };
27 |
28 | export default HTML;
29 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/LaTeX.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TextArea from '../TextArea';
3 | import PropTypes from 'prop-types';
4 |
5 | import {isLaTeXExpr as isWrapped} from './convertFormats';
6 |
7 | export default class LaTeX extends TextArea {
8 | constructor(props) {
9 | super(props);
10 |
11 | // Internally, represesent the LaTeX document without the
12 | // wrapping `$...$` characters.
13 | const unwrappedValue = this.unwrap(props.value);
14 |
15 | this.state = {
16 | value: unwrappedValue,
17 | };
18 | this.onChange = this.onChange.bind(this);
19 | this.onBlur = this.onBlur.bind(this);
20 | }
21 |
22 | UNSAFE_componentWillReceiveProps(nextProps) {
23 | const unwrappedNextValue = this.unwrap(nextProps.value);
24 |
25 | if (unwrappedNextValue !== this.state.value) {
26 | this.setState({
27 | value: unwrappedNextValue,
28 | });
29 | }
30 | }
31 |
32 | // Return a new value with wrapping `$...$` removed.
33 | unwrap(value) {
34 | if (isWrapped(value)) {
35 | return value.substr(1, value.length - 2);
36 | }
37 |
38 | return value;
39 | }
40 |
41 | // Wrap value in `$...$`.
42 | wrap(value) {
43 | if (!isWrapped(value)) {
44 | return `$${value}$`;
45 | }
46 |
47 | return value;
48 | }
49 |
50 | onChange(e) {
51 | this.setState({
52 | value: e.target.value,
53 | });
54 | }
55 |
56 | onBlur(e) {
57 | const value = this.wrap(e.target.value);
58 | this.props.onChange(value);
59 | }
60 |
61 | render() {
62 | const {className} = this.props;
63 | const editorClassNames = className ? className : 'text-editor__latex';
64 | return (
65 |
72 | );
73 | }
74 | }
75 |
76 | LaTeX.propTypes = {
77 | className: PropTypes.string,
78 | onChange: PropTypes.func.isRequired,
79 | placeholder: PropTypes.string,
80 | value: PropTypes.string,
81 | };
82 |
83 | LaTeX.defaultProps = {
84 | value: '',
85 | placeholder: '',
86 | };
87 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/RichText/LinkDecorator.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A DecoratorComponent is used by `draft-js` to render rich content
3 | * beyond inline styles. This Decorator renders LINK entities.
4 | *
5 | * See
6 | * https://facebook.github.io/draft-js/docs/advanced-topics-decorators.html#decorator-components
7 | */
8 |
9 | import React from 'react';
10 | import PropTypes from 'prop-types';
11 |
12 | const LinkDecorator = (props) => {
13 | return (
14 |
15 | {props.children}
16 |
17 | );
18 | };
19 |
20 | LinkDecorator.propTypes = {
21 | style: PropTypes.object.isRequired,
22 | children: PropTypes.oneOfType([PropTypes.array, PropTypes.element]).isRequired,
23 | };
24 |
25 | export default LinkDecorator;
26 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/RichText/StyleButton.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import PropTypes from 'prop-types';
3 | import classnames from 'classnames';
4 |
5 | class StyleButton extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.onToggle = this.onToggle.bind(this);
10 | }
11 |
12 | onToggle(ev) {
13 | // Prevent focus moving from editor to button
14 | ev.preventDefault();
15 | this.props.onToggle(this.props.value);
16 | }
17 |
18 | render() {
19 | const {active, label, value} = this.props;
20 |
21 | const className = classnames(
22 | 'rich-text-editor__styleButton',
23 | `rich-text-editor__styleButton__${value}`,
24 | {
25 | 'rich-text-editor__styleButton--active': active,
26 | }
27 | );
28 |
29 | return (
30 |
31 |
37 | {label}
38 |
39 |
40 | );
41 | }
42 | }
43 |
44 | StyleButton.propTypes = {
45 | active: PropTypes.bool,
46 |
47 | // A (styled) React element to display as label
48 | label: PropTypes.element.isRequired,
49 |
50 | // Callback for clicks
51 | onToggle: PropTypes.func.isRequired,
52 |
53 | // The value passed to `onToggle` when clicked
54 | value: PropTypes.string.isRequired,
55 | };
56 |
57 | export default StyleButton;
58 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/RichText/StyleButtonGroup.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import PropTypes from 'prop-types';
3 | import StyleButton from './StyleButton';
4 | import {LINK} from './configuration';
5 |
6 | class StyleButtonGroup extends Component {
7 | render() {
8 | const {currentStyle, linkIsSelected, styles, onToggle} = this.props;
9 |
10 | const isActive = (currentStyle, value) => {
11 | if (value === LINK) {
12 | return linkIsSelected;
13 | }
14 |
15 | if (typeof currentStyle.has === 'function') {
16 | return currentStyle.has(value);
17 | }
18 |
19 | return Boolean(currentStyle.value);
20 | };
21 |
22 | return (
23 |
24 | {styles.map(({label, value}) => (
25 |
32 | ))}
33 |
34 | );
35 | }
36 | }
37 |
38 | StyleButtonGroup.propTypes = {
39 | onToggle: PropTypes.func.isRequired,
40 | styles: PropTypes.arrayOf(
41 | PropTypes.shape({
42 | label: PropTypes.element.isRequired,
43 | value: PropTypes.string.isRequired,
44 | })
45 | ).isRequired,
46 |
47 | // A draft-js DraftInlineStyle instance
48 | // https://facebook.github.io/draft-js/docs/api-reference-editor-state.html#getcurrentinlinestyle
49 | currentStyle: PropTypes.object,
50 | linkIsSelected: PropTypes.bool,
51 | };
52 |
53 | export default StyleButtonGroup;
54 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/RichText/configuration.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {INLINE_STYLE} from 'draft-js-utils';
3 | import {LinkIcon} from 'plotly-icons';
4 | import {COLORS} from 'lib/constants';
5 |
6 | export const SUPERSCRIPT = 'SUPERSCRIPT';
7 | export const SUBSCRIPT = 'SUBSCRIPT';
8 | export const LINK = 'LINK';
9 |
10 | export const {BOLD, ITALIC} = INLINE_STYLE;
11 |
12 | export const STYLES_TO_HTML_TAGS = {
13 | [BOLD]: {element: 'b'},
14 | [ITALIC]: {element: 'i'},
15 | [SUPERSCRIPT]: {element: 'sup'},
16 | [SUBSCRIPT]: {element: 'sub'},
17 | [LINK]: {element: 'a'},
18 | };
19 |
20 | export const STYLE_MAP = {
21 | [BOLD]: {
22 | fontWeight: 'bolder',
23 | },
24 | [ITALIC]: {
25 | fontStyle: 'italic',
26 | },
27 | [SUBSCRIPT]: {
28 | /*
29 | * Can't use text-align; IE renders `text-bottom` properly, but
30 | * FF doesn't (same height as `bottom`). Chrome doesn't understand
31 | * `text-align: bottom`. Use relative positioning instead.
32 | */
33 | lineHeight: 0,
34 | fontSize: '65%',
35 | position: 'relative',
36 | bottom: '-3px',
37 | },
38 | [SUPERSCRIPT]: {
39 | /*
40 | * Can't use text-align; IE renders `text-top` properly, but
41 | * FF doesn't (same height as `top`). Chrome doesn't understand
42 | * `text-align: top`. Use relative positioning instead.
43 | */
44 | lineHeight: 0,
45 | fontSize: '65%',
46 | position: 'relative',
47 | top: '-5px',
48 | },
49 | [LINK]: {
50 | color: COLORS.editorLink,
51 | linkDecoration: 'none',
52 | cursor: 'pointer',
53 | },
54 | };
55 |
56 | export const INLINE_STYLES = [
57 | {
58 | label: B,
59 | value: BOLD,
60 | },
61 | {
62 | label: I,
63 | value: ITALIC,
64 | },
65 | {
66 | label: (
67 |
68 | x2
69 |
70 | ),
71 | value: SUBSCRIPT,
72 | },
73 | {
74 | label: (
75 |
76 | x2
77 |
78 | ),
79 | value: SUPERSCRIPT,
80 | },
81 | {
82 | label: (
83 |
84 |
85 |
86 | ),
87 | value: LINK,
88 | },
89 | ];
90 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/RichText/debounce.js:
--------------------------------------------------------------------------------
1 | const DEBOUNCE_DELAY = 250;
2 | let timeout;
3 |
4 | function clearTimeout() {
5 | window.clearTimeout(timeout);
6 | timeout = null;
7 | }
8 |
9 | export default function debounce(fn, args) {
10 | if (timeout) {
11 | clearTimeout();
12 | }
13 |
14 | timeout = window.setTimeout(() => {
15 | fn.apply(null, args);
16 | timeout = null;
17 | }, DEBOUNCE_DELAY);
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/RichText/decoratorStrategies.js:
--------------------------------------------------------------------------------
1 | /*
2 | * A decoratorStrategy is used by `draft-js` to determine how to render
3 | * content beyond inline styles. We use them to render LINK entities.
4 | *
5 | * See https://facebook.github.io/draft-js/docs/advanced-topics-decorators.html#content
6 | * and
7 | * https://facebook.github.io/draft-js/docs/advanced-topics-entities.html#content
8 | */
9 | import {Entity} from 'draft-js';
10 |
11 | const characterIsLinkEntity = (character) => {
12 | const entityKey = character.getEntity();
13 |
14 | if (entityKey === null) {
15 | return false;
16 | }
17 |
18 | const entity = Entity.get(entityKey);
19 |
20 | return entity.getType() === 'LINK';
21 | };
22 |
23 | export const findLinkEntities = (contentBlock, callback) => {
24 | contentBlock.findEntityRanges(characterIsLinkEntity, callback);
25 | };
26 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/RichText/getSelectionCoordinates.js:
--------------------------------------------------------------------------------
1 | const getCoordinates = () => {
2 | const coordinates = {x: 0, y: 0};
3 |
4 | const rect = document.getElementsByClassName('text-editor')[0].getBoundingClientRect();
5 |
6 | const LINK_POP_UP_WIDTH = 158.5;
7 |
8 | if (rect) {
9 | // Add to the offset
10 | coordinates.x += rect.width * 0.5 - LINK_POP_UP_WIDTH / 2;
11 | coordinates.y += rect.height * 0.5;
12 | }
13 |
14 | return coordinates;
15 | };
16 |
17 | export default getCoordinates;
18 |
--------------------------------------------------------------------------------
/src/components/widgets/text_editors/convertFormats.js:
--------------------------------------------------------------------------------
1 | const getTextBlockRegExp = () => /\\text\{([^}]*)}/g;
2 |
3 | /**
4 | * To match any character including newline whitespace, use `[\s\S]*`
5 | * instead of `.*`. http://stackoverflow.com/a/1068308
6 | *
7 | * @returns {RegExp} the regular expression
8 | */
9 | const getLaTeXWrappedRegExp = () => /^\$[\s\S]*\$$/;
10 |
11 | const stripHTMLTags = (html) => html.replace(/<[^>]*>/g, '').trim();
12 |
13 | const extractTextBlocks = (laTeX) => {
14 | let matchObj;
15 | let matchStr;
16 | const matches = [];
17 |
18 | // Need to stringify to match literally on `\t`.
19 | const stringifiedLaTeX = JSON.stringify(laTeX);
20 | const regExp = getTextBlockRegExp();
21 |
22 | /**
23 | * Find multiple matches with the ``//g` flag.
24 | * The `RegExp.prototype.exec` API mutates the RegExp object.
25 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec#Finding_successive_matches
26 | */
27 | while ((matchObj = regExp.exec(stringifiedLaTeX)) !== null) {
28 | matchStr = matchObj[1].trim().replace('\\n', '');
29 | matches.push(matchStr);
30 | }
31 |
32 | return matches.join('
');
33 | };
34 |
35 | const wrapLaTeX = (value) => (value ? `$${value}$` : '$$');
36 |
37 | const wrapText = (value) => (value ? `\\text{${value}}` : '\\text{}');
38 |
39 | // Exports
40 | // -------
41 |
42 | export const isLaTeXExpr = (value) => getLaTeXWrappedRegExp().test(value);
43 |
44 | export const hasTextExpression = (laTeX) => {
45 | const regExp = getTextBlockRegExp();
46 | const stringifiedLaTeX = JSON.stringify(laTeX);
47 |
48 | return regExp.test(stringifiedLaTeX);
49 | };
50 |
51 | export const htmlToLaTeX = (html) => {
52 | const breakTag = '
';
53 | const trimmedHTML = html.trim();
54 |
55 | // Handle empty input
56 | if (trimmedHTML === '') {
57 | return wrapLaTeX(wrapText());
58 | }
59 |
60 | // Handle input with only linebreaks
61 | if (trimmedHTML.replace(breakTag, '') === '') {
62 | return wrapLaTeX(wrapText());
63 | }
64 |
65 | return wrapLaTeX(
66 | trimmedHTML
67 | .split(breakTag)
68 |
69 | // Ignore empty linebreaks
70 | .map((para) => (para.length ? wrapText(stripHTMLTags(para)) : ''))
71 | .join('\n')
72 | );
73 | };
74 |
75 | export const laTeXToHTML = (laTeX) => {
76 | const trimmedLaTeX = laTeX.trim();
77 |
78 | return extractTextBlocks(trimmedLaTeX);
79 | };
80 |
--------------------------------------------------------------------------------
/src/default_panels/StyleImagesPanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | ImageAccordion,
5 | Radio,
6 | Dropzone,
7 | PositioningNumeric,
8 | PlotlySection,
9 | PositioningRef,
10 | Dropdown,
11 | } from '../components';
12 |
13 | const StyleImagesPanel = (props, {localize: _}) => (
14 |
15 |
22 |
23 |
24 |
25 |
35 |
43 |
44 |
45 |
46 |
56 |
57 |
58 |
59 |
60 |
61 |
71 |
72 |
73 |
74 |
75 | );
76 |
77 | StyleImagesPanel.contextTypes = {
78 | localize: PropTypes.func,
79 | };
80 |
81 | export default StyleImagesPanel;
82 |
--------------------------------------------------------------------------------
/src/default_panels/StyleShapesPanel.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import {
4 | ShapeAccordion,
5 | Radio,
6 | PlotlySection,
7 | PositioningRef,
8 | PositioningNumeric,
9 | Numeric,
10 | NumericFraction,
11 | ColorPicker,
12 | LineDashSelector,
13 | } from '../components';
14 |
15 | const StyleShapesPanel = (props, {localize: _}) => (
16 |
17 |
24 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | );
55 |
56 | StyleShapesPanel.contextTypes = {
57 | localize: PropTypes.func,
58 | };
59 |
60 | export default StyleShapesPanel;
61 |
--------------------------------------------------------------------------------
/src/default_panels/index.js:
--------------------------------------------------------------------------------
1 | export {default as GraphCreatePanel} from './GraphCreatePanel';
2 | export {default as GraphTransformsPanel} from './GraphTransformsPanel';
3 | export {default as StyleLayoutPanel} from './StyleLayoutPanel';
4 | export {default as StyleAxesPanel} from './StyleAxesPanel';
5 | export {default as StyleMapsPanel} from './StyleMapsPanel';
6 | export {default as StyleLegendPanel} from './StyleLegendPanel';
7 | export {default as StyleNotesPanel} from './StyleNotesPanel';
8 | export {default as StyleShapesPanel} from './StyleShapesPanel';
9 | export {default as StyleSlidersPanel} from './StyleSlidersPanel';
10 | export {default as StyleImagesPanel} from './StyleImagesPanel';
11 | export {default as StyleTracesPanel} from './StyleTracesPanel';
12 | export {default as StyleColorbarsPanel} from './StyleColorbarsPanel';
13 | export {default as StyleUpdateMenusPanel} from './StyleUpdateMenusPanel';
14 | export {default as GraphSubplotsPanel} from './GraphSubplotsPanel';
15 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import PlotlyEditor from './PlotlyEditor';
2 | import DefaultEditor from './DefaultEditor';
3 | import EditorControls from './EditorControls';
4 | import {EDITOR_ACTIONS} from './lib/constants';
5 |
6 | export {DefaultEditor, EditorControls, EDITOR_ACTIONS};
7 |
8 | export * from './lib';
9 | export * from './components';
10 | export * from './default_panels';
11 |
12 | export default PlotlyEditor;
13 |
--------------------------------------------------------------------------------
/src/lib/__tests__/connectAnnotationToLayout-test.js:
--------------------------------------------------------------------------------
1 | import NumericInput from '../../components/widgets/NumericInput';
2 | import React from 'react';
3 | import {Numeric} from '../../components/fields';
4 | import {TestEditor, fixtures, mount} from '../test-utils';
5 | import {connectLayoutToPlot, connectAnnotationToLayout} from '..';
6 |
7 | describe('connectAnnotationToLayout', () => {
8 | it('sends update to layout.annotation[index]', () => {
9 | const beforeUpdateLayout = jest.fn();
10 | const fixture = fixtures.scatter({
11 | layout: {annotations: [{text: 'hodor'}]},
12 | });
13 | const ConnectedNumeric = connectLayoutToPlot(connectAnnotationToLayout(Numeric));
14 |
15 | mount(
16 |
17 |
18 |
19 | )
20 | .find(NumericInput)
21 | .find('.js-numeric-increase')
22 | .simulate('click');
23 |
24 | const payload = beforeUpdateLayout.mock.calls[0][0];
25 | expect(payload.update).toEqual({'annotations[0].textangle': 1});
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/src/lib/__tests__/maybeAdjustSrc-test.js:
--------------------------------------------------------------------------------
1 | import {maybeAdjustSrc} from '../index';
2 | /* eslint-disable no-magic-numbers */
3 | describe('maybeAdjustSrc', () => {
4 | it('uses custom parsing function if one is provided', () => {
5 | const custom = (srcs) => srcs.join('$');
6 | const adjusted = maybeAdjustSrc(['z1', 'z2'], 'zsrc', 'heatmap', {
7 | fromSrc: custom,
8 | });
9 | expect(adjusted).toBe('z1$z2');
10 | });
11 |
12 | it('reduces src to string for special table case', () => {
13 | const adjusted = maybeAdjustSrc(['z1'], 'header.valuessrc', 'table');
14 | expect(adjusted).toBe('z1');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/lib/__tests__/maybeTransposeData-test.js:
--------------------------------------------------------------------------------
1 | import {maybeTransposeData} from '../index';
2 | /* eslint-disable no-magic-numbers */
3 | describe('maybeTransposeData', () => {
4 | it('transposes 2d data for row based traceTypes', () => {
5 | const transposed = maybeTransposeData(
6 | [
7 | [1, 2, 3],
8 | [4, 5, 6],
9 | ],
10 | 'zsrc',
11 | 'heatmap'
12 | );
13 |
14 | // [[1, 4], [2, 5], [3, 6]]
15 | expect(transposed.length).toBe(3);
16 | });
17 |
18 | it('transposes 1d data for row based traceTypes', () => {
19 | const transposed = maybeTransposeData([1, 2, 3], 'zsrc', 'heatmap');
20 |
21 | // [[1], [2], [3]]
22 | expect(transposed.length).toBe(3);
23 | });
24 |
25 | it('does not transpose data for column based traceTypes', () => {
26 | const transposed = maybeTransposeData(
27 | [
28 | [1, 2, 3],
29 | [4, 5, 6],
30 | ],
31 | 'header.valuessrc',
32 | 'table'
33 | );
34 |
35 | // [[1, 2, 3], [4, 5, 6]]
36 | expect(transposed.length).toBe(2);
37 | });
38 |
39 | it('removes extra array wrapping for special cases in tables', () => {
40 | const transposed = maybeTransposeData([[1, 2, 3]], 'header.valuessrc', 'table');
41 |
42 | // [1, 2, 3]
43 | expect(Array.isArray(transposed[0])).toBe(false);
44 | expect(transposed.length).toBe(3);
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/lib/__tests__/multiValued-test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Numeric} from '../../components';
3 | import {TestEditor, fixtures, plotly} from '../test-utils';
4 | import {connectLayoutToPlot, connectAxesToLayout} from '..';
5 | import {mount} from 'enzyme';
6 |
7 | describe('multiValued Numeric', () => {
8 | it('uses multiValued defaultContainer as default increment value', () => {
9 | const beforeUpdateLayout = jest.fn();
10 | const xaxisLowerRange = -30;
11 | const fixtureProps = fixtures.scatter({
12 | layout: {xaxis: {range: [xaxisLowerRange, 3]}, yaxis: {range: [0, 3]}},
13 | });
14 | const AxesNumeric = connectLayoutToPlot(connectAxesToLayout(Numeric));
15 |
16 | mount(
17 |
18 |
19 |
20 | )
21 | .find('.js-numeric-increase')
22 | .simulate('click');
23 |
24 | expect(beforeUpdateLayout).toBeCalled();
25 | const payload = beforeUpdateLayout.mock.calls[0][0];
26 | expect(payload.update).toEqual({
27 | 'xaxis.range[0]': xaxisLowerRange + 1,
28 | });
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/src/lib/__tests__/transpose-test.js:
--------------------------------------------------------------------------------
1 | import {transpose} from '../index';
2 | /* eslint-disable no-magic-numbers */
3 | describe('transpose', () => {
4 | it('correctly transposes 1d arrays', () => {
5 | const originalArray = [1, 2, 3];
6 | const transposedArray = transpose(originalArray);
7 |
8 | expect(transposedArray.length).toBe(3);
9 |
10 | transposedArray.forEach((subArray) => {
11 | expect(Array.isArray(subArray)).toBe(true);
12 | expect(subArray.length).toBe(1);
13 | });
14 |
15 | expect(transposedArray[0][0]).toBe(1);
16 | expect(transposedArray[1][0]).toBe(2);
17 | expect(transposedArray[2][0]).toBe(3);
18 | });
19 |
20 | it('correctly transposes 2d arrays', () => {
21 | const originalArray = [
22 | [1, 2, 3],
23 | [9, 8, 0],
24 | ];
25 | const transposedArray = transpose(originalArray);
26 |
27 | expect(transposedArray.length).toBe(3);
28 | transposedArray.forEach((subArray) => {
29 | expect(Array.isArray(subArray)).toBe(true);
30 | expect(subArray.length).toBe(2);
31 | });
32 |
33 | expect(transposedArray[0][0]).toBe(1);
34 | expect(transposedArray[0][1]).toBe(9);
35 | expect(transposedArray[1][0]).toBe(2);
36 | expect(transposedArray[1][1]).toBe(8);
37 | expect(transposedArray[2][0]).toBe(3);
38 | expect(transposedArray[2][1]).toBe(0);
39 | });
40 |
41 | it('correctly fills non symmetrical 2d arrays', () => {
42 | const originalArray = [
43 | [1, 2],
44 | [9, 8, 7],
45 | ];
46 | const transposedArray = transpose(originalArray);
47 |
48 | expect(transposedArray.length).toBe(3);
49 | transposedArray.forEach((subArray) => {
50 | expect(Array.isArray(subArray)).toBe(true);
51 | expect(subArray.length).toBe(2);
52 | });
53 |
54 | expect(transposedArray[0][0]).toBe(1);
55 | expect(transposedArray[0][1]).toBe(9);
56 | expect(transposedArray[1][0]).toBe(2);
57 | expect(transposedArray[1][1]).toBe(8);
58 | expect(transposedArray[2][0]).toBe(null);
59 | expect(transposedArray[2][1]).toBe(7);
60 | });
61 | });
62 |
--------------------------------------------------------------------------------
/src/lib/bem.js:
--------------------------------------------------------------------------------
1 | //
2 | // BEM helper
3 | //
4 | // bem() => 'plotly-editor'
5 | // bem('foo') => 'foo'
6 | // bem('foo', 'bar') => 'foo__bar'
7 | // bem('foo', ['mod']) => 'foo foo--mod'
8 | // bem('foo', 'bar', ['mod']) => 'foo__bar foo__bar--mod'
9 | // bem('foo', ['mod1', mod2']) => 'foo foo--mod1 foo--mod2'
10 |
11 | /* eslint-disable no-param-reassign */
12 | import {baseClass} from './constants';
13 |
14 | export default function bem(block, element, modifiers) {
15 | let i, modifier;
16 | const out = [];
17 |
18 | if (!block) {
19 | return baseClass;
20 | }
21 | if (Array.isArray(block)) {
22 | throw new Error('bem error: Argument `block` cannot be an array');
23 | } else if (Array.isArray(element)) {
24 | modifiers = element;
25 | element = null;
26 | }
27 |
28 | let className = block;
29 |
30 | if (element && element.length) {
31 | className += '__' + element;
32 | }
33 |
34 | out.push(className);
35 | if (modifiers) {
36 | for (i = 0; i < modifiers.length; i++) {
37 | modifier = modifiers[i];
38 | if (modifier && modifier.length) {
39 | out.push(className + '--' + modifier);
40 | }
41 | }
42 | }
43 |
44 | return out.join(' ');
45 | }
46 |
--------------------------------------------------------------------------------
/src/lib/connectLayoutToPlot.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import PropTypes from 'prop-types';
3 | import nestedProperty from 'plotly.js/src/lib/nested_property';
4 | import {getDisplayName} from '../lib';
5 | import {EDITOR_ACTIONS} from './constants';
6 |
7 | export default function connectLayoutToPlot(WrappedComponent) {
8 | class LayoutConnectedComponent extends Component {
9 | getChildContext() {
10 | const {layout, fullLayout, plotly, onUpdate} = this.context;
11 |
12 | const updateContainer = (update) => {
13 | if (!onUpdate) {
14 | return;
15 | }
16 | onUpdate({
17 | type: EDITOR_ACTIONS.UPDATE_LAYOUT,
18 | payload: {
19 | update,
20 | },
21 | });
22 | };
23 |
24 | return {
25 | getValObject: (attr) =>
26 | !plotly
27 | ? null
28 | : plotly.PlotSchema.getLayoutValObject(fullLayout, nestedProperty({}, attr).parts),
29 | updateContainer,
30 | container: layout,
31 | fullContainer: fullLayout,
32 | };
33 | }
34 |
35 | render() {
36 | return ;
37 | }
38 | }
39 |
40 | LayoutConnectedComponent.displayName = `LayoutConnected${getDisplayName(WrappedComponent)}`;
41 |
42 | LayoutConnectedComponent.contextTypes = {
43 | layout: PropTypes.object,
44 | fullLayout: PropTypes.object,
45 | plotly: PropTypes.object,
46 | onUpdate: PropTypes.func,
47 | };
48 |
49 | LayoutConnectedComponent.childContextTypes = {
50 | getValObject: PropTypes.func,
51 | updateContainer: PropTypes.func,
52 | container: PropTypes.object,
53 | fullContainer: PropTypes.object,
54 | };
55 |
56 | const {plotly_editor_traits} = WrappedComponent;
57 | LayoutConnectedComponent.plotly_editor_traits = plotly_editor_traits;
58 |
59 | return LayoutConnectedComponent;
60 | }
61 |
--------------------------------------------------------------------------------
/src/lib/connectSliderToLayout.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import PropTypes from 'prop-types';
3 | import {getDisplayName} from '../lib';
4 |
5 | export default function connectSliderToLayout(WrappedComponent) {
6 | class SliderConnectedComponent extends Component {
7 | constructor(props, context) {
8 | super(props, context);
9 | this.updateSlider = this.updateSlider.bind(this);
10 | this.setLocals(props, context);
11 | }
12 |
13 | UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
14 | this.setLocals(nextProps, nextContext);
15 | }
16 |
17 | setLocals(props, context) {
18 | const {sliderIndex} = props;
19 | const {container, fullContainer} = context;
20 |
21 | const sliders = container.sliders || [];
22 | const fullSliders = fullContainer.sliders || [];
23 | this.container = sliders[sliderIndex];
24 | this.fullContainer = fullSliders[sliderIndex];
25 | }
26 |
27 | getChildContext() {
28 | return {
29 | getValObject: (attr) =>
30 | !this.context.getValObject ? null : this.context.getValObject(`sliders[].${attr}`),
31 | updateContainer: this.updateSlider,
32 | container: this.container,
33 | fullContainer: this.fullContainer,
34 | };
35 | }
36 |
37 | updateSlider(update) {
38 | const newUpdate = {};
39 | const {sliderIndex} = this.props;
40 | for (const key in update) {
41 | const newkey = `sliders[${sliderIndex}].${key}`;
42 | newUpdate[newkey] = update[key];
43 | }
44 | this.context.updateContainer(newUpdate);
45 | }
46 |
47 | render() {
48 | return ;
49 | }
50 | }
51 |
52 | SliderConnectedComponent.displayName = `SliderConnected${getDisplayName(WrappedComponent)}`;
53 |
54 | SliderConnectedComponent.propTypes = {
55 | sliderIndex: PropTypes.number.isRequired,
56 | };
57 |
58 | SliderConnectedComponent.contextTypes = {
59 | container: PropTypes.object,
60 | fullContainer: PropTypes.object,
61 | onUpdate: PropTypes.func,
62 | updateContainer: PropTypes.func,
63 | getValObject: PropTypes.func,
64 | };
65 |
66 | SliderConnectedComponent.childContextTypes = {
67 | updateContainer: PropTypes.func,
68 | container: PropTypes.object,
69 | fullContainer: PropTypes.object,
70 | getValObject: PropTypes.func,
71 | };
72 |
73 | const {plotly_editor_traits} = WrappedComponent;
74 | SliderConnectedComponent.plotly_editor_traits = plotly_editor_traits;
75 |
76 | return SliderConnectedComponent;
77 | }
78 |
--------------------------------------------------------------------------------
/src/lib/dereference.js:
--------------------------------------------------------------------------------
1 | import walkObject from './walkObject';
2 | import {maybeTransposeData} from './index';
3 |
4 | const SRC_ATTR_PATTERN = /src$/;
5 |
6 | export function getColumnNames(srcArray, dataSourceOptions) {
7 | return srcArray
8 | .map((src) => {
9 | const columns = dataSourceOptions.filter((dso) => dso.value === src);
10 | if (columns.length === 1) {
11 | return columns[0].columnName || columns[0].label;
12 | }
13 | return '';
14 | })
15 | .join(' - ');
16 | }
17 |
18 | export default function dereference(
19 | container,
20 | dataSources,
21 | config = {deleteKeys: false},
22 | dataSourceOptions = null
23 | ) {
24 | const containerIsData = Array.isArray(container);
25 |
26 | const replacer = (key, parent, srcPath) => {
27 | if (!SRC_ATTR_PATTERN.test(key)) {
28 | return;
29 | }
30 |
31 | const dataKey = key.replace(SRC_ATTR_PATTERN, '');
32 |
33 | let srcRef = config.toSrc ? config.toSrc(parent[key]) : parent[key];
34 |
35 | // making this into an array to more easily lookup 1d and 2d srcs in dataSourceOptions
36 | if (!Array.isArray(srcRef)) {
37 | srcRef = [srcRef];
38 | }
39 |
40 | let dereferencedData = srcRef.map((ref) => {
41 | if (config.deleteKeys && !(ref in dataSources)) {
42 | delete parent[dataKey];
43 | delete parent[dataKey + 'src'];
44 | }
45 | return dataSources[ref];
46 | });
47 |
48 | // remove extra data wrapping
49 | if (srcRef.length === 1) {
50 | dereferencedData = dereferencedData[0];
51 | }
52 |
53 | if (!Array.isArray(dereferencedData)) {
54 | return;
55 | }
56 |
57 | if (containerIsData) {
58 | if (parent.type !== null) {
59 | // we're at the top level of the trace
60 | if (dataSourceOptions !== null) {
61 | parent.meta = parent.meta || {};
62 | parent.meta.columnNames = parent.meta.columnNames || {};
63 | parent.meta.columnNames[dataKey] = getColumnNames(srcRef, dataSourceOptions);
64 | }
65 | parent[dataKey] = maybeTransposeData(dereferencedData, srcPath, parent.type);
66 | } else {
67 | parent[dataKey] = dereferencedData;
68 | }
69 | } else {
70 | // container is layout
71 | parent[dataKey] = dereferencedData;
72 | }
73 | };
74 |
75 | if (containerIsData) {
76 | walkObject(container, replacer, {
77 | walkArraysMatchingKeys: ['data', 'transforms'],
78 | pathType: 'nestedProperty',
79 | });
80 | } else {
81 | // container is layout
82 | walkObject(container, replacer, {pathType: 'nestedProperty'});
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/lib/localize.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, {Component} from 'react';
3 | import {getDisplayName} from 'lib';
4 |
5 | export default function localize(Comp) {
6 | class LocalizedComponent extends Component {
7 | constructor(props, context) {
8 | super(props, context);
9 |
10 | const dictionaries = context.dictionaries;
11 | const locale = context.locale;
12 |
13 | this.localize = function localize(str) {
14 | return localizeString(dictionaries, locale, str);
15 | };
16 | }
17 |
18 | render() {
19 | return ;
20 | }
21 | }
22 | LocalizedComponent.displayName = `Localized${getDisplayName(Comp)}`;
23 | LocalizedComponent.contextTypes = LocalizedComponent.contextTypes || {};
24 | LocalizedComponent.contextTypes.dictionaries = PropTypes.object;
25 | LocalizedComponent.contextTypes.locale = PropTypes.string;
26 |
27 | LocalizedComponent.plotly_editor_traits = Comp.plotly_editor_traits;
28 |
29 | return LocalizedComponent;
30 | }
31 |
32 | export function localizeString(dictionaries, locale, key) {
33 | const dict = dictionaries[locale];
34 | if (dict && dict.hasOwnProperty(key)) {
35 | return dict[key];
36 | }
37 | return key;
38 | }
39 |
--------------------------------------------------------------------------------
/src/lib/strings.js:
--------------------------------------------------------------------------------
1 | /*eslint-disable */
2 |
3 | /**
4 | * Capitalize string
5 | */
6 | function capitalize(s) {
7 | return !s ? '' : s.charAt(0).toUpperCase() + s.substring(1);
8 | }
9 |
10 | /**
11 | * "Safer" String.toLowerCase()
12 | */
13 | function lowerCase(str) {
14 | return str.toLowerCase();
15 | }
16 |
17 | /**
18 | * "Safer" String.toUpperCase()
19 | */
20 | function upperCase(str) {
21 | return str.toUpperCase();
22 | }
23 |
24 | /**
25 | * Remove non-word chars.
26 | */
27 | function removeNonWord(str) {
28 | return str.replace(/[^0-9a-zA-Z\xC0-\xFF \-]/g, '');
29 | }
30 |
31 | /**
32 | * Convert string to camelCase text.
33 | */
34 | function camelCase(string) {
35 | return string
36 | .replace(/\-/g, ' ')
37 | .replace(/(\d)(?=(\d{1})+$)/g, '$1 ')
38 | .replace(/\s[a-z]/g, upperCase)
39 | .replace(/\s+/g, '')
40 | .replace(/^[A-Z]/g, lowerCase);
41 | }
42 |
43 | function pascalCase(str) {
44 | return camelCase(str).replace(/^[a-z]/, upperCase);
45 | }
46 |
47 | export {capitalize, lowerCase, upperCase, removeNonWord, camelCase, pascalCase};
48 |
49 | /* eslint-enable */
50 |
--------------------------------------------------------------------------------
/src/locales/en.js:
--------------------------------------------------------------------------------
1 | export default {
2 | foo: 'bar',
3 | };
4 |
--------------------------------------------------------------------------------
/src/locales/index.js:
--------------------------------------------------------------------------------
1 | import en from './en';
2 | import xx from './xx';
3 |
4 | export default {
5 | en: en,
6 | xx: xx,
7 | };
8 |
--------------------------------------------------------------------------------
/src/styles/_helpers.scss:
--------------------------------------------------------------------------------
1 | @use 'sass:map';
2 | .\+flex {
3 | display: flex;
4 | }
5 | .\+cursor-clickable {
6 | cursor: pointer;
7 | }
8 | .\+hover-grey:hover {
9 | color: var(--color-text-active);
10 | }
11 |
12 | .\+error {
13 | border-color: var(--color-sienna) !important;
14 | outline-color: var(--color-sienna) !important;
15 | }
16 |
17 | @function spacing($name, $true-val: false) {
18 | @if $true-val == true {
19 | @return map.get($spacings, $name);
20 | } @else {
21 | @return var(--spacing-#{$name});
22 | }
23 | }
24 |
25 | @function font($name, $true-val: false) {
26 | @if $true-val == true {
27 | @return map.get($fonts, $name);
28 | } @else {
29 | @return var(--font-#{$name});
30 | }
31 | }
32 |
33 | @function color($name, $true-val: false) {
34 | @if $true-val == true {
35 | @return map.get($colors, $name);
36 | } @else {
37 | @return var(--color-#{$name});
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/styles/_movement.scss:
--------------------------------------------------------------------------------
1 | @keyframes fade-in {
2 | from {
3 | opacity: 0;
4 | }
5 |
6 | to {
7 | opacity: 1;
8 | }
9 | }
10 |
11 | @keyframes fade-and-slide-in-from-bottom {
12 | from {
13 | opacity: 0;
14 | transform: translateY(20px);
15 | }
16 |
17 | to {
18 | opacity: 1;
19 | transform: none;
20 | }
21 | }
22 |
23 | @keyframes fsbr {
24 | from {
25 | opacity: 1;
26 | transform: none;
27 | }
28 | to {
29 | opacity: 0;
30 | transform: translateY(20px);
31 | }
32 | }
33 |
34 | @keyframes fade-out {
35 | from {
36 | opacity: 1;
37 | }
38 |
39 | to {
40 | opacity: 0;
41 | }
42 | }
43 |
44 | .animate,
45 | %animate {
46 | &--fade-in {
47 | opacity: 0;
48 | animation: fade-in 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
49 | }
50 | &--fade-out {
51 | opacity: 1;
52 | animation: fade-out 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
53 | }
54 | &--fade-and-slide-in-from-bottom {
55 | opacity: 0;
56 | transform: translateY(20px);
57 | animation: fade-and-slide-in-from-bottom 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
58 | }
59 | &--fsbr {
60 | opacity: 1;
61 | transform: none;
62 | animation: fsbr 0.1s forwards cubic-bezier(0.19, 1, 0.22, 1);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/styles/components/_main.scss:
--------------------------------------------------------------------------------
1 | @use 'sidebar/main';
2 | @use 'containers/main' as main2;
3 | @use 'fields/main' as main3;
4 | @use 'widgets/main' as main4;
5 |
--------------------------------------------------------------------------------
/src/styles/components/containers/_info.scss:
--------------------------------------------------------------------------------
1 | @use '../../mixins' as *;
2 |
3 | .info {
4 | &__title {
5 | @include heading('small');
6 | padding: var(--spacing-half-unit) var(--spacing-half-unit) var(--spacing-quarter-unit)
7 | var(--spacing-half-unit);
8 | }
9 | &__text {
10 | padding: var(--spacing-quarter-unit) var(--spacing-half-unit);
11 | color: var(--color-text-base);
12 | font-size: var(--font-size-small);
13 | font-weight: var(--font-weight-normal);
14 | line-height: var(--font-leading-body);
15 | }
16 | &__sub-text {
17 | color: var(--color-text-base);
18 | font-size: var(--font-size-small);
19 | font-weight: var(--font-weight-normal);
20 | font-style: italic;
21 | line-height: var(--font-leading-body);
22 | padding: var(--spacing-quarter-unit) var(--spacing-half-unit) var(--spacing-half-unit)
23 | var(--spacing-half-unit);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/styles/components/containers/_main.scss:
--------------------------------------------------------------------------------
1 | @use 'panel';
2 | @use 'fold';
3 | @use 'section';
4 | @use 'menupanel';
5 | @use 'info';
6 | @use 'modalbox';
7 | @use 'modal';
8 | @use 'tabs';
9 |
--------------------------------------------------------------------------------
/src/styles/components/containers/_menupanel.scss:
--------------------------------------------------------------------------------
1 | .menupanel {
2 | padding-top: 0;
3 | display: flex;
4 | justify-content: flex-end;
5 | flex-grow: 1;
6 |
7 | &--ownline {
8 | padding-top: var(--spacing-quarter-unit);
9 | width: 100%;
10 | }
11 | &__label {
12 | font-weight: var(--font-weight-semibold);
13 | padding-right: var(--spacing-quarter-unit);
14 | }
15 |
16 | &__icon {
17 | vertical-align: middle;
18 | svg {
19 | display: block;
20 | }
21 | // gets overridden by user agent stylesheet otherwise
22 | width: 15px !important;
23 | height: 15px !important;
24 | fill: var(--color-text-light) !important;
25 | padding-left: var(--spacing-quarter-unit);
26 | &:hover {
27 | cursor: pointer;
28 | fill: var(--color-accent) !important;
29 | }
30 | &-span {
31 | font-size: var(--font-size-small);
32 | display: flex;
33 | &--question {
34 | color: var(--color-text-base);
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/styles/components/containers/_modal.scss:
--------------------------------------------------------------------------------
1 | @use '../../mixins' as *;
2 |
3 | .modal {
4 | box-sizing: border-box;
5 | * {
6 | box-sizing: border-box;
7 | }
8 | position: fixed;
9 | top: 0;
10 | left: 0;
11 | width: 100vw;
12 | height: 100vh;
13 | display: flex;
14 | align-items: flex-start;
15 | overflow-y: auto;
16 | justify-content: center;
17 | @include z-index('orbit');
18 |
19 | &__backdrop {
20 | height: 100%;
21 | width: 100%;
22 | left: 0;
23 | top: 0;
24 | position: fixed;
25 | opacity: 0;
26 | will-change: opacity;
27 | &::before {
28 | content: '';
29 | height: 100%;
30 | width: 100%;
31 | left: 0;
32 | opacity: 0.5;
33 | top: 0;
34 | background: var(--color-background-dark);
35 | position: fixed;
36 | }
37 | }
38 |
39 | &__card {
40 | background: var(--color-background-top);
41 | border-radius: var(--border-radius);
42 | position: relative;
43 | @include z-index('orbit');
44 | max-width: calc(100% - var(--spacing-base-unit));
45 | box-shadow: var(--box-shadow-base);
46 | display: flex;
47 | flex-direction: column;
48 | will-change: opacity, transform;
49 | flex-grow: 0;
50 | margin: 3vh 10vw;
51 | }
52 |
53 | &__header {
54 | display: flex;
55 | justify-content: space-between;
56 | align-items: center;
57 | color: var(--color-text-base);
58 | padding: var(--spacing-half-unit);
59 | font-weight: var(--font-weight-semibold);
60 |
61 | &__close {
62 | opacity: 0.5;
63 | &:hover {
64 | cursor: pointer;
65 | opacity: 1;
66 | }
67 | svg {
68 | display: block;
69 | * {
70 | fill: currentColor;
71 | }
72 | }
73 | }
74 | }
75 | &__content {
76 | flex-grow: 1;
77 | background-color: var(--color-background-light);
78 | border-bottom-left-radius: var(--border-radius);
79 | border-bottom-right-radius: var(--border-radius);
80 | }
81 |
82 | // ANIMATIONS
83 |
84 | &__backdrop {
85 | @include animate('fade-in', 1s);
86 | }
87 | &__card {
88 | @include animate('fsb', 0.85s, 0.1s);
89 | }
90 |
91 | &--animate-out {
92 | pointer-events: none;
93 | .modal__backdrop {
94 | @include animate('fade-out', 0.85s);
95 | }
96 | .modal__card {
97 | @include animate('fsbr', 0.85s);
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/styles/components/containers/_modalbox.scss:
--------------------------------------------------------------------------------
1 | @use '../../mixins' as *;
2 |
3 | .modalbox {
4 | position: absolute;
5 | border-radius: var(--border-radius);
6 | overflow: hidden;
7 | text-transform: none;
8 | text-align: left;
9 | border: var(--border-default);
10 | align-content: center;
11 | box-shadow: var(--box-shadow-base);
12 | left: calc(var(--spacing-quarter-unit) * -1);
13 | width: calc(100% + var(--spacing-half-unit));
14 | top: calc(100% + var(--spacing-quarter-unit));
15 | background-color: var(--color-background-top);
16 | @include z-index('cloud');
17 |
18 | &__cover {
19 | position: fixed;
20 | top: 0;
21 | right: 0;
22 | bottom: 0;
23 | left: 0;
24 | @include z-index('underground');
25 | }
26 |
27 | &--dark {
28 | background-color: var(--color-background-inverse);
29 | }
30 |
31 | &--relative {
32 | position: relative;
33 | }
34 | }
35 |
36 | .field .modalbox {
37 | width: 100%;
38 | left: -1px;
39 | top: 100%;
40 | }
41 |
--------------------------------------------------------------------------------
/src/styles/components/containers/_section.scss:
--------------------------------------------------------------------------------
1 | .section {
2 | &__heading {
3 | position: relative;
4 | display: flex;
5 | font-size: var(--font-size-base);
6 | color: var(--color-text-section-header);
7 | font-weight: var(--font-weight-semibold);
8 | cursor: default;
9 | background-color: var(--color-background-light);
10 | padding: var(--spacing-quarter-unit) var(--spacing-half-unit);
11 | clear: both;
12 | text-transform: capitalize;
13 | }
14 | &:not(:first-child) {
15 | .section__heading {
16 | border-top: var(--border-light);
17 | }
18 | }
19 | &:first-child {
20 | .section__heading {
21 | border-top: 0;
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/components/containers/_tabs.scss:
--------------------------------------------------------------------------------
1 | .panel {
2 | .react-tabs {
3 | $tab-bar-height: 32px;
4 | flex-grow: 1;
5 | display: flex;
6 | flex-direction: column;
7 | &__tab {
8 | &-list {
9 | background: var(--color-background-medium);
10 | margin: 0;
11 | flex-shrink: 0;
12 | list-style: none;
13 | display: flex;
14 | align-items: flex-end;
15 | padding-top: var(--spacing-half-unit);
16 | padding-left: var(--spacing-half-unit);
17 | padding-right: var(--spacing-half-unit);
18 | padding-bottom: 0;
19 | height: $tab-bar-height;
20 | }
21 | // Tab Styles
22 | flex-grow: 1;
23 | flex-shrink: 0;
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 | padding: var(--spacing-quarter-unit);
28 | color: var(--color-text-base);
29 | font-size: var(--font-size-base);
30 | background: var(--color-background-medium);
31 | border: var(--border-default);
32 | border-bottom: 0;
33 | position: relative;
34 | background: var(--color-background-light);
35 | transition: border-color 0.15s ease-in-out;
36 |
37 | &:first-of-type {
38 | border-top-left-radius: var(--border-radius);
39 | }
40 | &:last-of-type {
41 | border-top-right-radius: var(--border-radius);
42 | }
43 |
44 | &:hover {
45 | background-color: var(--color-background-base);
46 | cursor: pointer;
47 | }
48 |
49 | &--selected {
50 | background-color: var(--color-background-base);
51 | pointer-events: none;
52 | margin-top: 0;
53 | color: var(--color-text-active);
54 | border-top-color: var(--color-accent);
55 | border-top-width: 2px;
56 |
57 | &::before {
58 | position: absolute;
59 | top: 100%;
60 | width: 100%;
61 | height: 1px;
62 | content: '';
63 | background-color: var(--color-background-base);
64 | left: 0;
65 | z-index: 4;
66 | }
67 | }
68 | &:not(:first-of-type):not(:last-of-type) {
69 | border-left: 0;
70 | }
71 | &:last-of-type {
72 | border-left: none;
73 | }
74 | &-panel {
75 | border-top: var(--border-default);
76 | display: none;
77 | &--selected {
78 | flex-grow: 1;
79 | display: flex;
80 | flex-direction: column;
81 | }
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/styles/components/fields/_main.scss:
--------------------------------------------------------------------------------
1 | @use 'field';
2 | @use 'symbolselector';
3 |
--------------------------------------------------------------------------------
/src/styles/components/fields/_symbolselector.scss:
--------------------------------------------------------------------------------
1 | @use '../../mixins' as *;
2 |
3 | .symbol-selector__toggle {
4 | border: var(--border-default);
5 | border-radius: var(--border-radius);
6 | width: 80px;
7 | cursor: pointer;
8 | padding: var(--spacing-quarter-unit) var(--spacing-quarter-unit) 3px var(--spacing-quarter-unit);
9 | @include clearfix();
10 | }
11 |
12 | .symbol-selector__toggle--dark {
13 | background-color: var(--color-background-inverse);
14 | }
15 |
16 | .symbol-selector__toggle__option {
17 | float: left;
18 | }
19 |
20 | .symbol-selector__toggle__caret {
21 | float: right;
22 | fill: var(--color-text-light);
23 | padding-top: var(--spacing-eighth-unit);
24 | // prevent user agent override
25 | width: 13px !important;
26 | height: 13px !important;
27 | }
28 |
29 | .symbol-selector__menu {
30 | max-width: 225px;
31 | position: absolute;
32 | @include z-index('orbit');
33 | border: var(--border-default);
34 | padding: var(--spacing-half-unit);
35 | box-shadow: 2px 2px var(--spacing-half-unit) var(--color-border-light);
36 | border-radius: var(--border-radius-small);
37 | left: var(--spacing-base-unit);
38 | }
39 |
40 | .symbol-selector__item {
41 | display: inline;
42 | }
43 |
44 | .symbol-selector__symbol {
45 | &:hover {
46 | background-color: var(--color-border-default);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/styles/components/widgets/_button.scss:
--------------------------------------------------------------------------------
1 | @use '../../mixins' as *;
2 |
3 | // button default styles
4 | button {
5 | display: inline-block;
6 | padding: var(--spacing-quarter-unit) var(--spacing-half-unit);
7 | line-height: 1;
8 | letter-spacing: 0.5px;
9 | text-transform: capitalize;
10 | text-align: center;
11 | cursor: pointer;
12 | height: 36px;
13 | outline: none;
14 | user-select: none;
15 | font-size: var(--font-size-medium);
16 | font-weight: var(--font-weight-semibold);
17 | font-family: var(--font-family-body);
18 | border-width: 1px;
19 | border-style: solid;
20 | border-color: transparent;
21 | border-radius: var(--border-radius);
22 | @include trans;
23 | &.button {
24 | padding-left: 0;
25 | }
26 | }
27 |
28 | .button {
29 | &__wrapper {
30 | display: flex;
31 | align-items: center;
32 | justify-content: center;
33 | position: relative;
34 | overflow: hidden;
35 | }
36 | &__label {
37 | padding-left: var(--spacing-half-unit);
38 | }
39 | &__icon {
40 | display: flex;
41 | padding-left: var(--spacing-quarter-unit);
42 | will-change: transform;
43 | svg {
44 | transform: scale(0.8);
45 | transform-origin: center center;
46 | display: block;
47 | path {
48 | fill: currentColor;
49 | }
50 | }
51 | & + .button__label {
52 | padding-left: 0;
53 | }
54 | }
55 |
56 | // If a button is placed in the sidebar,
57 | // it should grow to the width of the available space.
58 | @at-root .sidebar .button {
59 | width: calc(100% - var(--spacing-base-unit));
60 | margin-left: var(--spacing-half-unit);
61 | margin-right: var(--spacing-half-unit);
62 | }
63 |
64 | &--no-text {
65 | @include button();
66 | padding-right: var(--spacing-quarter-unit);
67 | margin-left: 5px;
68 | &--disabled {
69 | @include button();
70 | padding-right: var(--spacing-quarter-unit);
71 | margin-left: 5px;
72 | color: rgb(186, 186, 186);
73 | cursor: default;
74 | }
75 | }
76 |
77 | &--default {
78 | @include button();
79 | }
80 | &--primary {
81 | @include button(primary);
82 | }
83 | &--secondary {
84 | @include button(secondary);
85 | }
86 | &--tertiary {
87 | @include button(tertiary);
88 | }
89 | &--upgrade {
90 | @include button(upgrade);
91 | }
92 | &--header {
93 | @include button(header);
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/styles/components/widgets/_checkbox.scss:
--------------------------------------------------------------------------------
1 | @use '../../variables/main' as *;
2 |
3 | .checkbox {
4 | &__group {
5 | text-align: left;
6 | }
7 |
8 | &__item {
9 | user-select: none;
10 | cursor: default;
11 | padding-top: var(--spacing-eighth-unit);
12 | padding-bottom: var(--spacing-eighth-unit);
13 | &--vertical {
14 | display: block;
15 | clear: both;
16 | }
17 | }
18 | &__box {
19 | $size: 18px;
20 | height: $size;
21 | width: $size;
22 | border: var(--border-default);
23 | border-radius: var(--border-radius-small);
24 | cursor: pointer;
25 | display: inline-block;
26 | vertical-align: middle;
27 | text-align: center;
28 | position: relative;
29 | &:hover {
30 | background: var(--color-background-light);
31 | }
32 | &--checked {
33 | border: var(--border-accent-shade);
34 | background: var(--color-accent);
35 | &:hover {
36 | background: var(--color-accent-shade);
37 | }
38 |
39 | & + .checkbox__label {
40 | color: var(--color-text-active);
41 | }
42 | }
43 | }
44 | &__check {
45 | color: var(--color-text-base);
46 | font-size: var(--spacing-half-unit);
47 | position: absolute;
48 | height: 100%;
49 | width: 100%;
50 | left: 0;
51 | top: 0;
52 | display: flex;
53 | align-items: center;
54 | justify-content: center;
55 | svg {
56 | @include drop-shadow--dark-ui;
57 | path {
58 | fill: var(--color-white);
59 | }
60 | }
61 | }
62 | &__label {
63 | padding-left: var(--spacing-quarter-unit);
64 | font-size: var(--font-size-base);
65 | color: var(--color-text-base);
66 | display: inline-block;
67 | line-height: 20px;
68 | text-align: left;
69 | vertical-align: middle;
70 | cursor: pointer;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/styles/components/widgets/_colorscalepicker.scss:
--------------------------------------------------------------------------------
1 | // this width is also passed in as a prop in widgets/ColorscalePicker.js
2 | $colorscalepicker-width: 240px;
3 | $colorscale-clickable-part-width: 180px;
4 | $text-padding: 5px;
5 |
6 | // We're making our own custom colorscale picker to better adapt to RCE layout
7 | // so we're not displaying some parts of the original colorscalepicker (categody dropdown, description)
8 | .colorscalePickerTopContainer {
9 | display: none;
10 | }
11 |
12 | .colorscaleDescription {
13 | display: none;
14 | }
15 |
16 | // We're overriding some of the styles of the original css container class of react-colorscales
17 | // because we don't need this functionality
18 | .colorscalePickerContainer {
19 | min-width: $colorscalepicker-width;
20 | position: relative;
21 | padding: 0;
22 | resize: none;
23 | border: none;
24 | background: none;
25 | max-height: none;
26 | }
27 |
28 | // Some more styling adjustments to original react-
29 | .colorscalePickerContainer input:focus {
30 | outline: none;
31 | }
32 | .colorscalePickerContainer::-webkit-scrollbar {
33 | width: 5px;
34 | }
35 | .colorscalePickerBottomContainer {
36 | padding: $text-padding 0 0 0;
37 | }
38 | // Adjustments for cubehelix controls
39 | .colorscaleControlPanel {
40 | padding: 0;
41 | margin: 0;
42 | margin-left: 27%;
43 | width: 70%;
44 | }
45 |
46 | // Our custom component, containing all the colorpicker
47 | .customPickerContainer {
48 | text-align: left;
49 | width: $colorscale-clickable-part-width;
50 | }
51 |
52 | .customPickerContainer__clickable {
53 | margin-top: 5px;
54 | min-height: 27px;
55 | width: $colorscale-clickable-part-width;
56 | }
57 |
58 | // The expandable part of the component
59 | .customPickerContainer__expanded-content {
60 | margin-top: 10px;
61 | }
62 |
63 | .customPickerContainer__category-dropdown {
64 | max-width: 100%;
65 | }
66 |
67 | .customPickerContainer__palettes {
68 | margin-left: -60px;
69 | }
70 |
71 | .customPickerContainer__info {
72 | width: $colorscalepicker-width;
73 | }
74 |
75 | .customPickerContainer__category-dropdown {
76 | width: $colorscale-clickable-part-width !important;
77 | }
78 |
--------------------------------------------------------------------------------
/src/styles/components/widgets/_dropzone.scss:
--------------------------------------------------------------------------------
1 | .dropzone-container {
2 | flex-grow: 1;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | border: 1px dashed var(--color-border-default);
7 | height: 160px;
8 | box-sizing: border-box;
9 | padding: 8px;
10 | border-radius: var(--border-radius);
11 |
12 | &__content {
13 | width: 100%;
14 | height: 100%;
15 | text-align: center;
16 | background-color: var(--color-rhino-light-5);
17 | border-radius: var(--border-radius);
18 | display: flex;
19 | align-items: center;
20 | justify-content: center;
21 | }
22 |
23 | &__message {
24 | padding: 15px;
25 | }
26 |
27 | &__image {
28 | width: 100%;
29 | height: 100%;
30 | background-size: contain;
31 | background-position: center;
32 | position: relative;
33 | background-repeat: no-repeat;
34 | }
35 |
36 | &--active {
37 | border-color: var(--color-dodger);
38 | }
39 |
40 | &--rejected {
41 | border-color: red;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/styles/components/widgets/_main.scss:
--------------------------------------------------------------------------------
1 | @use 'button';
2 | @use 'checkbox';
3 | @use 'colorpicker';
4 | @use 'colorscalepicker';
5 | @use 'dropdown';
6 | @use 'numeric-input';
7 | @use 'text-input';
8 | @use 'radio-block';
9 | @use 'text-editor';
10 | @use 'rangeslider';
11 | @use 'trace-type-selector';
12 | @use 'dropzone';
13 | @use 'microtip';
14 | @use 'datetimepicker';
15 |
--------------------------------------------------------------------------------
/src/styles/components/widgets/_numeric-input.scss:
--------------------------------------------------------------------------------
1 | ::-webkit-input-placeholder {
2 | /* Chrome/Opera/Safari */
3 | color: var(--color-text-placeholder);
4 | }
5 | ::-moz-placeholder {
6 | /* Firefox 19+ */
7 | color: var(--color-text-placeholder);
8 | }
9 | :-moz-placeholder {
10 | /* Firefox 18- */
11 | color: var(--color-text-placeholder);
12 | }
13 |
14 | .numeric-input__wrapper {
15 | line-height: 20px;
16 | max-width: 100%;
17 | flex: 1;
18 | display: flex;
19 | align-items: center;
20 | color: var(--color-text-base);
21 | }
22 |
23 | .numeric-input__number {
24 | display: inline-block;
25 | border: var(--border-default);
26 | background: var(--color-background-inputs);
27 | cursor: text;
28 | overflow: hidden;
29 | text-overflow: ellipsis;
30 | white-space: nowrap;
31 | text-align: left;
32 | border-radius: var(--border-radius-small);
33 | padding: var(--spacing-quarter-unit);
34 | width: 62px;
35 | vertical-align: middle;
36 | font-size: inherit;
37 | color: inherit;
38 | font-family: inherit;
39 | }
40 |
41 | .numeric-input__caret-box {
42 | display: inline-block;
43 | max-height: 32px;
44 | margin-left: var(--spacing-quarter-unit);
45 | margin-right: var(--spacing-half-unit);
46 | vertical-align: middle;
47 | box-sizing: border-box;
48 | }
49 |
50 | .numeric-input__caret:first-child {
51 | margin-bottom: 2px;
52 | }
53 |
54 | .numeric-input__caret {
55 | cursor: pointer;
56 | background-color: var(--color-background-light);
57 | border: var(--border-default);
58 | border-radius: 1px;
59 | line-height: var(--spacing-half-unit);
60 | text-align: center;
61 | max-height: 13px;
62 | }
63 |
64 | .numeric-top-caret-modifier {
65 | // to prevent user agent override
66 | width: 13px !important;
67 | height: 13px !important;
68 | fill: var(--color-text-base) !important;
69 | }
70 |
71 | .numeric-bottom-caret-modifier {
72 | // to prevent user agent override
73 | width: 13px !important;
74 | height: 13px !important;
75 | fill: var(--color-text-base) !important;
76 | }
77 |
78 | .AxisInterval-milliseconds {
79 | width: 50%;
80 | }
81 |
--------------------------------------------------------------------------------
/src/styles/components/widgets/_radio-block.scss:
--------------------------------------------------------------------------------
1 | .radio-block {
2 | width: 100%;
3 | line-height: var(--font-leading-head);
4 | display: flex;
5 | &__option {
6 | flex-grow: 1;
7 | padding: var(--spacing-quarter-unit) var(--spacing-half-unit);
8 | background-color: var(--color-background-top);
9 | border: var(--border-default);
10 | display: inline-block;
11 | cursor: pointer;
12 | min-width: 0px;
13 | text-align: center;
14 | font-size: var(--font-size-small);
15 | &:not(.radio-block__option--active) {
16 | &:hover {
17 | background-color: var(--color-background-light);
18 | color: var(--color-text-active);
19 | }
20 | }
21 | &--active {
22 | background-color: var(--color-button-primary-base-fill);
23 | color: var(--color-button-primary-base-text);
24 | border: 1px solid var(--color-button-primary-base-border);
25 | margin-left: -1px;
26 | cursor: default;
27 | text-shadow: var(--text-shadow-dark-ui);
28 | font-weight: var(--font-weight-semibold);
29 | &:not(:first-child),
30 | &:last-child {
31 | border-left: var(--border-accent-shade) !important;
32 | }
33 | }
34 | &:not(:first-child):not(:last-child) {
35 | border-left: 0;
36 | }
37 | &:last-child {
38 | border-top-right-radius: var(--border-radius-small);
39 | border-bottom-right-radius: var(--border-radius-small);
40 | border-left: 0;
41 | }
42 | &:first-child {
43 | border-top-left-radius: var(--border-radius-small);
44 | border-bottom-left-radius: var(--border-radius-small);
45 | &:not(.radio-block__option--active) {
46 | border-left: var(--border-default);
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/styles/components/widgets/_text-input.scss:
--------------------------------------------------------------------------------
1 | ::-webkit-input-placeholder {
2 | /* Chrome/Opera/Safari */
3 | color: var(--color-text-placeholder);
4 | }
5 | ::-moz-placeholder {
6 | /* Firefox 19+ */
7 | color: var(--color-text-placeholder);
8 | }
9 | :-moz-placeholder {
10 | /* Firefox 18- */
11 | color: var(--color-text-placeholder);
12 | }
13 |
14 | .text-input {
15 | display: inline-block;
16 | border: var(--border-default);
17 | background: var(--color-background-inputs);
18 | cursor: text;
19 | overflow: hidden;
20 | text-overflow: ellipsis;
21 | white-space: nowrap;
22 | text-align: left;
23 | border-radius: var(--border-radius-small);
24 | padding: var(--spacing-quarter-unit) var(--spacing-quarter-unit) var(--spacing-quarter-unit)
25 | var(--spacing-half-unit);
26 | width: 140px;
27 | vertical-align: middle;
28 | font-size: inherit;
29 | color: inherit;
30 | font-family: inherit;
31 | }
32 |
--------------------------------------------------------------------------------
/src/styles/main.scss:
--------------------------------------------------------------------------------
1 | @charset 'utf-8';
2 | @use 'sass:meta';
3 | @use 'variables/main';
4 | @use 'mixins';
5 | @use 'helpers';
6 | @use 'movement';
7 | @use 'variables/colors';
8 | @use 'variables/layout';
9 |
10 | :root {
11 | --env: $ENV;
12 | }
13 |
14 | .plotly-editor--theme-provider {
15 | @include mixins.generateVars;
16 | }
17 |
18 | .editor_controls {
19 | position: relative;
20 | width: var(--editor-width);
21 | flex-shrink: 0;
22 | overflow: hidden;
23 | display: flex;
24 | @include meta.load-css('components/main');
25 | @include mixins.font-smoothing;
26 | font-family: var(--font-family-body);
27 | &__wrapper {
28 | display: flex;
29 | flex-grow: 1;
30 | }
31 | a {
32 | color: colors.$color-dodger-shade;
33 | cursor: pointer;
34 | }
35 | }
36 |
37 | .plotly_editor {
38 | display: flex;
39 | /*
40 | We are defining the max height of the app so that the editor knows how big to be
41 | currently the editor will take up whatever space it can if it is not constrained in its parent
42 | */
43 | flex-grow: 1;
44 | height: 100%;
45 | max-height: 100%;
46 | width: 100%;
47 | }
48 |
49 | .plotly_editor_plot {
50 | max-width: 100%;
51 | height: 100%;
52 | max-height: 100%;
53 | overflow: auto;
54 | flex-grow: 1;
55 | }
56 |
--------------------------------------------------------------------------------
/src/styles/variables/_colors.scss:
--------------------------------------------------------------------------------
1 | $color-white: #ffffff;
2 |
3 | // --
4 | // Text and Backgrounds
5 | // --
6 | $color-rhino-core: #2a3f5f; // Active state text, emphasized text
7 | $color-rhino-dark: #506784; // Default text color
8 | $color-rhino-medium-1: #a2b1c6;
9 | $color-rhino-medium-2: #c8d4e3; // Border Default
10 | $color-rhino-light-1: #dfe8f3; // Border Secondary
11 | $color-rhino-light-2: #ebf0f8; // Button Secondary
12 | $color-rhino-light-3: #f3f6fa; // Page Background
13 | $color-rhino-light-4: #fafbfd; // Modal Tabbed Content Background
14 | $color-rhino-light-5: #f8f8f9; // Modal Tabbed Content Background
15 |
16 | // --
17 | // Primary
18 | // $
19 | $color-dodger: #119dff; // Accent
20 | $color-dodger-shade: #0d76bf; // Link Hover
21 | $color-dodger-shade-mid: #0f89df; // Primary button hover
22 |
23 | // --
24 | // Accent
25 | // --
26 | $color-aqua: #09ffff;
27 | $color-aqua-shade: #19d3f3; // Body Text
28 | $color-lavender: #e763fa;
29 | $color-lavender-shade: #ab63fa;
30 | $color-lavender-shade-mid: #934bde;
31 | $color-lavender-shade-dark: #7b35c3;
32 | $color-cornflower: #636efa;
33 | $color-emerald: #00cc96; // CTA's on blue backgrounds
34 | $color-sienna: #ef553b; // CTA's on blue backgrounds
35 |
36 | // Color Map
37 | $colors: (
38 | white: $color-white,
39 | rhino-core: $color-rhino-core,
40 | rhino-dark: $color-rhino-dark,
41 | rhino-medium-1: $color-rhino-medium-1,
42 | rhino-medium-2: $color-rhino-medium-2,
43 | rhino-light-1: $color-rhino-light-1,
44 | rhino-light-2: $color-rhino-light-2,
45 | rhino-light-3: $color-rhino-light-3,
46 | rhino-light-4: $color-rhino-light-4,
47 | rhino-light-5: $color-rhino-light-5,
48 | dodger: $color-dodger,
49 | dodger-shade: $color-dodger-shade,
50 | dodger-shade-mid: $color-dodger-shade,
51 | aqua: $color-aqua,
52 | aqua-shade: $color-aqua-shade,
53 | lavender: $color-lavender,
54 | lavender-shade: $color-lavender-shade,
55 | lavender-shade-mid: $color-lavender-shade-mid,
56 | cornflower: $color-cornflower,
57 | emerald: $color-emerald,
58 | sienna: $color-sienna,
59 | );
60 |
--------------------------------------------------------------------------------
/src/styles/variables/_defaults.scss:
--------------------------------------------------------------------------------
1 | @forward 'colors';
2 | @forward 'typography';
3 | @forward 'units';
4 | @forward 'layout';
5 | @forward 'theme';
6 | $ENV: 'default' !default;
7 |
--------------------------------------------------------------------------------
/src/styles/variables/_layout.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Layout
3 | */
4 |
5 | :root {
6 | --layout-panel-width: 335px;
7 | --layout-sidebar-width: 100px;
8 | }
9 |
--------------------------------------------------------------------------------
/src/styles/variables/_main.scss:
--------------------------------------------------------------------------------
1 | @use 'defaults';
2 | @use 'typography';
3 |
4 | /*
5 | * Typography
6 | */
7 | $font-weight-light: var(--font-weight-light);
8 | $font-weight-normal: var(--font-weight-normal);
9 | $font-weight-semibold: var(--font-weight-semibold);
10 | $font-size: var(--font-size-body);
11 | $font-size-small: var(--font-size-small);
12 | $font-size-medium: var(--font-size-medium);
13 | $h5-size: var(--font-size-h5);
14 |
15 | /*
16 | * SPACING
17 | */
18 | $base-spacing-unit: var(--spacing-base-unit);
19 | $half-spacing-unit: var(--spacing-half-unit);
20 | $quarter-spacing-unit: var(--spacing-quarter-unit);
21 | $sixth-spacing-unit: var(--spacing-sixth-unit);
22 | $eighth-spacing-unit: var(--spacing-eighth-unit);
23 |
24 | /*
25 | * BORDERS
26 | */
27 |
28 | /*
29 | * Typography
30 | */
31 | $ff-default: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
32 |
33 | /*
34 | * Effects
35 | */
36 | $base-box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.2);
37 | $text-shadow--dark-ui: 0 1px 2px rgba(42, 63, 95, 0.7);
38 | $text-shadow--dark-ui--inactive: 0 1px 1px rgba(42, 63, 95, 0.4);
39 |
40 | @mixin drop-shadow--dark-ui {
41 | filter: drop-shadow($text-shadow--dark-ui);
42 | }
43 | @mixin drop-shadow--dark-ui--lighter {
44 | filter: drop-shadow($text-shadow--dark-ui--inactive);
45 | }
46 |
--------------------------------------------------------------------------------
/src/styles/variables/_typography.scss:
--------------------------------------------------------------------------------
1 | $font-weight-light: 400;
2 | $font-weight-normal: 500;
3 | $font-weight-semibold: 600;
4 | $font-weight-bold: 700;
5 | $font-size: 13px;
6 | $font-size-small: 12px;
7 | $font-size-medium: 14px;
8 | $font-size-large: 14px;
9 | $h5-size: 16px;
10 |
11 | $font: (
12 | size: (
13 | base: $font-size,
14 | small: $font-size-small,
15 | medium: $font-size-medium,
16 | large: $font-size-large,
17 | heading-base: 24px,
18 | heading-small: 18px,
19 | heading-large: 28px,
20 | h5: 16px,
21 | ),
22 | weight: (
23 | light: $font-weight-light,
24 | normal: $font-weight-normal,
25 | semibold: $font-weight-semibold,
26 | bold: $font-weight-bold,
27 | ),
28 | leading: (
29 | body: 1.6,
30 | head: 1.2,
31 | ),
32 | letter-spacing: (
33 | headings: 0.5px,
34 | ),
35 | family: (
36 | body: (
37 | 'Open Sans',
38 | --apple-default,
39 | sans-serif,
40 | ),
41 | headings: (
42 | 'Dosis',
43 | 'Arial',
44 | sans-serif,
45 | ),
46 | ),
47 | );
48 |
--------------------------------------------------------------------------------
/src/styles/variables/_units.scss:
--------------------------------------------------------------------------------
1 | $default-border-radius: 2px;
2 | $default-border-radius-5: 5px;
3 | $default-base-spacing-unit: 24px;
4 | $default-half-spacing-unit: 12px;
5 | $default-quarter-spacing-unit: 6px;
6 | $default-sixth-spacing-unit: 4px;
7 | $default-eighth-spacing-unit: 3px;
8 |
9 | $spacing: (
10 | base-unit: 24px,
11 | half-unit: 12px,
12 | quarter-unit: 6px,
13 | sixth-unit: 4px,
14 | eighth-unit: 3px,
15 | );
16 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 |
3 | module.exports = () => ({
4 | entry: ['react-hot-loader/patch', './dev/index.js'],
5 | output: {
6 | filename: 'bundle.js',
7 | },
8 | module: {
9 | rules: [
10 | {
11 | test: /\.js?$/,
12 | use: 'babel-loader',
13 | exclude: [/node_modules/],
14 | },
15 | {
16 | test: /\.(css|scss)?$/,
17 | use: ['style-loader', 'css-loader', 'sass-loader'],
18 | },
19 | ],
20 | },
21 | resolve: {
22 | alias: {
23 | 'react-dom': '@hot-loader/react-dom',
24 | },
25 | },
26 | plugins: [new webpack.IgnorePlugin({resourceRegExp: /vertx/})],
27 | devServer: {
28 | open: false,
29 | static: './dev',
30 | client: {overlay: {errors: true, warnings: false}},
31 | },
32 | devtool: 'eval-source-map',
33 | target: 'browserslist',
34 | });
35 |
--------------------------------------------------------------------------------