Skip to content

Commit 0e20d19

Browse files
authored
Merge pull request #813 from plotly/binsize
BinSize adjustments
2 parents e132b50 + a5ea833 commit 0e20d19

File tree

9 files changed

+188
-42
lines changed

9 files changed

+188
-42
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@
4545
"babel-traverse": "^6.26.0",
4646
"css-loader": "^0.28.11",
4747
"cssnano": "^3.10.0",
48-
"enzyme": "^3.1.0",
49-
"enzyme-adapter-react-16": "^1.0.4",
48+
"enzyme": "3.8.0",
49+
"enzyme-adapter-react-16": "1.7.1",
5050
"eslint": "^5.4.0",
5151
"eslint-config-prettier": "^3.0.1",
5252
"eslint-plugin-import": "^2.8.0",

src/EditorControls.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
shamefullyAdjustSplitStyleTargetContainers,
1212
shamefullyDeleteRelatedAnalysisTransforms,
1313
shamefullyAdjustSizeref,
14-
shamefullyAdjustBinSize,
1514
} from './shame';
1615
import {EDITOR_ACTIONS} from './lib/constants';
1716
import isNumeric from 'fast-isnumeric';
@@ -83,8 +82,6 @@ class EditorControls extends Component {
8382
for (const attr in payload.update) {
8483
const traceIndex = payload.traceIndexes[i];
8584

86-
shamefullyAdjustBinSize(graphDiv, payload, traceIndex);
87-
8885
const splitTraceGroup = payload.splitTraceGroup
8986
? payload.splitTraceGroup.toString()
9087
: null;
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import React, {Component} from 'react';
2+
import Field from './Field';
3+
import Dropdown from '../widgets/Dropdown';
4+
import NumericInput from '../widgets/NumericInput';
5+
import PropTypes from 'prop-types';
6+
import {connectToContainer} from 'lib';
7+
import {isDateTime} from 'plotly.js/src/lib';
8+
import {isJSDate} from 'plotly.js/src/lib/dates';
9+
10+
const MILLISECONDS_IN_SECOND = 1000;
11+
const MILLISECONDS_IN_MINUTE = MILLISECONDS_IN_SECOND * 60; // eslint-disable-line
12+
const MILLISECONDS_IN_DAY = MILLISECONDS_IN_MINUTE * 60 * 24; // eslint-disable-line
13+
const DAYS_IN_MONTH = 30;
14+
const MONTHS_IN_YEAR = 12; //eslint-disable-line
15+
16+
function getSmallestUnit(milliseconds) {
17+
const units = {
18+
seconds: MILLISECONDS_IN_SECOND,
19+
minutes: MILLISECONDS_IN_MINUTE,
20+
days: MILLISECONDS_IN_DAY,
21+
};
22+
23+
let smallestUnit = 'milliseconds';
24+
25+
['seconds', 'minutes', 'days'].forEach(unit => {
26+
if (
27+
milliseconds % units[unit] === 0 &&
28+
(smallestUnit === 'milliseconds' ||
29+
(smallestUnit !== 'milliseconds' &&
30+
milliseconds / units[smallestUnit] > milliseconds / units[unit]))
31+
) {
32+
smallestUnit = unit;
33+
}
34+
});
35+
36+
return smallestUnit;
37+
}
38+
39+
class UnconnectedAxisInterval extends Component {
40+
constructor(props) {
41+
super(props);
42+
43+
const initialUnit =
44+
props.fullValue && typeof props.fullValue === 'string' && props.fullValue[0] === 'M'
45+
? parseInt(props.fullValue.substring(1), 10) % MONTHS_IN_YEAR === 0
46+
? 'years'
47+
: 'months'
48+
: getSmallestUnit(props.fullValue);
49+
50+
this.state = {
51+
units: initialUnit,
52+
};
53+
}
54+
55+
update(value) {
56+
let adjustedValue = value < 0 ? 0 : value;
57+
58+
if (this.state.units === 'years') {
59+
adjustedValue = 'M' + adjustedValue * MONTHS_IN_YEAR;
60+
}
61+
62+
if (this.state.units === 'months') {
63+
adjustedValue = 'M' + adjustedValue;
64+
}
65+
66+
if (this.state.units === 'days') {
67+
adjustedValue = adjustedValue * MILLISECONDS_IN_DAY;
68+
}
69+
70+
if (this.state.units === 'minutes') {
71+
adjustedValue = adjustedValue * MILLISECONDS_IN_MINUTE;
72+
}
73+
74+
if (this.state.units === 'seconds') {
75+
adjustedValue = adjustedValue * MILLISECONDS_IN_SECOND;
76+
}
77+
78+
this.props.updatePlot(adjustedValue);
79+
}
80+
81+
onUnitChange(value) {
82+
const isFullValueMonthFormat =
83+
typeof this.props.fullValue === 'string' && this.props.fullValue[0] === 'M';
84+
85+
const milliseconds = isFullValueMonthFormat
86+
? parseInt(this.props.fullValue.substring(1), 10) * DAYS_IN_MONTH * MILLISECONDS_IN_DAY
87+
: this.props.fullValue;
88+
89+
this.setState({units: value});
90+
91+
if (['years', 'months'].includes(value)) {
92+
this.props.updatePlot('M' + Math.round(milliseconds / MILLISECONDS_IN_DAY / DAYS_IN_MONTH));
93+
} else {
94+
this.props.updatePlot(milliseconds);
95+
}
96+
}
97+
98+
getDisplayValue(value) {
99+
const numericValue =
100+
typeof value === 'string' && value[0] === 'M' ? parseInt(value.substring(1), 10) : value;
101+
102+
if (this.state.units === 'years') {
103+
return numericValue / MONTHS_IN_YEAR;
104+
}
105+
if (this.state.units === 'months') {
106+
return numericValue;
107+
}
108+
if (this.state.units === 'days') {
109+
return Math.round(numericValue / MILLISECONDS_IN_DAY);
110+
}
111+
if (this.state.units === 'minutes') {
112+
return Math.round(numericValue / MILLISECONDS_IN_MINUTE);
113+
}
114+
if (this.state.units === 'seconds') {
115+
return Math.round(numericValue / MILLISECONDS_IN_SECOND);
116+
}
117+
if (this.state.units === 'milliseconds') {
118+
return numericValue;
119+
}
120+
return null;
121+
}
122+
123+
render() {
124+
const _ = this.context.localize;
125+
const attrHead = this.props.attr.split('.')[0];
126+
const binStartValue = this.props.fullContainer[attrHead].start;
127+
const BinStartIsDate =
128+
typeof binStartValue === 'string' && (isDateTime(binStartValue) || isJSDate(binStartValue));
129+
130+
return BinStartIsDate ? (
131+
<Field {...this.props}>
132+
<Dropdown
133+
options={[
134+
{value: 'years', label: _('Years')},
135+
{value: 'months', label: _('Months')},
136+
{value: 'days', label: _('Days')},
137+
{value: 'minutes', label: _('Minutes')},
138+
{value: 'seconds', label: _('Seconds')},
139+
{value: 'milliseconds', label: _('Milliseconds')},
140+
]}
141+
clearable={false}
142+
onChange={value => this.onUnitChange(value)}
143+
value={this.state.units}
144+
/>
145+
<div style={{height: '7px', width: '100%', display: 'block'}}> </div>
146+
<NumericInput
147+
value={this.getDisplayValue(this.props.fullValue)}
148+
onUpdate={value => this.update(value)}
149+
editableClassName="AxisInterval-milliseconds"
150+
/>
151+
</Field>
152+
) : (
153+
<Field {...this.props}>
154+
<NumericInput
155+
value={this.props.fullValue}
156+
onUpdate={value => this.props.updatePlot(value)}
157+
/>
158+
</Field>
159+
);
160+
}
161+
}
162+
163+
UnconnectedAxisInterval.contextTypes = {
164+
localize: PropTypes.func,
165+
};
166+
167+
UnconnectedAxisInterval.propTypes = {
168+
fullValue: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
169+
updatePlot: PropTypes.func,
170+
attr: PropTypes.string,
171+
fullContainer: PropTypes.object,
172+
...Field.propTypes,
173+
};
174+
175+
export default connectToContainer(UnconnectedAxisInterval);

src/components/fields/derived.js

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -699,15 +699,3 @@ export const HoverColor = connectToContainer(UnconnectedColorPicker, {
699699
return plotProps;
700700
},
701701
});
702-
703-
export const BinSize = connectToContainer(UnconnectedNumeric, {
704-
modifyPlotProps: (props, context, plotProps) => {
705-
const {localize: _} = context;
706-
if (typeof plotProps.fullValue === 'string' && plotProps.fullValue[0] === 'M') {
707-
plotProps.fullValue = plotProps.fullValue.substring(1);
708-
plotProps.min = 1;
709-
plotProps.max = 12;
710-
plotProps.units = parseInt(plotProps.fullValue, 10) === 1 ? _('Month') : _('Months');
711-
}
712-
},
713-
});

src/components/fields/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import DropdownCustom from './DropdownCustom';
3232
import MultiColorPicker from './MultiColorPicker';
3333
import RectanglePositioner from './RectanglePositioner';
3434
import LocationSelector from './LocationSelector';
35+
import AxisInterval from './AxisInterval';
3536
import {
3637
AnnotationArrowRef,
3738
AnnotationRef,
@@ -58,7 +59,6 @@ import {
5859
HoveronDropdown,
5960
HovermodeDropdown,
6061
TickFormat,
61-
BinSize,
6262
} from './derived';
6363
import {LineDashSelector, LineShapeSelector} from './LineSelectors';
6464

@@ -124,6 +124,6 @@ export {
124124
LocationSelector,
125125
HoveronDropdown,
126126
HovermodeDropdown,
127-
BinSize,
127+
AxisInterval,
128128
NumericOrDate,
129129
};

src/components/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
HovermodeDropdown,
6161
TickFormat,
6262
NumericOrDate,
63+
AxisInterval,
6364
} from './fields';
6465

6566
import {
@@ -181,4 +182,5 @@ export {
181182
HoveronDropdown,
182183
HovermodeDropdown,
183184
NumericOrDate,
185+
AxisInterval,
184186
};

src/default_panels/StyleTracesPanel.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ import {
3434
VisibilitySelect,
3535
GroupCreator,
3636
NumericOrDate,
37+
AxisInterval,
3738
} from '../components';
3839
import {
3940
BinningDropdown,
4041
NumericReciprocal,
4142
ShowInLegend,
4243
TextInfo,
4344
HoveronDropdown,
44-
BinSize,
4545
} from '../components/fields/derived';
4646

4747
const StyleTracesPanel = (props, {localize: _}) => (
@@ -229,13 +229,13 @@ const StyleTracesPanel = (props, {localize: _}) => (
229229
<PlotlySection name={_('Binning')}>
230230
<NumericOrDate label={_('X Bin Start')} attr="xbins.start" axis="x" />
231231
<NumericOrDate label={_('X Bin End')} attr="xbins.end" axis="x" />
232-
<BinSize label={_('X Bin Size')} attr="xbins.size" axis="x" />
233232
<Numeric label={_('Max X Bins')} attr="nbinsx" />
233+
<AxisInterval label={_('X Bin Size')} attr="xbins.size" axis="x" />
234234

235235
<NumericOrDate label={_('Y Bin Start')} attr="ybins.start" axis="y" />
236236
<NumericOrDate label={_('Y Bin End')} attr="ybins.end" axis="y" />
237-
<BinSize label={_('Y Bin Size')} attr="ybins.size" axis="y" />
238237
<Numeric label={_('Max Y Bins')} attr="nbinsy" />
238+
<AxisInterval label={_('Y Bin Size')} attr="ybins.size" axis="y" />
239239
</PlotlySection>
240240
<PlotlySection label={_('Bar Position')}>
241241
<NumericOrDate label={_('Base')} attr="base" />

src/shame.js

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
*/
44
import {getFromId} from 'plotly.js/src/plots/cartesian/axis_ids';
55
import nestedProperty from 'plotly.js/src/lib/nested_property';
6-
import {isDateTime} from 'plotly.js/src/lib';
76

87
// Temporary fix for:
98
// https://github.com/plotly/react-chart-editor/issues/103
@@ -213,22 +212,3 @@ export const shamefullyAdjustSizeref = (gd, {update}) => {
213212
update['marker.sizemode'] = 'area';
214213
}
215214
};
216-
217-
export const shamefullyAdjustBinSize = (gd, {update}, traceIndexInDataArray) => {
218-
const traceIndexInFullDataArray = gd._fullData.filter(t => t.index === traceIndexInDataArray)[0]
219-
._expandedIndex;
220-
const binSizeAttrs = Object.keys(update).filter(attr => attr.includes('bins.size'));
221-
222-
binSizeAttrs.forEach(attr => {
223-
const attrHead = attr.split('.')[0];
224-
if (
225-
gd._fullData[traceIndexInFullDataArray] &&
226-
gd._fullData[traceIndexInFullDataArray][attrHead] &&
227-
gd._fullData[traceIndexInFullDataArray][attrHead].start &&
228-
isDateTime(gd._fullData[traceIndexInFullDataArray][attrHead].start)
229-
) {
230-
const monthNum = update[attr];
231-
update[attr] = 'M' + monthNum;
232-
}
233-
});
234-
};

src/styles/components/widgets/_numeric-input.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,7 @@
8484
height: 13px !important;
8585
fill: var(--color-text-base) !important;
8686
}
87+
88+
.AxisInterval-milliseconds {
89+
width: 50%;
90+
}

0 commit comments

Comments
 (0)