diff --git a/package.json b/package.json index 6756abf21..3319308fb 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "react-color": "^2.13.8", "react-colorscales": "^0.4.2", "react-dom": "^16.2.0", + "react-rangeslider": "^2.2.0", "react-select": "^1.0.0-rc.10", "react-tabs": "^2.2.1", "tinycolor2": "^1.4.1" diff --git a/src/components/fields/Numeric.js b/src/components/fields/Numeric.js index 6dd952a1d..1ebd75392 100644 --- a/src/components/fields/Numeric.js +++ b/src/components/fields/Numeric.js @@ -25,6 +25,7 @@ export class UnconnectedNumeric extends Component { onChange={this.props.updatePlot} onUpdate={this.props.updatePlot} showArrows={!this.props.hideArrows} + showSlider={this.props.showSlider} /> ); @@ -38,6 +39,7 @@ UnconnectedNumeric.propTypes = { max: PropTypes.number, multiValued: PropTypes.bool, hideArrows: PropTypes.bool, + showSlider: PropTypes.bool, step: PropTypes.number, updatePlot: PropTypes.func, ...Field.propTypes, diff --git a/src/components/fields/derived.js b/src/components/fields/derived.js index 5adc0b9c6..59ce5dbf0 100644 --- a/src/components/fields/derived.js +++ b/src/components/fields/derived.js @@ -105,14 +105,45 @@ export const AxesRange = connectToContainer(UnconnectedNumeric, { }, }); -class NumericFraction extends UnconnectedNumeric {} -NumericFraction.propTypes = UnconnectedNumeric.propTypes; -NumericFraction.defaultProps = { +class UnconnectedNumericFraction extends UnconnectedNumeric {} +UnconnectedNumericFraction.propTypes = UnconnectedNumeric.propTypes; +UnconnectedNumericFraction.defaultProps = { units: '%', + showSlider: true, }; +const numericFractionModifyPlotProps = (props, context, plotProps) => { + const {attrMeta, fullValue, updatePlot} = plotProps; + const min = attrMeta.min || 0; + const max = attrMeta.max || 1; + if (isNumeric(fullValue)) { + plotProps.fullValue = Math.round(100 * (fullValue - min) / (max - min)); + } + + plotProps.updatePlot = v => { + if (isNumeric(v)) { + updatePlot(v / 100 * (max - min) + min); + } else { + updatePlot(v); + } + }; + plotProps.max = 100; + plotProps.min = 0; +}; + +export const NumericFraction = connectToContainer(UnconnectedNumericFraction, { + modifyPlotProps: numericFractionModifyPlotProps, +}); + +export const LayoutNumericFraction = connectLayoutToPlot( + connectToContainer(UnconnectedNumericFraction, { + supplyPlotProps: supplyLayoutPlotProps, + modifyPlotProps: numericFractionModifyPlotProps, + }) +); + export const LayoutNumericFractionInverse = connectLayoutToPlot( - connectToContainer(NumericFraction, { + connectToContainer(UnconnectedNumericFraction, { supplyPlotProps: supplyLayoutPlotProps, modifyPlotProps: (props, context, plotProps) => { const {attrMeta, fullValue, updatePlot} = plotProps; @@ -142,36 +173,6 @@ export const LayoutNumericFractionInverse = connectLayoutToPlot( }) ); -export const LayoutNumericFraction = connectLayoutToPlot( - connectToContainer(NumericFraction, { - supplyPlotProps: supplyLayoutPlotProps, - modifyPlotProps: (props, context, plotProps) => { - const {attrMeta, fullValue, updatePlot} = plotProps; - if (isNumeric(fullValue)) { - plotProps.fullValue = fullValue * 100; - } - - plotProps.updatePlot = v => { - if (isNumeric(v)) { - updatePlot(v / 100); - } else { - updatePlot(v); - } - }; - - if (attrMeta) { - if (isNumeric(attrMeta.max)) { - plotProps.max = attrMeta.max * 100; - } - - if (isNumeric(attrMeta.min)) { - plotProps.min = attrMeta.min * 100; - } - } - }, - }) -); - export const AnnotationArrowRef = connectToContainer(UnconnectedDropdown, { modifyPlotProps: (props, context, plotProps) => { const {fullContainer: {xref, yref}, plotly, graphDiv} = context; @@ -271,6 +272,34 @@ export const PositioningRef = connectToContainer(UnconnectedDropdown, { }, }); +export const PositioningNumeric = connectToContainer(UnconnectedNumeric, { + modifyPlotProps: (props, context, plotProps) => { + const {fullContainer, fullValue, updatePlot} = plotProps; + if ( + fullContainer && + (fullContainer[props.attr[0] + 'ref'] === 'paper' || + fullContainer[props.attr[props.attr.length - 1] + 'ref'] === 'paper') + ) { + plotProps.units = '%'; + plotProps.showSlider = true; + plotProps.max = 100; + plotProps.min = 0; + plotProps.step = 1; + if (isNumeric(fullValue)) { + plotProps.fullValue = Math.round(100 * fullValue); + } + + plotProps.updatePlot = v => { + if (isNumeric(v)) { + updatePlot(v / 100); + } else { + updatePlot(v); + } + }; + } + }, +}); + function computeAxesRefOptions(axes) { const options = []; for (let i = 0; i < axes.length; i++) { diff --git a/src/components/fields/index.js b/src/components/fields/index.js index 47b910cfd..053742ba3 100644 --- a/src/components/fields/index.js +++ b/src/components/fields/index.js @@ -24,6 +24,9 @@ import { GeoProjections, GeoScope, HoverInfo, + NumericFraction, + PositioningNumeric, + NumericFractionInverse, LayoutNumericFraction, LayoutNumericFractionInverse, TraceOrientation, @@ -53,6 +56,9 @@ export { Info, LayoutNumericFraction, LayoutNumericFractionInverse, + NumericFraction, + NumericFractionInverse, + PositioningNumeric, LineDashSelector, LineShapeSelector, Numeric, diff --git a/src/components/index.js b/src/components/index.js index 1d004da59..e950d48fc 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -21,6 +21,9 @@ import { Info, LayoutNumericFraction, LayoutNumericFractionInverse, + NumericFraction, + PositioningNumeric, + NumericFractionInverse, LineDashSelector, LineShapeSelector, Numeric, @@ -81,6 +84,9 @@ export { GeoScope, HoverInfo, Info, + NumericFraction, + PositioningNumeric, + NumericFractionInverse, LayoutNumericFraction, LayoutNumericFractionInverse, LayoutPanel, diff --git a/src/components/widgets/NumericInput.js b/src/components/widgets/NumericInput.js index 6ae7ed70a..514925efb 100644 --- a/src/components/widgets/NumericInput.js +++ b/src/components/widgets/NumericInput.js @@ -2,6 +2,7 @@ import EditableText from './EditableText'; import React, {Component} from 'react'; import PropTypes from 'prop-types'; import isNumeric from 'fast-isnumeric'; +import Slider from 'react-rangeslider'; import {CarretDownIcon, CarretUpIcon} from 'plotly-icons'; export const UP_ARROW = 38; @@ -94,7 +95,7 @@ export default class NumericInput extends Component { } renderArrows() { - if (!this.props.showArrows) { + if (!this.props.showArrows || this.props.showSlider) { return null; } @@ -116,6 +117,23 @@ export default class NumericInput extends Component { ); } + renderSlider() { + if (!this.props.showSlider) { + return null; + } + + return ( + + ); + } + render() { return (
@@ -129,6 +147,7 @@ export default class NumericInput extends Component { onKeyDown={this.onKeyDown} /> {this.renderArrows()} + {this.renderSlider()}
); } @@ -143,6 +162,7 @@ NumericInput.propTypes = { onUpdate: PropTypes.func.isRequired, placeholder: PropTypes.string, showArrows: PropTypes.bool, + showSlider: PropTypes.bool, step: PropTypes.number, value: PropTypes.any, }; diff --git a/src/default_panels/StyleAxesPanel.js b/src/default_panels/StyleAxesPanel.js index 077255968..620459d67 100644 --- a/src/default_panels/StyleAxesPanel.js +++ b/src/default_panels/StyleAxesPanel.js @@ -6,6 +6,7 @@ import { Dropdown, FontSelector, Numeric, + NumericFraction, Radio, TextEditor, MenuPanel, @@ -292,18 +293,8 @@ const StyleAxesPanel = ({localize: _}) => (
- - + +
( {label: _('Stretch'), value: 'stretch'}, ]} /> - - + +
@@ -49,7 +49,7 @@ const StyleImagesPanel = ({localize: _}) => (
- +
@@ -66,7 +66,7 @@ const StyleImagesPanel = ({localize: _}) => (
- +
); diff --git a/src/default_panels/StyleNotesPanel.js b/src/default_panels/StyleNotesPanel.js index f0dc79b85..a3a92ec88 100644 --- a/src/default_panels/StyleNotesPanel.js +++ b/src/default_panels/StyleNotesPanel.js @@ -9,6 +9,7 @@ import { FontSelector, Info, Numeric, + PositioningNumeric, Radio, TextEditor, Section, @@ -40,8 +41,8 @@ const StyleNotesPanel = ({localize: _}) => ( - - + +
@@ -64,7 +65,7 @@ const StyleNotesPanel = ({localize: _}) => (
- +
@@ -87,7 +88,7 @@ const StyleNotesPanel = ({localize: _}) => (
- + ); diff --git a/src/default_panels/StyleShapesPanel.js b/src/default_panels/StyleShapesPanel.js index b0efbecba..66ddc07cf 100644 --- a/src/default_panels/StyleShapesPanel.js +++ b/src/default_panels/StyleShapesPanel.js @@ -5,6 +5,7 @@ import { Radio, Section, PositioningRef, + PositioningNumeric, Numeric, ColorPicker, LineDashSelector, @@ -32,14 +33,14 @@ const StyleShapesPanel = ({localize: _}) => (
- - + +
- - + +
diff --git a/src/default_panels/StyleTracesPanel.js b/src/default_panels/StyleTracesPanel.js index a343f1030..181df8212 100644 --- a/src/default_panels/StyleTracesPanel.js +++ b/src/default_panels/StyleTracesPanel.js @@ -10,6 +10,7 @@ import { LineDashSelector, LineShapeSelector, Numeric, + NumericFraction, Radio, TextEditor, Section, @@ -247,54 +248,25 @@ const StyleTracesPanel = ({localize: _}) => (
- - - - - - + + + + + -
- - - + + +
diff --git a/src/index.js b/src/index.js index 7dafb090a..cb8703e8d 100644 --- a/src/index.js +++ b/src/index.js @@ -40,6 +40,9 @@ import { Layout, LayoutNumericFraction, LayoutNumericFractionInverse, + NumericFraction, + PositioningNumeric, + NumericFractionInverse, LayoutPanel, LineDashSelector, LineShapeSelector, @@ -98,6 +101,9 @@ export { Layout, LayoutNumericFraction, LayoutNumericFractionInverse, + NumericFraction, + PositioningNumeric, + NumericFractionInverse, LayoutPanel, LineDashSelector, LineShapeSelector, diff --git a/src/styles/components/fields/_field.scss b/src/styles/components/fields/_field.scss index 39ddb35ff..297716237 100644 --- a/src/styles/components/fields/_field.scss +++ b/src/styles/components/fields/_field.scss @@ -40,6 +40,7 @@ } &__title { width: 30%; + //flex-shrink:0; padding-left: var(--spacing-half-unit); display: block; font-size: var(--font-size-small); diff --git a/src/styles/components/widgets/_main.scss b/src/styles/components/widgets/_main.scss index 9ddc2c0b2..7d8ac46b9 100644 --- a/src/styles/components/widgets/_main.scss +++ b/src/styles/components/widgets/_main.scss @@ -6,3 +6,4 @@ @import 'numeric-input'; @import 'radio-block'; @import 'text-editor'; +@import "rangeslider"; diff --git a/src/styles/components/widgets/_numeric-input.scss b/src/styles/components/widgets/_numeric-input.scss index c0661035c..160229fd5 100644 --- a/src/styles/components/widgets/_numeric-input.scss +++ b/src/styles/components/widgets/_numeric-input.scss @@ -1,20 +1,26 @@ -::-webkit-input-placeholder { /* Chrome/Opera/Safari */ +::-webkit-input-placeholder { + /* Chrome/Opera/Safari */ color: var(--color-text-placeholder); } -::-moz-placeholder { /* Firefox 19+ */ +::-moz-placeholder { + /* Firefox 19+ */ color: var(--color-text-placeholder); } -:-ms-input-placeholder { /* IE 10+ */ +:-ms-input-placeholder { + /* IE 10+ */ color: var(--color-text-placeholder); } -:-moz-placeholder { /* Firefox 18- */ +:-moz-placeholder { + /* Firefox 18- */ color: var(--color-text-placeholder); } - .numeric-input__wrapper { line-height: 20px; max-width: 100%; + width: 100%; + display: flex; + align-items: center; color: var(--color-text-base); } @@ -28,7 +34,8 @@ white-space: nowrap; text-align: left; border-radius: var(--border-radius-small); - padding: var(--spacing-quarter-unit) var(--spacing-quarter-unit) var(--spacing-quarter-unit) var(--spacing-half-unit); + padding: var(--spacing-quarter-unit) var(--spacing-quarter-unit) + var(--spacing-quarter-unit) var(--spacing-half-unit); width: 62px; vertical-align: middle; font-size: inherit; diff --git a/src/styles/components/widgets/_rangeslider.scss b/src/styles/components/widgets/_rangeslider.scss new file mode 100644 index 000000000..4880c950a --- /dev/null +++ b/src/styles/components/widgets/_rangeslider.scss @@ -0,0 +1,205 @@ +/** +* Rangeslider +*/ +.rangeslider { + margin: 0 var(--spacing-quarter-unit); + min-width: 60px; + position: relative; + background: var(--color-background-light); + -ms-touch-action: none; + touch-action: none; + border: 1px solid var(--color-border-default); + flex-grow: 1; + + + &, + .rangeslider__fill { + display: block; + } + .rangeslider__handle { + background: #fff; + border: 1px solid var(--color-border-default); + cursor: pointer; + display: inline-block; + position: absolute; + .rangeslider__active { + opacity: 1; + } + } + + .rangeslider__handle-tooltip { + $size: 20px; + width: $size; + height: $size; + text-align: center; + position: absolute; + background-color: rgba(0, 0, 0, 0.8); + font-weight: normal; + font-size: 14px; + transition: all 100ms ease-in; + border-radius: 4px; + display: inline-block; + color: white; + left: 50%; + transform: translate3d(-50%, 0, 0); + span { + margin-top: 12px; + display: inline-block; + line-height: 100%; + } + &:after { + content: ' '; + position: absolute; + width: 0; + height: 0; + } + } +} + +/** +* Rangeslider - Horizontal slider +*/ +.rangeslider-horizontal { + height: 6px; + border-radius: 10px; + .rangeslider__fill { + height: 100%; + background-color: var(--color-accent); + border: var(--border-accent); + border-radius: 10px; + transform: translateY(-1px); + top: 0; + } + .rangeslider__handle { + $size: 20px; + width: $size/3; + height: $size*1.5; + border-radius: $size; + top: 50%; + transform: translate3d(-50%, -50%, 0); + display: flex; + align-items: center; + justify-content: center; + &:after { + content: ' '; + position: absolute; + width: $size/5; + height: $size; + border-radius: $size/4; + background-color: var(--color-accent); + display: none; + } + } + .rangeslider__handle-tooltip { + top: -55px; + &:after { + border-left: 8px solid transparent; + border-right: 8px solid transparent; + border-top: 8px solid rgba(0, 0, 0, 0.8); + left: 50%; + bottom: -8px; + transform: translate3d(-50%, 0, 0); + } + } +} + +/** +* Rangeslider - Vertical slider +*/ +.rangeslider-vertical { + margin: 20px auto; + height: 150px; + max-width: 10px; + background-color: transparent; + + .rangeslider__fill, + .rangeslider__handle { + position: absolute; + } + + .rangeslider__fill { + width: 100%; + background-color: #7cb342; + box-shadow: none; + bottom: 0; + } + .rangeslider__handle { + width: 30px; + height: 10px; + left: -10px; + box-shadow: none; + } + .rangeslider__handle-tooltip { + left: -100%; + top: 50%; + transform: translate3d(-50%, -50%, 0); + &:after { + border-top: 8px solid transparent; + border-bottom: 8px solid transparent; + border-left: 8px solid rgba(0, 0, 0, 0.8); + left: 100%; + top: 12px; + } + } +} + +/** +* Rangeslider - Reverse +*/ + +.rangeslider-reverse { + &.rangeslider-horizontal { + .rangeslider__fill { + right: 0; + } + } + &.rangeslider-vertical { + .rangeslider__fill { + top: 0; + bottom: inherit; + } + } +} + +/** +* Rangeslider - Labels +*/ +.rangeslider__labels { + position: relative; + .rangeslider-vertical & { + position: relative; + list-style-type: none; + margin: 0 0 0 24px; + padding: 0; + text-align: left; + width: 250px; + height: 100%; + left: 10px; + + .rangeslider__label-item { + position: absolute; + transform: translate3d(0, -50%, 0); + + &::before { + content: ''; + width: 10px; + height: 2px; + background: black; + position: absolute; + left: -14px; + top: 50%; + transform: translateY(-50%); + z-index: -1; + } + } + } + + .rangeslider__label-item { + position: absolute; + font-size: 14px; + cursor: pointer; + display: inline-block; + top: 10px; + transform: translate3d(-50%, 0, 0); + } +}