Skip to content

Commit

Permalink
Some cleanup.
Browse files Browse the repository at this point in the history
  • Loading branch information
fbarl committed Jul 24, 2017
1 parent 8d9b485 commit 5544990
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 173 deletions.
134 changes: 72 additions & 62 deletions client/app/scripts/components/time-travel-timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { connect } from 'react-redux';
import { drag } from 'd3-drag';
import { scaleUtc } from 'd3-scale';
import { event as d3Event, select } from 'd3-selection';

import { nowInSecondsPrecision } from '../utils/time-utils';
import {
jumpToTime,
} from '../actions/app-actions';
Expand Down Expand Up @@ -51,7 +53,8 @@ const TICK_ROWS = {
};

const FADE_OUT_FACTOR = 1.4;
const MIN_TICK_SPACING_PX = 70;
const ZOOM_SENSITIVITY = 1.0015;
const MIN_TICK_SPACING_PX = 80;
const MAX_TICK_SPACING_PX = 415;
const MIN_DURATION_PER_PX = moment.duration(250, 'milliseconds');
const INIT_DURATION_PER_PX = moment.duration(1, 'minute');
Expand Down Expand Up @@ -93,8 +96,8 @@ class TimeTravelTimeline extends React.Component {
super(props, context);

this.state = {
timestampNow: moment(),
focusedTimestamp: moment(),
timestampNow: nowInSecondsPrecision(),
focusedTimestamp: nowInSecondsPrecision(),
durationPerPixel: INIT_DURATION_PER_PX,
boundingRect: { width: 0, height: 0 },
isDragging: false,
Expand All @@ -112,8 +115,7 @@ class TimeTravelTimeline extends React.Component {
this.handlePan = this.handlePan.bind(this);
this.handleZoom = this.handleZoom.bind(this);

this.debouncedUpdateTimestamp = debounce(
this.updateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL);
this.debouncedJumpTo = debounce(this.jumpTo.bind(this), TIMELINE_DEBOUNCE_INTERVAL);
}

componentDidMount() {
Expand All @@ -124,9 +126,9 @@ class TimeTravelTimeline extends React.Component {
.on('drag', this.handlePan);
this.svg.call(this.drag);

// Force periodic re-renders to update the slider position as time goes by.
// Force periodic updates of the availability range as time goes by.
this.timer = setInterval(() => {
this.setState({ timestampNow: moment().startOf('second') });
this.setState({ timestampNow: nowInSecondsPrecision() });
}, TIMELINE_TICK_INTERVAL);
}

Expand All @@ -141,8 +143,8 @@ class TimeTravelTimeline extends React.Component {
this.setState({ boundingRect: this.svgRef.getBoundingClientRect() });
}

updateTimestamp(timestamp) {
this.props.jumpToTime(moment(timestamp));
saveSvgRef(ref) {
this.svgRef = ref;
}

handlePanStart() {
Expand All @@ -161,7 +163,7 @@ class TimeTravelTimeline extends React.Component {
}

handleZoom(e) {
const scale = Math.pow(1.0015, e.deltaY);
const scale = Math.pow(ZOOM_SENSITIVITY, e.deltaY);
let durationPerPixel = scaleDuration(this.state.durationPerPixel, scale);
if (durationPerPixel > MAX_DURATION_PER_PX) durationPerPixel = MAX_DURATION_PER_PX;
if (durationPerPixel < MIN_DURATION_PER_PX) durationPerPixel = MIN_DURATION_PER_PX;
Expand All @@ -171,31 +173,28 @@ class TimeTravelTimeline extends React.Component {
jumpTo(timestamp) {
const { timestampNow } = this.state;
const focusedTimestamp = timestamp > timestampNow ? timestampNow : timestamp;
this.props.onUpdateTimestamp(focusedTimestamp);
this.props.onTimelinePan(focusedTimestamp);
this.setState({ focusedTimestamp });
this.debouncedJumpTo.cancel();
}

jumpForward() {
const { focusedTimestamp, durationPerPixel, boundingRect } = this.state;
const duration = scaleDuration(durationPerPixel, boundingRect.width / 4);
const timestamp = moment(focusedTimestamp).add(duration);
this.jumpTo(timestamp);
const newTimestamp = moment(focusedTimestamp).add(duration);
this.jumpTo(newTimestamp);
}

jumpBackward() {
const { focusedTimestamp, durationPerPixel, boundingRect } = this.state;
const duration = scaleDuration(durationPerPixel, boundingRect.width / 4);
const timestamp = moment(focusedTimestamp).subtract(duration);
this.jumpTo(timestamp);
}

saveSvgRef(ref) {
this.svgRef = ref;
const newTimestamp = moment(focusedTimestamp).subtract(duration);
this.jumpTo(newTimestamp);
}

getTimeScale() {
const { durationPerPixel, focusedTimestamp } = this.state;
const roundedTimestamp = moment(focusedTimestamp).startOf('second').utc();
const roundedTimestamp = moment(focusedTimestamp).utc().startOf('second');
const startDate = moment(roundedTimestamp).subtract(durationPerPixel);
const endDate = moment(roundedTimestamp).add(durationPerPixel);
return scaleUtc()
Expand All @@ -205,74 +204,85 @@ class TimeTravelTimeline extends React.Component {

findOptimalDuration(durations) {
const { durationPerPixel } = this.state;
const minimalDuration = scaleDuration(durationPerPixel, MIN_TICK_SPACING_PX);
return find(durations, d => d > minimalDuration);
}

renderTimestampTick({ timestamp, position, isBehind }, periodFormat, opacity) {
const { timestampNow } = this.state;
const disabled = timestamp.isAfter(timestampNow) || opacity < 0.2;
const handleClick = () => this.jumpTo(timestamp);

return (
<g transform={`translate(${position}, 0)`} key={timestamp.format()}>
{!isBehind && <line y2="75" stroke="#ddd" strokeWidth="1" />}
<foreignObject width="100" height="20">
<a className="timestamp-label" disabled={disabled} onClick={!disabled && handleClick}>
{isBehind && '←'}
{timestamp.utc().format(periodFormat)}
</a>
</foreignObject>
</g>
);
const minimalDuration = scaleDuration(durationPerPixel, 1.1 * MIN_TICK_SPACING_PX);
return find(durations, d => d >= minimalDuration);
}

getTicks(period, parentPeriod, duration) {
parentPeriod = parentPeriod || period;
const startPosition = -this.state.boundingRect.width / 2;
const endPosition = this.state.boundingRect.width / 2;

getTicks(period, parentPeriod) {
// First find the optimal duration between the ticks - if no satisfactory
// duration could be found, don't render any ticks for the given period.
const duration = this.findOptimalDuration(TICK_ROWS[period].intervals);
if (!duration) return [];

// Get the boundary values for the displayed part of the timeline.
const timeScale = this.getTimeScale();
const startPosition = -this.state.boundingRect.width / 2;
const endPosition = this.state.boundingRect.width / 2;
const startDate = moment(timeScale.invert(startPosition));
const endDate = moment(timeScale.invert(endPosition));
const ticks = [];

let timestamp = moment(startDate).startOf(parentPeriod);
let turningPoint = moment(timestamp).add(1, parentPeriod);

// Start counting the timestamps from the most recent timestamp that is not shown
// on screen. The values are always rounded up to the timestamps of the next bigger
// period (e.g. for days it would be months, for months it would be years).
let timestamp = moment(startDate).utc().startOf(parentPeriod || period);
while (timestamp.isBefore(startDate)) {
timestamp = moment(timestamp).add(duration);
}
timestamp = moment(timestamp).subtract(duration);

ticks.push({
timestamp: moment(timestamp).subtract(duration),
// Make that hidden timestamp the first one in the list, but position
// it inside the visible range with a prepended arrow to the past.
const ticks = [{
timestamp: moment(timestamp),
position: startPosition,
isBehind: true,
});
}];

// Continue adding ticks till the end of the visible range.
do {
const position = timeScale(timestamp);
// If the new timestamp enters into a new bigger period, we round it down to the
// beginning of that period. E.g. instead of going [Jan 22nd, Jan 29th, Feb 5th],
// we output [Jan 22nd, Jan 29th, Feb 1st]. Right now this case only happens between
// days and months, but in theory it could happen whenever bigger periods are not
// divisible by the duration we are using as a step between the ticks.
let newTimestamp = moment(timestamp).add(duration);
if (parentPeriod && newTimestamp.get(parentPeriod) !== timestamp.get(parentPeriod)) {
newTimestamp = moment(newTimestamp).utc().startOf(parentPeriod);
}
timestamp = newTimestamp;

while (ticks.length > 0 && position - last(ticks).position < 0.85 * MIN_TICK_SPACING_PX) {
// If the new tick is too close to the previous one, drop that previous tick.
const position = timeScale(timestamp);
const previousPosition = last(ticks) && last(ticks).position;
if (position - previousPosition < MIN_TICK_SPACING_PX) {
ticks.pop();
}
ticks.push({ timestamp, position });

timestamp = moment(timestamp).add(duration);
if (parentPeriod && timestamp >= turningPoint) {
timestamp = turningPoint;
turningPoint = moment(turningPoint).add(1, parentPeriod);
}
ticks.push({ timestamp, position });
} while (timestamp.isBefore(endDate));

return ticks;
}

renderTimestampTick({ timestamp, position, isBehind }, periodFormat, opacity) {
const { timestampNow } = this.state;
const disabled = timestamp.isAfter(timestampNow) || opacity < 0.2;
const handleClick = () => this.jumpTo(timestamp);

return (
<g transform={`translate(${position}, 0)`} key={timestamp.format()}>
{!isBehind && <line y2="75" stroke="#ddd" strokeWidth="1" />}
<foreignObject width="100" height="20">
<a className="timestamp-label" disabled={disabled} onClick={!disabled && handleClick}>
{isBehind && '←'}{timestamp.utc().format(periodFormat)}
</a>
</foreignObject>
</g>
);
}

renderPeriodBar(period, parentPeriod) {
const duration = this.findOptimalDuration(TICK_ROWS[period].intervals);
const ticks = this.getTicks(period, parentPeriod, duration);
const ticks = this.getTicks(period, parentPeriod);

const periodFormat = TICK_ROWS[period].format;
const p = getFadeInFactor(period, this.state.durationPerPixel);
Expand Down
91 changes: 13 additions & 78 deletions client/app/scripts/components/time-travel.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,26 @@ import {
} from '../actions/app-actions';

import {
TIMELINE_TICK_INTERVAL,
TIMELINE_DEBOUNCE_INTERVAL,
} from '../constants/timer';


const getTimestampStates = (timestamp) => {
timestamp = timestamp || moment();
return {
sliderValue: moment(timestamp).valueOf(),
inputValue: moment(timestamp).utc().format(),
};
};

// const ONE_HOUR_MS = moment.duration(1, 'hour');
// const FIVE_MINUTES_MS = moment.duration(5, 'minutes');

class TimeTravel extends React.Component {
constructor(props, context) {
super(props, context);

this.state = {
// TODO: Showing a three months of history is quite arbitrary;
// we should instead get some meaningful 'beginning of time' from
// the backend and make the slider show whole active history.
sliderMinValue: moment().subtract(6, 'months').valueOf(),
...getTimestampStates(props.pausedAt),
};
this.state = getTimestampStates(props.pausedAt);

this.handleInputChange = this.handleInputChange.bind(this);
this.handleSliderChange = this.handleSliderChange.bind(this);
this.handleTimelinePan = this.handleTimelinePan.bind(this);
this.handleJumpClick = this.handleJumpClick.bind(this);
this.renderMarks = this.renderMarks.bind(this);
this.renderMark = this.renderMark.bind(this);
this.travelTo = this.travelTo.bind(this);

this.debouncedUpdateTimestamp = debounce(
Expand All @@ -55,17 +42,12 @@ class TimeTravel extends React.Component {
this.trackSliderChange.bind(this), TIMELINE_DEBOUNCE_INTERVAL);
}

componentDidMount() {
// Force periodic re-renders to update the slider position as time goes by.
this.timer = setInterval(() => { this.forceUpdate(); }, TIMELINE_TICK_INTERVAL);
}

componentWillReceiveProps(props) {
this.setState(getTimestampStates(props.pausedAt));
}

componentWillUnmount() {
clearInterval(this.timer);
// TODO: Causing bug?
this.props.resumeTime();
}

Expand All @@ -81,7 +63,6 @@ class TimeTravel extends React.Component {
this.setState({ inputValue: ev.target.value });

if (timestamp.isValid()) {
timestamp = Math.max(timestamp, this.state.sliderMinValue);
timestamp = Math.min(timestamp, moment().valueOf());
this.travelTo(timestamp);

Expand All @@ -93,9 +74,9 @@ class TimeTravel extends React.Component {
}
}

// TODO: Redo
handleJumpClick(millisecondsDelta) {
let timestamp = this.state.sliderValue + millisecondsDelta;
timestamp = Math.max(timestamp, this.state.sliderMinValue);
timestamp = Math.min(timestamp, moment().valueOf());
this.travelTo(timestamp, true);
}
Expand Down Expand Up @@ -123,64 +104,18 @@ class TimeTravel extends React.Component {
});
}

renderMark({ timestampValue, label }) {
const sliderMaxValue = moment().valueOf();
const pos = (sliderMaxValue - timestampValue) / (sliderMaxValue - this.state.sliderMinValue);

// Ignore the month marks that are very close to 'Now'
if (label !== 'Now' && pos < 0.05) return null;

const style = { marginLeft: `calc(${(1 - pos) * 100}% - 32px)`, width: '64px' };
return (
<div
style={style}
className="time-travel-markers-tick"
key={timestampValue}>
<span className="vertical-tick" />
<a className="link" onClick={() => this.travelTo(timestampValue)}>{label}</a>
</div>
);
}

renderMarks() {
const { sliderMinValue } = this.state;
const sliderMaxValue = moment().valueOf();
const ticks = [{ timestampValue: sliderMaxValue, label: 'Now' }];
let monthsBack = 0;
let timestamp;

do {
timestamp = moment().utc().subtract(monthsBack, 'months').startOf('month');
if (timestamp.valueOf() >= sliderMinValue) {
// Months are broken by the year tag, e.g. November, December, 2016, February, etc...
let label = timestamp.format('MMMM');
if (label === 'January') {
label = timestamp.format('YYYY');
}
ticks.push({ timestampValue: timestamp.valueOf(), label });
}
monthsBack += 1;
} while (timestamp.valueOf() >= sliderMinValue);

return (
<div className="time-travel-markers">
{map(ticks, tick => this.renderMark(tick))}
</div>
);
}

render() {
const { inputValue } = this.state;
// const { sliderValue, sliderMinValue, inputValue } = this.state;
// const sliderMaxValue = moment().valueOf();

const className = classNames('time-travel', { visible: this.props.showingTimeTravel });

return (
<div className={className}>
<TimeTravelTimeline onUpdateTimestamp={this.handleSliderChange} />
<div className={classNames('time-travel', { visible: this.props.showingTimeTravel })}>
<TimeTravelTimeline
onTimelinePan={this.handleTimelinePan}
onJumpClick={this.handleJumpClick}
/>
<div className="time-travel-timestamp">
<input value={inputValue} onChange={this.handleInputChange} /> UTC
<input
value={this.state.inputValue}
onChange={this.handleInputChange}
/> UTC
</div>
</div>
);
Expand Down
Loading

0 comments on commit 5544990

Please sign in to comment.