diff --git a/package.json b/package.json
index 396ded1c3..bf13bc8a3 100644
--- a/package.json
+++ b/package.json
@@ -45,8 +45,8 @@
"babel-traverse": "^6.26.0",
"css-loader": "^0.28.11",
"cssnano": "^3.10.0",
- "enzyme": "^3.1.0",
- "enzyme-adapter-react-16": "^1.0.4",
+ "enzyme": "3.8.0",
+ "enzyme-adapter-react-16": "1.7.1",
"eslint": "^5.4.0",
"eslint-config-prettier": "^3.0.1",
"eslint-plugin-import": "^2.8.0",
diff --git a/src/EditorControls.js b/src/EditorControls.js
index c70b3fab8..9a5458886 100644
--- a/src/EditorControls.js
+++ b/src/EditorControls.js
@@ -11,7 +11,6 @@ import {
shamefullyAdjustSplitStyleTargetContainers,
shamefullyDeleteRelatedAnalysisTransforms,
shamefullyAdjustSizeref,
- shamefullyAdjustBinSize,
} from './shame';
import {EDITOR_ACTIONS} from './lib/constants';
import isNumeric from 'fast-isnumeric';
@@ -83,8 +82,6 @@ class EditorControls extends Component {
for (const attr in payload.update) {
const traceIndex = payload.traceIndexes[i];
- shamefullyAdjustBinSize(graphDiv, payload, traceIndex);
-
const splitTraceGroup = payload.splitTraceGroup
? payload.splitTraceGroup.toString()
: null;
diff --git a/src/components/fields/AxisInterval.js b/src/components/fields/AxisInterval.js
new file mode 100644
index 000000000..cd460aa6a
--- /dev/null
+++ b/src/components/fields/AxisInterval.js
@@ -0,0 +1,175 @@
+import React, {Component} from 'react';
+import Field from './Field';
+import Dropdown from '../widgets/Dropdown';
+import NumericInput from '../widgets/NumericInput';
+import PropTypes from 'prop-types';
+import {connectToContainer} from 'lib';
+import {isDateTime} from 'plotly.js/src/lib';
+import {isJSDate} from 'plotly.js/src/lib/dates';
+
+const MILLISECONDS_IN_SECOND = 1000;
+const MILLISECONDS_IN_MINUTE = MILLISECONDS_IN_SECOND * 60; // eslint-disable-line
+const MILLISECONDS_IN_DAY = MILLISECONDS_IN_MINUTE * 60 * 24; // eslint-disable-line
+const DAYS_IN_MONTH = 30;
+const MONTHS_IN_YEAR = 12; //eslint-disable-line
+
+function getSmallestUnit(milliseconds) {
+ const units = {
+ seconds: MILLISECONDS_IN_SECOND,
+ minutes: MILLISECONDS_IN_MINUTE,
+ days: MILLISECONDS_IN_DAY,
+ };
+
+ let smallestUnit = 'milliseconds';
+
+ ['seconds', 'minutes', 'days'].forEach(unit => {
+ if (
+ milliseconds % units[unit] === 0 &&
+ (smallestUnit === 'milliseconds' ||
+ (smallestUnit !== 'milliseconds' &&
+ milliseconds / units[smallestUnit] > milliseconds / units[unit]))
+ ) {
+ smallestUnit = unit;
+ }
+ });
+
+ return smallestUnit;
+}
+
+class UnconnectedAxisInterval extends Component {
+ constructor(props) {
+ super(props);
+
+ const initialUnit =
+ props.fullValue && typeof props.fullValue === 'string' && props.fullValue[0] === 'M'
+ ? parseInt(props.fullValue.substring(1), 10) % MONTHS_IN_YEAR === 0
+ ? 'years'
+ : 'months'
+ : getSmallestUnit(props.fullValue);
+
+ this.state = {
+ units: initialUnit,
+ };
+ }
+
+ update(value) {
+ let adjustedValue = value < 0 ? 0 : value;
+
+ if (this.state.units === 'years') {
+ adjustedValue = 'M' + adjustedValue * MONTHS_IN_YEAR;
+ }
+
+ if (this.state.units === 'months') {
+ adjustedValue = 'M' + adjustedValue;
+ }
+
+ if (this.state.units === 'days') {
+ adjustedValue = adjustedValue * MILLISECONDS_IN_DAY;
+ }
+
+ if (this.state.units === 'minutes') {
+ adjustedValue = adjustedValue * MILLISECONDS_IN_MINUTE;
+ }
+
+ if (this.state.units === 'seconds') {
+ adjustedValue = adjustedValue * MILLISECONDS_IN_SECOND;
+ }
+
+ this.props.updatePlot(adjustedValue);
+ }
+
+ onUnitChange(value) {
+ const isFullValueMonthFormat =
+ typeof this.props.fullValue === 'string' && this.props.fullValue[0] === 'M';
+
+ const milliseconds = isFullValueMonthFormat
+ ? parseInt(this.props.fullValue.substring(1), 10) * DAYS_IN_MONTH * MILLISECONDS_IN_DAY
+ : this.props.fullValue;
+
+ this.setState({units: value});
+
+ if (['years', 'months'].includes(value)) {
+ this.props.updatePlot('M' + Math.round(milliseconds / MILLISECONDS_IN_DAY / DAYS_IN_MONTH));
+ } else {
+ this.props.updatePlot(milliseconds);
+ }
+ }
+
+ getDisplayValue(value) {
+ const numericValue =
+ typeof value === 'string' && value[0] === 'M' ? parseInt(value.substring(1), 10) : value;
+
+ if (this.state.units === 'years') {
+ return numericValue / MONTHS_IN_YEAR;
+ }
+ if (this.state.units === 'months') {
+ return numericValue;
+ }
+ if (this.state.units === 'days') {
+ return Math.round(numericValue / MILLISECONDS_IN_DAY);
+ }
+ if (this.state.units === 'minutes') {
+ return Math.round(numericValue / MILLISECONDS_IN_MINUTE);
+ }
+ if (this.state.units === 'seconds') {
+ return Math.round(numericValue / MILLISECONDS_IN_SECOND);
+ }
+ if (this.state.units === 'milliseconds') {
+ return numericValue;
+ }
+ return null;
+ }
+
+ render() {
+ const _ = this.context.localize;
+ const attrHead = this.props.attr.split('.')[0];
+ const binStartValue = this.props.fullContainer[attrHead].start;
+ const BinStartIsDate =
+ typeof binStartValue === 'string' && (isDateTime(binStartValue) || isJSDate(binStartValue));
+
+ return BinStartIsDate ? (
+
+ this.onUnitChange(value)}
+ value={this.state.units}
+ />
+
+ this.update(value)}
+ editableClassName="AxisInterval-milliseconds"
+ />
+
+ ) : (
+
+ this.props.updatePlot(value)}
+ />
+
+ );
+ }
+}
+
+UnconnectedAxisInterval.contextTypes = {
+ localize: PropTypes.func,
+};
+
+UnconnectedAxisInterval.propTypes = {
+ fullValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
+ updatePlot: PropTypes.func,
+ attr: PropTypes.string,
+ fullContainer: PropTypes.object,
+ ...Field.propTypes,
+};
+
+export default connectToContainer(UnconnectedAxisInterval);
diff --git a/src/components/fields/derived.js b/src/components/fields/derived.js
index 188d365e4..bac55cb9a 100644
--- a/src/components/fields/derived.js
+++ b/src/components/fields/derived.js
@@ -699,15 +699,3 @@ export const HoverColor = connectToContainer(UnconnectedColorPicker, {
return plotProps;
},
});
-
-export const BinSize = connectToContainer(UnconnectedNumeric, {
- modifyPlotProps: (props, context, plotProps) => {
- const {localize: _} = context;
- if (typeof plotProps.fullValue === 'string' && plotProps.fullValue[0] === 'M') {
- plotProps.fullValue = plotProps.fullValue.substring(1);
- plotProps.min = 1;
- plotProps.max = 12;
- plotProps.units = parseInt(plotProps.fullValue, 10) === 1 ? _('Month') : _('Months');
- }
- },
-});
diff --git a/src/components/fields/index.js b/src/components/fields/index.js
index 880def307..cfae37e96 100644
--- a/src/components/fields/index.js
+++ b/src/components/fields/index.js
@@ -32,6 +32,7 @@ import DropdownCustom from './DropdownCustom';
import MultiColorPicker from './MultiColorPicker';
import RectanglePositioner from './RectanglePositioner';
import LocationSelector from './LocationSelector';
+import AxisInterval from './AxisInterval';
import {
AnnotationArrowRef,
AnnotationRef,
@@ -58,7 +59,6 @@ import {
HoveronDropdown,
HovermodeDropdown,
TickFormat,
- BinSize,
} from './derived';
import {LineDashSelector, LineShapeSelector} from './LineSelectors';
@@ -124,6 +124,6 @@ export {
LocationSelector,
HoveronDropdown,
HovermodeDropdown,
- BinSize,
+ AxisInterval,
NumericOrDate,
};
diff --git a/src/components/index.js b/src/components/index.js
index 93c342ce9..7109c5704 100644
--- a/src/components/index.js
+++ b/src/components/index.js
@@ -60,6 +60,7 @@ import {
HovermodeDropdown,
TickFormat,
NumericOrDate,
+ AxisInterval,
} from './fields';
import {
@@ -181,4 +182,5 @@ export {
HoveronDropdown,
HovermodeDropdown,
NumericOrDate,
+ AxisInterval,
};
diff --git a/src/default_panels/StyleTracesPanel.js b/src/default_panels/StyleTracesPanel.js
index d290e25bd..28e7c4fd5 100644
--- a/src/default_panels/StyleTracesPanel.js
+++ b/src/default_panels/StyleTracesPanel.js
@@ -34,6 +34,7 @@ import {
VisibilitySelect,
GroupCreator,
NumericOrDate,
+ AxisInterval,
} from '../components';
import {
BinningDropdown,
@@ -41,7 +42,6 @@ import {
ShowInLegend,
TextInfo,
HoveronDropdown,
- BinSize,
} from '../components/fields/derived';
const StyleTracesPanel = (props, {localize: _}) => (
@@ -229,13 +229,13 @@ const StyleTracesPanel = (props, {localize: _}) => (
-
+
-
+
diff --git a/src/shame.js b/src/shame.js
index 3bd7fb625..14b6fcecd 100644
--- a/src/shame.js
+++ b/src/shame.js
@@ -3,7 +3,6 @@
*/
import {getFromId} from 'plotly.js/src/plots/cartesian/axis_ids';
import nestedProperty from 'plotly.js/src/lib/nested_property';
-import {isDateTime} from 'plotly.js/src/lib';
// Temporary fix for:
// https://github.com/plotly/react-chart-editor/issues/103
@@ -213,22 +212,3 @@ export const shamefullyAdjustSizeref = (gd, {update}) => {
update['marker.sizemode'] = 'area';
}
};
-
-export const shamefullyAdjustBinSize = (gd, {update}, traceIndexInDataArray) => {
- const traceIndexInFullDataArray = gd._fullData.filter(t => t.index === traceIndexInDataArray)[0]
- ._expandedIndex;
- const binSizeAttrs = Object.keys(update).filter(attr => attr.includes('bins.size'));
-
- binSizeAttrs.forEach(attr => {
- const attrHead = attr.split('.')[0];
- if (
- gd._fullData[traceIndexInFullDataArray] &&
- gd._fullData[traceIndexInFullDataArray][attrHead] &&
- gd._fullData[traceIndexInFullDataArray][attrHead].start &&
- isDateTime(gd._fullData[traceIndexInFullDataArray][attrHead].start)
- ) {
- const monthNum = update[attr];
- update[attr] = 'M' + monthNum;
- }
- });
-};
diff --git a/src/styles/components/widgets/_numeric-input.scss b/src/styles/components/widgets/_numeric-input.scss
index bb17b2a6c..be383d268 100644
--- a/src/styles/components/widgets/_numeric-input.scss
+++ b/src/styles/components/widgets/_numeric-input.scss
@@ -84,3 +84,7 @@
height: 13px !important;
fill: var(--color-text-base) !important;
}
+
+.AxisInterval-milliseconds {
+ width: 50%;
+}