diff --git a/src/DefaultEditor.js b/src/DefaultEditor.js index 2ea8abf38..df421feab 100644 --- a/src/DefaultEditor.js +++ b/src/DefaultEditor.js @@ -1,11 +1,13 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import { + SubPanel, ColorPicker, DataSelector, Dropdown, Flaglist, Fold, + Info, Numeric, Panel, PanelMenuWrapper, @@ -14,6 +16,7 @@ import { TraceAccordion, TraceSelector, } from './components'; +import {DEFAULT_FONTS} from './constants'; import {localize, connectLayoutToPlot} from './lib'; const LayoutPanel = connectLayoutToPlot(Panel); @@ -105,13 +108,7 @@ class DefaultEditor extends Component {
- +
@@ -144,30 +141,19 @@ class DefaultEditor extends Component {
- + - +
- + @@ -175,8 +161,8 @@ class DefaultEditor extends Component { label={_('Connect Gaps')} attr="connectgaps" options={[ - {value: true, label: 'Connect'}, - {value: false, label: 'Blank'}, + {label: _('Connect'), value: true}, + {label: _('Blank'), value: false}, ]} />
@@ -187,7 +173,6 @@ class DefaultEditor extends Component { +
+
+ + + +
+
+ + + +
+
+ +
+ + {_( + 'The positioning inputs are relative to the ' + + 'anchor points on the text box' + )} + + + +
+
+ + +
+
+ +
+
+
diff --git a/src/PlotlyEditor.css b/src/PlotlyEditor.css index bda622f7e..41922a6f4 100644 --- a/src/PlotlyEditor.css +++ b/src/PlotlyEditor.css @@ -121,6 +121,10 @@ padding: 0 12px; } +.plotly-editor__field__no-title--center { + text-align: center; +} + .plotly-editor__field__title { padding-left: 0.5em; color: #69738a; @@ -136,7 +140,7 @@ padding-right: 12px; } -.plotly-editor__field__widget--prefix { +.plotly-editor__field__widget--postfix { width: 50%; padding-right: 0px; } diff --git a/src/__tests__/connectLayoutToPlot-test.js b/src/__tests__/connectLayoutToPlot-test.js new file mode 100644 index 000000000..8b18bff0a --- /dev/null +++ b/src/__tests__/connectLayoutToPlot-test.js @@ -0,0 +1,71 @@ +import {Numeric} from '../components/fields'; +import {Fold, Panel, Section} from '../components/containers'; +import NumericInput from '../components/widgets/NumericInputStatefulWrapper'; +import React from 'react'; +import {EDITOR_ACTIONS} from '../constants'; +import {TestEditor, fixtures, plotly} from '../lib/test-utils'; +import {connectLayoutToPlot} from '../lib'; +import {mount} from 'enzyme'; + +const Layouts = [Panel, Fold, Section].map(connectLayoutToPlot); +const Editor = props => ( + +); + +Layouts.forEach(Layout => { + describe(`<${Layout.displayName}>`, () => { + it(`wraps container with fullValue pointing to gd._fullLayout`, () => { + const wrapper = mount( + + + + + + ) + .find('[attr="width"]') + .find(NumericInput); + + expect(wrapper.prop('value')).toBe(100); + }); + + it(`sends updates to gd._layout`, () => { + const onUpdate = jest.fn(); + const wrapper = mount( + + + + + + ) + .find('[attr="width"]') + .find(NumericInput); + + wrapper.prop('onChange')(200); + const event = onUpdate.mock.calls[0][0]; + expect(event.type).toBe(EDITOR_ACTIONS.UPDATE_LAYOUT); + expect(event.payload).toEqual({update: {width: 200}}); + }); + + it(`automatically computes min and max defaults`, () => { + const onUpdate = jest.fn(); + const wrapper = mount( + + + + + + ) + .find('[attr="legend.x"]') + .find(NumericInput); + + expect(wrapper.prop('min')).toBe(-2); + expect(wrapper.prop('max')).toBe(3); + }); + }); +}); diff --git a/src/__tests__/connectTraceToPlot-test.js b/src/__tests__/connectTraceToPlot-test.js new file mode 100644 index 000000000..82c8788b4 --- /dev/null +++ b/src/__tests__/connectTraceToPlot-test.js @@ -0,0 +1,70 @@ +import {Numeric} from '../components/fields'; +import {Fold, Panel, Section} from '../components/containers'; +import NumericInput from '../components/widgets/NumericInputStatefulWrapper'; +import React from 'react'; +import {EDITOR_ACTIONS} from '../constants'; +import {TestEditor, fixtures, plotly} from '../lib/test-utils'; +import {connectTraceToPlot} from '../lib'; +import {mount} from 'enzyme'; + +const Traces = [Panel, Fold, Section].map(connectTraceToPlot); +const Editor = props => ( + +); + +const defaultMarkerSize = 6; + +Traces.forEach(Trace => { + describe(`<${Trace.displayName}>`, () => { + it('wraps container with fullValue pointing to gd._fullData[i]', () => { + const wrapper = mount( + + + + + + ) + .find('[attr="marker.size"]') + .find(NumericInput); + + expect(wrapper.prop('value')).toBe(defaultMarkerSize); + }); + + it('sends updates to gd.data', () => { + const onUpdate = jest.fn(); + const wrapper = mount( + + + + + + ) + .find('[attr="marker.size"]') + .find(NumericInput); + + wrapper.prop('onChange')(200); + const event = onUpdate.mock.calls[0][0]; + expect(event.type).toBe(EDITOR_ACTIONS.UPDATE_TRACES); + expect(event.payload).toEqual({ + update: {'marker.size': 200}, + traceIndexes: [0], + }); + }); + + it('automatically computes min and max defaults', () => { + const onUpdate = jest.fn(); + const wrapper = mount( + + + + + + ) + .find('[attr="marker.size"]') + .find(NumericInput); + + expect(wrapper.prop('min')).toBe(0); + expect(wrapper.prop('max')).toBe(undefined); + }); + }); +}); diff --git a/src/components/containers/Fold.js b/src/components/containers/Fold.js index 7c4d36696..d6cd510a1 100644 --- a/src/components/containers/Fold.js +++ b/src/components/containers/Fold.js @@ -8,11 +8,11 @@ export default class Fold extends Component { const {canDelete, name} = this.props; const doDelete = canDelete && typeof deleteContainer === 'function'; return ( -
+
{this.props.name} {doDelete ? ( @@ -28,9 +28,7 @@ export default class Fold extends Component { return (
{this.props.hideHeader ? null : this.renderHeader()} -
- {this.props.children} -
+
{this.props.children}
); } diff --git a/src/components/containers/Section.js b/src/components/containers/Section.js index 3691930bd..5b681598a 100644 --- a/src/components/containers/Section.js +++ b/src/components/containers/Section.js @@ -1,6 +1,7 @@ +import SubPanel from './SubPanel'; import React, {Component, cloneElement} from 'react'; import PropTypes from 'prop-types'; -import {bem} from '../../lib'; +import {icon} from '../../lib'; import unpackPlotProps from '../../lib/unpackPlotProps'; function childIsVisible(child) { @@ -11,42 +12,63 @@ class Section extends Component { constructor(props, context) { super(props, context); - this.children = this.processChildren(context); + this.children = null; + this.subPanel = null; + + this.processAndSetChildren(context); } componentWillReceiveProps(nextProps, nextContext) { - this.children = this.processChildren(nextContext); + this.processAndSetChildren(nextContext); } - processChildren(context) { + processAndSetChildren(context) { let children = this.props.children; if (!Array.isArray(children)) { children = [children]; } - children = children.filter(c => Boolean(c)); + + const attrChildren = []; + let subPanel = null; for (let i = 0; i < children.length; i++) { let child = children[i]; + if (!child) { + continue; + } + if (child.type === SubPanel) { + // Process the first subPanel. Ignore the rest. + if (subPanel) { + continue; + } + subPanel = child; + continue; + } let isAttr = !!child.props.attr; let plotProps = isAttr ? unpackPlotProps(child.props, context, child.constructor) - : {}; + : {isVisible: true}; let childProps = Object.assign({plotProps}, child.props); childProps.key = i; - - children[i] = cloneElement(child, childProps, child.children); + attrChildren.push(cloneElement(child, childProps)); } - return children; + this.children = attrChildren.length ? attrChildren : null; + this.subPanel = subPanel; } render() { - const hasVisibleChildren = this.children.some(childIsVisible); + const hasVisibleChildren = + (this.children && this.children.some(childIsVisible)) || + Boolean(this.subPanel); return hasVisibleChildren ? ( -
-
{this.props.name}
+
+
+ {this.props.name} + {this.subPanel} +
{this.children}
) : null; diff --git a/src/components/containers/SubPanel.js b/src/components/containers/SubPanel.js new file mode 100644 index 000000000..e20e9a5fc --- /dev/null +++ b/src/components/containers/SubPanel.js @@ -0,0 +1,42 @@ +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; + +export default class SubPanel extends Component { + constructor() { + super(); + this.state = {isVisible: false}; + + this.toggleVisibility = this.toggleVisibility.bind(this); + } + + toggleVisibility() { + this.setState({isVisible: !this.state.isVisible}); + } + + render() { + const toggleClass = `subpanel__toggle ${this.props.toggleIconClass}`; + const isVisible = this.props.show || this.state.isVisible; + return ( + + + + + {isVisible ? ( +
+
+
{this.props.children}
+
+ ) : null} + + ); + } +} + +SubPanel.propTypes = { + toggleIconClass: PropTypes.string.isRequired, + show: PropTypes.bool, +}; + +SubPanel.defaultProps = { + toggleIconClass: 'plotlyjs_editor__icon-cog', +}; diff --git a/src/components/containers/__tests__/Section-test.js b/src/components/containers/__tests__/Section-test.js index 445e4b89d..702d9e9d9 100644 --- a/src/components/containers/__tests__/Section-test.js +++ b/src/components/containers/__tests__/Section-test.js @@ -1,6 +1,7 @@ import React from 'react'; import Section from '../Section'; -import {Flaglist, Numeric} from '../../fields'; +import SubPanel from '../SubPanel'; +import {Flaglist, Info, Numeric} from '../../fields'; import {TestEditor, fixtures, plotly} from '../../../lib/test-utils'; import {connectTraceToPlot} from '../../../lib'; import {mount} from 'enzyme'; @@ -47,6 +48,24 @@ describe('Section', () => { ).toBe(false); }); + it('is visible if it contains any non attr children', () => { + const wrapper = mount( + +
+ INFO +
+
+ ).find('[name="test-section"]'); + + expect(wrapper.children().length).toBe(1); + expect(wrapper.find(Info).exists()).toBe(true); + expect(wrapper.find(Info).text()).toBe('INFO'); + }); + it('is not visible if it contains no visible children', () => { // pull and hole are not scatter attrs. Section should not show. const wrapper = mount( @@ -71,4 +90,26 @@ describe('Section', () => { expect(wrapper.find(Flaglist).exists()).toBe(false); expect(wrapper.find(Numeric).exists()).toBe(false); }); + + it('will render first subPanel even with no visible attrs', () => { + const wrapper = mount( + +
+ + INFO + + + MISINFORMATION + +
+
+ ).find('[name="test-section"]'); + + expect(wrapper.find(Info).length).toBe(1); + expect(wrapper.find(Info).text()).toBe('INFO'); + }); }); diff --git a/src/components/containers/index.js b/src/components/containers/index.js index 2b4148a82..af6ee56e2 100644 --- a/src/components/containers/index.js +++ b/src/components/containers/index.js @@ -1,6 +1,7 @@ +import SubPanel from './SubPanel'; import Fold from './Fold'; import Panel from './Panel'; import Section from './Section'; import TraceAccordion from './TraceAccordion'; -export {Fold, Panel, Section, TraceAccordion}; +export {SubPanel, Fold, Panel, Section, TraceAccordion}; diff --git a/src/components/fields/Color.js b/src/components/fields/Color.js index f3e9b7d63..0b24eb07a 100644 --- a/src/components/fields/Color.js +++ b/src/components/fields/Color.js @@ -1,12 +1,13 @@ -import React, {Component} from 'react'; import ColorPicker from '../widgets/ColorPicker'; import Field from './Field'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; import {bem, connectToContainer} from '../../lib'; class Color extends Component { render() { return ( - + + + @@ -14,16 +13,21 @@ export default class Field extends Component {
); - widgetModifier = ['prefix']; } - if (this.props.noTitle) { + + if (!this.props.label) { + const noTitleModifier = this.props.center ? ['center'] : null; return (
-
{this.props.children}
+
+ {this.props.children} +
{postfix}
); } + + const widgetModifier = this.props.postfix ? ['postfix'] : null; return (
@@ -39,7 +43,11 @@ export default class Field extends Component { } Field.propTypes = { + center: PropTypes.bool, label: PropTypes.string, - noTitle: PropTypes.bool, postfix: PropTypes.string, }; + +Field.defaultProps = { + center: false, +}; diff --git a/src/components/fields/Flaglist.js b/src/components/fields/Flaglist.js index 79a74be8c..49ebbd8be 100644 --- a/src/components/fields/Flaglist.js +++ b/src/components/fields/Flaglist.js @@ -1,12 +1,13 @@ -import React, {Component} from 'react'; -import FlaglistCheckboxGroup from '../widgets/FlaglistCheckboxGroup'; import Field from './Field'; +import FlaglistCheckboxGroup from '../widgets/FlaglistCheckboxGroup'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; import {bem, connectToContainer} from '../../lib'; class Flaglist extends Component { render() { return ( - + {this.props.children}; + } +} + +Info.propTypes = { + ...Field.propTypes, +}; diff --git a/src/components/fields/Numeric.js b/src/components/fields/Numeric.js index 8b820e288..fa1222368 100644 --- a/src/components/fields/Numeric.js +++ b/src/components/fields/Numeric.js @@ -1,12 +1,13 @@ -import React, {Component} from 'react'; -import NumericInput from '../widgets/NumericInputStatefulWrapper'; import Field from './Field'; +import NumericInput from '../widgets/NumericInputStatefulWrapper'; +import PropTypes from 'prop-types'; +import React, {Component} from 'react'; import {bem, connectToContainer} from '../../lib'; class Numeric extends Component { render() { return ( - + + overrides {center: false} +// default prop. This can be overridden manually using props for . +Radio.defaultProps = { + center: true, +}; + export default connectToContainer(Radio); diff --git a/src/components/fields/__tests__/Radio-test.js b/src/components/fields/__tests__/Radio-test.js new file mode 100644 index 000000000..631646cb2 --- /dev/null +++ b/src/components/fields/__tests__/Radio-test.js @@ -0,0 +1,50 @@ +import React from 'react'; +import Field from '../Field'; +import Radio from '../Radio'; +import {Section} from '../../containers'; +import {TestEditor, fixtures, plotly} from '../../../lib/test-utils'; +import {connectTraceToPlot} from '../../../lib'; +import {mount} from 'enzyme'; + +const Trace = connectTraceToPlot(Section); + +describe('', () => { + it('enables centering by default', () => { + const wrapper = mount( + + + + + + ).find(Field); + + expect(wrapper.prop('center')).toBe(true); + }); + + it('permits centering to be disabled', () => { + const wrapper = mount( + + + + + + ).find(Field); + + expect(wrapper.prop('center')).toBe(false); + }); +}); diff --git a/src/components/fields/index.js b/src/components/fields/index.js index b252c8306..a18b3f12f 100644 --- a/src/components/fields/index.js +++ b/src/components/fields/index.js @@ -1,6 +1,7 @@ import ColorPicker from './Color'; import Dropdown from './Dropdown'; import Flaglist from './Flaglist'; +import Info from './Info'; import Radio from './Radio'; import DataSelector from './DataSelector'; import Numeric from './Numeric'; @@ -10,6 +11,7 @@ export { ColorPicker, Dropdown, Flaglist, + Info, Radio, DataSelector, Numeric, diff --git a/src/components/index.js b/src/components/index.js index 734b56c4d..4bd7f548c 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -2,21 +2,24 @@ import { ColorPicker, Dropdown, Flaglist, + Info, Radio, DataSelector, Numeric, TraceSelector, } from './fields'; -import {Fold, Panel, Section, TraceAccordion} from './containers'; +import {SubPanel, Fold, Panel, Section, TraceAccordion} from './containers'; import PanelMenuWrapper from './PanelMenuWrapper'; export { + SubPanel, ColorPicker, DataSelector, Dropdown, Flaglist, Fold, + Info, Numeric, Panel, PanelMenuWrapper, diff --git a/src/components/widgets/AccordionMenu.js b/src/components/widgets/AccordionMenu.js deleted file mode 100644 index 22d5e69fb..000000000 --- a/src/components/widgets/AccordionMenu.js +++ /dev/null @@ -1,405 +0,0 @@ -import AccordionMenuItem from "./AccordionMenuItem"; -import Dropdown from "./widgets/Dropdown"; -import R from "ramda"; -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; -import { dropdownOptionShape } from "@workspace/utils/customPropTypes"; - -export class SimpleAddMenu extends Component { - render() { - const { addNewMenuClass, disableAddMenu, addNewMenuHandler } = this.props; - - const className = classnames( - "btnbase", - "btn--primary", - "js-test-add-new-layer", - "+float-right", - addNewMenuClass, - { "--disabled": disableAddMenu } - ); - const buttonText = this.props.addMenuText || ""; - - return ( -
- {"+ " + buttonText} -
- ); - } -} - -/* - * See the main AccordionMenu component PropTypes section for more information. - */ -SimpleAddMenu.propTypes = { - addNewMenuClass: PropTypes.string, - addNewMenuHandler: PropTypes.func, - disableAddMenu: PropTypes.bool, - addMenuText: PropTypes.string, -}; - -export class DropdownAddMenu extends Component { - constructor(props) { - super(props); - } - - render() { - const { title, options } = this.props.addNewMenuConfig; - - const simpleAddMenuPlaceholder = ; - - /* - * Always display `+` (SimpleAddMenu) and show a - * dropdown when you click on it. - * We're always displaying the `+` by returning SimpleAddMenu in - * the valueRenderer and the placeholder - */ - return ( -
- simpleAddMenuPlaceholder} - /> -
- ); - } -} - -/* - * See the main AccordionMenu component PropTypes section for more information. - */ -DropdownAddMenu.propTypes = { - addNewMenuConfig: PropTypes.shape({ - title: PropTypes.string.isRequired, - options: PropTypes.arrayOf(dropdownOptionShape).isRequired, - }), - addNewMenuClass: PropTypes.string, - addNewMenuHandler: PropTypes.func, -}; - -function idOrIndex(id, index) { - if (typeof id === "string") { - return id; - } - - return String(index); -} - -/* - * Each AccordionMenuItem is passed in as an object and its content - * as an element - * The expand and collapse buttons set the state of each submenu - */ -export default class AccordionMenu extends Component { - constructor(props) { - super(props); - this.state = this.initialState(props); - - this.toggleAllHandler = this.toggleAll.bind(this); - - // Used to construct a collapse/expand handler for each sub-menu. - const makeHandler = index => this.toggleSubMenu(index); - this.toggleSubMenuHandlerConstructor = makeHandler.bind(this); - } - - componentWillReceiveProps(newProps) { - const newSubmenuStates = this.initialState(newProps).subMenuStates; - - /* - * Only update openess state when adding a new AccordionMenuItem - * e.g for +Trace / +Filter / +Note - */ - if ( - this.props.assignOpenState && - newProps.subMenus.length !== Object.keys(this.state.subMenuStates).length - ) { - this.setState({ - subMenuStates: newSubmenuStates, - }); - return; - } - - // Only update sub-menu states of those that are newly added. - this.setState({ - subMenuStates: R.merge(newSubmenuStates, this.state.subMenuStates), - }); - } - - initialState(props) { - const subMenuStates = {}; - props.subMenus.forEach((subMenu, i) => { - const id = idOrIndex(subMenu.id, i); - subMenuStates[id] = subMenu.isOpen; - }); - - return { subMenuStates }; - } - - /** - * Check if any sub-menu is open. - * @return {Boolean} True if all sub-menus are closed. - */ - anySubMenusOpen() { - return R.any(R.identity, this.getMenuStates()); - } - - /** - * Set all the sub-menus to a given state. - * @param {Boolean} state the collapse state for all sub-menus - */ - setMenuStates(state) { - const subMenuStates = {}; - const setState = (menu, index) => { - const id = idOrIndex(menu.id, index); - subMenuStates[id] = state; - }; - this.props.subMenus.forEach(setState); - this.setState({ subMenuStates }); - } - - /** - * Get all the sub-menu collapse states. - * @return {Boolean[]} state: the collapse state for all sub-menus - */ - getMenuStates() { - const getState = id => this.state.subMenuStates[id]; - const menuStates = Object.keys(this.state.subMenuStates).map(getState); - return menuStates; - } - - /** - * Given the state of all menus, expand or collapse all menus at once. - */ - toggleAll() { - /* - * If any sub-menu is open, collapse all sub-menus - * Else expand all sub-menus - */ - - if (this.anySubMenusOpen()) { - this.setMenuStates(false); - } else { - this.setMenuStates(true); - } - } - - /** - * Toggle an individual submenu - * @param {Number} id: The sub-menu ref id. - */ - toggleSubMenu(id) { - this.setState({ - subMenuStates: R.merge(this.state.subMenuStates, { - [id]: !this.state.subMenuStates[id], - }), - }); - } - - /** - * Renders each sub-menu in the accordion menu. - * @return {AccordionMenuItem[]} sub-menu DOM elements - */ - renderSubmenuItems() { - return this.props.subMenus.map((subMenuConfig, i) => { - const { title, titleColor, iconClass, content } = subMenuConfig; - - const isRemovable = R.pathOr( - R.has("removeMenuHandler", this.props), - ["isRemovable"], - subMenuConfig - ); - - const id = idOrIndex(subMenuConfig.id, i); - - return ( - this.toggleSubMenuHandlerConstructor(id)} - accordionMenuModifier={this.props.accordionMenuModifier || null} - > - {content} - - ); - }); - } - - render() { - const { addNewMenuHandler, addNewMenuConfig, header } = this.props; - - /* - * If the developer passes in a handler we are going to set up - * an add new menu UI. - */ - const doAddMenu = typeof addNewMenuHandler === "function"; - - /* - * if the developer doesn't pass in options we just call the handler - * when the user clicks on a "+" button. - */ - const doSimpleAddMenu = doAddMenu && !addNewMenuConfig; - - /* - * if the developer passes in options we are going to set up a - * standalone box with a dropdown and a "+" button. - */ - const doDropdownAddMenu = doAddMenu && Boolean(addNewMenuConfig); - - /* - * Only render the components when needed. - */ - const simpleAddMenu = doSimpleAddMenu ? ( - - ) : null; - - const dropdownAddMenu = doDropdownAddMenu ? ( - - ) : null; - - let collapseButtonText; - if (this.anySubMenusOpen()) { - collapseButtonText = "Collapse All"; - } else { - collapseButtonText = "Expand All"; - } - - const collapseButton = - this.props.subMenus.length > 0 ? ( -
- {this.anySubMenusOpen() ? ( - - ) : ( - - )} - {collapseButtonText} -
- ) : null; - - const accordionClassName = classnames( - "accordion-menu__button-area", - "+clearfix" - ); - - return ( -
-
- {collapseButton} - {simpleAddMenu} - {dropdownAddMenu} -
- {header} -
- {this.props.subMenus && this.props.subMenus.length > 0 ? ( - this.renderSubmenuItems() - ) : ( - this.props.placeholder && this.props.placeholder() - )} -
-
- ); - } -} - -AccordionMenu.defaultProps = { - assignOpenState: false, -}; - -AccordionMenu.propTypes = { - subMenus: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, - titleColor: PropTypes.string, - isOpen: PropTypes.bool, - iconClass: PropTypes.string, - content: PropTypes.element, - id: PropTypes.string, - isRemovable: PropTypes.bool, - }) - ), - - /* - * If this handler is passed in as a prop the Accordion menu - * will have a "+" button in the top right with a click handler - * set to the this function. - */ - addNewMenuHandler: PropTypes.func, - - // Make add layer button unclickable - disableAddMenu: PropTypes.bool, - - /* - * A class in which to style the "+" button (if it is shown) - */ - addNewMenuClass: PropTypes.string, - - /* - * Text to include in the "+" button (if it is shown) - */ - addMenuText: PropTypes.string, - - /* - * If this object is present the accordian menu will show a "create - * new menu" box with a dropdown consisting of the options - * given in the list instead of the simple "+" button. The - * addNewMenuClass handler will be passed the value in the dropdown - * once the user clicks the plus button. - */ - addNewMenuConfig: PropTypes.shape({ - title: PropTypes.string.isRequired, - options: PropTypes.arrayOf(dropdownOptionShape).isRequired, - }), - - /* - * If this handler is passed in submenus will show an 'x' - * which when called evokes this function with the id - * of the submenu. - */ - removeMenuHandler: PropTypes.func, - - /* - * Optionally display a generic header item at the top of the AccordionMenu - */ - - header: PropTypes.element, - - parentClassName: PropTypes.string, - - /* - * On first render get open state from parent Component - */ - assignOpenState: PropTypes.bool, - - /* - * Class to modify default look of accoridon menu - */ - accordionMenuModifier: PropTypes.string, - - /* - * Optional method to render a placeholder when no subMenus present - */ - placeholder: PropTypes.func, -}; diff --git a/src/components/widgets/AccordionMenuItem.js b/src/components/widgets/AccordionMenuItem.js deleted file mode 100644 index fea9b13d7..000000000 --- a/src/components/widgets/AccordionMenuItem.js +++ /dev/null @@ -1,126 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import classnames from "classnames"; - -class AccordionMenuItem extends Component { - constructor(props) { - super(props); - this.handleRemoveMenu = this.handleRemoveMenu.bind(this); - this.renderMenuContent = this.renderMenuContent.bind(this); - } - - handleRemoveMenu(event) { - const { removeMenuHandler, id } = this.props; - - event.stopPropagation(); - - if (typeof removeMenuHandler === "function") { - removeMenuHandler(id); - } - } - - renderMenuContent() { - const { isOpen, children, id, accordionMenuModifier } = this.props; - - const subMenuDisplay = classnames( - "accordion-item__content", - `js-accordion-menu-content-${id}`, - accordionMenuModifier - ); - - if (!isOpen) { - return ( -
- {children} -
- ); - } - - return
{children}
; - } - - render() { - const { - isOpen, - removeMenuHandler, - titleColor, - onToggle, - iconClass, - title, - isRemovable, - } = this.props; - - const subMenuOpen = classnames( - ["accordion-item__top", "js-test-menu-item-click"], - { - "accordion-item__top--active": isOpen, - } - ); - - const iconDirection = classnames("accordion-item__top__arrow", { - "+rotate-90 ": !isOpen, - }); - - const topIcon = classnames("accordion-item__top__icon", { - "accordion-item__top__icon_active": isOpen, - }); - - const titleClass = classnames( - "js-test-title", - "accordion-item__top__title", - { - "accordion-item__top__title_active": isOpen, - } - ); - - const closeClass = classnames( - "icon-close-thin", - "accordion-item__top__close", - "js-accordion-menu-item-close" - ); - - let closeIcon = null; - if (isRemovable && typeof removeMenuHandler === "function") { - closeIcon = ; - } - - const titleStyling = { color: titleColor || "" }; - - return ( -
-
- -
- -
- - {iconClass ? ( -
- -
- ) : null} - -
{title}
-
- {closeIcon} -
- {this.renderMenuContent()} -
- ); - } -} - -AccordionMenuItem.propTypes = { - children: PropTypes.element.isRequired, - iconClass: PropTypes.string, - id: PropTypes.string.isRequired, - isOpen: PropTypes.bool, - onToggle: PropTypes.func, - removeMenuHandler: PropTypes.func, - title: PropTypes.string.isRequired, - titleColor: PropTypes.string, - accordionMenuModifier: PropTypes.string, - isRemovable: PropTypes.bool, -}; - -export default AccordionMenuItem; diff --git a/src/components/widgets/Anchor.js b/src/components/widgets/Anchor.js deleted file mode 100644 index dce4ec1ff..000000000 --- a/src/components/widgets/Anchor.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; -import Dropdown from "./widgets/Dropdown"; -import connectWorkspacePlot from "@workspace/utils/connectWorkspacePlot"; - -/* - * The Anchor component is a control for specifing the `anchor` axis property - * in plotly.js: https://plot.ly/javascript/reference/#layout-xaxis-anchor - */ - -const Anchor = ({ options, onChange, value }) => ( - -); - -Anchor.propTypes = { - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - options: PropTypes.array.isRequired, - axisLetter: PropTypes.oneOf(["x", "y"]), -}; - -function mapPlotToProps(plot, props) { - const { axisLetter } = props; - const options = plot - .keysAtPath(["_fullLayout"]) - .filter(key => key.startsWith(`${axisLetter}axis`)) - .map(axisName => ({ - label: axisName, - value: axisName.replace("axis", ""), - })); - options.unshift({ label: "Unanchored", value: "free" }); - return { options }; -} - -export default connectWorkspacePlot(mapPlotToProps)(Anchor); diff --git a/src/components/widgets/AnchorPositioning.js b/src/components/widgets/AnchorPositioning.js deleted file mode 100644 index a4a98a335..000000000 --- a/src/components/widgets/AnchorPositioning.js +++ /dev/null @@ -1,97 +0,0 @@ -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import hat from "hat"; - -/* - * Anchor Positioning. - * Positions: top left, top center, top right, middle left, middle center, - * middle right, bottom left, bottom center, bottom right - */ - -export default class AnchorPositioning extends Component { - constructor(props) { - super(props); - this.state = { - uid: hat(), - }; - - this.renderRadio = this.renderRadio.bind(this); - this.handleChange = this.handleChange.bind(this); - } - - handleChange(e) { - const newActiveOption = e.target.value; - this.props.onOptionChange(newActiveOption); - } - - renderRadio(value) { - const label = value; - const activeRadio = this.props.activeOption - ? this.props.activeOption - : "middle center"; - const defaultActive = activeRadio === value; - - return ( - - -
- - - - ); - } - - render() { - return ( -
-
-
-
-
-
-
-
-
-
- {this.props.backgroundText} -
-
-
- {this.renderRadio("top left")} - {this.renderRadio("top center")} - {this.renderRadio("top right")} -
-
- {this.renderRadio("middle left")} - {this.renderRadio("middle center")} - {this.renderRadio("middle right")} -
-
- {this.renderRadio("bottom left")} - {this.renderRadio("bottom center")} - {this.renderRadio("bottom right")} -
-
-
- ); - } -} - -AnchorPositioning.propTypes = { - onOptionChange: PropTypes.func.isRequired, - activeOption: PropTypes.string, - backgroundText: PropTypes.string, -}; - -AnchorPositioning.defaultProps = { - backgroundText: "Text...", -}; diff --git a/src/components/widgets/ArrowSelector.js b/src/components/widgets/ArrowSelector.js deleted file mode 100644 index d3fb49b15..000000000 --- a/src/components/widgets/ArrowSelector.js +++ /dev/null @@ -1,117 +0,0 @@ -import Dropdown from "./Dropdown"; -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import { MIXED_VALUES } from "@workspace/constants/workspace"; - -class ArrowSelector extends Component { - constructor(props) { - super(props); - this.state = { - activeOption: this.props.activeOption || 0, - }; - this.onSelect = this.onSelect.bind(this); - this.arrowGenerator = this.arrowGenerator.bind(this); - this.renderOption = this.renderOption.bind(this); - this.renderValue = this.renderValue.bind(this); - } - - componentWillReceiveProps(nextProps) { - if (nextProps.activeOption === MIXED_VALUES) { - // set the active option empty if it is MIXED_VALUES - this.setState({ - activeOption: "", - }); - return; - } - - // Reset the value to the graph's actual value - if (nextProps.activeOption !== this.state.activeOption) { - this.setState({ - activeOption: nextProps.activeOption, - }); - } - } - - arrowGenerator() { - const { Plotly } = this.props; - const arrowArray = Plotly.Annotations.ARROWPATHS; - const allArrows = arrowArray.map(each => { - return ( - - - - - ); - }); - - return allArrows.map((each, index) => { - return { - label: each, - value: index, - key: "arrow" + index, - }; - }); - } - - onSelect(chosenArrow) { - this.setState({ - activeOption: chosenArrow, - }); - - this.props.onChange(chosenArrow); - } - - renderOption(option) { - return ( -
  • -
    {option.label}
    -
  • - ); - } - - renderValue(option) { - return
    {option.label}
    ; - } - - render() { - return ( - - - - ); - } -} - -ArrowSelector.propTypes = { - activeOption: PropTypes.number, - onChange: PropTypes.func, - Plotly: PropTypes.object.isRequired, -}; - -export default ArrowSelector; diff --git a/src/components/widgets/AutomaticAnnotation.js b/src/components/widgets/AutomaticAnnotation.js deleted file mode 100644 index 453ea5e5b..000000000 --- a/src/components/widgets/AutomaticAnnotation.js +++ /dev/null @@ -1,333 +0,0 @@ -import AccordionMenuItem from "./AccordionMenuItem"; -import AnnotationEditor from "./annotation_editor/AnnotationEditor"; -import ColorPicker from "./ColorPicker"; -import FontSelector from "./FontSelector"; -import NumericInputStatefulWrapper from "./NumericInputStatefulWrapper"; -import RadioBlocks from "./RadioBlocks"; -import React, { Component } from "react"; -import PropTypes from "prop-types"; -import ToolMenuItem from "./ToolMenuItem"; -import { _ } from "../../lib"; -import { contains, has, empty, merge } from "ramda"; -import { relayout } from "@workspace/actions/workspace"; -// import { showNotification } from "@common/actions/notification"; - -export const DEFAULT_ANNOTATION_VALUES = { - ax: 0, - ay: -20, - annotationTemplate: "{x}, {y}", - - align: "left", - fontcolor: "rgb(60, 60, 60)", - fontsize: 12, - fontfamily: '"Open Sans", verdana, arial, sans-serif', - - showarrow: true, - arrowcolor: "rgb(60, 60, 60)", - arrowwidth: 1, -}; - -export default class AutomaticAnnotationWidget extends Component { - constructor() { - super(); - this.state = merge(DEFAULT_ANNOTATION_VALUES, { showControls: false }); - this.plotlyClickHandler = this.plotlyClickHandler.bind(this); - this.renderControls = this.renderControls.bind(this); - } - - componentDidMount() { - const gd = document.querySelector(".js-plot-panel"); - gd.on("plotly_click", this.plotlyClickHandler); - } - - componentWillUnmount() { - const gd = document.querySelector(".js-plot-panel"); - // TODO: removeListener seems to be the right API, but I've - // seen removeEventListener elsewhere in the code - gd.removeListener("plotly_click", this.plotlyClickHandler); - } - - plotlyClickHandler(clickObject) { - const { annotationsLength, dispatch } = this.props; - - const { - ax, - ay, - annotationTemplate, - align, - fontcolor, - fontsize, - fontfamily, - showarrow, - arrowcolor, - arrowwidth, - } = this.state; - - const relayoutObject = {}; - - for (var i = 0; i < clickObject.points.length; i++) { - const point = clickObject.points[i]; - - /* - * Annotations can only be bound to x and y axes - * 'z', 'lat', 'lon', etc axes aren't supported. - * Validate the the point has an 'xaxis' and 'yaxis' - * and no 'z' axis. - */ - if (!(has("xaxis", point) && has("yaxis", point))) { - showNotification(` - Clicking on data points to add annotations isn't supported - for this chart type. It is only supported for 2D plots.`); - continue; - } else if ( - has("xaxis", point) && - has("yaxis", point) && - has("zaxis", point) - ) { - showNotification(` - Adding annotations by clicking on points - isn't supported for 3D charts yet. - `); - continue; - } - - // fullData is confusingly named. this is really the full trace. - const trace = point.fullData; - const pointNumber = point.pointNumber; - const templateVariables = ["x", "y", "text", "z"]; - let text = annotationTemplate; - let annotationIndex = Number.isNaN(annotationsLength) - ? 0 - : annotationsLength; - for (let j = 0; j < templateVariables.length; j++) { - const t = templateVariables[j]; - - /* - * the point object contains data about the hover point - * that was clicked on. this might actually be different - * than the trace data itself. For example, in box plots, - * the point.x and point.y contains positional data of - * the 5 hover flags on the box plot - * (median, quartials, tails) not the underlying data. - * - * the 'text' data is found in the actual trace. - */ - - // 'x', 'y', 'z' data - if (has(t, point)) { - text = text.replace(`{${t}}`, point[t]); - } else if ( - has(t, trace) && - Array.isArray(trace[t]) && - point.pointNumber < trace[t].length - ) { - // 'text' data - text = text.replace(`{${t}}`, trace[t][pointNumber]); - } - } - - const annotation = { - text, - // position the annotation with an arrow (which may be transparent) - showarrow: DEFAULT_ANNOTATION_VALUES.showarrow, - x: point.x, - y: point.y, - xref: point.xaxis._id, - yref: point.yaxis._id, - ax, - ay, - arrowcolor: showarrow ? arrowcolor : "rgba(0, 0, 0, 0)", - - align, - font: { - color: fontcolor, - family: fontfamily, - size: fontsize, - }, - arrowwidth, - arrowhead: 0, - }; - - relayoutObject[`annotations[${annotationIndex}]`] = annotation; - annotationIndex += 1; - } - - if (empty(relayoutObject)) { - dispatch(relayout(relayoutObject)); - } - } - - renderControls() { - return ( -
    - - - {contains("
    ", this.state.annotationTemplate) ? ( - - { - this.setState({ align }); - }} - /> - - ) : null} - - - this.setState({ ay })} - step={10} - /> - - - - this.setState({ ax })} - step={10} - /> - - - {/* Standard Properties */} - - - { - this.setState({ fontcolor }); - }} - dispatch={this.props.dispatch} - selectedColor={this.state.fontcolor} - /> - - - - this.setState({ fontsize })} - step={10} - /> - - - - { - this.setState({ fontfamily }); - }} - dispatch={this.props.dispatch} - activeOption={this.state.fontfamily} - /> - - - - { - this.setState({ showarrow }); - }} - /> - - - {this.state.showarrow ? ( - - { - this.setState({ arrowcolor }); - }} - dispatch={this.props.dispatch} - selectedColor={this.state.color} - /> - - ) : null} - - {this.state.showarrow ? ( - - this.setState({ arrowwidth })} - step={1} - /> - - ) : null} -
    - ); - } - - render() { - return ( -
    -
    - {_("Add annotations by clicking on data points in the graph. ")} -
    - -
    - - this.setState({ - showControls: !this.state.showControls, - })} - title={"Annotation Template"} - > - {this.renderControls()} - - -
    -
    - {_("Done")} -
    -
    -
    -
    - ); - } -} - -AutomaticAnnotationWidget.propTypes = { - dispatch: PropTypes.func.isRequired, - annotationsLength: PropTypes.number.isRequired, - onClose: PropTypes.func.isRequired, -}; diff --git a/src/components/widgets/Button.js b/src/components/widgets/Button.js deleted file mode 100644 index eb998a55b..000000000 --- a/src/components/widgets/Button.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -class Button extends Component { - render() { - const { onClick, buttonText } = this.props; - - return ( -
    -
    - {buttonText} -
    -
    - ); - } -} - -Button.propTypes = { - onClick: PropTypes.func, - buttonText: PropTypes.string, -}; - -export default Button; diff --git a/src/components/widgets/CategorizedSelectTrace.js b/src/components/widgets/CategorizedSelectTrace.js deleted file mode 100644 index 0559d7964..000000000 --- a/src/components/widgets/CategorizedSelectTrace.js +++ /dev/null @@ -1,426 +0,0 @@ -import { - CHART_TYPE_ICON, - CATEGORY_LAYOUT, -} from "@workspace/constants/workspace"; -import { _ } from "../../lib"; -import { GET_ENCODING_SCHEMA } from "@workspace/constants/graphTable"; -import { immutableTraceSelectOptionsShape } from "@workspace/utils/customPropTypes"; -import { SELECT_PLOT_META } from "@workspace/constants/selectPlot"; -import * as WorkspaceActions from "@workspace/actions/workspace"; -import classnames from "classnames"; -import R from "ramda"; -import React, { Component } from "react"; -import PropTypes from "prop-types"; - -export const ESC_KEYCODE = 27; - -/* - * Scrolling over the menu overlay must be 'faked' via hard-updating the - * scroll-top property of EditModePanel. - */ -function smoothScroll(element, increment) { - if (element.scrollAmount) { - element.scrollAmount += increment; - } else { - element.scrollAmount = increment; - } - - window.requestAnimationFrame(() => { - const delta = Math.ceil(element.scrollAmount / 7); - element.scrollTop += delta; - element.scrollAmount -= delta; - if (Math.abs(element.scrollAmount - 0) > Number.MIN_VALUE * 100) { - smoothScroll(element, 0); - } else { - element.scrollAmount = 0; - } - }); -} - -/* - * This component provides a table style dropdown with chart types - * for each category of plots. It generates a map of lists as a skeleton - * for the table of choices where the chart categories are sequenced as - * described by CATEGORY_LAYOUT in workspace/constants/workspace.js - */ -class CategorizedSelectTrace extends Component { - constructor(props) { - super(props); - - this.handleSelectOption = this.handleSelectOption.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleToggle = this.handleToggle.bind(this); - this.closeWithEsc = this.closeWithEsc.bind(this); - this.handleScroll = this.handleScroll.bind(this); - this.handleScrollOnOverlay = this.handleScrollOnOverlay.bind(this); - this.renderIconContainer = this.renderIconContainer.bind(this); - } - - componentDidMount() { - window.addEventListener("click", this.handleClose); - window.addEventListener("keydown", this.closeWithEsc); - - // Handle scroll in capture mode not bubble mode. - window.addEventListener("scroll", this.handleScroll, true); - } - - componentWillUnmount() { - window.removeEventListener("click", this.handleClose); - window.removeEventListener("keydown", this.closeWithEsc); - window.removeEventListener("scroll", this.handleScroll); - - if (this.scrollDelayHandle) { - clearTimeout(this.scrollDelayHandle); - } - } - - getIconClassName(chartType, padding = true) { - const iconClass = CHART_TYPE_ICON[chartType]; - - return classnames({ - [iconClass]: Boolean(iconClass), - "+soft-half-right": padding && Boolean(iconClass), - }); - } - - getImgThumbnailSrc(chartType) { - const IMG_DIR = "/static/webapp/images/plot-thumbs/"; - - const imgSrc = IMG_DIR + SELECT_PLOT_META[chartType].imgThumb; - - return imgSrc; - } - - categorizedTraceOptions() { - /* - * Generates a map of lists which represents the chart types seperated - * into each chart category. - */ - const categorize = (categorizedOptions, option) => { - const category = GET_ENCODING_SCHEMA()[option.get("type")].meta.category; - const categoryOptions = categorizedOptions[category] || []; - - categoryOptions.push(option.toJS()); - - return R.assoc(category, categoryOptions, categorizedOptions); - }; - - return this.props.traceOptions.reduce(categorize, {}); - } - - selectedOption() { - const { selectedTraceValue, traceOptions } = this.props; - const selectedOptionPredicate = option => { - return option.get("value") === selectedTraceValue; - }; - - return traceOptions.find(selectedOptionPredicate).toJS(); - } - - handleSelectOption(selectOptionCallback) { - selectOptionCallback(); - this.props.onMenuToggle(false); - } - - handleClose() { - this.props.onMenuToggle(false); - } - - handleToggle(event) { - event.stopPropagation(); - this.props.onMenuToggle(); - } - - closeWithEsc(event) { - if (event.keyCode === ESC_KEYCODE) { - this.handleClose(); - } - } - - // Must reposition chart select menu when the user scrolls. - handleScroll() { - if (this.scrollDelayHandle) { - clearTimeout(this.scrollDelayHandle); - } - - if (this.props.isOpen) { - this.scrollDelayHandle = setTimeout(() => { - this.scrollDelayHandle = null; - this.handleRepositionOverlay(); - }, 10); - } - } - - /* - * Since scrolling over the overlay does scoll the element, no scroll event - * is fired and propgated. To allow the editModePanel to scroll, - * mousewheel events are used to simulate smooth scrolling. - */ - handleScrollOnOverlay(event) { - const editModePanel = document.querySelector("#js-edit-mode-panel"); - smoothScroll(editModePanel, event.deltaY); - } - - computeOverlayPosition() { - const styles = {}; - if (this.refs.input) { - const position = this.refs.input.getBoundingClientRect(); - styles.top = position.bottom; - styles.left = position.left; - } - return styles; - } - - handleRepositionOverlay() { - const overlayStyles = this.computeOverlayPosition(); - const overlay = document.querySelector("#js-chart-select-overlay"); - - /* - * In order to quickly update the position of the overlay - * the style property of the overlay is hard-updated (not via setState). - * This avoids the problems of the overlay only moving to the correct - * position after scrolling. - */ - overlay.style.left = `${overlayStyles.left}px`; - overlay.style.top = `${overlayStyles.top}px`; - } - - renderSelectInput() { - const { label, type } = this.selectedOption(); - const inputClassName = classnames( - "categorized-select-trace__input", - "dropdown-container", - "arrowless-categorized-select-trace__input", - "js-categorized-select-trace__input" - ); - - /* - * React select classes and DOM structure are reused to force - * the dropdown overlay to behave like other dropdown menus - * in the workspace. - * data-chart-type is used by Splinter to match against types - * as labels are purely a front-end construction. - */ - return ( -
    -
    -
    -
    - - - {label} - -
    - - - -
    -
    -
    - ); - } - - renderFooterMessage() { - if (!this.props.footerMessage) { - return null; - } - - const footerClassName = classnames( - "categorized-select-trace__overlay__footer", - "+weight-light" - ); - - return ( -
    - {_(this.props.footerMessage)} -
    - ); - } - - renderIconContainer(chartType) { - const { dispatch } = this.props; - let plotMeta = SELECT_PLOT_META[chartType] || {}; - let tipDirection = "right"; - let [feedLink, helpLink, exampleLink] = [null, null, null]; - - if (["scattermapbox", "choropleth", "scattergeo"].includes(chartType)) { - tipDirection = "left"; - } - - if (plotMeta.feedQuery) { - feedLink = ( -
    -