diff --git a/src/components/fields/AxesCreator.js b/src/components/fields/AxesCreator.js
index febe736d1..13e19d816 100644
--- a/src/components/fields/AxesCreator.js
+++ b/src/components/fields/AxesCreator.js
@@ -99,7 +99,6 @@ UnconnectedAxisCreator.propTypes = {
attr: PropTypes.string,
label: PropTypes.string,
options: PropTypes.array,
- canAddAxis: PropTypes.bool,
container: PropTypes.object,
fullContainer: PropTypes.object,
updateContainer: PropTypes.func,
diff --git a/src/components/fields/Field.js b/src/components/fields/Field.js
index a91bec8bb..399d4c6ff 100644
--- a/src/components/fields/Field.js
+++ b/src/components/fields/Field.js
@@ -73,6 +73,7 @@ class Field extends Component {
) : null}
{children}
+ {extraComponent ? extraComponent : null}
{multiValued && !suppressMultiValuedMessage ? (
{getMultiValueText('title', _)}
@@ -80,7 +81,6 @@ class Field extends Component {
{getMultiValueText('subText', _)}
) : null}
- {extraComponent ? extraComponent : null}
{units ? (
diff --git a/src/components/fields/GroupCreator.js b/src/components/fields/GroupCreator.js
new file mode 100644
index 000000000..e7fb7ff25
--- /dev/null
+++ b/src/components/fields/GroupCreator.js
@@ -0,0 +1,84 @@
+import React, {Component} from 'react';
+import {connectToContainer} from 'lib';
+import Field from './Field';
+import Dropdown from './Dropdown';
+import PropTypes from 'prop-types';
+import Button from '../widgets/Button';
+import {PlusIcon} from 'plotly-icons';
+import {MULTI_VALUED} from 'lib/constants';
+
+class UnconnectedGroupCreator extends Component {
+ getAllGroups() {
+ return [...new Set(this.context.data.map(t => t[this.props.attr]))].filter(g => Boolean(g));
+ }
+
+ canAddGroup() {
+ const {fullContainer, attr} = this.props;
+ const currentGroup = fullContainer[attr];
+ const currentTraceIndex = fullContainer.index;
+
+ if (fullContainer.index === MULTI_VALUED) {
+ return this.getAllGroups().length === 0;
+ }
+
+ return (
+ !currentGroup ||
+ this.context.fullData.some(d => d.index !== currentTraceIndex && d[attr] === currentGroup)
+ );
+ }
+
+ addAndUpdateGroup() {
+ const allGroups = this.context.fullData
+ .map(t => parseInt(t[this.props.attr], 10))
+ .filter(n => Number.isInteger(n));
+ // don't want to pass empty array to max
+ allGroups.push(0);
+
+ const lastGroupNumber = Math.max.apply(Math, allGroups);
+
+ this.props.updatePlot(lastGroupNumber + 1);
+ }
+
+ render() {
+ const {localize: _} = this.context;
+ const {attr, label, prefix, updatePlot} = this.props;
+
+ const options = [{label: _('None'), value: ''}];
+ const allGroups = this.getAllGroups();
+ allGroups.forEach(g => options.push({label: `${prefix} ${g}`, value: g}));
+ options.sort((a, b) => a.value - b.value);
+
+ const icon =
;
+ const addButton = this.canAddGroup() ? (
+