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%; +}