diff --git a/src/components/multiRangeSlider/MultiRangeSlider.js b/src/components/multiRangeSlider/MultiRangeSlider.js new file mode 100644 index 000000000..5e0a9a478 --- /dev/null +++ b/src/components/multiRangeSlider/MultiRangeSlider.js @@ -0,0 +1,93 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import PropTypes from 'prop-types'; +import './multiRangeSlider.css'; + +const MultiRangeSlider = ({ min, max, onChange }) => { + const [minVal, setMinVal] = useState(min); + const [maxVal, setMaxVal] = useState(max); + const minValRef = useRef(min); + const maxValRef = useRef(max); + const range = useRef(null); + + // Convert to percentage + const getPercent = useCallback( + (value) => Math.round(((value - min) / (max - min)) * 100), + [min, max], + ); + + // Set width of the range to decrease from the left side + useEffect(() => { + const minPercent = getPercent(minVal); + const maxPercent = getPercent(maxValRef.current); + + if (range.current) { + range.current.style.left = `${minPercent}%`; + range.current.style.width = `${maxPercent - minPercent}%`; + } + }, [minVal, getPercent]); + + // Set width of the range to decrease from the right side + useEffect(() => { + const minPercent = getPercent(minValRef.current); + const maxPercent = getPercent(maxVal); + + if (range.current) { + range.current.style.width = `${maxPercent - minPercent}%`; + } + }, [maxVal, getPercent]); + + // Get min and max values when their state changes + useEffect(() => { + onChange({ min: minVal, max: maxVal }); + }, [minVal, maxVal, onChange]); + + return ( + <> +
+ {'Y axis range'} +
+
+ { + const value = Math.min(Number(event.target.value), maxVal - 1); + + setMinVal(value); + minValRef.current = value; + }} + className='mrs_thumb mrs_thumb--left' + style={{ zIndex: minVal > max - 100 && '5' }} /> + { + const value = Math.max(Number(event.target.value), minVal + 1); + + setMaxVal(value); + maxValRef.current = value; + }} + className='mrs_thumb mrs_thumb--right' /> + +
+
+
+
{minVal}
+
{maxVal}
+
+
+ + ); +}; + +MultiRangeSlider.propTypes = { + min: PropTypes.number.isRequired, + max: PropTypes.number.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default MultiRangeSlider; diff --git a/src/components/multiRangeSlider/multiRangeSlider.css b/src/components/multiRangeSlider/multiRangeSlider.css new file mode 100644 index 000000000..abf219bcb --- /dev/null +++ b/src/components/multiRangeSlider/multiRangeSlider.css @@ -0,0 +1,108 @@ +.mrs_container { + all: revert; + height: 3vh; + display: flex; + align-items: left; + justify-content: center; +} + +.mrs_slider { + all: revert; + position: relative; + width: 200px; +} + +.mrs_slider__track, +.mrs_slider__range, +.mrs_slider__left-value, +.mrs_slider__right-value { + all: revert; + position: absolute; +} + +.mrs_slider__track, +.mrs_slider__range { + border-radius: 3px; + height: 5px; +} + +.mrs_slider__track { + background-color: #949494; + width: 100%; + z-index: 1; +} + +.mrs_slider__range { + background-color: #5947ff; + z-index: 2; +} + +.mrs_slider__left-value, +.mrs_slider__right-value { + color: #000000; + font-size: 12px; + margin-top: 20px; +} + +.mrs_slider__left-value { + left: 6px; +} + +.mrs_slider__right-value { + right: -4px; +} + +/* Removing the default appearance */ +.mrs_thumb, +.mrs_thumb::-webkit-slider-thumb { + all: revert; + -webkit-appearance: none; + -webkit-tap-highlight-color: transparent; +} + +.mrs_thumb { + all: revert; + pointer-events: none; + position: absolute; + height: 0; + width: 200px; + outline: none; +} + +.mrs_thumb--left { + z-index: 3; +} + +.mrs_thumb--right { + z-index: 4; +} + +/* For Chrome browsers */ +.mrs_thumb::-webkit-slider-thumb { + all: revert; + background-color: #f1f5f7; + border: none; + border-radius: 50%; + box-shadow: 0 0 1px 1px #ced4da; + cursor: pointer; + height: 18px; + width: 18px; + margin-top: 4px; + pointer-events: all; + position: relative; +} + +/* For Firefox browsers */ +.mrs_thumb::-moz-range-thumb { + all: revert; + background-color: #f1f5f7; + border: none; + border-radius: 50%; + box-shadow: 0 0 1px 1px #ced4da; + cursor: pointer; + height: 18px; + width: 18px; + margin-top: 4px; + pointer-events: all; + position: relative; +} \ No newline at end of file diff --git a/src/env/development.js b/src/env/development.js index fb495d2a6..7799783ba 100644 --- a/src/env/development.js +++ b/src/env/development.js @@ -71,7 +71,7 @@ import { TimeSeriesSingle } from '../examples/timeseries_single'; */ // import { AllTypes } from "../examples/all_types"; -const data = VideoRectangles; +const data = TimeSeries; function getData(task) { if (task && task.data) { diff --git a/src/tags/object/TimeSeries.js b/src/tags/object/TimeSeries.js index 6b7671274..15f1e34e4 100644 --- a/src/tags/object/TimeSeries.js +++ b/src/tags/object/TimeSeries.js @@ -27,6 +27,8 @@ import PersistentStateMixin from '../../mixins/PersistentState'; import './TimeSeries/Channel'; import { AnnotationMixin } from '../../mixins/AnnotationMixin'; +import MultiRangeSlider from '../../components/multiRangeSlider/MultiRangeSlider'; + /** * The `TimeSeries` tag can be used to label time series data. Read more about Time Series Labeling on [the time series template page](../templates/time_series.html). * @@ -66,6 +68,7 @@ import { AnnotationMixin } from '../../mixins/AnnotationMixin'; * @param {string} [durationDisplayFormat] Format used to display temporal duration value for brush range. If the temporal column is a date, use strftime to format it. If it's a number, use [d3 number](https://github.com/d3/d3-format#locale_format) formatting. * @param {string} [sep=,] Separator for your CSV file. * @param {string} [overviewChannels] Comma-separated list of channel names or indexes displayed in overview. + * @param {string} [ylim] Comma-separated list of y axis limits. * @param {string} [overviewWidth=25%] Default width of overview window in percents * @param {boolean} [fixedScale=false] Whether to scale y-axis to the maximum to fit all the values. If false, current view scales to fit only the displayed values. */ @@ -80,7 +83,7 @@ const TagAttrs = types.model({ durationdisplayformat: '.0f', overviewchannels: '', // comma-separated list of channels to show overviewwidth: '25%', - + ylim: '', // comma-separated list of y axis limits fixedscale: false, multiaxis: types.optional(types.boolean, false), // show channels in the same view @@ -819,6 +822,7 @@ const Overview = observer(({ item, data, series }) => { return
; }); + const HtxTimeSeriesViewRTS = ({ item }) => { const ref = React.createRef(); @@ -836,12 +840,26 @@ const HtxTimeSeriesViewRTS = ({ item }) => {
); + const ylim = item.ylim; + const datarange = ylim.split(','); + return ( -
+
{Tree.renderChildren(item, item.annotation)} + {item.ylim && ( +
+ { + item.updatedYLim = [min, max]; + }} + /> +
+ )}
); }; diff --git a/src/tags/object/TimeSeries/Channel.js b/src/tags/object/TimeSeries/Channel.js index 60704dd94..80162223e 100644 --- a/src/tags/object/TimeSeries/Channel.js +++ b/src/tags/object/TimeSeries/Channel.js @@ -758,6 +758,29 @@ class ChannelD3 extends React.Component { translateY = min / diffY; this.y.domain([min, max]); + + } else { + const ylim = item.parent?.ylim; + + if (ylim !== '') { + // use custom range + const updatedylim = item.parent?.updatedYLim; + + if (typeof updatedylim === 'undefined') { + const datarange = ylim.split(','); + + let min = 0; + let max = 1; + + if (datarange[0] !== '') min = Number(datarange[0]); + if (datarange[1] !== '') max = Number(datarange[1]); + + this.y.domain([min, max]); + + } else { + this.y.domain([updatedylim[0], updatedylim[1]]); + } + } } // zoomStep - zoom level when we need to switch between optimized and original data