Add slider for time range

This commit is contained in:
Tanner Collin 2024-07-15 21:41:09 +00:00
parent 45272a6242
commit e549afce96
4 changed files with 125 additions and 61 deletions

View File

@ -15,6 +15,7 @@
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-is": "^17.0.2", "react-is": "^17.0.2",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
"react-range-slider-input": "^3.0.7",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },

View File

@ -39,6 +39,16 @@ h2 {
z-index: 9999; z-index: 9999;
} }
.time-slider {
padding: 1em 0.5em;
}
.range {
color: white;
text-align: center;
padding: 0.5rem;
}
.submenu { .submenu {
background-color: #666; background-color: #666;
max-width: 40em; max-width: 40em;

View File

@ -5,35 +5,23 @@ import Datetime from 'react-datetime';
import 'react-datetime/css/react-datetime.css'; import 'react-datetime/css/react-datetime.css';
import axios from 'axios'; import axios from 'axios';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import RangeSlider from 'react-range-slider-input';
import './App.css'; import './App.css';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import 'react-range-slider-input/dist/style.css';
let tzcache = {}; let tzcache = {};
// num: number of steps per duration
// secs: number of seconds per step
const durations = [ const durations = [
{id: 0, len: 'Day', win: '1m', full: '1 min', delta: [1, 'days'], format: 'HH'}, {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'}, {id: 1, len: 'Week', win: '3m', full: '3 min', delta: [7, 'days'], format: 'HH', num: 3360, secs: 180},
{id: 2, len: 'Week', win: '10m', full: '10 min', delta: [7, 'days'], format: 'HH'}, {id: 2, len: 'Month', win: '10m', full: '10 min', delta: [1, 'months'], format: 'D', num: 4380, secs: 600},
{id: 3, len: 'Month', win: '10m', full: '10 min', delta: [1, 'months'], format: 'D'}, {id: 3, len: 'Year', win: '2h', full: '2 hour', delta: [1, 'years'], format: 'M/D', num: 4380, secs: 7200},
{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'},
]; ];
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) { function useSensor(measurement, name, end, duration) {
const [data, setData] = useState(false); const [data, setData] = useState(false);
const [loading, setLoading] = 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 [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) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
@ -102,46 +90,74 @@ function Owntracks({end, duration}) {
} }
function Graphs({end, duration}) { function Graphs({end, duration, range}) {
return ( return (
<div className='container'> <div className='container'>
<Owntracks end={end} duration={duration} /> <Owntracks end={end} duration={duration} range={range} />
</div> </div>
); );
} }
function Menu({duration, setDuration, end, setEnd}) { function Menu({duration, setDuration, end, setEnd, range, setRange}) {
const [submenu, setSubmenu] = useState(false); const [submenu, setSubmenu] = useState(false);
const [showRange, setShowRange] = useState(false);
const chooseDuration = (x) => { const chooseDuration = (x) => {
setSubmenu(false); setSubmenu(false);
setRange(false);
setDuration(x); setDuration(x);
}; };
const chooseEnd = (x) => { const chooseEnd = (x) => {
setSubmenu(false); setSubmenu(false);
const newEnd = x.add(...duration.delta); const newEnd = x.add(...duration.delta);
setRange(false);
setEnd(newEnd); setEnd(newEnd);
}; };
const chooseNow = (x) => { const chooseNow = (x) => {
setSubmenu(false); setSubmenu(false);
setRange(false);
setEnd(moment()); setEnd(moment());
}; };
const next = () => { const next = () => {
setSubmenu(false); setSubmenu(false);
setRange(false);
setEnd(prevEnd => moment(prevEnd).add(...duration.delta)); setEnd(prevEnd => moment(prevEnd).add(...duration.delta));
} }
const prev = () => { const prev = () => {
setSubmenu(false); setSubmenu(false);
setRange(false);
setEnd(prevEnd => moment(prevEnd).subtract(...duration.delta)); 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 ( return (
<div className='menu'> <div className='menu'>
{!!submenu &&<div className='submenu'> {showRange && <div className='range'>
{moment(range[0]).format('lll')} - {moment(range[1]).format('lll')}
</div>}
{submenu ?
<div className='submenu'>
{submenu === 'end' && {submenu === 'end' &&
<> <>
<div className='submenu-header'> <div className='submenu-header'>
@ -169,11 +185,26 @@ function Menu({duration, setDuration, end, setEnd}) {
</div> </div>
{durations.map(x => {durations.map(x =>
<button key={x.id} onClick={() => chooseDuration(x)}>Last {x.len} / {x.full} data</button> <button key={x.id} onClick={() => chooseDuration(x)}>Last {x.len} ({x.full} data)</button>
)} )}
</> </>
} }
</div>} </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'> <div className='menu-container'>
<button onClick={() => prev()}>&lt;</button> <button onClick={() => prev()}>&lt;</button>
@ -201,6 +232,7 @@ function Menu({duration, setDuration, end, setEnd}) {
function App() { function App() {
const [duration, setDuration] = useState(durations[0]); const [duration, setDuration] = useState(durations[0]);
const [end, setEnd] = useState(moment()); const [end, setEnd] = useState(moment());
const [range, setRange] = useState(false);
return ( return (
<div> <div>
@ -209,11 +241,14 @@ function App() {
setDuration={setDuration} setDuration={setDuration}
end={end} end={end}
setEnd={setEnd} setEnd={setEnd}
range={range}
setRange={setRange}
/> />
<Graphs <Graphs
end={end} end={end}
duration={duration} duration={duration}
range={range}
/> />
</div> </div>
); );

View File

@ -3471,6 +3471,11 @@ cliui@^6.0.0:
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
wrap-ansi "^6.2.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: co@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 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" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== 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: core-js@^3.6.5:
version "3.33.0" version "3.33.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40" resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.0.tgz#70366dbf737134761edb017990cf5ce6c6369c40"
@ -9512,6 +9522,14 @@ react-leaflet@^4.2.1:
dependencies: dependencies:
"@react-leaflet/core" "^2.1.0" "@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: react-refresh@^0.8.3:
version "0.8.3" version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"