Skip to content

Commit

Permalink
feat(time-slider): add ranges and format
Browse files Browse the repository at this point in the history
  • Loading branch information
pwambach committed Oct 28, 2019
1 parent d959e15 commit b90b8c5
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 93 deletions.
42 changes: 34 additions & 8 deletions src/scripts/components/time-slider/time-slider.styl
Original file line number Diff line number Diff line change
@@ -1,15 +1,41 @@
$range-height = 5px

.timeSlider
position: absolute
bottom: 6em
display: flex
justify-content: center
width: 100%
text-align: center

.input
.container
width: 80%

.label
display: inline-block
width: 80%
color: white
text-align: left
font-size: 2em
.label
display: flex
justify-content: space-between
align-items: flex-end
color: white
font-size: 2em

.labelMin, .labelMax
font-size: 0.6em

.ranges
position: relative
width: 100%
height: $range-height * 2

.rangeMain, .rangeCompare
position: absolute
height: $range-height

.rangeMain
background: yellow

.rangeCompare
top: $range-height
background: orange

input
margin: 0
width: 100%
159 changes: 74 additions & 85 deletions src/scripts/components/time-slider/time-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,128 +8,117 @@ import React, {
import {useSelector, useDispatch} from 'react-redux';
import debounce from 'lodash.debounce';

import {languageSelector} from '../../reducers/language';
import {selectedLayersSelector} from '../../reducers/layers/selected';
import {detailedLayersSelector} from '../../reducers/layers/details';
import setGlobeTime from '../../actions/set-globe-time';
import {getTimeRanges} from '../../libs/get-time-ranges';

import styles from './time-slider.styl';

// debounce the time update
const DELAY = 200;

const TimeSlider: FunctionComponent = () => {
const dispatch = useDispatch();
const [time, setTime] = useState(0);
const stepSize = 1000 * 60 * 60 * 24; // one day
const language = useSelector(languageSelector);
const selectedLayers = useSelector(selectedLayersSelector);
const detailedLayers = useSelector(detailedLayersSelector);
const debouncedSetGlobeTime = useCallback(
debounce((newTime: number) => dispatch(setGlobeTime(newTime)), DELAY, {
maxWait: DELAY
}),
[]
);

// get only active layers
const activeLayers = useMemo(
() =>
Object.values(selectedLayers)
.filter(Boolean)
.map(id => detailedLayers[id])
.filter(Boolean),
[selectedLayers, detailedLayers]
// date format
const mainLayer =
(selectedLayers.main && detailedLayers[selectedLayers.main]) || null;
const mainDateFormat = mainLayer && mainLayer.timeFormat;
const {format} = useMemo(
() => new Intl.DateTimeFormat(language, mainDateFormat || {}),
[language, mainDateFormat]
);

const timestampsPerLayer = useMemo(
() => activeLayers.map(({timestamps}) => timestamps),
[activeLayers]
// ranges
const {main, compare, combined} = useMemo(
() => getTimeRanges(selectedLayers, detailedLayers),
[selectedLayers, detailedLayers]
);
const timestampsAvailable = combined.timestamps.length > 0;
const totalRange = combined.max - combined.min;

// get combined and sorted timestamps from all active layers
const combinedTimestamps = useMemo(
() =>
timestampsPerLayer
// @ts-ignore
.flat()
.map((isoString: string) => Number(new Date(isoString)))
.sort((a: number, b: number) => a - b),
[timestampsPerLayer]
// update app state
const debouncedSetGlobeTime = useCallback(
debounce((newTime: number) => dispatch(setGlobeTime(newTime)), DELAY, {
maxWait: DELAY
}),
[]
);

const min = combinedTimestamps[0];
const max = combinedTimestamps[combinedTimestamps.length - 1];
const timestampsAvailable = combinedTimestamps.length > 0;

// clamp time according to min/max
useEffect(() => {
if (time < min) {
setTime(min);
if (time < combined.min) {
setTime(combined.min);
}

if (time > max) {
setTime(max);
if (time > combined.max) {
setTime(combined.max);
}
}, [time, min, max]);
}, [time, combined.min, combined.max]);

// return nothing when no timesteps available
if (!timestampsAvailable) {
return null;
}

const getRangeStyle = (
min: number,
max: number
): {left: string; right: string} => {
const left = Math.round(((min - combined.min) / totalRange) * 100);
const right = 100 - Math.round(((max - combined.min) / totalRange) * 100);

return {
left: `${left}%`,
right: `${right}%`
};
};

return (
<div className={styles.timeSlider}>
<div className={styles.label}>
{new Date(time).toISOString().substr(0, 10)}
<div className={styles.container}>
<div className={styles.label}>
<div className={styles.labelMin}>{format(combined.min)}</div>
<div>{format(time)}</div>
<div className={styles.labelMax}>{format(combined.max)}</div>
</div>

<input
className={styles.input}
type="range"
value={time}
onChange={({target}) => {
const newTime = parseInt(target.value, 10);
setTime(newTime);
debouncedSetGlobeTime(newTime);
}}
min={combined.min}
max={combined.max}
step={stepSize}
/>

<div className={styles.ranges}>
{main && (
<div
className={styles.rangeMain}
style={getRangeStyle(main.min, main.max)}></div>
)}
{compare && (
<div
className={styles.rangeCompare}
style={getRangeStyle(compare.min, compare.max)}></div>
)}
</div>
</div>

<input
className={styles.input}
type="range"
value={time}
onChange={({target}) => {
const newTime = parseInt(target.value, 10);
setTime(newTime);
debouncedSetGlobeTime(newTime);
}}
min={min}
max={max}
step={stepSize}
/>
</div>
);
};

export default TimeSlider;

// eslint-disable-next-line complexity
function getTimestamps(selectedLayers, detailedLayers) {
const mainLayer = selectedLayers.main && detailedLayers[selectedLayers.main];
const compareLayer =
selectedLayers.compare && detailedLayers[selectedLayers.compare];

const main = (mainLayer && getTimeRange(mainLayer.timestamps)) || null;
const compare =
(compareLayer && getTimeRange(compareLayer.timestamps)) || null;

const combined = getTimeRange([
...((main && main.timestamps) || []),
...((compare && compare.timestamps) || [])
]);

return {
main,
compare,
combined
};
}

function getTimeRange(timestamps: string[]) {
const sorted = timestamps
.map((isoString: string) => new Date(isoString))
.sort((a: Date, b: Date) => Number(a) - Number(b));

return {
min: sorted[0].toISOString(),
max: sorted[sorted.length - 1].toISOString(),
timestamps: sorted.map(date => date.toISOString())
};
}
53 changes: 53 additions & 0 deletions src/scripts/libs/get-time-ranges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {SelectedLayersState} from '../reducers/layers/selected';
import {DetailsById} from '../reducers/layers/details';

import {Layer} from '../types/layer';
import {TimeRange} from '../types/time-range';

// eslint-disable-next-line complexity
export function getTimeRanges(
selectedLayers: SelectedLayersState,
detailedLayers: DetailsById
): {
main: TimeRange | null;
compare: TimeRange | null;
combined: TimeRange;
} {
const mainId = selectedLayers.main;
const mainLayer = (mainId && detailedLayers[mainId]) || null;
const mainRange = getLayerTimeRange(mainLayer);
const mainTimestamps = (mainRange && mainRange.timestamps) || [];

const compareId = selectedLayers.compare;
const compareLayer = (compareId && detailedLayers[compareId]) || null;
const compareRange = getLayerTimeRange(compareLayer);
const compareTimestamps = (compareRange && compareRange.timestamps) || [];

const combinedRange = getTimeRange([...mainTimestamps, ...compareTimestamps]);

return {
main: mainRange,
compare: compareRange,
combined: combinedRange
};
}

function getLayerTimeRange(layer: Layer | null): TimeRange | null {
if (!layer || layer.timestamps.length < 2) {
return null;
}

return getTimeRange(layer.timestamps);
}

function getTimeRange(timestamps: string[]): TimeRange {
const sorted = timestamps
.map((isoString: string) => new Date(isoString))
.sort((a: Date, b: Date) => Number(a) - Number(b));

return {
min: Number(sorted[0]) || 0,
max: Number(sorted[sorted.length - 1]) || 0,
timestamps: sorted.map(date => date.toISOString())
};
}
5 changes: 5 additions & 0 deletions src/scripts/types/time-range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface TimeRange {
min: number;
max: number;
timestamps: string[];
}

0 comments on commit b90b8c5

Please sign in to comment.