Skip to content

Commit

Permalink
feat(time-slider): highlight active ticks (#341)
Browse files Browse the repository at this point in the history
  • Loading branch information
pwambach authored May 7, 2020
1 parent 00e2283 commit f08b170
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 103 deletions.
35 changes: 35 additions & 0 deletions src/scripts/components/time-slider-range/time-slider-range.styl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@require '../../../variables.styl'

$range-height = 5px

boxShadow()
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.2), 0px 3px 4px rgba(0, 0, 0, 0.12), 0px 2px 4px rgba(0, 0, 0, 0.14)

.track
position: absolute
top: $range-height
width: 100%
height: $range-height
border-radius: emCalc(2px)
background: $darkGrey4

&:not(:first-of-type)
top: $range-height * 3

.range
position: absolute
top: 0
z-index: 1
display: flex
height: $range-height
border-radius: emCalc(2px)
background: $main

.tick
position: absolute
width: emCalc(5px)
height: emCalc(5px)
border-radius: 50%
background-color: rgba($darkGrey4, 0.5)
transition: 0.2s all ease-out
transform: translateX(-50%)
52 changes: 52 additions & 0 deletions src/scripts/components/time-slider-range/time-slider-range.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, {FunctionComponent} from 'react';
import {TimeRange} from '../../types/time-range';

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

interface Props {
range: TimeRange;
combined: TimeRange;
selectedTimeIndex: number;
}

const TimeSliderRange: FunctionComponent<Props> = ({
range,
combined,
selectedTimeIndex
}) => {
const totalRange = combined.max - combined.min;
const left = Math.round(((range.min - combined.min) / totalRange) * 100);
const right =
100 - Math.round(((range.max - combined.min) / totalRange) * 100);
const rangeStyle = {
left: `${left}%`,
right: `${right}%`
};

const getTickStyle = (timestamp: string, isSelected: boolean) => {
const tickPosition =
((Date.parse(timestamp) - range.min) / (range.max - range.min)) * 100;

return {
left: `${tickPosition}%`,
backgroundColor: isSelected ? 'white' : undefined, // eslint-disable-line no-undefined
transform: isSelected ? 'translateX(-50%) scale(2)' : undefined // eslint-disable-line no-undefined
};
};

return (
<div className={styles.track}>
<div className={styles.range} style={rangeStyle}>
{range.timestamps.map((timestamp, index) => (
<div
key={timestamp}
className={styles.tick}
style={getTickStyle(timestamp, index === selectedTimeIndex)}
/>
))}
</div>
</div>
);
};

export default TimeSliderRange;
40 changes: 3 additions & 37 deletions src/scripts/components/time-slider/time-slider.styl
Original file line number Diff line number Diff line change
Expand Up @@ -43,38 +43,12 @@ compareThumb()
width: 100%
height: $range-height * 2

input
position: relative
z-index: 2
outline: 0

.mainTrack, .compareTrack
position: absolute
top: $range-height
width: 100%
height: $range-height
border-radius: emCalc(2px)
background: $darkGrey4

.compareTrack
top: $range-height

.compareBothTracks
top: $range-height * 3

.rangeMain, .rangeCompare
position: absolute
top: 0
z-index: 1
display: flex
overflow-x: hidden
height: $range-height
border-radius: emCalc(2px)
background: $main

.input, .compareInput
position: relative
z-index: 2
margin: 0
width: 100%
outline: 0
background: transparent
-webkit-appearance: none

Expand Down Expand Up @@ -106,14 +80,6 @@ compareThumb()
.compareInput::-ms-thumb
compareThumb()

.ticks
position: absolute
width: emCalc(5px)
height: emCalc(5px)
border-radius: 50%
background-color: rgba($darkGrey4, 0.5)
transform: translateX(-50%)

.timeOutput
position: absolute
bottom: 15px
Expand Down
101 changes: 40 additions & 61 deletions src/scripts/components/time-slider/time-slider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,21 @@ import setGlobeTime from '../../actions/set-globe-time';
import {getTimeRanges} from '../../libs/get-time-ranges';
import {State} from '../../reducers';
import {selectedLayerIdsSelector} from '../../selectors/layers/selected-ids';

import {TimeRange} from '../../types/time-range';
import {getLayerTimeIndex} from '../../libs/get-layer-tile-url';
import TimeSliderRange from '../time-slider-range/time-slider-range';

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

// debounce the time update
const DELAY = 200;

