Add slider for time range
This commit is contained in:
parent
45272a6242
commit
e549afce96
|
@ -15,6 +15,7 @@
|
|||
"react-dom": "^17.0.2",
|
||||
"react-is": "^17.0.2",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"react-range-slider-input": "^3.0.7",
|
||||
"react-scripts": "4.0.3",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
|
@ -31,8 +32,8 @@
|
|||
]
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -39,6 +39,16 @@ h2 {
|
|||
z-index: 9999;
|
||||
}
|
||||
|
||||
.time-slider {
|
||||
padding: 1em 0.5em;
|
||||
}
|
||||
|
||||
.range {
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.submenu {
|
||||
background-color: #666;
|
||||
max-width: 40em;
|
||||
|
|
|
@ -5,35 +5,23 @@ import Datetime from 'react-datetime';
|
|||
import 'react-datetime/css/react-datetime.css';
|
||||
import axios from 'axios';
|
||||
import moment from 'moment-timezone';
|
||||
import RangeSlider from 'react-range-slider-input';
|
||||
import './App.css';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import 'react-range-slider-input/dist/style.css';
|
||||
|
||||
|
||||
let tzcache = {};
|
||||
|
||||
// num: number of steps per duration
|
||||
// secs: number of seconds per step
|
||||
const durations = [
|
||||
{id: 0, len: 'Day', win: '1m', full: '1 min', delta: [1, 'days'], format: 'HH'},
|
||||
{id: 1, len: 'Week', win: '3m', full: '3 min', delta: [7, 'days'], format: 'HH'},
|
||||
{id: 2, len: 'Week', win: '10m', full: '10 min', delta: [7, 'days'], format: 'HH'},
|
||||
{id: 3, len: 'Month', win: '10m', full: '10 min', delta: [1, 'months'], format: 'D'},
|
||||
{id: 4, len: 'Month', win: '1h', full: '1 hour', delta: [1, 'months'], format: 'D'},
|
||||
{id: 5, len: 'Year', win: '2h', full: '2 hours', delta: [1, 'years'], format: 'M/D'},
|
||||
{id: 6, len: 'Year', win: '1d', full: '1 day', delta: [1, 'years'], format: 'M/D'},
|
||||
{id: 0, len: 'Day', win: '1m', full: '1 min', delta: [1, 'days'], format: 'HH', num: 1440, secs: 60},
|
||||
{id: 1, len: 'Week', win: '3m', full: '3 min', delta: [7, 'days'], format: 'HH', num: 3360, secs: 180},
|
||||
{id: 2, len: 'Month', win: '10m', full: '10 min', delta: [1, 'months'], format: 'D', num: 4380, secs: 600},
|
||||
{id: 3, len: 'Year', win: '2h', full: '2 hour', delta: [1, 'years'], format: 'M/D', num: 4380, secs: 7200},
|
||||
];
|
||||
|
||||
const units = {
|
||||
'PM10': ' ug/m³',
|
||||
'PM2.5': ' ug/m³',
|
||||
'VOC': ' / 500',
|
||||
'CO2': ' ppm',
|
||||
'Energy': ' kWh',
|
||||
'Power': ' W',
|
||||
'Temperature': ' °C',
|
||||
'Humidity': '%',
|
||||
'Setpoint': ' °C',
|
||||
'State': '',
|
||||
'Lux': ' lx',
|
||||
};
|
||||
|
||||
function useSensor(measurement, name, end, duration) {
|
||||
const [data, setData] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
@ -63,10 +51,10 @@ function useSensor(measurement, name, end, duration) {
|
|||
|
||||
|
||||
|
||||
function Owntracks({end, duration}) {
|
||||
function Owntracks({end, duration, range}) {
|
||||
const [data, loading] = useSensor('owntracks', 'OwnTracks', end, duration);
|
||||
|
||||
const coords = data.length ? data.map(({ lat, lon }) => [lat, lon]).filter(([lat, lon]) => lat !== null || lon !== null) : [];
|
||||
const coords = data.length ? data.filter(x => !range || (x.time >= range[0] && x.time <= range[1])).map(({ lat, lon }) => [lat, lon]).filter(([lat, lon]) => lat !== null || lon !== null) : [];
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
@ -102,79 +90,122 @@ function Owntracks({end, duration}) {
|
|||
}
|
||||
|
||||
|
||||
function Graphs({end, duration}) {
|
||||
function Graphs({end, duration, range}) {
|
||||
return (
|
||||
<div className='container'>
|
||||
<Owntracks end={end} duration={duration} />
|
||||
<Owntracks end={end} duration={duration} range={range} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Menu({duration, setDuration, end, setEnd}) {
|
||||
function Menu({duration, setDuration, end, setEnd, range, setRange}) {
|
||||
const [submenu, setSubmenu] = useState(false);
|
||||
const [showRange, setShowRange] = useState(false);
|
||||
|
||||
const chooseDuration = (x) => {
|
||||
setSubmenu(false);
|
||||
setRange(false);
|
||||
setDuration(x);
|
||||
};
|
||||
|
||||
const chooseEnd = (x) => {
|
||||
setSubmenu(false);
|
||||
const newEnd = x.add(...duration.delta);
|
||||
setRange(false);
|
||||
setEnd(newEnd);
|
||||
};
|
||||
|
||||
const chooseNow = (x) => {
|
||||
setSubmenu(false);
|
||||
setRange(false);
|
||||
setEnd(moment());
|
||||
};
|
||||
|
||||
const next = () => {
|
||||
setSubmenu(false);
|
||||
setRange(false);
|
||||
setEnd(prevEnd => moment(prevEnd).add(...duration.delta));
|
||||
}
|
||||
|
||||
const prev = () => {
|
||||
setSubmenu(false);
|
||||
setRange(false);
|
||||
setEnd(prevEnd => moment(prevEnd).subtract(...duration.delta));
|
||||
}
|
||||
|
||||
const onSlider = (slider) => {
|
||||
console.log(slider);
|
||||
// good luck remembering how this works
|
||||
const lowOffset = slider[0] * duration.secs - duration.num * duration.secs;
|
||||
const highOffset = slider[1] * duration.secs - duration.num * duration.secs;
|
||||
|
||||
const low = moment.unix(end.unix() + lowOffset);
|
||||
const high = moment.unix(end.unix() + highOffset);
|
||||
|
||||
const lowStr = low.utc().format('YYYY-MM-DDTHH:mm:ss[Z]');
|
||||
const highStr = high.utc().format('YYYY-MM-DDTHH:mm:ss[Z]');
|
||||
|
||||
console.log(lowStr, highStr);
|
||||
setRange([lowStr, highStr]);
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='menu'>
|
||||
{!!submenu &&<div className='submenu'>
|
||||
{submenu === 'end' &&
|
||||
<>
|
||||
<div className='submenu-header'>
|
||||
<h2>Choose start date:</h2>
|
||||
<button onClick={() => setSubmenu(false)}>×</button>
|
||||
</div>
|
||||
|
||||
<div className='datepicker'>
|
||||
<Datetime
|
||||
input={false}
|
||||
timeFormat={false}
|
||||
onChange={(x) => chooseEnd(x)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button onClick={chooseNow}>Jump to Now</button>
|
||||
</>
|
||||
}
|
||||
|
||||
{submenu === 'duration' &&
|
||||
<>
|
||||
<div className='submenu-header'>
|
||||
<h2>Choose duration:</h2>
|
||||
<button onClick={() => setSubmenu(false)}>×</button>
|
||||
</div>
|
||||
|
||||
{durations.map(x =>
|
||||
<button key={x.id} onClick={() => chooseDuration(x)}>Last {x.len} / {x.full} data</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
{showRange && <div className='range'>
|
||||
{moment(range[0]).format('lll')} - {moment(range[1]).format('lll')}
|
||||
</div>}
|
||||
|
||||
{submenu ?
|
||||
<div className='submenu'>
|
||||
{submenu === 'end' &&
|
||||
<>
|
||||
<div className='submenu-header'>
|
||||
<h2>Choose start date:</h2>
|
||||
<button onClick={() => setSubmenu(false)}>×</button>
|
||||
</div>
|
||||
|
||||
<div className='datepicker'>
|
||||
<Datetime
|
||||
input={false}
|
||||
timeFormat={false}
|
||||
onChange={(x) => chooseEnd(x)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button onClick={chooseNow}>Jump to Now</button>
|
||||
</>
|
||||
}
|
||||
|
||||
{submenu === 'duration' &&
|
||||
<>
|
||||
<div className='submenu-header'>
|
||||
<h2>Choose duration:</h2>
|
||||
<button onClick={() => setSubmenu(false)}>×</button>
|
||||
</div>
|
||||
|
||||
{durations.map(x =>
|
||||
<button key={x.id} onClick={() => chooseDuration(x)}>Last {x.len} ({x.full} data)</button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<div className='time-slider'>
|
||||
<RangeSlider
|
||||
min={0}
|
||||
max={duration.num}
|
||||
defaultValue={[0, duration.num]}
|
||||
onInput={onSlider}
|
||||
onThumbDragStart={() => setShowRange(true)}
|
||||
onThumbDragEnd={() => setShowRange(false)}
|
||||
onRangeDragStart={() => setShowRange(true)}
|
||||
onRangeDragEnd={() => setShowRange(false)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className='menu-container'>
|
||||
<button onClick={() => prev()}><</button>
|
||||
|
||||
|
@ -201,6 +232,7 @@ function Menu({duration, setDuration, end, setEnd}) {
|
|||
function App() {
|
||||
const [duration, setDuration] = useState(durations[0]);
|
||||
const [end, setEnd] = useState(moment());
|
||||
const [range, setRange] = useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -209,11 +241,14 @@ function App() {
|
|||
setDuration={setDuration}
|
||||
end={end}
|
||||
setEnd={setEnd}
|
||||
range={range}
|
||||
setRange={setRange}
|
||||
/>
|
||||
|
||||
<Graphs
|
||||
end={end}
|
||||
duration={duration}
|
||||
range={range}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -3471,6 +3471,11 @@ cliui@^6.0.0:
|
|||
strip-ansi "^6.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
clsx@^1.1.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
@ -3710,6 +3715,11 @@ core-js@^2.4.0:
|
|||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||
|
||||
core-js@^3.22.4:
|
||||
version "3.37.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.1.tgz#d21751ddb756518ac5a00e4d66499df981a62db9"
|
||||
integrity sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==
|
||||
|
||||
core-js@^3.6.5:
|
||||
version "3.33.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
|
||||
|
@ -9512,6 +9522,14 @@ react-leaflet@^4.2.1:
|
|||
dependencies:
|
||||
"@react-leaflet/core" "^2.1.0"
|
||||
|
||||
react-range-slider-input@^3.0.7:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/react-range-slider-input/-/react-range-slider-input-3.0.7.tgz#88ceb118b33d7eb0550cec1f77fc3e60e0f880f9"
|
||||
integrity sha512-yAJDDMUNkILOcZSCLCVbwgnoAM3v0AfdDysTCMXDwY/+ZRNRlv98TyHbVCwPFEd7qiI8Ca/stKb0GAy//NybYw==
|
||||
dependencies:
|
||||
clsx "^1.1.1"
|
||||
core-js "^3.22.4"
|
||||
|
||||
react-refresh@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
|
|
Loading…
Reference in New Issue
Block a user