// eslint-disable-next-line complexity
const TimeSlider: FunctionComponent = () => {
const selectedLayerIds = useSelector(selectedLayerIdsSelector);
const {mainId, compareId} = selectedLayerIds;
const dispatch = useDispatch();
const language = useSelector(languageSelector);
const globeTime = useSelector(timeSelector);

const [time, setTime] = useState(globeTime);
const stepSize = 1000 * 60 * 60 * 24; // one day
const mainLayerDetails = useSelector((state: State) =>
Expand All @@ -52,8 +52,26 @@ const TimeSlider: FunctionComponent = () => {
() => getTimeRanges(mainLayerDetails, compareLayerDetails),
[mainLayerDetails, compareLayerDetails]
);
const timestampsAvailable = combined.timestamps.length > 0;
const totalRange = combined.max - combined.min;
const timeIndexMain = useMemo(
() => getLayerTimeIndex(time, rangeMain?.timestamps || []),
[time, rangeMain]
);
const timeIndexCompare = useMemo(
() => getLayerTimeIndex(time, rangeCompare?.timestamps || []),
[time, rangeCompare]
);
const timeSelectedMain =
rangeMain && new Date(rangeMain.timestamps[timeIndexMain]);
const timeSelectedCompare =
rangeCompare && new Date(rangeCompare.timestamps[timeIndexCompare]);

// get the label time - the tick time which is closest to the slider's time
const timeDiffMain = Math.abs(Number(timeSelectedMain) - time);
const timeDiffCompare = Math.abs(Number(timeSelectedCompare) - time);
const labelTime =
typeof timeDiffCompare === 'number' && timeDiffCompare < timeDiffMain
? timeSelectedCompare
: timeSelectedMain;

// update app state
const debouncedSetGlobeTime = useCallback(
Expand All @@ -75,44 +93,18 @@ const TimeSlider: FunctionComponent = () => {
}, [time, combined.min, combined.max]);

// return nothing when no timesteps available
if (!timestampsAvailable) {
if (combined.timestamps.length === 0) {
return null;
}

const outputPosition = Number(
const labelPosition = Number(
((time - combined.min) * 100) / (combined.max - combined.min)
);

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}%`
};
};

const getTickStyle = (timestamp: string, range: TimeRange) => {
const tickPosition = Number(
((Date.parse(timestamp) - range.min) / (range.max - range.min)) * 100
);
return {
left: `${tickPosition}%`
};
};

const inputStyles = cx(
styles.input,
rangeMain && rangeCompare && styles.compareInput
);
const compareStyles = cx(
styles.compareTrack,
rangeMain && rangeCompare && styles.compareBothTracks
);

return (
<div className={styles.timeSlider}>
Expand All @@ -131,42 +123,29 @@ const TimeSlider: FunctionComponent = () => {
max={combined.max}
step={stepSize}
/>

<output
className={styles.timeOutput}
style={{
left: `${outputPosition}%`
left: `${labelPosition}%`
}}>
{format(time)}
{labelTime ? format(labelTime) : false}
</output>

{rangeMain && (
<div className={styles.mainTrack}>
<div
className={styles.rangeMain}
style={getRangeStyle(rangeMain.min, rangeMain.max)}>
{rangeMain.timestamps.map(timestamp => (
<div
key={timestamp}
className={styles.ticks}
style={getTickStyle(timestamp, rangeMain)}
/>
))}
</div>
</div>
<TimeSliderRange
range={rangeMain}
combined={combined}
selectedTimeIndex={timeIndexMain}
/>
)}

{rangeCompare && (
<div className={compareStyles}>
<div
className={styles.rangeCompare}
style={getRangeStyle(rangeCompare.min, rangeCompare.max)}>
{rangeCompare.timestamps.map(timestamp => (
<div
key={timestamp}
className={styles.ticks}
style={getTickStyle(timestamp, rangeCompare)}
/>
))}
</div>
</div>
<TimeSliderRange
range={rangeCompare}
combined={combined}
selectedTimeIndex={timeIndexCompare}
/>
)}
</div>
</div>
Expand Down
21 changes: 16 additions & 5 deletions src/scripts/libs/get-layer-tile-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function getLayerTileUrl(
const url =
isElectron() && isOffline() ? getOfflineTilesUrl() : config.api.layerTiles;

const timeIndex = getLayerTime(time, layer.timestamps).toString();
const timeIndex = getLayerTimeIndex(time, layer.timestamps).toString();
return replaceUrlPlaceholders(url, {
id: layer.id,
timeIndex
Expand All @@ -35,17 +35,28 @@ export function getLayerTileUrl(
* Returns the best matching time of all layer timestamps
* based on the current global time
*/
function getLayerTime(sliderTime: number, timestamps: string[]): number {
export function getLayerTimeIndex(
sliderTime: number,
timestamps: string[]
): number {
let minDiff = Infinity;
let index = timestamps.length - 1;

for (let i = timestamps.length - 1; i >= 0; i--) {
const layerTime = Number(new Date(timestamps[i]));
const tickTime = Number(new Date(timestamps[i]));
const diff = Math.abs(sliderTime - tickTime);

if (sliderTime >= layerTime) {
if (diff < minDiff && tickTime <= sliderTime) {
minDiff = diff;
index = i;
break;
}
}

const firstTimestamp = Number(new Date(timestamps[0]));

if (sliderTime <= firstTimestamp) {
index = 0;
}

return index;
}

0 comments on commit f08b170

Please sign in to comment.