From 7757bd291d1705e4e12121eb784b2b7cf44a7108 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 10 Jul 2017 17:31:57 +0200 Subject: [PATCH 01/31] Experimental. --- .../components/time-travel-timeline.js | 128 ++++++++++++++++++ client/app/scripts/components/time-travel.js | 40 ++---- client/app/styles/_base.scss | 14 +- client/package.json | 4 +- client/yarn.lock | 8 ++ 5 files changed, 161 insertions(+), 33 deletions(-) create mode 100644 client/app/scripts/components/time-travel-timeline.js diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js new file mode 100644 index 0000000000..5190094e43 --- /dev/null +++ b/client/app/scripts/components/time-travel-timeline.js @@ -0,0 +1,128 @@ +import React from 'react'; +import moment from 'moment'; +import { connect } from 'react-redux'; +import { fromJS } from 'immutable'; +import { zoom } from 'd3-zoom'; +import { scaleTime } from 'd3-scale'; +// import { timeFormat } from 'd3-time-format'; +// import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth } from 'd3-time'; +import { event as d3Event, select } from 'd3-selection'; + +import { transformToString } from '../utils/transform-utils'; + + +// function multiFormat(date) { +// if (timeSecond(date) < date) timeFormat(':%S'); +// if (timeMinute(date) < date) timeFormat('%I:%M'); +// if (timeHour(date) < date) timeFormat('%I %p'); +// if (timeDay(date) < date) timeFormat('%a %d'); +// if (timeWeek(date) < date) timeFormat('%b %d'); +// if (timeMonth(date) < date) timeFormat('%B'); +// return timeFormat('%Y'); +// } +// const customTimeFormat = d3.time.format.multi([ +// [".%L", function(d) { return d.getMilliseconds(); }], +// [":%S", function(d) { return d.getSeconds(); }], +// ["%I:%M", function(d) { return d.getMinutes(); }], +// ["%I %p", function(d) { return d.getHours(); }], +// ["%a %d", function(d) { return d.getDay() && d.getDate() != 1; }], +// ["%b %d", function(d) { return d.getDate() != 1; }], +// ["%B", function(d) { return d.getMonth(); }], +// ["%Y", function() { return true; }] +// ]); + +const EARLIEST_TIMESTAMP = moment(new Date(2000, 0)); + +class TimeTravelTimeline extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + translateX: 0, + translateY: 15, + scaleX: 1, + }; + + this.x = scaleTime() + .domain([EARLIEST_TIMESTAMP.toDate(), moment().toDate()]) + .range([-10000, 0]); + + this.saveSvgRef = this.saveSvgRef.bind(this); + this.zoomed = this.zoomed.bind(this); + } + + componentDidMount() { + this.zoom = zoom().on('zoom', this.zoomed); + this.svg = select('svg#time-travel-timeline'); + // console.log(this.svg.getBoundingClientRect()); + + this.setZoomTriggers(true); + // this.updateZoomLimits(this.props); + // this.restoreZoomState(this.props); + } + + componentWillUnmount() { + this.setZoomTriggers(false); + } + + componentWillReceiveProps() { + console.log(this.svgRef.getBoundingClientRect()); + } + + setZoomTriggers(zoomingEnabled) { + if (zoomingEnabled) { + // use d3-zoom defaults but exclude double clicks + this.svg.call(this.zoom) + .on('dblclick.zoom', null); + } else { + this.svg.on('.zoom', null); + } + } + + zoomed() { + this.setState({ + translateX: d3Event.transform.x, + scaleX: d3Event.transform.k, + }); + } + + saveSvgRef(ref) { + this.svgRef = ref; + } + + renderAxis() { + const ticks = this.x.domain([new Date(2000, 0, 1, 0), new Date(2001, 0, 1, 0)]).ticks(10); + return ( + + + + {fromJS(ticks).map(date => ( + + ))} + + + ); + } + + render() { + return ( + + + + + {this.renderAxis()} + + ); + } +} + + +function mapStateToProps(state) { + return { + viewportWidth: state.getIn(['viewport', 'width']), + }; +} + +export default connect(mapStateToProps)(TimeTravelTimeline); diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js index 979913d799..e044c39ae8 100644 --- a/client/app/scripts/components/time-travel.js +++ b/client/app/scripts/components/time-travel.js @@ -1,10 +1,11 @@ import React from 'react'; -import Slider from 'rc-slider'; +// import Slider from 'rc-slider'; import moment from 'moment'; import classNames from 'classnames'; import { connect } from 'react-redux'; import { debounce, map } from 'lodash'; +import TimeTravelTimeline from './time-travel-timeline'; import { trackMixpanelEvent } from '../utils/tracking-utils'; import { jumpToTime, @@ -26,8 +27,8 @@ const getTimestampStates = (timestamp) => { }; }; -const ONE_HOUR_MS = moment.duration(1, 'hour'); -const FIVE_MINUTES_MS = moment.duration(5, 'minutes'); +// const ONE_HOUR_MS = moment.duration(1, 'hour'); +// const FIVE_MINUTES_MS = moment.duration(5, 'minutes'); class TimeTravel extends React.Component { constructor(props, context) { @@ -167,38 +168,17 @@ class TimeTravel extends React.Component { } render() { - const { sliderValue, sliderMinValue, inputValue } = this.state; - const sliderMaxValue = moment().valueOf(); + const { inputValue } = this.state; + // const { sliderValue, sliderMinValue, inputValue } = this.state; + // const sliderMaxValue = moment().valueOf(); const className = classNames('time-travel', { visible: this.props.showingTimeTravel }); return (
-
- {this.renderMarks()} - -
-
- this.handleJumpClick(-ONE_HOUR_MS)}> - 1 hour - - this.handleJumpClick(-FIVE_MINUTES_MS)}> - 5 mins - - - UTC - - this.handleJumpClick(+FIVE_MINUTES_MS)}> - 5 mins - - this.handleJumpClick(+ONE_HOUR_MS)}> - 1 hour - + +
+ UTC
); diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 1f0f12539c..09044a004b 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -9,6 +9,9 @@ url("../../node_modules/materialize-css/fonts/roboto/Roboto-Regular.ttf"); } +// TODO: Remove this line once the configure button stops being added to Scope. +.setup-nav-button { display: none; } + .browsehappy { margin: 0.2em 0; background: #ccc; @@ -179,7 +182,7 @@ } .header { - margin-top: 38px; + margin-top: 15px; pointer-events: none; width: 100%; @@ -249,9 +252,16 @@ overflow: hidden; height: 0; + #time-travel-timeline { + background-color: rgba(255, 255, 255, 0.85); + box-shadow: inset 0 0 7px #aaa; + pointer-events: all; + } + &.visible { - height: 50px; + height: 60px; margin-bottom: 15px; + margin-top: -5px; } &-markers { diff --git a/client/package.json b/client/package.json index a4880faf38..3bad883a0b 100644 --- a/client/package.json +++ b/client/package.json @@ -11,14 +11,16 @@ "babel-polyfill": "6.23.0", "classnames": "2.2.5", "d3-array": "1.2.0", + "d3-axis": "^1.0.8", "d3-color": "1.0.3", + "d3-drag": "1.0.4", "d3-format": "1.2.0", "d3-scale": "1.0.5", "d3-selection": "1.0.5", "d3-shape": "1.0.6", + "d3-time": "^1.0.7", "d3-time-format": "2.0.5", "d3-transition": "1.0.4", - "d3-drag": "1.0.4", "d3-zoom": "1.1.4", "dagre": "0.7.4", "debug": "2.6.6", diff --git a/client/yarn.lock b/client/yarn.lock index 8518dc6ffd..72b28de39f 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1666,6 +1666,10 @@ d3-array@1, d3-array@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.0.tgz#147d269720e174c4057a7f42be8b0f3f2ba53108" +d3-axis@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa" + d3-collection@1: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.3.tgz#00bdea94fbc1628d435abbae2f4dc2164e37dd34" @@ -1735,6 +1739,10 @@ d3-time@1: version "1.0.6" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.6.tgz#a55b13d7d15d3a160ae91708232e0835f1d5e945" +d3-time@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.7.tgz#94caf6edbb7879bb809d0d1f7572bc48482f7270" + d3-timer@1: version "1.0.5" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.5.tgz#b266d476c71b0d269e7ac5f352b410a3b6fe6ef0" From cb7d7705a7e9042508360359a23a3e958c7d311b Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 19 Jul 2017 12:25:53 +0200 Subject: [PATCH 02/31] Getting somewhere. --- .../components/time-travel-timeline.js | 221 ++++++++++++++---- client/app/scripts/components/time-travel.js | 2 +- .../app/scripts/components/zoomable-canvas.js | 2 + client/app/styles/_base.scss | 151 ++++++++---- 4 files changed, 281 insertions(+), 95 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 5190094e43..18ebfe1c60 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -1,59 +1,87 @@ import React from 'react'; import moment from 'moment'; +import { debounce } from 'lodash'; import { connect } from 'react-redux'; import { fromJS } from 'immutable'; import { zoom } from 'd3-zoom'; -import { scaleTime } from 'd3-scale'; -// import { timeFormat } from 'd3-time-format'; -// import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth } from 'd3-time'; +import { drag } from 'd3-drag'; +// import { zoom, zoomIdentity } from 'd3-zoom'; +import { scaleUtc } from 'd3-scale'; +import { timeFormat } from 'd3-time-format'; +import { timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time'; import { event as d3Event, select } from 'd3-selection'; +import { + jumpToTime, +} from '../actions/app-actions'; + + +import { + TIMELINE_DEBOUNCE_INTERVAL, +} from '../constants/timer'; + +// import { transformToString } from '../utils/transform-utils'; + +const formatSecond = timeFormat(':%S'); +const formatMinute = timeFormat('%I:%M'); +const formatHour = timeFormat('%I %p'); +const formatDay = timeFormat('%b %d'); +const formatMonth = timeFormat('%B'); +const formatYear = timeFormat('%Y'); + +function multiFormat(date) { + date = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), + date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()); + if (timeMinute(date) < date) return formatSecond(date); + if (timeHour(date) < date) return formatMinute(date); + if (timeDay(date) < date) return formatHour(date); + if (timeMonth(date) < date) return formatDay(date); + if (timeYear(date) < date) return formatMonth(date); + return formatYear(date); +} + -import { transformToString } from '../utils/transform-utils'; - - -// function multiFormat(date) { -// if (timeSecond(date) < date) timeFormat(':%S'); -// if (timeMinute(date) < date) timeFormat('%I:%M'); -// if (timeHour(date) < date) timeFormat('%I %p'); -// if (timeDay(date) < date) timeFormat('%a %d'); -// if (timeWeek(date) < date) timeFormat('%b %d'); -// if (timeMonth(date) < date) timeFormat('%B'); -// return timeFormat('%Y'); -// } -// const customTimeFormat = d3.time.format.multi([ -// [".%L", function(d) { return d.getMilliseconds(); }], -// [":%S", function(d) { return d.getSeconds(); }], -// ["%I:%M", function(d) { return d.getMinutes(); }], -// ["%I %p", function(d) { return d.getHours(); }], -// ["%a %d", function(d) { return d.getDay() && d.getDate() != 1; }], -// ["%b %d", function(d) { return d.getDate() != 1; }], -// ["%B", function(d) { return d.getMonth(); }], -// ["%Y", function() { return true; }] -// ]); - -const EARLIEST_TIMESTAMP = moment(new Date(2000, 0)); +// const timeScale = scaleUtc().clamp(false) +// .domain([new Date(1990, 1), new Date(2020, 1)]) +// .range([0, 10000]); + +// const EARLIEST_TIMESTAMP = moment(new Date(2000, 0)); +// const M = 1000; class TimeTravelTimeline extends React.Component { constructor(props, context) { super(props, context); this.state = { + // shift: timeScale(moment().toDate()), translateX: 0, translateY: 15, scaleX: 1, }; - this.x = scaleTime() - .domain([EARLIEST_TIMESTAMP.toDate(), moment().toDate()]) - .range([-10000, 0]); + this.width = 2000; + // this.getDisplayedTimeScale = this.getDisplayedTimeScale.bind(this); this.saveSvgRef = this.saveSvgRef.bind(this); + this.dragged = this.dragged.bind(this); this.zoomed = this.zoomed.bind(this); + + // this.zoomToDate + + this.debouncedUpdateTimestamp = debounce( + this.updateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); } componentDidMount() { - this.zoom = zoom().on('zoom', this.zoomed); this.svg = select('svg#time-travel-timeline'); + // const w = 1200; + // const h = 60; + this.drag = drag() + .on('drag', this.dragged); + this.zoom = zoom() + // .translateExtent([[0, 0], [w, h]]) + // .extent([[-M, -30], [M, 30]]) + .on('zoom', this.zoomed); + // this.svg.style('transform-origin', '50% 50% 0'); // console.log(this.svg.getBoundingClientRect()); this.setZoomTriggers(true); @@ -66,11 +94,16 @@ class TimeTravelTimeline extends React.Component { } componentWillReceiveProps() { - console.log(this.svgRef.getBoundingClientRect()); + this.width = this.svgRef.getBoundingClientRect().width; + } + + updateTimestamp(timestamp) { + this.props.jumpToTime(moment(timestamp)); } setZoomTriggers(zoomingEnabled) { if (zoomingEnabled) { + this.svg.call(this.drag); // use d3-zoom defaults but exclude double clicks this.svg.call(this.zoom) .on('dblclick.zoom', null); @@ -79,11 +112,64 @@ class TimeTravelTimeline extends React.Component { } } + getTimeScale() { + const { scaleX, translateX } = this.state; + const S = 4000 / scaleX; + const p = this.props.pausedAt ? moment(this.props.pausedAt) : moment(); + const x = p.add(translateX / S, 'hours'); + const k = S / scaleX; + console.log(x, k); + // const x = moment(p).subtract(this.state.translateX, 'seconds'); + // // console.log('L', p, x); + // // const pausedAt = this.props.pausedAt || moment(); + // const k = 1000 / this.state.scaleX; + // return scaleUtc() + // .domain([x.subtract(k, 'seconds').toDate(), x.add(k, 'seconds').toDate()]) + // .range([0, 2000]); + return scaleUtc() + .domain([x.subtract(k, 'hours').toDate(), x.add(k, 'hours').toDate()]) + .range([-2000 / scaleX, 2000 / scaleX]); + } + zoomed() { - this.setState({ - translateX: d3Event.transform.x, - scaleX: d3Event.transform.k, - }); + // const { x, k } = d3Event.transform; + // const { scaleX, translateX } = this.state; + // const { width } = this.svgRef.getBoundingClientRect(); + // // const halfWidth = 0; // width / 2; + // const diff = (width * 0.5) - translateX; + // const fac = diff * 0.5 * (1 - (k / scaleX)); + // console.log(diff, fac); + // + // if (k !== scaleX) { + // const newTransform = zoomIdentity + // .translate(translateX, 0) + // .scale(k) + // .translate(fac, 0); + // console.log(newTransform); + // this.setState({ translateX: newTransform.x, scaleX: newTransform.k }); + // this.svg.call(this.zoom.transform, newTransform); + // } else if (x !== translateX) { + // const newTransform = zoomIdentity + // .translate(x, 0) + // .scale(scaleX); + // console.log(newTransform); + // this.setState({ translateX: newTransform.x, scaleX: newTransform.k }); + // this.svg.call(this.zoom.transform, newTransform); + // } + // this.svg.call(this.zoom.transform, zoomIdentity + // .translate(this.state.translateX, 0) + // .scale(this.state.scaleX, 1)); + // console.log(d3Event); + // console.log(zoomTransform(this.svgRef)); + // console.log('ZOOM', d3Event.transform.k); + this.setState({ scaleX: d3Event.transform.k }); + + // this.debouncedUpdateTimestamp(this.getDisplayedTimeScale().invert(-d3Event.transform.x)); + } + + dragged() { + // console.log('DRAG', this.state.translateX + d3Event.dx); + this.setState({ translateX: this.state.translateX + d3Event.dx }); } saveSvgRef(ref) { @@ -91,15 +177,35 @@ class TimeTravelTimeline extends React.Component { } renderAxis() { - const ticks = this.x.domain([new Date(2000, 0, 1, 0), new Date(2001, 0, 1, 0)]).ticks(10); + // const { translateX, scaleX } = this.state; + // const pausedAt = this.props.pausedAt ? moment(this.props.pausedAt) : moment(); + // const timeScale = this.getDisplayedTimeScale(); + // + // const startDate = this.getDisplayedTimeScale() + // .invert(-this.state.translateX - (1000 * this.state.scaleX)); + // const endDate = this.getDisplayedTimeScale() + // .invert(-this.state.translateX + (1000 * this.state.scaleX)); + // const ticks = this.getDisplayedTimeScale().domain([startDate, endDate]).ticks(10); + // console.log(startDate, -this.state.translateX - (1000 * this.state.scaleX)); + // console.log(endDate, -this.state.translateX + (1000 * this.state.scaleX)); + // console.log(ticks); + + const timeScale = this.getTimeScale(); + // const cd = pausedAt.subtract((translateX / scaleX) * (30 / 10000), 'years'); + // console.log(cd.toDate()); + const ticks = timeScale.ticks(10); + + // return ( - - + {fromJS(ticks).map(date => ( - + width="50" height="20"> + {multiFormat(date)} + ))} @@ -107,13 +213,25 @@ class TimeTravelTimeline extends React.Component { } render() { + const { translateX, scaleX } = this.state; return ( - - - - - {this.renderAxis()} - +
+ + + + + + {this.renderAxis()} + + + + + +
); } } @@ -122,7 +240,14 @@ class TimeTravelTimeline extends React.Component { function mapStateToProps(state) { return { viewportWidth: state.getIn(['viewport', 'width']), + pausedAt: state.get('pausedAt'), }; } -export default connect(mapStateToProps)(TimeTravelTimeline); + +export default connect( + mapStateToProps, + { + jumpToTime, + } +)(TimeTravelTimeline); diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js index e044c39ae8..f00c49dbeb 100644 --- a/client/app/scripts/components/time-travel.js +++ b/client/app/scripts/components/time-travel.js @@ -177,7 +177,7 @@ class TimeTravel extends React.Component { return (
-
+
UTC
diff --git a/client/app/scripts/components/zoomable-canvas.js b/client/app/scripts/components/zoomable-canvas.js index 205b4f2c16..557b15fd7b 100644 --- a/client/app/scripts/components/zoomable-canvas.js +++ b/client/app/scripts/components/zoomable-canvas.js @@ -199,6 +199,8 @@ class ZoomableCanvas extends React.Component { translateY: d3Event.transform.y, }); + console.log(updatedState); + this.setState(updatedState); this.debouncedCacheZoom(); } diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 09044a004b..9cd78969ca 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -182,7 +182,7 @@ } .header { - margin-top: 15px; + padding: 15px 10px 0; pointer-events: none; width: 100%; @@ -242,24 +242,43 @@ } .time-travel { - align-items: center; - display: flex; + // align-items: center; + // display: flex; position: relative; - margin: 0 30px 0 15px; + margin-bottom: 15px; z-index: 2001; transition: all .15s $base-ease; overflow: hidden; height: 0; - #time-travel-timeline { - background-color: rgba(255, 255, 255, 0.85); - box-shadow: inset 0 0 7px #aaa; - pointer-events: all; + .time-travel-timeline { + align-items: center; + display: flex; + height: 60px; + + svg { + background-color: rgba(255, 255, 255, 0.85); + box-shadow: inset 0 0 7px #aaa; + pointer-events: all; + margin: 0 7px; + } + + &:after { + content: ''; + position: absolute; + display: block; + left: 50%; + top: 0; + border: 1px solid white; + background-color: red; + height: 60px; + width: 3px; + } } &.visible { - height: 60px; + height: 100px; margin-bottom: 15px; margin-top: -5px; } @@ -296,42 +315,82 @@ .rc-slider-rail { background-color: $text-tertiary-color; } } - &-jump-controls { - display: flex; - - .button.jump { - display: block; - margin: 8px; - font-size: 0.625rem; - pointer-events: all; - text-align: center; - text-transform: uppercase; - word-spacing: -1px; - - .fa { - display: block; - font-size: 150%; - margin-bottom: 3px; - } - } - - &-timestamp { - border: 1px solid #ccc; - border-radius: 4px; - padding: 2px 8px; - pointer-events: all; - margin: 4px 8px 25px; - - input { - border: 0; - background-color: transparent; - font-family: $mono-font; - font-size: 0.875rem; - margin-right: 2px; - outline: 0; - } - } - } + &-timestamp { + border: 1px solid #ccc; + border-radius: 4px; + padding: 2px 8px; + pointer-events: all; + margin: 4px auto 25px; + width: 225px; + + input { + border: 0; + background-color: transparent; + font-family: $mono-font; + font-size: 0.875rem; + margin-right: 2px; + outline: 0; + } + + // &:before, &:after { + // content: ''; + // position: absolute; + // display: block; + // margin: 0 100px; + // } + // + // &:before { + // top: 0; + // border: 1px solid white; + // background-color: red; + // height: 69px; + // width: 3px; + // } + // + // &:after { + // border: 1px solid #ccc; + // top: 70px; + // height: 20px; + // width: 1px; + // } + } + + // &-jump-controls { + // display: flex; + // + // .button.jump { + // display: block; + // margin: 8px; + // font-size: 0.625rem; + // pointer-events: all; + // text-align: center; + // text-transform: uppercase; + // word-spacing: -1px; + // + // .fa { + // display: block; + // font-size: 150%; + // margin-bottom: 3px; + // } + // } + // + // &-timestamp { + // border: 1px solid #ccc; + // border-radius: 4px; + // padding: 2px 8px; + // pointer-events: all; + // margin: 4px 8px 25px; + // + // input { + // border: 0; + // background-color: transparent; + // font-family: $mono-font; + // font-size: 0.875rem; + // margin-right: 2px; + // outline: 0; + // } + // } + // } } @@ -1493,7 +1552,7 @@ .time-control { position: absolute; - right: 36px; + right: 20px; &-controls { align-items: center; From 558f1a25a6443db515fdb16bbc9d3b2d78ef7eaf Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 19 Jul 2017 14:36:42 +0200 Subject: [PATCH 03/31] Good zooming behaviour. --- .../components/time-travel-timeline.js | 105 +++++++----------- 1 file changed, 41 insertions(+), 64 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 18ebfe1c60..89522ee25e 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -14,7 +14,6 @@ import { jumpToTime, } from '../actions/app-actions'; - import { TIMELINE_DEBOUNCE_INTERVAL, } from '../constants/timer'; @@ -39,23 +38,20 @@ function multiFormat(date) { return formatYear(date); } - // const timeScale = scaleUtc().clamp(false) // .domain([new Date(1990, 1), new Date(2020, 1)]) // .range([0, 10000]); // const EARLIEST_TIMESTAMP = moment(new Date(2000, 0)); -// const M = 1000; +const R = 10000; class TimeTravelTimeline extends React.Component { constructor(props, context) { super(props, context); this.state = { - // shift: timeScale(moment().toDate()), - translateX: 0, - translateY: 15, - scaleX: 1, + focusedTimestamp: moment(), + timelineRange: moment.duration(1000000, 'seconds'), }; this.width = 2000; @@ -112,64 +108,39 @@ class TimeTravelTimeline extends React.Component { } } - getTimeScale() { - const { scaleX, translateX } = this.state; - const S = 4000 / scaleX; - const p = this.props.pausedAt ? moment(this.props.pausedAt) : moment(); - const x = p.add(translateX / S, 'hours'); - const k = S / scaleX; - console.log(x, k); - // const x = moment(p).subtract(this.state.translateX, 'seconds'); - // // console.log('L', p, x); - // // const pausedAt = this.props.pausedAt || moment(); - // const k = 1000 / this.state.scaleX; - // return scaleUtc() - // .domain([x.subtract(k, 'seconds').toDate(), x.add(k, 'seconds').toDate()]) - // .range([0, 2000]); - return scaleUtc() - .domain([x.subtract(k, 'hours').toDate(), x.add(k, 'hours').toDate()]) - .range([-2000 / scaleX, 2000 / scaleX]); - } + // getTimeScale() { + // const { scaleX, translateX } = this.state; + // const S = 4000 / scaleX; + // const p = this.props.pausedAt ? moment(this.props.pausedAt) : moment(); + // const x = p.add(translateX / S, 'hours'); + // const k = S / scaleX; + // // console.log(x, k); + // // const x = moment(p).subtract(this.state.translateX, 'seconds'); + // // // console.log('L', p, x); + // // // const pausedAt = this.props.pausedAt || moment(); + // // const k = 1000 / this.state.scaleX; + // // return scaleUtc() + // // .domain([x.subtract(k, 'seconds').toDate(), x.add(k, 'seconds').toDate()]) + // // .range([0, 2000]); + // return scaleUtc() + // .domain([x.subtract(k, 'hours').toDate(), x.add(k, 'hours').toDate()]) + // .range([-2000 / scaleX, 2000 / scaleX]); + // } zoomed() { - // const { x, k } = d3Event.transform; - // const { scaleX, translateX } = this.state; - // const { width } = this.svgRef.getBoundingClientRect(); - // // const halfWidth = 0; // width / 2; - // const diff = (width * 0.5) - translateX; - // const fac = diff * 0.5 * (1 - (k / scaleX)); - // console.log(diff, fac); - // - // if (k !== scaleX) { - // const newTransform = zoomIdentity - // .translate(translateX, 0) - // .scale(k) - // .translate(fac, 0); - // console.log(newTransform); - // this.setState({ translateX: newTransform.x, scaleX: newTransform.k }); - // this.svg.call(this.zoom.transform, newTransform); - // } else if (x !== translateX) { - // const newTransform = zoomIdentity - // .translate(x, 0) - // .scale(scaleX); - // console.log(newTransform); - // this.setState({ translateX: newTransform.x, scaleX: newTransform.k }); - // this.svg.call(this.zoom.transform, newTransform); - // } - // this.svg.call(this.zoom.transform, zoomIdentity - // .translate(this.state.translateX, 0) - // .scale(this.state.scaleX, 1)); - // console.log(d3Event); - // console.log(zoomTransform(this.svgRef)); - // console.log('ZOOM', d3Event.transform.k); - this.setState({ scaleX: d3Event.transform.k }); + const timelineRange = moment.duration(1000000 / d3Event.transform.k, 'seconds'); + console.log('ZOOM', timelineRange.toJSON()); + this.setState({ timelineRange }); // this.debouncedUpdateTimestamp(this.getDisplayedTimeScale().invert(-d3Event.transform.x)); } dragged() { - // console.log('DRAG', this.state.translateX + d3Event.dx); - this.setState({ translateX: this.state.translateX + d3Event.dx }); + const { focusedTimestamp, timelineRange } = this.state; + const mv = timelineRange.as('seconds') / R; + const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv, 'seconds'); + console.log('DRAG', newTimestamp.toDate()); + this.setState({ focusedTimestamp: newTimestamp }); } saveSvgRef(ref) { @@ -177,7 +148,7 @@ class TimeTravelTimeline extends React.Component { } renderAxis() { - // const { translateX, scaleX } = this.state; + const { timelineRange, focusedTimestamp } = this.state; // const pausedAt = this.props.pausedAt ? moment(this.props.pausedAt) : moment(); // const timeScale = this.getDisplayedTimeScale(); // @@ -190,10 +161,16 @@ class TimeTravelTimeline extends React.Component { // console.log(endDate, -this.state.translateX + (1000 * this.state.scaleX)); // console.log(ticks); - const timeScale = this.getTimeScale(); + // const timeScale = this.getTimeScale(); // const cd = pausedAt.subtract((translateX / scaleX) * (30 / 10000), 'years'); // console.log(cd.toDate()); - const ticks = timeScale.ticks(10); + const startDate = moment(focusedTimestamp).subtract(timelineRange); + const endDate = moment(focusedTimestamp).add(timelineRange); + const timeScale = scaleUtc() + .domain([startDate, endDate]) + .range([-R, R]); + const ticks = timeScale.ticks(100); + // console.log(startDate.toDate(), endDate.toDate(), timelineRange.toJSON()); // return ( @@ -201,8 +178,9 @@ class TimeTravelTimeline extends React.Component { {fromJS(ticks).map(date => ( {multiFormat(date)} @@ -213,7 +191,6 @@ class TimeTravelTimeline extends React.Component { } render() { - const { translateX, scaleX } = this.state; return (
@@ -224,7 +201,7 @@ class TimeTravelTimeline extends React.Component { id="time-travel-timeline" width="100%" height="100%" ref={this.saveSvgRef}> - + {this.renderAxis()} From 906f0b0a408801e5ddef8a34c0aee7516ed0e99c Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 19 Jul 2017 15:05:32 +0200 Subject: [PATCH 04/31] Working timeline zooming & panning. --- .../components/time-travel-timeline.js | 91 ++++--------------- client/app/scripts/components/time-travel.js | 4 +- .../app/scripts/components/zoomable-canvas.js | 2 - 3 files changed, 22 insertions(+), 75 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 89522ee25e..52283e385f 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -5,7 +5,6 @@ import { connect } from 'react-redux'; import { fromJS } from 'immutable'; import { zoom } from 'd3-zoom'; import { drag } from 'd3-drag'; -// import { zoom, zoomIdentity } from 'd3-zoom'; import { scaleUtc } from 'd3-scale'; import { timeFormat } from 'd3-time-format'; import { timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time'; @@ -18,13 +17,11 @@ import { TIMELINE_DEBOUNCE_INTERVAL, } from '../constants/timer'; -// import { transformToString } from '../utils/transform-utils'; - const formatSecond = timeFormat(':%S'); -const formatMinute = timeFormat('%I:%M'); -const formatHour = timeFormat('%I %p'); +const formatMinute = timeFormat('%H:%M'); +const formatHour = timeFormat('%H:00'); const formatDay = timeFormat('%b %d'); -const formatMonth = timeFormat('%B'); +const formatMonth = timeFormat('%b'); const formatYear = timeFormat('%Y'); function multiFormat(date) { @@ -38,12 +35,8 @@ function multiFormat(date) { return formatYear(date); } -// const timeScale = scaleUtc().clamp(false) -// .domain([new Date(1990, 1), new Date(2020, 1)]) -// .range([0, 10000]); - -// const EARLIEST_TIMESTAMP = moment(new Date(2000, 0)); const R = 10000; +const C = 1000000; class TimeTravelTimeline extends React.Component { constructor(props, context) { @@ -51,45 +44,34 @@ class TimeTravelTimeline extends React.Component { this.state = { focusedTimestamp: moment(), - timelineRange: moment.duration(1000000, 'seconds'), + timelineRange: moment.duration(C, 'seconds'), }; this.width = 2000; - // this.getDisplayedTimeScale = this.getDisplayedTimeScale.bind(this); this.saveSvgRef = this.saveSvgRef.bind(this); this.dragged = this.dragged.bind(this); this.zoomed = this.zoomed.bind(this); - // this.zoomToDate - this.debouncedUpdateTimestamp = debounce( this.updateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); } componentDidMount() { this.svg = select('svg#time-travel-timeline'); - // const w = 1200; - // const h = 60; - this.drag = drag() - .on('drag', this.dragged); - this.zoom = zoom() - // .translateExtent([[0, 0], [w, h]]) - // .extent([[-M, -30], [M, 30]]) - .on('zoom', this.zoomed); - // this.svg.style('transform-origin', '50% 50% 0'); - // console.log(this.svg.getBoundingClientRect()); - + this.drag = drag().on('drag', this.dragged); + this.zoom = zoom().on('zoom', this.zoomed); this.setZoomTriggers(true); - // this.updateZoomLimits(this.props); - // this.restoreZoomState(this.props); } componentWillUnmount() { this.setZoomTriggers(false); } - componentWillReceiveProps() { + componentWillReceiveProps(nextProps) { + if (nextProps.pausedAt) { + this.setState({ focusedTimestamp: nextProps.pausedAt }); + } this.width = this.svgRef.getBoundingClientRect().width; } @@ -108,39 +90,20 @@ class TimeTravelTimeline extends React.Component { } } - // getTimeScale() { - // const { scaleX, translateX } = this.state; - // const S = 4000 / scaleX; - // const p = this.props.pausedAt ? moment(this.props.pausedAt) : moment(); - // const x = p.add(translateX / S, 'hours'); - // const k = S / scaleX; - // // console.log(x, k); - // // const x = moment(p).subtract(this.state.translateX, 'seconds'); - // // // console.log('L', p, x); - // // // const pausedAt = this.props.pausedAt || moment(); - // // const k = 1000 / this.state.scaleX; - // // return scaleUtc() - // // .domain([x.subtract(k, 'seconds').toDate(), x.add(k, 'seconds').toDate()]) - // // .range([0, 2000]); - // return scaleUtc() - // .domain([x.subtract(k, 'hours').toDate(), x.add(k, 'hours').toDate()]) - // .range([-2000 / scaleX, 2000 / scaleX]); - // } - zoomed() { - const timelineRange = moment.duration(1000000 / d3Event.transform.k, 'seconds'); - console.log('ZOOM', timelineRange.toJSON()); + const timelineRange = moment.duration(C / d3Event.transform.k, 'seconds'); + // console.log('ZOOM', timelineRange.toJSON()); this.setState({ timelineRange }); - - // this.debouncedUpdateTimestamp(this.getDisplayedTimeScale().invert(-d3Event.transform.x)); } dragged() { const { focusedTimestamp, timelineRange } = this.state; const mv = timelineRange.as('seconds') / R; const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv, 'seconds'); - console.log('DRAG', newTimestamp.toDate()); + // console.log('DRAG', newTimestamp.toDate()); this.setState({ focusedTimestamp: newTimestamp }); + this.props.onUpdateTimestamp(newTimestamp); + // this.debouncedUpdateTimestamp(this.getDisplayedTimeScale().invert(-d3Event.transform.x)); } saveSvgRef(ref) { @@ -149,30 +112,14 @@ class TimeTravelTimeline extends React.Component { renderAxis() { const { timelineRange, focusedTimestamp } = this.state; - // const pausedAt = this.props.pausedAt ? moment(this.props.pausedAt) : moment(); - // const timeScale = this.getDisplayedTimeScale(); - // - // const startDate = this.getDisplayedTimeScale() - // .invert(-this.state.translateX - (1000 * this.state.scaleX)); - // const endDate = this.getDisplayedTimeScale() - // .invert(-this.state.translateX + (1000 * this.state.scaleX)); - // const ticks = this.getDisplayedTimeScale().domain([startDate, endDate]).ticks(10); - // console.log(startDate, -this.state.translateX - (1000 * this.state.scaleX)); - // console.log(endDate, -this.state.translateX + (1000 * this.state.scaleX)); - // console.log(ticks); - - // const timeScale = this.getTimeScale(); - // const cd = pausedAt.subtract((translateX / scaleX) * (30 / 10000), 'years'); - // console.log(cd.toDate()); const startDate = moment(focusedTimestamp).subtract(timelineRange); const endDate = moment(focusedTimestamp).add(timelineRange); const timeScale = scaleUtc() .domain([startDate, endDate]) .range([-R, R]); - const ticks = timeScale.ticks(100); - // console.log(startDate.toDate(), endDate.toDate(), timelineRange.toJSON()); + const ticks = timeScale.ticks(150); - // + // ${10 * Math.log(timelineRange.as('seconds') / C)} return ( @@ -186,11 +133,13 @@ class TimeTravelTimeline extends React.Component { ))} + ); } render() { + console.log(this.state.focusedTimestamp.toDate()); return (
diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js index f00c49dbeb..ebc1b53fff 100644 --- a/client/app/scripts/components/time-travel.js +++ b/client/app/scripts/components/time-travel.js @@ -81,7 +81,7 @@ class TimeTravel extends React.Component { if (timestamp.isValid()) { timestamp = Math.max(timestamp, this.state.sliderMinValue); timestamp = Math.min(timestamp, moment().valueOf()); - this.travelTo(timestamp, true); + this.travelTo(timestamp); trackMixpanelEvent('scope.time.timestamp.edit', { layout: this.props.topologyViewMode, @@ -176,7 +176,7 @@ class TimeTravel extends React.Component { return (
- +
UTC
diff --git a/client/app/scripts/components/zoomable-canvas.js b/client/app/scripts/components/zoomable-canvas.js index 557b15fd7b..205b4f2c16 100644 --- a/client/app/scripts/components/zoomable-canvas.js +++ b/client/app/scripts/components/zoomable-canvas.js @@ -199,8 +199,6 @@ class ZoomableCanvas extends React.Component { translateY: d3Event.transform.y, }); - console.log(updatedState); - this.setState(updatedState); this.debouncedCacheZoom(); } From 4d064c69a3a0e4185c135fbede3650810156497f Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 19 Jul 2017 15:12:35 +0200 Subject: [PATCH 05/31] Clickable timestamps. --- .../app/scripts/components/time-travel-timeline.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 52283e385f..29056a4f68 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -52,6 +52,7 @@ class TimeTravelTimeline extends React.Component { this.saveSvgRef = this.saveSvgRef.bind(this); this.dragged = this.dragged.bind(this); this.zoomed = this.zoomed.bind(this); + this.jumpTo = this.jumpTo.bind(this); this.debouncedUpdateTimestamp = debounce( this.updateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); @@ -106,6 +107,11 @@ class TimeTravelTimeline extends React.Component { // this.debouncedUpdateTimestamp(this.getDisplayedTimeScale().invert(-d3Event.transform.x)); } + jumpTo(timestamp) { + this.setState({ focusedTimestamp: timestamp }); + this.props.onUpdateTimestamp(timestamp); + } + saveSvgRef(ref) { this.svgRef = ref; } @@ -129,7 +135,11 @@ class TimeTravelTimeline extends React.Component { key={moment(date).format()} style={{ textAlign: 'center' }} width="50" height="20"> - {multiFormat(date)} +
this.jumpTo(moment(date))}> + {multiFormat(date)} + ))} From 449544b7fa9ed2b43434168562d929b09d53ca19 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 19 Jul 2017 15:27:37 +0200 Subject: [PATCH 06/31] Dragging cursor --- .../components/time-travel-timeline.js | 23 +++++++++++++++---- client/app/styles/_base.scss | 20 ++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 29056a4f68..2c19918ef3 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -1,5 +1,6 @@ import React from 'react'; import moment from 'moment'; +import classNames from 'classnames'; import { debounce } from 'lodash'; import { connect } from 'react-redux'; import { fromJS } from 'immutable'; @@ -45,11 +46,14 @@ class TimeTravelTimeline extends React.Component { this.state = { focusedTimestamp: moment(), timelineRange: moment.duration(C, 'seconds'), + isDragging: false, }; this.width = 2000; this.saveSvgRef = this.saveSvgRef.bind(this); + this.dragStarted = this.dragStarted.bind(this); + this.dragEnded = this.dragEnded.bind(this); this.dragged = this.dragged.bind(this); this.zoomed = this.zoomed.bind(this); this.jumpTo = this.jumpTo.bind(this); @@ -60,7 +64,10 @@ class TimeTravelTimeline extends React.Component { componentDidMount() { this.svg = select('svg#time-travel-timeline'); - this.drag = drag().on('drag', this.dragged); + this.drag = drag() + .on('start', this.dragStarted) + .on('end', this.dragEnded) + .on('drag', this.dragged); this.zoom = zoom().on('zoom', this.zoomed); this.setZoomTriggers(true); } @@ -97,6 +104,10 @@ class TimeTravelTimeline extends React.Component { this.setState({ timelineRange }); } + dragStarted() { + this.setState({ isDragging: true }); + } + dragged() { const { focusedTimestamp, timelineRange } = this.state; const mv = timelineRange.as('seconds') / R; @@ -104,7 +115,10 @@ class TimeTravelTimeline extends React.Component { // console.log('DRAG', newTimestamp.toDate()); this.setState({ focusedTimestamp: newTimestamp }); this.props.onUpdateTimestamp(newTimestamp); - // this.debouncedUpdateTimestamp(this.getDisplayedTimeScale().invert(-d3Event.transform.x)); + } + + dragEnded() { + this.setState({ isDragging: false }); } jumpTo(timestamp) { @@ -149,15 +163,16 @@ class TimeTravelTimeline extends React.Component { } render() { - console.log(this.state.focusedTimestamp.toDate()); + const className = classNames({ dragging: this.state.isDragging }); return (
diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 9cd78969ca..55c370711e 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -46,6 +46,19 @@ transition-delay: .5s; } +// From https://stackoverflow.com/a/18294634 +.grabbable { + cursor: move; /* fallback if grab cursor is unsupported */ + cursor: grab; + cursor: -moz-grab; + cursor: -webkit-grab; +} +.grabbing { + cursor: grabbing; + cursor: -moz-grabbing; + cursor: -webkit-grabbing; +} + .shadow-2 { box-shadow: 0 3px 10px rgba(0, 0, 0, 0.16), 0 3px 10px rgba(0, 0, 0, 0.23); } @@ -258,10 +271,17 @@ height: 60px; svg { + @extend .grabbable; background-color: rgba(255, 255, 255, 0.85); box-shadow: inset 0 0 7px #aaa; pointer-events: all; margin: 0 7px; + + &.dragging { @extend .grabbing; } + + .timestamp-label { + padding: 3px; + } } &:after { From 9172788f816e0badfffd8c9fa0b21eec133247ce Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 19 Jul 2017 15:55:46 +0200 Subject: [PATCH 07/31] Timeline panning buttons. --- .../components/time-travel-timeline.js | 19 ++- client/app/styles/_base.scss | 120 ++++-------------- 2 files changed, 38 insertions(+), 101 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 2c19918ef3..b4b6b50603 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -57,6 +57,8 @@ class TimeTravelTimeline extends React.Component { this.dragged = this.dragged.bind(this); this.zoomed = this.zoomed.bind(this); this.jumpTo = this.jumpTo.bind(this); + this.jumpForward = this.jumpForward.bind(this); + this.jumpBackward = this.jumpBackward.bind(this); this.debouncedUpdateTimestamp = debounce( this.updateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); @@ -113,8 +115,7 @@ class TimeTravelTimeline extends React.Component { const mv = timelineRange.as('seconds') / R; const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv, 'seconds'); // console.log('DRAG', newTimestamp.toDate()); - this.setState({ focusedTimestamp: newTimestamp }); - this.props.onUpdateTimestamp(newTimestamp); + this.jumpTo(newTimestamp); } dragEnded() { @@ -126,6 +127,18 @@ class TimeTravelTimeline extends React.Component { this.props.onUpdateTimestamp(timestamp); } + jumpForward() { + const d = this.state.timelineRange.asMilliseconds() / 3; + const timestamp = moment(this.state.focusedTimestamp).add(d); + this.jumpTo(timestamp); + } + + jumpBackward() { + const d = this.state.timelineRange.asMilliseconds() / 3; + const timestamp = moment(this.state.focusedTimestamp).subtract(d); + this.jumpTo(timestamp); + } + saveSvgRef(ref) { this.svgRef = ref; } @@ -179,7 +192,7 @@ class TimeTravelTimeline extends React.Component { {this.renderAxis()} - +
diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 55c370711e..6c9f9a89e7 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -265,6 +265,16 @@ overflow: hidden; height: 0; + &.visible { + height: 100px; + margin-bottom: 15px; + margin-top: -5px; + } + + .button { + pointer-events: all; + } + .time-travel-timeline { align-items: center; display: flex; @@ -297,50 +307,12 @@ } } - &.visible { - height: 100px; - margin-bottom: 15px; - margin-top: -5px; - } - - &-markers { - position: relative; - - &-tick { - text-align: center; - position: absolute; - - .vertical-tick { - border: 1px solid $text-tertiary-color; - border-radius: 1px; - display: block; - margin: 1px auto 2px; - height: 12px; - width: 0; - } - - .link { - display: inline-block; - pointer-events: all; - margin-top: 1px; - } - } - } - - &-slider-wrapper { - margin: 0 50px 20px 10px; - pointer-events: all; - flex-grow: 1; - - .rc-slider-rail { background-color: $text-tertiary-color; } - } - &-timestamp { border: 1px solid #ccc; border-radius: 4px; padding: 2px 8px; pointer-events: all; - margin: 4px auto 25px; + margin: 8px auto 25px; width: 225px; input { @@ -352,65 +324,17 @@ outline: 0; } - // &:before, &:after { - // content: ''; - // position: absolute; - // display: block; - // margin: 0 100px; - // } - // - // &:before { - // top: 0; - // border: 1px solid white; - // background-color: red; - // height: 69px; - // width: 3px; - // } - // - // &:after { - // border: 1px solid #ccc; - // top: 70px; - // height: 20px; - // width: 1px; - // } - } - - // &-jump-controls { - // display: flex; - // - // .button.jump { - // display: block; - // margin: 8px; - // font-size: 0.625rem; - // pointer-events: all; - // text-align: center; - // text-transform: uppercase; - // word-spacing: -1px; - // - // .fa { - // display: block; - // font-size: 150%; - // margin-bottom: 3px; - // } - // } - // - // &-timestamp { - // border: 1px solid #ccc; - // border-radius: 4px; - // padding: 2px 8px; - // pointer-events: all; - // margin: 4px 8px 25px; - // - // input { - // border: 0; - // background-color: transparent; - // font-family: $mono-font; - // font-size: 0.875rem; - // margin-right: 2px; - // outline: 0; - // } - // } - // } + &:before { + content: ''; + position: relative; + display: block; + margin: -15px auto 3px; + background-color: #ccc; + height: 13px; + width: 1px; + left: 1px + } + } } From 6967c3f161b21f7b354708cb2e3ced992d1ea392 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 19 Jul 2017 16:25:59 +0200 Subject: [PATCH 08/31] Capping at current time. --- .../components/time-travel-timeline.js | 27 ++++++++++++++----- client/app/scripts/constants/timer.js | 2 +- client/app/styles/_base.scss | 9 +++++-- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index b4b6b50603..49c9c0d355 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -15,6 +15,7 @@ import { } from '../actions/app-actions'; import { + TIMELINE_TICK_INTERVAL, TIMELINE_DEBOUNCE_INTERVAL, } from '../constants/timer'; @@ -71,10 +72,15 @@ class TimeTravelTimeline extends React.Component { .on('end', this.dragEnded) .on('drag', this.dragged); this.zoom = zoom().on('zoom', this.zoomed); + this.setZoomTriggers(true); + + // Force periodic re-renders to update the slider position as time goes by. + this.timer = setInterval(() => { this.forceUpdate(); }, TIMELINE_TICK_INTERVAL); } componentWillUnmount() { + clearInterval(this.timer); this.setZoomTriggers(false); } @@ -112,8 +118,8 @@ class TimeTravelTimeline extends React.Component { dragged() { const { focusedTimestamp, timelineRange } = this.state; - const mv = timelineRange.as('seconds') / R; - const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv, 'seconds'); + const mv = timelineRange.asMilliseconds() / R; + const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv); // console.log('DRAG', newTimestamp.toDate()); this.jumpTo(newTimestamp); } @@ -123,19 +129,20 @@ class TimeTravelTimeline extends React.Component { } jumpTo(timestamp) { + timestamp = timestamp > moment() ? moment() : timestamp; this.setState({ focusedTimestamp: timestamp }); this.props.onUpdateTimestamp(timestamp); } jumpForward() { - const d = this.state.timelineRange.asMilliseconds() / 3; - const timestamp = moment(this.state.focusedTimestamp).add(d); + const d = this.state.timelineRange.asMilliseconds() / 4 / R; + const timestamp = moment(this.state.focusedTimestamp).add(d * this.width); this.jumpTo(timestamp); } jumpBackward() { - const d = this.state.timelineRange.asMilliseconds() / 3; - const timestamp = moment(this.state.focusedTimestamp).subtract(d); + const d = this.state.timelineRange.asMilliseconds() / 4 / R; + const timestamp = moment(this.state.focusedTimestamp).subtract(d * this.width); this.jumpTo(timestamp); } @@ -152,9 +159,16 @@ class TimeTravelTimeline extends React.Component { .range([-R, R]); const ticks = timeScale.ticks(150); + const nowX = timeScale(moment()); + // ${10 * Math.log(timelineRange.as('seconds') / C)} return ( + + {fromJS(ticks).map(date => ( ))} - ); } diff --git a/client/app/scripts/constants/timer.js b/client/app/scripts/constants/timer.js index 65107e23cf..3709837596 100644 --- a/client/app/scripts/constants/timer.js +++ b/client/app/scripts/constants/timer.js @@ -9,4 +9,4 @@ export const VIEWPORT_RESIZE_DEBOUNCE_INTERVAL = 200; export const ZOOM_CACHE_DEBOUNCE_INTERVAL = 500; export const TIMELINE_DEBOUNCE_INTERVAL = 500; -export const TIMELINE_TICK_INTERVAL = 500; +export const TIMELINE_TICK_INTERVAL = 100; diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 6c9f9a89e7..f7f93eae03 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -289,6 +289,11 @@ &.dragging { @extend .grabbing; } + .available-range { + fill: #888; + fill-opacity: 0.1; + } + .timestamp-label { padding: 3px; } @@ -328,9 +333,9 @@ content: ''; position: relative; display: block; - margin: -15px auto 3px; + margin: -12px auto 3px; background-color: #ccc; - height: 13px; + height: 9px; width: 1px; left: 1px } From dea9daa698ea4650719bd0042dd272a86836ff05 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 19 Jul 2017 17:27:23 +0200 Subject: [PATCH 09/31] Scale limits. --- .../components/time-travel-timeline.js | 72 +++++++++---------- client/app/scripts/constants/timer.js | 2 +- client/app/styles/_base.scss | 1 + 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 49c9c0d355..1e90a35092 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -1,14 +1,12 @@ import React from 'react'; import moment from 'moment'; import classNames from 'classnames'; -import { debounce } from 'lodash'; +import { debounce, map } from 'lodash'; import { connect } from 'react-redux'; import { fromJS } from 'immutable'; import { zoom } from 'd3-zoom'; import { drag } from 'd3-drag'; import { scaleUtc } from 'd3-scale'; -import { timeFormat } from 'd3-time-format'; -import { timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time'; import { event as d3Event, select } from 'd3-selection'; import { jumpToTime, @@ -19,25 +17,17 @@ import { TIMELINE_DEBOUNCE_INTERVAL, } from '../constants/timer'; -const formatSecond = timeFormat(':%S'); -const formatMinute = timeFormat('%H:%M'); -const formatHour = timeFormat('%H:00'); -const formatDay = timeFormat('%b %d'); -const formatMonth = timeFormat('%b'); -const formatYear = timeFormat('%Y'); - -function multiFormat(date) { - date = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), - date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()); - if (timeMinute(date) < date) return formatSecond(date); - if (timeHour(date) < date) return formatMinute(date); - if (timeDay(date) < date) return formatHour(date); - if (timeMonth(date) < date) return formatDay(date); - if (timeYear(date) < date) return formatMonth(date); - return formatYear(date); + +function timestampShortLabel(timestamp) { + if (moment(timestamp).startOf('minute').isBefore(timestamp)) return timestamp.format(':ss'); + if (moment(timestamp).startOf('hour').isBefore(timestamp)) return timestamp.format('HH:mm'); + if (moment(timestamp).startOf('day').isBefore(timestamp)) return timestamp.format('HH:00'); + if (moment(timestamp).startOf('month').isBefore(timestamp)) return timestamp.format('MMM DD'); + if (moment(timestamp).startOf('year').isBefore(timestamp)) return timestamp.format('MMM'); + return timestamp.format('YYYY'); } -const R = 10000; +const R = 2000; const C = 1000000; class TimeTravelTimeline extends React.Component { @@ -45,6 +35,7 @@ class TimeTravelTimeline extends React.Component { super(props, context); this.state = { + timestampNow: moment(), focusedTimestamp: moment(), timelineRange: moment.duration(C, 'seconds'), isDragging: false, @@ -71,12 +62,16 @@ class TimeTravelTimeline extends React.Component { .on('start', this.dragStarted) .on('end', this.dragEnded) .on('drag', this.dragged); - this.zoom = zoom().on('zoom', this.zoomed); + this.zoom = zoom() + .scaleExtent([0.002, 8000]) + .on('zoom', this.zoomed); this.setZoomTriggers(true); // Force periodic re-renders to update the slider position as time goes by. - this.timer = setInterval(() => { this.forceUpdate(); }, TIMELINE_TICK_INTERVAL); + this.timer = setInterval(() => { + this.setState({ timestampNow: moment().startOf('second') }); + }, TIMELINE_TICK_INTERVAL); } componentWillUnmount() { @@ -129,9 +124,10 @@ class TimeTravelTimeline extends React.Component { } jumpTo(timestamp) { - timestamp = timestamp > moment() ? moment() : timestamp; - this.setState({ focusedTimestamp: timestamp }); - this.props.onUpdateTimestamp(timestamp); + const { timestampNow } = this.state; + const focusedTimestamp = timestamp > timestampNow ? timestampNow : timestamp; + this.props.onUpdateTimestamp(focusedTimestamp); + this.setState({ focusedTimestamp }); } jumpForward() { @@ -151,15 +147,17 @@ class TimeTravelTimeline extends React.Component { } renderAxis() { - const { timelineRange, focusedTimestamp } = this.state; - const startDate = moment(focusedTimestamp).subtract(timelineRange); - const endDate = moment(focusedTimestamp).add(timelineRange); + const { timelineRange, focusedTimestamp, timestampNow } = this.state; + const rTimestamp = moment(focusedTimestamp).startOf('second').utc(); + const startDate = moment(rTimestamp).subtract(timelineRange); + const endDate = moment(rTimestamp).add(timelineRange); const timeScale = scaleUtc() .domain([startDate, endDate]) .range([-R, R]); - const ticks = timeScale.ticks(150); - const nowX = timeScale(moment()); + const ticks = map(timeScale.ticks(30), t => moment(t).utc()); + + const nowX = Math.min(timeScale(timestampNow), R); // ${10 * Math.log(timelineRange.as('seconds') / C)} return ( @@ -167,19 +165,19 @@ class TimeTravelTimeline extends React.Component { + x={-2 * R} y={-30} width={2 * R} height={60} /> - {fromJS(ticks).map(date => ( + {fromJS(ticks).map(timestamp => ( + width="100" height="20"> this.jumpTo(moment(date))}> - {multiFormat(date)} + onClick={() => this.jumpTo(timestamp)}> + {timestampShortLabel(timestamp)} ))} diff --git a/client/app/scripts/constants/timer.js b/client/app/scripts/constants/timer.js index 3709837596..3b14691a85 100644 --- a/client/app/scripts/constants/timer.js +++ b/client/app/scripts/constants/timer.js @@ -9,4 +9,4 @@ export const VIEWPORT_RESIZE_DEBOUNCE_INTERVAL = 200; export const ZOOM_CACHE_DEBOUNCE_INTERVAL = 500; export const TIMELINE_DEBOUNCE_INTERVAL = 500; -export const TIMELINE_TICK_INTERVAL = 100; +export const TIMELINE_TICK_INTERVAL = 1000; diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index f7f93eae03..525491f9b1 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -272,6 +272,7 @@ } .button { + padding: 2px; pointer-events: all; } From b16dac9faf1d3429fc2834aaaf0e949edc8b3359 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 21 Jul 2017 10:48:10 +0200 Subject: [PATCH 10/31] Better ticks. --- .../components/time-travel-timeline.js | 150 ++++++++++++++---- client/app/styles/_base.scss | 7 +- 2 files changed, 121 insertions(+), 36 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 1e90a35092..85aafb6ab3 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -1,7 +1,7 @@ import React from 'react'; import moment from 'moment'; import classNames from 'classnames'; -import { debounce, map } from 'lodash'; +import { debounce } from 'lodash'; import { connect } from 'react-redux'; import { fromJS } from 'immutable'; import { zoom } from 'd3-zoom'; @@ -18,14 +18,30 @@ import { } from '../constants/timer'; -function timestampShortLabel(timestamp) { - if (moment(timestamp).startOf('minute').isBefore(timestamp)) return timestamp.format(':ss'); - if (moment(timestamp).startOf('hour').isBefore(timestamp)) return timestamp.format('HH:mm'); - if (moment(timestamp).startOf('day').isBefore(timestamp)) return timestamp.format('HH:00'); - if (moment(timestamp).startOf('month').isBefore(timestamp)) return timestamp.format('MMM DD'); - if (moment(timestamp).startOf('year').isBefore(timestamp)) return timestamp.format('MMM'); - return timestamp.format('YYYY'); -} +// function timestampShortLabel(timestamp) { +// if (moment(timestamp).startOf('minute').isBefore(timestamp)) return timestamp.format(':ss'); +// if (moment(timestamp).startOf('hour').isBefore(timestamp)) return timestamp.format('HH:mm'); +// if (moment(timestamp).startOf('day').isBefore(timestamp)) return timestamp.format('HH:00'); +// if (moment(timestamp).startOf('month').isBefore(timestamp)) return timestamp.format('MMM DD'); +// if (moment(timestamp).startOf('year').isBefore(timestamp)) return timestamp.format('MMM'); +// return timestamp.format('YYYY'); +// } + +const fixedDurations = [ + moment.duration(5, 'seconds'), + moment.duration(15, 'seconds'), + moment.duration(1, 'minute'), + moment.duration(5, 'minutes'), + moment.duration(15, 'minutes'), + moment.duration(1, 'hour'), + moment.duration(3, 'hours'), + moment.duration(6, 'hours'), + moment.duration(1, 'day'), + moment.duration(1, 'week'), + moment.duration(1, 'month'), + moment.duration(3, 'months'), + moment.duration(1, 'year'), +]; const R = 2000; const C = 1000000; @@ -63,7 +79,7 @@ class TimeTravelTimeline extends React.Component { .on('end', this.dragEnded) .on('drag', this.dragged); this.zoom = zoom() - .scaleExtent([0.002, 8000]) + .scaleExtent([0.003, 8000]) .on('zoom', this.zoomed); this.setZoomTriggers(true); @@ -146,44 +162,112 @@ class TimeTravelTimeline extends React.Component { this.svgRef = ref; } - renderAxis() { - const { timelineRange, focusedTimestamp, timestampNow } = this.state; + getTimeScale() { + const { timelineRange, focusedTimestamp } = this.state; const rTimestamp = moment(focusedTimestamp).startOf('second').utc(); const startDate = moment(rTimestamp).subtract(timelineRange); const endDate = moment(rTimestamp).add(timelineRange); - const timeScale = scaleUtc() + return scaleUtc() .domain([startDate, endDate]) .range([-R, R]); + } - const ticks = map(timeScale.ticks(30), t => moment(t).utc()); + renderPeriodBar(period, prevPeriod, periodFormat, yShift, [startIndex, endIndex]) { + const timeScale = this.getTimeScale(); + const startDate = moment(timeScale.invert(-R)); + const endDate = moment(timeScale.invert(R)); + const numSeconds = endDate.diff(startDate, 'seconds', true); + // console.log(numSeconds); + const ts = []; + + let duration = null; + for (let i = startIndex; i <= endIndex; i += 1) { + if (numSeconds / fixedDurations[i].asSeconds() < 50) { + duration = fixedDurations[i]; + break; + } + } - const nowX = Math.min(timeScale(timestampNow), R); + // console.log(duration.asSeconds()); + if (!duration) return null; + + let t = moment(startDate).startOf(prevPeriod); + let turningPoint = moment(t).add(1, prevPeriod); + do { + const p = timeScale(t); + if (p > -this.width && p < this.width) { + ts.push(t); + } + t = moment(t).add(duration); + if (prevPeriod !== period && t >= turningPoint) { + t = turningPoint; + turningPoint = moment(turningPoint).add(1, prevPeriod); + } + } while (timeScale(t) < this.width); + + // console.log(ts); + + return ( + + {fromJS(ts).map(timestamp => ( + + + + this.jumpTo(timestamp)}> + {timestamp.format(periodFormat)} + + + + ))} + + ); + } + + renderAxis() { + const timeScale = this.getTimeScale(); + // const ticks = map(timeScale.ticks(30), t => moment(t).utc()); + const nowX = Math.min(timeScale(this.state.timestampNow), R); - // ${10 * Math.log(timelineRange.as('seconds') / C)} return ( - - - {fromJS(ticks).map(timestamp => ( - - this.jumpTo(timestamp)}> - {timestampShortLabel(timestamp)} - - - ))} + x={-2 * R} y={0} width={2 * R} height={70} /> + + {this.renderPeriodBar('year', 'year', 'YYYY', 0, [12, 12])} + {this.renderPeriodBar('month', 'year', 'MMMM', 13, [10, 11])} + {this.renderPeriodBar('day', 'month', 'Do', 26, [8, 9])} + {this.renderPeriodBar('minute', 'day', 'HH:mm', 39, [2, 7])} + {this.renderPeriodBar('second', 'minute', 's [secs]', 52, [0, 1])} ); + + // ${10 * Math.log(timelineRange.as('seconds') / C)} + // + // return ( + // + // + // + // {fromJS(ticks).map(timestamp => ( + // + // this.jumpTo(timestamp)}> + // {timestampShortLabel(timestamp)} + // + // + // ))} + // + // + // ); } render() { @@ -196,7 +280,7 @@ class TimeTravelTimeline extends React.Component { diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 525491f9b1..54acc9577b 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -266,7 +266,7 @@ height: 0; &.visible { - height: 100px; + height: 105px; margin-bottom: 15px; margin-top: -5px; } @@ -279,7 +279,7 @@ .time-travel-timeline { align-items: center; display: flex; - height: 60px; + height: 70px; svg { @extend .grabbable; @@ -307,8 +307,9 @@ left: 50%; top: 0; border: 1px solid white; + border-top: 0; background-color: red; - height: 60px; + height: 70px; width: 3px; } } From f4e35c6e5c02af140e967a73ad4c947d0cecb277 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 21 Jul 2017 11:50:15 +0200 Subject: [PATCH 11/31] Time tags fading in smoothly. --- .../components/time-travel-timeline.js | 52 +++++++++++++++---- client/app/scripts/components/time-travel.js | 6 ++- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 85aafb6ab3..4b853bd1d9 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -43,6 +43,32 @@ const fixedDurations = [ moment.duration(1, 'year'), ]; +const yFunc = (scale, start) => { + const end = start * 1.4; + if (scale < start) return 0; + if (scale > end) return 1; + return (Math.log(scale) - Math.log(start)) / (Math.log(end) - Math.log(start)); +}; + +const getShift = (period, scale) => { + const yearShift = 1; + const monthShift = yFunc(scale, 0.0052); + const dayShift = yFunc(scale, 0.067); + const minuteShift = yFunc(scale, 1.9); + const secondShift = yFunc(scale, 2500); + // console.log(scale, yearShift, monthShift, dayShift, minuteShift, secondShift); + let result = 0; + switch (period) { + case 'year': result = yearShift + monthShift + dayShift + minuteShift + secondShift; break; + case 'month': result = monthShift + dayShift + minuteShift + secondShift; break; + case 'day': result = dayShift + minuteShift + secondShift; break; + case 'minute': result = minuteShift + secondShift; break; + case 'second': result = secondShift; break; + default: result = 0; break; + } + return result; +}; + const R = 2000; const C = 1000000; @@ -55,6 +81,7 @@ class TimeTravelTimeline extends React.Component { focusedTimestamp: moment(), timelineRange: moment.duration(C, 'seconds'), isDragging: false, + scaleX: 1, }; this.width = 2000; @@ -120,7 +147,7 @@ class TimeTravelTimeline extends React.Component { zoomed() { const timelineRange = moment.duration(C / d3Event.transform.k, 'seconds'); // console.log('ZOOM', timelineRange.toJSON()); - this.setState({ timelineRange }); + this.setState({ timelineRange, scaleX: d3Event.transform.k }); } dragStarted() { @@ -172,7 +199,7 @@ class TimeTravelTimeline extends React.Component { .range([-R, R]); } - renderPeriodBar(period, prevPeriod, periodFormat, yShift, [startIndex, endIndex]) { + renderPeriodBar(period, prevPeriod, periodFormat, [startIndex, endIndex]) { const timeScale = this.getTimeScale(); const startDate = moment(timeScale.invert(-R)); const endDate = moment(timeScale.invert(R)); @@ -188,6 +215,8 @@ class TimeTravelTimeline extends React.Component { } } + const behind = (period === 'day') ? 2 : 0; + // console.log(duration.asSeconds()); if (!duration) return null; @@ -199,7 +228,7 @@ class TimeTravelTimeline extends React.Component { ts.push(t); } t = moment(t).add(duration); - if (prevPeriod !== period && t >= turningPoint) { + if (prevPeriod !== period && t >= moment(turningPoint).subtract(behind, period)) { t = turningPoint; turningPoint = moment(turningPoint).add(1, prevPeriod); } @@ -207,8 +236,11 @@ class TimeTravelTimeline extends React.Component { // console.log(ts); + const p = getShift(period, this.state.scaleX); + const shift = 60 * (1 - (p * 0.2)); + const opacity = Math.min(p * p, 1); return ( - + {fromJS(ts).map(timestamp => ( @@ -234,12 +266,12 @@ class TimeTravelTimeline extends React.Component { className="available-range" transform={`translate(${nowX}, 0)`} x={-2 * R} y={0} width={2 * R} height={70} /> - - {this.renderPeriodBar('year', 'year', 'YYYY', 0, [12, 12])} - {this.renderPeriodBar('month', 'year', 'MMMM', 13, [10, 11])} - {this.renderPeriodBar('day', 'month', 'Do', 26, [8, 9])} - {this.renderPeriodBar('minute', 'day', 'HH:mm', 39, [2, 7])} - {this.renderPeriodBar('second', 'minute', 's [secs]', 52, [0, 1])} + + {this.renderPeriodBar('year', 'year', 'YYYY', [12, 12])} + {this.renderPeriodBar('month', 'year', 'MMMM', [10, 11])} + {this.renderPeriodBar('day', 'month', 'Do', [8, 9])} + {this.renderPeriodBar('minute', 'day', 'HH:mm', [2, 7])} + {this.renderPeriodBar('second', 'minute', 's [secs]', [0, 1])} ); diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js index ebc1b53fff..17ee9c66e4 100644 --- a/client/app/scripts/components/time-travel.js +++ b/client/app/scripts/components/time-travel.js @@ -70,8 +70,10 @@ class TimeTravel extends React.Component { } handleSliderChange(timestamp) { - this.travelTo(timestamp, true); - this.debouncedTrackSliderChange(); + if (!timestamp.isSame(this.props.pausedAt)) { + this.travelTo(timestamp, true); + this.debouncedTrackSliderChange(); + } } handleInputChange(ev) { From 0401bbbbb3133708f6f0fea2a9ded2d29a229cb3 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 21 Jul 2017 15:55:20 +0200 Subject: [PATCH 12/31] Removed seconds. --- .../components/time-travel-timeline.js | 62 ++++--------------- 1 file changed, 11 insertions(+), 51 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 4b853bd1d9..a2b2a135da 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -18,18 +18,7 @@ import { } from '../constants/timer'; -// function timestampShortLabel(timestamp) { -// if (moment(timestamp).startOf('minute').isBefore(timestamp)) return timestamp.format(':ss'); -// if (moment(timestamp).startOf('hour').isBefore(timestamp)) return timestamp.format('HH:mm'); -// if (moment(timestamp).startOf('day').isBefore(timestamp)) return timestamp.format('HH:00'); -// if (moment(timestamp).startOf('month').isBefore(timestamp)) return timestamp.format('MMM DD'); -// if (moment(timestamp).startOf('year').isBefore(timestamp)) return timestamp.format('MMM'); -// return timestamp.format('YYYY'); -// } - const fixedDurations = [ - moment.duration(5, 'seconds'), - moment.duration(15, 'seconds'), moment.duration(1, 'minute'), moment.duration(5, 'minutes'), moment.duration(15, 'minutes'), @@ -55,15 +44,13 @@ const getShift = (period, scale) => { const monthShift = yFunc(scale, 0.0052); const dayShift = yFunc(scale, 0.067); const minuteShift = yFunc(scale, 1.9); - const secondShift = yFunc(scale, 2500); - // console.log(scale, yearShift, monthShift, dayShift, minuteShift, secondShift); + console.log(scale, yearShift, monthShift, dayShift, minuteShift); let result = 0; switch (period) { - case 'year': result = yearShift + monthShift + dayShift + minuteShift + secondShift; break; - case 'month': result = monthShift + dayShift + minuteShift + secondShift; break; - case 'day': result = dayShift + minuteShift + secondShift; break; - case 'minute': result = minuteShift + secondShift; break; - case 'second': result = secondShift; break; + case 'year': result = yearShift + monthShift + dayShift + minuteShift; break; + case 'month': result = monthShift + dayShift + minuteShift; break; + case 'day': result = dayShift + minuteShift; break; + case 'minute': result = minuteShift; break; default: result = 0; break; } return result; @@ -106,7 +93,7 @@ class TimeTravelTimeline extends React.Component { .on('end', this.dragEnded) .on('drag', this.dragged); this.zoom = zoom() - .scaleExtent([0.003, 8000]) + .scaleExtent([0.003, 1000]) .on('zoom', this.zoomed); this.setZoomTriggers(true); @@ -237,7 +224,7 @@ class TimeTravelTimeline extends React.Component { // console.log(ts); const p = getShift(period, this.state.scaleX); - const shift = 60 * (1 - (p * 0.2)); + const shift = 60 * (1 - (p * 0.25)); const opacity = Math.min(p * p, 1); return ( @@ -257,7 +244,6 @@ class TimeTravelTimeline extends React.Component { renderAxis() { const timeScale = this.getTimeScale(); - // const ticks = map(timeScale.ticks(30), t => moment(t).utc()); const nowX = Math.min(timeScale(this.state.timestampNow), R); return ( @@ -267,39 +253,13 @@ class TimeTravelTimeline extends React.Component { transform={`translate(${nowX}, 0)`} x={-2 * R} y={0} width={2 * R} height={70} /> - {this.renderPeriodBar('year', 'year', 'YYYY', [12, 12])} - {this.renderPeriodBar('month', 'year', 'MMMM', [10, 11])} - {this.renderPeriodBar('day', 'month', 'Do', [8, 9])} - {this.renderPeriodBar('minute', 'day', 'HH:mm', [2, 7])} - {this.renderPeriodBar('second', 'minute', 's [secs]', [0, 1])} + {this.renderPeriodBar('year', 'year', 'YYYY', [10, 10])} + {this.renderPeriodBar('month', 'year', 'MMMM', [8, 9])} + {this.renderPeriodBar('day', 'month', 'Do', [6, 7])} + {this.renderPeriodBar('minute', 'day', 'HH:mm', [0, 5])} ); - - // ${10 * Math.log(timelineRange.as('seconds') / C)} - // - // return ( - // - // - // - // {fromJS(ticks).map(timestamp => ( - // - // this.jumpTo(timestamp)}> - // {timestampShortLabel(timestamp)} - // - // - // ))} - // - // - // ); } render() { From a67733eb1c717ee2f68c7c6906af1303ea9d1c45 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 21 Jul 2017 16:08:47 +0200 Subject: [PATCH 13/31] Better tick spacing. --- .../app/scripts/components/time-travel-timeline.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index a2b2a135da..9b2683fd59 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -58,6 +58,7 @@ const getShift = (period, scale) => { const R = 2000; const C = 1000000; +const MIN_TICK_SPACING = 40; class TimeTravelTimeline extends React.Component { constructor(props, context) { @@ -93,7 +94,7 @@ class TimeTravelTimeline extends React.Component { .on('end', this.dragEnded) .on('drag', this.dragged); this.zoom = zoom() - .scaleExtent([0.003, 1000]) + .scaleExtent([0.002, 1000]) .on('zoom', this.zoomed); this.setZoomTriggers(true); @@ -202,7 +203,7 @@ class TimeTravelTimeline extends React.Component { } } - const behind = (period === 'day') ? 2 : 0; + // const behind = (period === 'day') ? 2 : 0; // console.log(duration.asSeconds()); if (!duration) return null; @@ -212,10 +213,13 @@ class TimeTravelTimeline extends React.Component { do { const p = timeScale(t); if (p > -this.width && p < this.width) { + if (p - timeScale(ts[ts.length - 1]) < MIN_TICK_SPACING) { + ts.pop(); + } ts.push(t); } t = moment(t).add(duration); - if (prevPeriod !== period && t >= moment(turningPoint).subtract(behind, period)) { + if (prevPeriod !== period && t >= turningPoint) { t = turningPoint; turningPoint = moment(turningPoint).add(1, prevPeriod); } @@ -224,7 +228,7 @@ class TimeTravelTimeline extends React.Component { // console.log(ts); const p = getShift(period, this.state.scaleX); - const shift = 60 * (1 - (p * 0.25)); + const shift = 2 + (55 * (1 - (p * 0.25))); const opacity = Math.min(p * p, 1); return ( From 282ce2030a6f92faa7c77a03addf6ecdbaea05f2 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 21 Jul 2017 16:22:40 +0200 Subject: [PATCH 14/31] Vertical panning as zooming. --- .../components/time-travel-timeline.js | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 9b2683fd59..c983caac1e 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -59,6 +59,8 @@ const getShift = (period, scale) => { const R = 2000; const C = 1000000; const MIN_TICK_SPACING = 40; +const MIN_ZOOM = 0.002; +const MAX_ZOOM = 1000; class TimeTravelTimeline extends React.Component { constructor(props, context) { @@ -94,7 +96,7 @@ class TimeTravelTimeline extends React.Component { .on('end', this.dragEnded) .on('drag', this.dragged); this.zoom = zoom() - .scaleExtent([0.002, 1000]) + .scaleExtent([MIN_ZOOM, MAX_ZOOM]) .on('zoom', this.zoomed); this.setZoomTriggers(true); @@ -133,9 +135,9 @@ class TimeTravelTimeline extends React.Component { } zoomed() { - const timelineRange = moment.duration(C / d3Event.transform.k, 'seconds'); - // console.log('ZOOM', timelineRange.toJSON()); - this.setState({ timelineRange, scaleX: d3Event.transform.k }); + const scaleX = d3Event.transform.k; + const timelineRange = moment.duration(C / scaleX, 'seconds'); + this.setState({ timelineRange, scaleX }); } dragStarted() { @@ -143,10 +145,19 @@ class TimeTravelTimeline extends React.Component { } dragged() { - const { focusedTimestamp, timelineRange } = this.state; + // const { focusedTimestamp, timelineRange } = this.state; + // const mv = timelineRange.asMilliseconds() / R; + // const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv); + // this.jumpTo(newTimestamp); + + let scaleX = this.state.scaleX * Math.pow(1.02, -d3Event.dy); + scaleX = Math.max(Math.min(scaleX, MAX_ZOOM), MIN_ZOOM); + const timelineRange = moment.duration(C / scaleX, 'seconds'); + this.setState({ timelineRange, scaleX }); + + const { focusedTimestamp } = this.state; const mv = timelineRange.asMilliseconds() / R; const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv); - // console.log('DRAG', newTimestamp.toDate()); this.jumpTo(newTimestamp); } From c5e8f926831d0a164b8200099a4afcbbed8cf25a Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 21 Jul 2017 18:14:30 +0200 Subject: [PATCH 15/31] Organizing the code.. --- .../components/time-travel-timeline.js | 109 ++++++++++-------- client/app/styles/_base.scss | 2 + 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index c983caac1e..031b8e43c4 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -17,18 +17,23 @@ import { TIMELINE_DEBOUNCE_INTERVAL, } from '../constants/timer'; - -const fixedDurations = [ +const MINUTE_INTERVALS = [ moment.duration(1, 'minute'), moment.duration(5, 'minutes'), moment.duration(15, 'minutes'), moment.duration(1, 'hour'), moment.duration(3, 'hours'), moment.duration(6, 'hours'), +]; +const DAY_INTERVALS = [ moment.duration(1, 'day'), moment.duration(1, 'week'), +]; +const MONTH_INTERVALS = [ moment.duration(1, 'month'), moment.duration(3, 'months'), +]; +const YEAR_INTERVALS = [ moment.duration(1, 'year'), ]; @@ -56,8 +61,12 @@ const getShift = (period, scale) => { return result; }; -const R = 2000; -const C = 1000000; +function scaleDuration(duration, scale) { + return moment.duration(duration.asMilliseconds() * scale); +} + +const WIDTH = 5000; +const C = 2000000; const MIN_TICK_SPACING = 40; const MIN_ZOOM = 0.002; const MAX_ZOOM = 1000; @@ -150,14 +159,14 @@ class TimeTravelTimeline extends React.Component { // const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv); // this.jumpTo(newTimestamp); - let scaleX = this.state.scaleX * Math.pow(1.02, -d3Event.dy); + let scaleX = this.state.scaleX * Math.pow(1.00, (d3Event.dx === 0 ? -d3Event.dy : 0)); scaleX = Math.max(Math.min(scaleX, MAX_ZOOM), MIN_ZOOM); const timelineRange = moment.duration(C / scaleX, 'seconds'); this.setState({ timelineRange, scaleX }); const { focusedTimestamp } = this.state; - const mv = timelineRange.asMilliseconds() / R; - const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv); + const dragDuration = scaleDuration(timelineRange, -d3Event.dx / WIDTH); + const newTimestamp = moment(focusedTimestamp).add(dragDuration); this.jumpTo(newTimestamp); } @@ -173,14 +182,16 @@ class TimeTravelTimeline extends React.Component { } jumpForward() { - const d = this.state.timelineRange.asMilliseconds() / 4 / R; - const timestamp = moment(this.state.focusedTimestamp).add(d * this.width); + const { focusedTimestamp, timelineRange } = this.state; + const duration = scaleDuration(timelineRange, this.width / WIDTH / 2); + const timestamp = moment(focusedTimestamp).add(duration); this.jumpTo(timestamp); } jumpBackward() { - const d = this.state.timelineRange.asMilliseconds() / 4 / R; - const timestamp = moment(this.state.focusedTimestamp).subtract(d * this.width); + const { focusedTimestamp, timelineRange } = this.state; + const duration = scaleDuration(timelineRange, this.width / WIDTH / 2); + const timestamp = moment(focusedTimestamp).subtract(duration); this.jumpTo(timestamp); } @@ -190,41 +201,43 @@ class TimeTravelTimeline extends React.Component { getTimeScale() { const { timelineRange, focusedTimestamp } = this.state; - const rTimestamp = moment(focusedTimestamp).startOf('second').utc(); - const startDate = moment(rTimestamp).subtract(timelineRange); - const endDate = moment(rTimestamp).add(timelineRange); + const timelineHalfRange = scaleDuration(timelineRange, 0.5); + const roundedTimestamp = moment(focusedTimestamp).startOf('second').utc(); + const startDate = moment(roundedTimestamp).subtract(timelineHalfRange); + const endDate = moment(roundedTimestamp).add(timelineHalfRange); return scaleUtc() .domain([startDate, endDate]) - .range([-R, R]); + .range([-WIDTH / 2, WIDTH / 2]); } - renderPeriodBar(period, prevPeriod, periodFormat, [startIndex, endIndex]) { + timestampTransform(timestamp) { + const timeScale = this.getTimeScale(); + return `translate(${timeScale(timestamp)}, 0)`; + } + + renderPeriodBar(period, prevPeriod, periodFormat, durations) { + const { timelineRange } = this.state; const timeScale = this.getTimeScale(); - const startDate = moment(timeScale.invert(-R)); - const endDate = moment(timeScale.invert(R)); - const numSeconds = endDate.diff(startDate, 'seconds', true); - // console.log(numSeconds); - const ts = []; let duration = null; - for (let i = startIndex; i <= endIndex; i += 1) { - if (numSeconds / fixedDurations[i].asSeconds() < 50) { - duration = fixedDurations[i]; + for (let i = 0; i < durations.length; i += 1) { + if (timelineRange < scaleDuration(durations[i], 65)) { + duration = durations[i]; break; } } - // const behind = (period === 'day') ? 2 : 0; - - // console.log(duration.asSeconds()); if (!duration) return null; + const startDate = moment(timeScale.invert(-WIDTH / 2)); let t = moment(startDate).startOf(prevPeriod); let turningPoint = moment(t).add(1, prevPeriod); + const ts = []; + do { const p = timeScale(t); if (p > -this.width && p < this.width) { - if (p - timeScale(ts[ts.length - 1]) < MIN_TICK_SPACING) { + while (ts.length > 0 && p - timeScale(ts[ts.length - 1]) < MIN_TICK_SPACING) { ts.pop(); } ts.push(t); @@ -236,15 +249,13 @@ class TimeTravelTimeline extends React.Component { } } while (timeScale(t) < this.width); - // console.log(ts); - const p = getShift(period, this.state.scaleX); const shift = 2 + (55 * (1 - (p * 0.25))); const opacity = Math.min(p * p, 1); return ( {fromJS(ts).map(timestamp => ( - + this.jumpTo(timestamp)}> @@ -257,21 +268,26 @@ class TimeTravelTimeline extends React.Component { ); } - renderAxis() { - const timeScale = this.getTimeScale(); - const nowX = Math.min(timeScale(this.state.timestampNow), R); + renderAvailableRange() { + const { timestampNow } = this.state; + return ( + + ); + } + renderAxis() { return ( - + {this.renderAvailableRange()} - {this.renderPeriodBar('year', 'year', 'YYYY', [10, 10])} - {this.renderPeriodBar('month', 'year', 'MMMM', [8, 9])} - {this.renderPeriodBar('day', 'month', 'Do', [6, 7])} - {this.renderPeriodBar('minute', 'day', 'HH:mm', [0, 5])} + {this.renderPeriodBar('year', 'year', 'YYYY', YEAR_INTERVALS)} + {this.renderPeriodBar('month', 'year', 'MMMM', MONTH_INTERVALS)} + {this.renderPeriodBar('day', 'month', 'Do', DAY_INTERVALS)} + {this.renderPeriodBar('minute', 'day', 'HH:mm', MINUTE_INTERVALS)} ); @@ -284,13 +300,8 @@ class TimeTravelTimeline extends React.Component { - - + + {this.renderAxis()} diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 54acc9577b..a7090051b7 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -287,6 +287,8 @@ box-shadow: inset 0 0 7px #aaa; pointer-events: all; margin: 0 7px; + width: 100%; + height: 100%; &.dragging { @extend .grabbing; } From d566b3a017f902ec466434b2f7db62d1ab4f5401 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 21 Jul 2017 18:36:29 +0200 Subject: [PATCH 16/31] Replaced d3-zoom with native events. --- .../components/time-travel-timeline.js | 74 +++++++------------ 1 file changed, 27 insertions(+), 47 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 031b8e43c4..e8d85cf06f 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -4,7 +4,6 @@ import classNames from 'classnames'; import { debounce } from 'lodash'; import { connect } from 'react-redux'; import { fromJS } from 'immutable'; -import { zoom } from 'd3-zoom'; import { drag } from 'd3-drag'; import { scaleUtc } from 'd3-scale'; import { event as d3Event, select } from 'd3-selection'; @@ -49,7 +48,7 @@ const getShift = (period, scale) => { const monthShift = yFunc(scale, 0.0052); const dayShift = yFunc(scale, 0.067); const minuteShift = yFunc(scale, 1.9); - console.log(scale, yearShift, monthShift, dayShift, minuteShift); + // console.log(scale, yearShift, monthShift, dayShift, minuteShift); let result = 0; switch (period) { case 'year': result = yearShift + monthShift + dayShift + minuteShift; break; @@ -86,14 +85,15 @@ class TimeTravelTimeline extends React.Component { this.width = 2000; this.saveSvgRef = this.saveSvgRef.bind(this); - this.dragStarted = this.dragStarted.bind(this); - this.dragEnded = this.dragEnded.bind(this); - this.dragged = this.dragged.bind(this); - this.zoomed = this.zoomed.bind(this); this.jumpTo = this.jumpTo.bind(this); this.jumpForward = this.jumpForward.bind(this); this.jumpBackward = this.jumpBackward.bind(this); + this.handlePanning = this.handlePanning.bind(this); + this.handlePanStart = this.handlePanStart.bind(this); + this.handlePanEnd = this.handlePanEnd.bind(this); + this.handleZoom = this.handleZoom.bind(this); + this.debouncedUpdateTimestamp = debounce( this.updateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); } @@ -101,14 +101,10 @@ class TimeTravelTimeline extends React.Component { componentDidMount() { this.svg = select('svg#time-travel-timeline'); this.drag = drag() - .on('start', this.dragStarted) - .on('end', this.dragEnded) - .on('drag', this.dragged); - this.zoom = zoom() - .scaleExtent([MIN_ZOOM, MAX_ZOOM]) - .on('zoom', this.zoomed); - - this.setZoomTriggers(true); + .on('start', this.handlePanStart) + .on('end', this.handlePanEnd) + .on('drag', this.handlePanning); + this.svg.call(this.drag); // Force periodic re-renders to update the slider position as time goes by. this.timer = setInterval(() => { @@ -118,7 +114,6 @@ class TimeTravelTimeline extends React.Component { componentWillUnmount() { clearInterval(this.timer); - this.setZoomTriggers(false); } componentWillReceiveProps(nextProps) { @@ -132,46 +127,31 @@ class TimeTravelTimeline extends React.Component { this.props.jumpToTime(moment(timestamp)); } - setZoomTriggers(zoomingEnabled) { - if (zoomingEnabled) { - this.svg.call(this.drag); - // use d3-zoom defaults but exclude double clicks - this.svg.call(this.zoom) - .on('dblclick.zoom', null); - } else { - this.svg.on('.zoom', null); - } - } - - zoomed() { - const scaleX = d3Event.transform.k; + handleZoom(e) { + let scaleX = this.state.scaleX * Math.pow(1.002, -e.deltaY); + scaleX = Math.min(Math.max(scaleX, MIN_ZOOM), MAX_ZOOM); const timelineRange = moment.duration(C / scaleX, 'seconds'); this.setState({ timelineRange, scaleX }); } - dragStarted() { - this.setState({ isDragging: true }); - } - - dragged() { - // const { focusedTimestamp, timelineRange } = this.state; - // const mv = timelineRange.asMilliseconds() / R; - // const newTimestamp = moment(focusedTimestamp).subtract(d3Event.dx * mv); - // this.jumpTo(newTimestamp); + handlePanning() { + // let scaleX = this.state.scaleX * Math.pow(1.00, (d3Event.dx === 0 ? -d3Event.dy : 0)); + // scaleX = Math.max(Math.min(scaleX, MAX_ZOOM), MIN_ZOOM); + // const timelineRange = moment.duration(C / scaleX, 'seconds'); + // this.setState({ timelineRange, scaleX }); - let scaleX = this.state.scaleX * Math.pow(1.00, (d3Event.dx === 0 ? -d3Event.dy : 0)); - scaleX = Math.max(Math.min(scaleX, MAX_ZOOM), MIN_ZOOM); - const timelineRange = moment.duration(C / scaleX, 'seconds'); - this.setState({ timelineRange, scaleX }); - - const { focusedTimestamp } = this.state; + const { focusedTimestamp, timelineRange } = this.state; const dragDuration = scaleDuration(timelineRange, -d3Event.dx / WIDTH); const newTimestamp = moment(focusedTimestamp).add(dragDuration); this.jumpTo(newTimestamp); } - dragEnded() { - this.setState({ isDragging: false }); + handlePanStart() { + this.setState({ isPanning: true }); + } + + handlePanEnd() { + this.setState({ isPanning: false }); } jumpTo(timestamp) { @@ -294,13 +274,13 @@ class TimeTravelTimeline extends React.Component { } render() { - const className = classNames({ dragging: this.state.isDragging }); + const className = classNames({ dragging: this.state.isPanning }); return (
- + {this.renderAxis()} From a614551d0d36be13d7ff4ca21c64c075e05f4a97 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Fri, 21 Jul 2017 19:14:17 +0200 Subject: [PATCH 17/31] Got rid of scaleX --- .../components/time-travel-timeline.js | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index e8d85cf06f..b6f6194641 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -36,19 +36,28 @@ const YEAR_INTERVALS = [ moment.duration(1, 'year'), ]; -const yFunc = (scale, start) => { - const end = start * 1.4; - if (scale < start) return 0; - if (scale > end) return 1; - return (Math.log(scale) - Math.log(start)) / (Math.log(end) - Math.log(start)); +function scaleDuration(duration, scale) { + return moment.duration(duration.asMilliseconds() * scale); +} + +function durLog(duration) { + return Math.log(duration.asMilliseconds()); +} + +const yFunc = (duration, startDuration) => { + const endDuration = scaleDuration(startDuration, 1.4); + if (durLog(duration) < durLog(startDuration)) return 1; + if (durLog(duration) > durLog(endDuration)) return 0; + return (durLog(endDuration) - durLog(duration)) / + (durLog(endDuration) - durLog(startDuration)); }; -const getShift = (period, scale) => { +const getShift = (period, duration) => { const yearShift = 1; - const monthShift = yFunc(scale, 0.0052); - const dayShift = yFunc(scale, 0.067); - const minuteShift = yFunc(scale, 1.9); - // console.log(scale, yearShift, monthShift, dayShift, minuteShift); + const monthShift = yFunc(duration, moment.duration(150, 'months')); + const dayShift = yFunc(duration, moment.duration(350, 'days')); + const minuteShift = yFunc(duration, moment.duration(16000, 'minutes')); + let result = 0; switch (period) { case 'year': result = yearShift + monthShift + dayShift + minuteShift; break; @@ -60,15 +69,11 @@ const getShift = (period, scale) => { return result; }; -function scaleDuration(duration, scale) { - return moment.duration(duration.asMilliseconds() * scale); -} - const WIDTH = 5000; const C = 2000000; const MIN_TICK_SPACING = 40; -const MIN_ZOOM = 0.002; -const MAX_ZOOM = 1000; +const MIN_DUR = moment.duration(30, 'minutes'); +const MAX_DUR = moment.duration(40, 'years'); class TimeTravelTimeline extends React.Component { constructor(props, context) { @@ -79,7 +84,6 @@ class TimeTravelTimeline extends React.Component { focusedTimestamp: moment(), timelineRange: moment.duration(C, 'seconds'), isDragging: false, - scaleX: 1, }; this.width = 2000; @@ -128,10 +132,11 @@ class TimeTravelTimeline extends React.Component { } handleZoom(e) { - let scaleX = this.state.scaleX * Math.pow(1.002, -e.deltaY); - scaleX = Math.min(Math.max(scaleX, MIN_ZOOM), MAX_ZOOM); - const timelineRange = moment.duration(C / scaleX, 'seconds'); - this.setState({ timelineRange, scaleX }); + const scale = Math.pow(1.0015, e.deltaY); + let timelineRange = scaleDuration(this.state.timelineRange, scale); + if (timelineRange.asMilliseconds() > MAX_DUR.asMilliseconds()) timelineRange = MAX_DUR; + if (timelineRange.asMilliseconds() < MIN_DUR.asMilliseconds()) timelineRange = MIN_DUR; + this.setState({ timelineRange }); } handlePanning() { @@ -229,7 +234,7 @@ class TimeTravelTimeline extends React.Component { } } while (timeScale(t) < this.width); - const p = getShift(period, this.state.scaleX); + const p = getShift(period, this.state.timelineRange); const shift = 2 + (55 * (1 - (p * 0.25))); const opacity = Math.min(p * p, 1); return ( @@ -249,11 +254,12 @@ class TimeTravelTimeline extends React.Component { } renderAvailableRange() { - const { timestampNow } = this.state; + const timeScale = this.getTimeScale(); + const nowShift = Math.min(timeScale(this.state.timestampNow), this.width / 2); return ( ); From 3ab7e82b919a5986c5df276f22a5d54472df8bb1 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Sun, 23 Jul 2017 16:21:41 +0200 Subject: [PATCH 18/31] More code beautified. --- .../components/time-travel-timeline.js | 109 +++++++++--------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index b6f6194641..398137ba70 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -1,7 +1,7 @@ import React from 'react'; import moment from 'moment'; import classNames from 'classnames'; -import { debounce } from 'lodash'; +import { debounce, clamp } from 'lodash'; import { connect } from 'react-redux'; import { fromJS } from 'immutable'; import { drag } from 'd3-drag'; @@ -40,23 +40,35 @@ function scaleDuration(duration, scale) { return moment.duration(duration.asMilliseconds() * scale); } -function durLog(duration) { - return Math.log(duration.asMilliseconds()); +function isShorterThan(duration1, duration2) { + return duration1.asMilliseconds() < duration2.asMilliseconds(); } -const yFunc = (duration, startDuration) => { - const endDuration = scaleDuration(startDuration, 1.4); - if (durLog(duration) < durLog(startDuration)) return 1; - if (durLog(duration) > durLog(endDuration)) return 0; - return (durLog(endDuration) - durLog(duration)) / - (durLog(endDuration) - durLog(startDuration)); +function isLongerThan(duration1, duration2) { + return duration1.asMilliseconds() > duration2.asMilliseconds(); +} + +const WIDTH = 5000; +const FADE_OUT_FACTOR = 1.4; +const MIN_TICK_SPACING = 70; +const MAX_TICK_SPACING = 415; +const MIN_DURATION = moment.duration(250, 'milliseconds'); +const MAX_DURATION = moment.duration(3, 'days'); + +const yFunc = (currentDuration, fadedInDuration) => { + const durationLog = d => Math.log(d.asMilliseconds()); + const fadedOutDuration = scaleDuration(fadedInDuration, FADE_OUT_FACTOR); + const transitionFactor = durationLog(fadedOutDuration) - durationLog(currentDuration); + const transitionLength = durationLog(fadedOutDuration) - durationLog(fadedInDuration); + return clamp(transitionFactor / transitionLength, 0, 1); }; const getShift = (period, duration) => { + const durationMultiplier = 1 / MAX_TICK_SPACING; + const minuteShift = yFunc(duration, scaleDuration(moment.duration(1, 'day'), durationMultiplier)); + const dayShift = yFunc(duration, scaleDuration(moment.duration(1, 'month'), durationMultiplier)); + const monthShift = yFunc(duration, scaleDuration(moment.duration(1, 'year'), durationMultiplier)); const yearShift = 1; - const monthShift = yFunc(duration, moment.duration(150, 'months')); - const dayShift = yFunc(duration, moment.duration(350, 'days')); - const minuteShift = yFunc(duration, moment.duration(16000, 'minutes')); let result = 0; switch (period) { @@ -69,12 +81,6 @@ const getShift = (period, duration) => { return result; }; -const WIDTH = 5000; -const C = 2000000; -const MIN_TICK_SPACING = 40; -const MIN_DUR = moment.duration(30, 'minutes'); -const MAX_DUR = moment.duration(40, 'years'); - class TimeTravelTimeline extends React.Component { constructor(props, context) { super(props, context); @@ -82,11 +88,12 @@ class TimeTravelTimeline extends React.Component { this.state = { timestampNow: moment(), focusedTimestamp: moment(), - timelineRange: moment.duration(C, 'seconds'), + durationPerPixel: moment.duration(1, 'minute'), isDragging: false, }; - this.width = 2000; + this.width = 0; + this.height = 0; this.saveSvgRef = this.saveSvgRef.bind(this); this.jumpTo = this.jumpTo.bind(this); @@ -125,32 +132,13 @@ class TimeTravelTimeline extends React.Component { this.setState({ focusedTimestamp: nextProps.pausedAt }); } this.width = this.svgRef.getBoundingClientRect().width; + this.height = this.svgRef.getBoundingClientRect().height; } updateTimestamp(timestamp) { this.props.jumpToTime(moment(timestamp)); } - handleZoom(e) { - const scale = Math.pow(1.0015, e.deltaY); - let timelineRange = scaleDuration(this.state.timelineRange, scale); - if (timelineRange.asMilliseconds() > MAX_DUR.asMilliseconds()) timelineRange = MAX_DUR; - if (timelineRange.asMilliseconds() < MIN_DUR.asMilliseconds()) timelineRange = MIN_DUR; - this.setState({ timelineRange }); - } - - handlePanning() { - // let scaleX = this.state.scaleX * Math.pow(1.00, (d3Event.dx === 0 ? -d3Event.dy : 0)); - // scaleX = Math.max(Math.min(scaleX, MAX_ZOOM), MIN_ZOOM); - // const timelineRange = moment.duration(C / scaleX, 'seconds'); - // this.setState({ timelineRange, scaleX }); - - const { focusedTimestamp, timelineRange } = this.state; - const dragDuration = scaleDuration(timelineRange, -d3Event.dx / WIDTH); - const newTimestamp = moment(focusedTimestamp).add(dragDuration); - this.jumpTo(newTimestamp); - } - handlePanStart() { this.setState({ isPanning: true }); } @@ -159,6 +147,21 @@ class TimeTravelTimeline extends React.Component { this.setState({ isPanning: false }); } + handlePanning() { + const { focusedTimestamp, durationPerPixel } = this.state; + const dragDuration = scaleDuration(durationPerPixel, d3Event.dx); + const newTimestamp = moment(focusedTimestamp).subtract(dragDuration); + this.jumpTo(newTimestamp); + } + + handleZoom(e) { + const scale = Math.pow(1.0015, e.deltaY); + let durationPerPixel = scaleDuration(this.state.durationPerPixel, scale); + if (isLongerThan(durationPerPixel, MAX_DURATION)) durationPerPixel = MAX_DURATION; + if (isShorterThan(durationPerPixel, MIN_DURATION)) durationPerPixel = MIN_DURATION; + this.setState({ durationPerPixel }); + } + jumpTo(timestamp) { const { timestampNow } = this.state; const focusedTimestamp = timestamp > timestampNow ? timestampNow : timestamp; @@ -167,15 +170,15 @@ class TimeTravelTimeline extends React.Component { } jumpForward() { - const { focusedTimestamp, timelineRange } = this.state; - const duration = scaleDuration(timelineRange, this.width / WIDTH / 2); + const { focusedTimestamp, durationPerPixel } = this.state; + const duration = scaleDuration(durationPerPixel, this.width / 4); const timestamp = moment(focusedTimestamp).add(duration); this.jumpTo(timestamp); } jumpBackward() { - const { focusedTimestamp, timelineRange } = this.state; - const duration = scaleDuration(timelineRange, this.width / WIDTH / 2); + const { focusedTimestamp, durationPerPixel } = this.state; + const duration = scaleDuration(durationPerPixel, this.width / 4); const timestamp = moment(focusedTimestamp).subtract(duration); this.jumpTo(timestamp); } @@ -185,14 +188,14 @@ class TimeTravelTimeline extends React.Component { } getTimeScale() { - const { timelineRange, focusedTimestamp } = this.state; - const timelineHalfRange = scaleDuration(timelineRange, 0.5); + const { durationPerPixel, focusedTimestamp } = this.state; + const timelineHalfRange = scaleDuration(durationPerPixel, 0.5); const roundedTimestamp = moment(focusedTimestamp).startOf('second').utc(); const startDate = moment(roundedTimestamp).subtract(timelineHalfRange); const endDate = moment(roundedTimestamp).add(timelineHalfRange); return scaleUtc() .domain([startDate, endDate]) - .range([-WIDTH / 2, WIDTH / 2]); + .range([-0.5, 0.5]); } timestampTransform(timestamp) { @@ -201,12 +204,12 @@ class TimeTravelTimeline extends React.Component { } renderPeriodBar(period, prevPeriod, periodFormat, durations) { - const { timelineRange } = this.state; + const { durationPerPixel } = this.state; const timeScale = this.getTimeScale(); let duration = null; for (let i = 0; i < durations.length; i += 1) { - if (timelineRange < scaleDuration(durations[i], 65)) { + if (scaleDuration(durationPerPixel, MIN_TICK_SPACING) < durations[i]) { duration = durations[i]; break; } @@ -222,7 +225,7 @@ class TimeTravelTimeline extends React.Component { do { const p = timeScale(t); if (p > -this.width && p < this.width) { - while (ts.length > 0 && p - timeScale(ts[ts.length - 1]) < MIN_TICK_SPACING) { + while (ts.length > 0 && p - timeScale(ts[ts.length - 1]) < 0.85 * MIN_TICK_SPACING) { ts.pop(); } ts.push(t); @@ -234,7 +237,7 @@ class TimeTravelTimeline extends React.Component { } } while (timeScale(t) < this.width); - const p = getShift(period, this.state.timelineRange); + const p = getShift(period, this.state.durationPerPixel); const shift = 2 + (55 * (1 - (p * 0.25))); const opacity = Math.min(p * p, 1); return ( @@ -255,12 +258,12 @@ class TimeTravelTimeline extends React.Component { renderAvailableRange() { const timeScale = this.getTimeScale(); - const nowShift = Math.min(timeScale(this.state.timestampNow), this.width / 2); + const nowShift = Math.min(timeScale(this.state.timestampNow), WIDTH / 2); return ( ); } From df0ca0942da53c0da1cee681c1fa605b0c537340 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 24 Jul 2017 17:03:49 +0200 Subject: [PATCH 19/31] Almost done polishing the code. --- .../components/time-travel-timeline.js | 260 ++++++++++-------- client/app/styles/_base.scss | 5 + 2 files changed, 152 insertions(+), 113 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 398137ba70..75b9cdb535 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -1,9 +1,8 @@ import React from 'react'; import moment from 'moment'; import classNames from 'classnames'; -import { debounce, clamp } from 'lodash'; +import { debounce, map, clamp, find, last } from 'lodash'; import { connect } from 'react-redux'; -import { fromJS } from 'immutable'; import { drag } from 'd3-drag'; import { scaleUtc } from 'd3-scale'; import { event as d3Event, select } from 'd3-selection'; @@ -16,55 +15,62 @@ import { TIMELINE_DEBOUNCE_INTERVAL, } from '../constants/timer'; -const MINUTE_INTERVALS = [ - moment.duration(1, 'minute'), - moment.duration(5, 'minutes'), - moment.duration(15, 'minutes'), - moment.duration(1, 'hour'), - moment.duration(3, 'hours'), - moment.duration(6, 'hours'), -]; -const DAY_INTERVALS = [ - moment.duration(1, 'day'), - moment.duration(1, 'week'), -]; -const MONTH_INTERVALS = [ - moment.duration(1, 'month'), - moment.duration(3, 'months'), -]; -const YEAR_INTERVALS = [ - moment.duration(1, 'year'), -]; -function scaleDuration(duration, scale) { - return moment.duration(duration.asMilliseconds() * scale); -} +const TICK_ROWS = { + minute: { + format: 'HH:mm', + intervals: [ + moment.duration(1, 'minute'), + moment.duration(5, 'minutes'), + moment.duration(15, 'minutes'), + moment.duration(1, 'hour'), + moment.duration(3, 'hours'), + moment.duration(6, 'hours'), + ], + }, + day: { + format: 'Do', + intervals: [ + moment.duration(1, 'day'), + moment.duration(1, 'week'), + ], + }, + month: { + format: 'MMMM', + intervals: [ + moment.duration(1, 'month'), + moment.duration(3, 'months'), + ], + }, + year: { + format: 'YYYY', + intervals: [ + moment.duration(1, 'year'), + ], + }, +}; -function isShorterThan(duration1, duration2) { - return duration1.asMilliseconds() < duration2.asMilliseconds(); -} +const FADE_OUT_FACTOR = 1.4; +const MIN_TICK_SPACING_PX = 70; +const MAX_TICK_SPACING_PX = 415; +const MIN_DURATION_PER_PX = moment.duration(250, 'milliseconds'); +const INIT_DURATION_PER_PX = moment.duration(1, 'minute'); +const MAX_DURATION_PER_PX = moment.duration(3, 'days'); -function isLongerThan(duration1, duration2) { - return duration1.asMilliseconds() > duration2.asMilliseconds(); +function scaleDuration(duration, scale) { + return moment.duration(duration.asMilliseconds() * scale); } -const WIDTH = 5000; -const FADE_OUT_FACTOR = 1.4; -const MIN_TICK_SPACING = 70; -const MAX_TICK_SPACING = 415; -const MIN_DURATION = moment.duration(250, 'milliseconds'); -const MAX_DURATION = moment.duration(3, 'days'); - -const yFunc = (currentDuration, fadedInDuration) => { +function yFunc(currentDuration, fadedInDuration) { const durationLog = d => Math.log(d.asMilliseconds()); const fadedOutDuration = scaleDuration(fadedInDuration, FADE_OUT_FACTOR); const transitionFactor = durationLog(fadedOutDuration) - durationLog(currentDuration); const transitionLength = durationLog(fadedOutDuration) - durationLog(fadedInDuration); return clamp(transitionFactor / transitionLength, 0, 1); -}; +} -const getShift = (period, duration) => { - const durationMultiplier = 1 / MAX_TICK_SPACING; +function getFadeInFactor(period, duration) { + const durationMultiplier = 1 / MAX_TICK_SPACING_PX; const minuteShift = yFunc(duration, scaleDuration(moment.duration(1, 'day'), durationMultiplier)); const dayShift = yFunc(duration, scaleDuration(moment.duration(1, 'month'), durationMultiplier)); const monthShift = yFunc(duration, scaleDuration(moment.duration(1, 'year'), durationMultiplier)); @@ -79,7 +85,8 @@ const getShift = (period, duration) => { default: result = 0; break; } return result; -}; +} + class TimeTravelTimeline extends React.Component { constructor(props, context) { @@ -88,21 +95,21 @@ class TimeTravelTimeline extends React.Component { this.state = { timestampNow: moment(), focusedTimestamp: moment(), - durationPerPixel: moment.duration(1, 'minute'), + durationPerPixel: INIT_DURATION_PER_PX, + boundingRect: { width: 0, height: 0 }, isDragging: false, }; - this.width = 0; - this.height = 0; - this.saveSvgRef = this.saveSvgRef.bind(this); this.jumpTo = this.jumpTo.bind(this); this.jumpForward = this.jumpForward.bind(this); this.jumpBackward = this.jumpBackward.bind(this); - this.handlePanning = this.handlePanning.bind(this); + this.findOptimalDuration = this.findOptimalDuration.bind(this); + this.handlePanStart = this.handlePanStart.bind(this); this.handlePanEnd = this.handlePanEnd.bind(this); + this.handlePan = this.handlePan.bind(this); this.handleZoom = this.handleZoom.bind(this); this.debouncedUpdateTimestamp = debounce( @@ -110,11 +117,11 @@ class TimeTravelTimeline extends React.Component { } componentDidMount() { - this.svg = select('svg#time-travel-timeline'); + this.svg = select('.time-travel-timeline svg'); this.drag = drag() .on('start', this.handlePanStart) .on('end', this.handlePanEnd) - .on('drag', this.handlePanning); + .on('drag', this.handlePan); this.svg.call(this.drag); // Force periodic re-renders to update the slider position as time goes by. @@ -131,8 +138,7 @@ class TimeTravelTimeline extends React.Component { if (nextProps.pausedAt) { this.setState({ focusedTimestamp: nextProps.pausedAt }); } - this.width = this.svgRef.getBoundingClientRect().width; - this.height = this.svgRef.getBoundingClientRect().height; + this.setState({ boundingRect: this.svgRef.getBoundingClientRect() }); } updateTimestamp(timestamp) { @@ -147,7 +153,7 @@ class TimeTravelTimeline extends React.Component { this.setState({ isPanning: false }); } - handlePanning() { + handlePan() { const { focusedTimestamp, durationPerPixel } = this.state; const dragDuration = scaleDuration(durationPerPixel, d3Event.dx); const newTimestamp = moment(focusedTimestamp).subtract(dragDuration); @@ -157,8 +163,8 @@ class TimeTravelTimeline extends React.Component { handleZoom(e) { const scale = Math.pow(1.0015, e.deltaY); let durationPerPixel = scaleDuration(this.state.durationPerPixel, scale); - if (isLongerThan(durationPerPixel, MAX_DURATION)) durationPerPixel = MAX_DURATION; - if (isShorterThan(durationPerPixel, MIN_DURATION)) durationPerPixel = MIN_DURATION; + if (durationPerPixel > MAX_DURATION_PER_PX) durationPerPixel = MAX_DURATION_PER_PX; + if (durationPerPixel < MIN_DURATION_PER_PX) durationPerPixel = MIN_DURATION_PER_PX; this.setState({ durationPerPixel }); } @@ -170,15 +176,15 @@ class TimeTravelTimeline extends React.Component { } jumpForward() { - const { focusedTimestamp, durationPerPixel } = this.state; - const duration = scaleDuration(durationPerPixel, this.width / 4); + const { focusedTimestamp, durationPerPixel, boundingRect } = this.state; + const duration = scaleDuration(durationPerPixel, boundingRect.width / 4); const timestamp = moment(focusedTimestamp).add(duration); this.jumpTo(timestamp); } jumpBackward() { - const { focusedTimestamp, durationPerPixel } = this.state; - const duration = scaleDuration(durationPerPixel, this.width / 4); + const { focusedTimestamp, durationPerPixel, boundingRect } = this.state; + const duration = scaleDuration(durationPerPixel, boundingRect.width / 4); const timestamp = moment(focusedTimestamp).subtract(duration); this.jumpTo(timestamp); } @@ -189,81 +195,107 @@ class TimeTravelTimeline extends React.Component { getTimeScale() { const { durationPerPixel, focusedTimestamp } = this.state; - const timelineHalfRange = scaleDuration(durationPerPixel, 0.5); const roundedTimestamp = moment(focusedTimestamp).startOf('second').utc(); - const startDate = moment(roundedTimestamp).subtract(timelineHalfRange); - const endDate = moment(roundedTimestamp).add(timelineHalfRange); + const startDate = moment(roundedTimestamp).subtract(durationPerPixel); + const endDate = moment(roundedTimestamp).add(durationPerPixel); return scaleUtc() .domain([startDate, endDate]) - .range([-0.5, 0.5]); + .range([-1, 1]); } - timestampTransform(timestamp) { - const timeScale = this.getTimeScale(); - return `translate(${timeScale(timestamp)}, 0)`; + findOptimalDuration(durations) { + const { durationPerPixel } = this.state; + const minimalDuration = scaleDuration(durationPerPixel, MIN_TICK_SPACING_PX); + return find(durations, d => d > minimalDuration); } - renderPeriodBar(period, prevPeriod, periodFormat, durations) { - const { durationPerPixel } = this.state; + renderTimestampTick({ timestamp, position, isBehind }, periodFormat, opacity) { + const { timestampNow } = this.state; + const disabled = timestamp.isAfter(timestampNow) || opacity < 0.2; + const handleClick = () => this.jumpTo(timestamp); + + return ( + + {!isBehind && } + + + {isBehind && '←'} + {timestamp.utc().format(periodFormat)} + + + + ); + } + + getTicks(period, parentPeriod, duration) { + parentPeriod = parentPeriod || period; + const startPosition = -this.state.boundingRect.width / 2; + const endPosition = this.state.boundingRect.width / 2; + + if (!duration) return []; + const timeScale = this.getTimeScale(); + const startDate = moment(timeScale.invert(startPosition)); + const endDate = moment(timeScale.invert(endPosition)); + const ticks = []; - let duration = null; - for (let i = 0; i < durations.length; i += 1) { - if (scaleDuration(durationPerPixel, MIN_TICK_SPACING) < durations[i]) { - duration = durations[i]; - break; - } - } + let timestamp = moment(startDate).startOf(parentPeriod); + let turningPoint = moment(timestamp).add(1, parentPeriod); - if (!duration) return null; + while (timestamp.isBefore(startDate)) { + timestamp = moment(timestamp).add(duration); + } - const startDate = moment(timeScale.invert(-WIDTH / 2)); - let t = moment(startDate).startOf(prevPeriod); - let turningPoint = moment(t).add(1, prevPeriod); - const ts = []; + ticks.push({ + timestamp: moment(timestamp).subtract(duration), + position: startPosition, + isBehind: true, + }); do { - const p = timeScale(t); - if (p > -this.width && p < this.width) { - while (ts.length > 0 && p - timeScale(ts[ts.length - 1]) < 0.85 * MIN_TICK_SPACING) { - ts.pop(); - } - ts.push(t); + const position = timeScale(timestamp); + + while (ticks.length > 0 && position - last(ticks).position < 0.85 * MIN_TICK_SPACING_PX) { + ticks.pop(); } - t = moment(t).add(duration); - if (prevPeriod !== period && t >= turningPoint) { - t = turningPoint; - turningPoint = moment(turningPoint).add(1, prevPeriod); + ticks.push({ timestamp, position }); + + timestamp = moment(timestamp).add(duration); + if (parentPeriod && timestamp >= turningPoint) { + timestamp = turningPoint; + turningPoint = moment(turningPoint).add(1, parentPeriod); } - } while (timeScale(t) < this.width); + } while (timestamp.isBefore(endDate)); + + return ticks; + } + + renderPeriodBar(period, parentPeriod) { + const duration = this.findOptimalDuration(TICK_ROWS[period].intervals); + const ticks = this.getTicks(period, parentPeriod, duration); + + const periodFormat = TICK_ROWS[period].format; + const p = getFadeInFactor(period, this.state.durationPerPixel); + const verticalPosition = 60 - (p * 15); + const opacity = Math.min(p, 1); - const p = getShift(period, this.state.durationPerPixel); - const shift = 2 + (55 * (1 - (p * 0.25))); - const opacity = Math.min(p * p, 1); return ( - - {fromJS(ts).map(timestamp => ( - - - - this.jumpTo(timestamp)}> - {timestamp.format(periodFormat)} - - - - ))} + + {map(ticks, tick => this.renderTimestampTick(tick, periodFormat, opacity))} ); } - renderAvailableRange() { + renderDisabledShadow() { const timeScale = this.getTimeScale(); - const nowShift = Math.min(timeScale(this.state.timestampNow), WIDTH / 2); + const nowShift = timeScale(this.state.timestampNow); + const { width, height } = this.state.boundingRect; + return ( ); } @@ -271,12 +303,12 @@ class TimeTravelTimeline extends React.Component { renderAxis() { return ( - {this.renderAvailableRange()} + {this.renderDisabledShadow()} - {this.renderPeriodBar('year', 'year', 'YYYY', YEAR_INTERVALS)} - {this.renderPeriodBar('month', 'year', 'MMMM', MONTH_INTERVALS)} - {this.renderPeriodBar('day', 'month', 'Do', DAY_INTERVALS)} - {this.renderPeriodBar('minute', 'day', 'HH:mm', MINUTE_INTERVALS)} + {this.renderPeriodBar('year')} + {this.renderPeriodBar('month', 'year')} + {this.renderPeriodBar('day', 'month')} + {this.renderPeriodBar('minute', 'day')} ); @@ -284,13 +316,15 @@ class TimeTravelTimeline extends React.Component { render() { const className = classNames({ dragging: this.state.isPanning }); + const halfWidth = this.state.boundingRect.width / 2; + return (
- - + + {this.renderAxis()} diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index a7090051b7..4e05b602e3 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -299,6 +299,11 @@ .timestamp-label { padding: 3px; + + &[disabled] { + color: #aaa; + cursor: inherit; + } } } From 4d79b22d6340858a7977f7f259e69ffd373c1de8 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 24 Jul 2017 19:18:35 +0200 Subject: [PATCH 20/31] Some cleanup. --- .../components/time-travel-timeline.js | 134 ++++++++++-------- client/app/scripts/components/time-travel.js | 91 ++---------- client/app/scripts/reducers/root.js | 6 +- .../app/scripts/selectors/graph-view/graph.js | 2 +- .../utils/__tests__/timer-utils-test.js | 18 --- client/app/scripts/utils/timer-utils.js | 11 -- 6 files changed, 89 insertions(+), 173 deletions(-) delete mode 100644 client/app/scripts/utils/__tests__/timer-utils-test.js delete mode 100644 client/app/scripts/utils/timer-utils.js diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 75b9cdb535..c2787b02bb 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -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'; @@ -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'); @@ -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, @@ -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() { @@ -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); } @@ -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() { @@ -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; @@ -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() @@ -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 ( - - {!isBehind && } - - - {isBehind && '←'} - {timestamp.utc().format(periodFormat)} - - - - ); + 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 ( + + {!isBehind && } + + + {isBehind && '←'}{timestamp.utc().format(periodFormat)} + + + + ); + } + 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); diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js index 17ee9c66e4..549f93005d 100644 --- a/client/app/scripts/components/time-travel.js +++ b/client/app/scripts/components/time-travel.js @@ -14,7 +14,6 @@ import { } from '../actions/app-actions'; import { - TIMELINE_TICK_INTERVAL, TIMELINE_DEBOUNCE_INTERVAL, } from '../constants/timer'; @@ -22,31 +21,19 @@ import { 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( @@ -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(); } @@ -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); @@ -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); } @@ -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 ( - - ); - } - - 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 ( -
- {map(ticks, tick => this.renderMark(tick))} -
- ); - } - 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 ( -
- +
+
- UTC + UTC
); diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 2e8d76fe35..778a91ba02 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -1,6 +1,5 @@ /* eslint-disable import/no-webpack-loader-syntax, import/no-unresolved */ import debug from 'debug'; -import moment from 'moment'; import { size, each, includes, isEqual } from 'lodash'; import { fromJS, @@ -20,6 +19,7 @@ import { isResourceViewModeSelector, } from '../selectors/topology'; import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming'; +import { nowInSecondsPrecision } from '../utils/time-utils'; import { applyPinnedSearches } from '../utils/search-utils'; import { findTopologyById, @@ -366,13 +366,13 @@ export function rootReducer(state = initialState, action) { case ActionTypes.PAUSE_TIME_AT_NOW: { state = state.set('showingTimeTravel', false); - return state.set('pausedAt', moment().utc()); + return state.set('pausedAt', nowInSecondsPrecision()); } case ActionTypes.START_TIME_TRAVEL: { state = state.set('timeTravelTransitioning', false); state = state.set('showingTimeTravel', true); - return state.set('pausedAt', moment().utc()); + return state.set('pausedAt', nowInSecondsPrecision()); } case ActionTypes.JUMP_TO_TIME: { diff --git a/client/app/scripts/selectors/graph-view/graph.js b/client/app/scripts/selectors/graph-view/graph.js index 72783d9e04..0d7600aff5 100644 --- a/client/app/scripts/selectors/graph-view/graph.js +++ b/client/app/scripts/selectors/graph-view/graph.js @@ -7,7 +7,7 @@ import { canvasWidthSelector, canvasHeightSelector } from '../canvas'; import { activeTopologyOptionsSelector } from '../topology'; import { shownNodesSelector } from '../node-filters'; import { doLayout } from '../../charts/nodes-layout'; -import timer from '../../utils/timer-utils'; +import { timer } from '../../utils/time-utils'; const log = debug('scope:nodes-chart'); diff --git a/client/app/scripts/utils/__tests__/timer-utils-test.js b/client/app/scripts/utils/__tests__/timer-utils-test.js deleted file mode 100644 index de7cc61296..0000000000 --- a/client/app/scripts/utils/__tests__/timer-utils-test.js +++ /dev/null @@ -1,18 +0,0 @@ -import expect from 'expect'; -import timer from '../timer-utils'; - -describe('timer', () => { - it('records how long a function takes to execute', () => { - const add100k = (number) => { - for (let i = 0; i < 100000; i += 1) { - number += 1; - } - return number; - }; - - const timedFn = timer(add100k); - const result = timedFn(70); - expect(result).toEqual(100070); - expect(timedFn.time).toBeA('number'); - }); -}); diff --git a/client/app/scripts/utils/timer-utils.js b/client/app/scripts/utils/timer-utils.js deleted file mode 100644 index c030328910..0000000000 --- a/client/app/scripts/utils/timer-utils.js +++ /dev/null @@ -1,11 +0,0 @@ -// Replacement for timely dependency - -export default function timer(fn) { - const timedFn = (...args) => { - const start = new Date(); - const result = fn.apply(fn, args); - timedFn.time = new Date() - start; - return result; - }; - return timedFn; -} From c47e1da0b28864527e50816b1974c9a6680df9f1 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 25 Jul 2017 11:57:59 +0200 Subject: [PATCH 21/31] Better request triggers. --- .../components/time-travel-timeline.js | 74 +++++++-------- client/app/scripts/components/time-travel.js | 93 ++++++++++--------- .../utils/__tests__/time-utils-test.js | 18 ++++ client/app/scripts/utils/time-utils.js | 26 ++++++ client/app/scripts/utils/tracking-utils.js | 3 +- client/app/styles/_base.scss | 6 +- 6 files changed, 130 insertions(+), 90 deletions(-) create mode 100644 client/app/scripts/utils/__tests__/time-utils-test.js create mode 100644 client/app/scripts/utils/time-utils.js diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index c2787b02bb..37ca5aaf80 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -1,21 +1,19 @@ import React from 'react'; import moment from 'moment'; import classNames from 'classnames'; -import { debounce, map, clamp, find, last } from 'lodash'; +import { map, clamp, find, last } from 'lodash'; 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'; + nowInSecondsPrecision, + clampToNowInSecondsPrecision, + scaleDuration, +} from '../utils/time-utils'; -import { - TIMELINE_TICK_INTERVAL, - TIMELINE_DEBOUNCE_INTERVAL, -} from '../constants/timer'; +import { TIMELINE_TICK_INTERVAL } from '../constants/timer'; const TICK_ROWS = { @@ -52,17 +50,14 @@ const TICK_ROWS = { }, }; -const FADE_OUT_FACTOR = 1.4; -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'); const MAX_DURATION_PER_PX = moment.duration(3, 'days'); +const MIN_TICK_SPACING_PX = 80; +const MAX_TICK_SPACING_PX = 415; +const ZOOM_SENSITIVITY = 1.0015; +const FADE_OUT_FACTOR = 1.4; -function scaleDuration(duration, scale) { - return moment.duration(duration.asMilliseconds() * scale); -} function yFunc(currentDuration, fadedInDuration) { const durationLog = d => Math.log(d.asMilliseconds()); @@ -104,18 +99,17 @@ class TimeTravelTimeline extends React.Component { }; this.saveSvgRef = this.saveSvgRef.bind(this); - this.jumpTo = this.jumpTo.bind(this); + this.jumpRelativePixels = this.jumpRelativePixels.bind(this); this.jumpForward = this.jumpForward.bind(this); this.jumpBackward = this.jumpBackward.bind(this); + this.jumpTo = this.jumpTo.bind(this); this.findOptimalDuration = this.findOptimalDuration.bind(this); + this.handleZoom = this.handleZoom.bind(this); this.handlePanStart = this.handlePanStart.bind(this); this.handlePanEnd = this.handlePanEnd.bind(this); this.handlePan = this.handlePan.bind(this); - this.handleZoom = this.handleZoom.bind(this); - - this.debouncedJumpTo = debounce(this.jumpTo.bind(this), TIMELINE_DEBOUNCE_INTERVAL); } componentDidMount() { @@ -152,14 +146,16 @@ class TimeTravelTimeline extends React.Component { } handlePanEnd() { + this.props.onTimelinePanEnd(this.state.focusedTimestamp); this.setState({ isPanning: false }); } handlePan() { - const { focusedTimestamp, durationPerPixel } = this.state; - const dragDuration = scaleDuration(durationPerPixel, d3Event.dx); - const newTimestamp = moment(focusedTimestamp).subtract(dragDuration); - this.jumpTo(newTimestamp); + const dragDuration = scaleDuration(this.state.durationPerPixel, -d3Event.dx); + const timestamp = moment(this.state.focusedTimestamp).add(dragDuration); + const focusedTimestamp = clampToNowInSecondsPrecision(timestamp); + this.props.onTimelinePan(focusedTimestamp); + this.setState({ focusedTimestamp }); } handleZoom(e) { @@ -171,25 +167,23 @@ class TimeTravelTimeline extends React.Component { } jumpTo(timestamp) { - const { timestampNow } = this.state; - const focusedTimestamp = timestamp > timestampNow ? timestampNow : timestamp; - this.props.onTimelinePan(focusedTimestamp); + const focusedTimestamp = clampToNowInSecondsPrecision(timestamp); + this.props.onInstantJump(focusedTimestamp); this.setState({ focusedTimestamp }); - this.debouncedJumpTo.cancel(); + } + + jumpRelativePixels(pixels) { + const duration = scaleDuration(this.state.durationPerPixel, pixels); + const timestamp = moment(this.state.focusedTimestamp).add(duration); + this.jumpTo(timestamp); } jumpForward() { - const { focusedTimestamp, durationPerPixel, boundingRect } = this.state; - const duration = scaleDuration(durationPerPixel, boundingRect.width / 4); - const newTimestamp = moment(focusedTimestamp).add(duration); - this.jumpTo(newTimestamp); + this.jumpRelativePixels(this.state.boundingRect.width / 4); } jumpBackward() { - const { focusedTimestamp, durationPerPixel, boundingRect } = this.state; - const duration = scaleDuration(durationPerPixel, boundingRect.width / 4); - const newTimestamp = moment(focusedTimestamp).subtract(duration); - this.jumpTo(newTimestamp); + this.jumpRelativePixels(-this.state.boundingRect.width / 4); } getTimeScale() { @@ -325,7 +319,7 @@ class TimeTravelTimeline extends React.Component { } render() { - const className = classNames({ dragging: this.state.isPanning }); + const className = classNames({ panning: this.state.isPanning }); const halfWidth = this.state.boundingRect.width / 2; return ( @@ -354,10 +348,4 @@ function mapStateToProps(state) { }; } - -export default connect( - mapStateToProps, - { - jumpToTime, - } -)(TimeTravelTimeline); +export default connect(mapStateToProps)(TimeTravelTimeline); diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js index 549f93005d..ee1fbc9366 100644 --- a/client/app/scripts/components/time-travel.js +++ b/client/app/scripts/components/time-travel.js @@ -3,19 +3,18 @@ import React from 'react'; import moment from 'moment'; import classNames from 'classnames'; import { connect } from 'react-redux'; -import { debounce, map } from 'lodash'; +import { debounce } from 'lodash'; import TimeTravelTimeline from './time-travel-timeline'; import { trackMixpanelEvent } from '../utils/tracking-utils'; +import { clampToNowInSecondsPrecision } from '../utils/time-utils'; import { jumpToTime, resumeTime, timeTravelStartTransition, } from '../actions/app-actions'; -import { - TIMELINE_DEBOUNCE_INTERVAL, -} from '../constants/timer'; +import { TIMELINE_DEBOUNCE_INTERVAL } from '../constants/timer'; const getTimestampStates = (timestamp) => { @@ -33,13 +32,16 @@ class TimeTravel extends React.Component { this.handleInputChange = this.handleInputChange.bind(this); this.handleTimelinePan = this.handleTimelinePan.bind(this); - this.handleJumpClick = this.handleJumpClick.bind(this); - this.travelTo = this.travelTo.bind(this); + this.handleTimelinePanEnd = this.handleTimelinePanEnd.bind(this); + this.handleInstantJump = this.handleInstantJump.bind(this); + + this.trackTimestampEdit = this.trackTimestampEdit.bind(this); + this.trackTimelineClick = this.trackTimelineClick.bind(this); + this.trackTimelinePan = this.trackTimelinePan.bind(this); + this.instantUpdateTimestamp = this.instantUpdateTimestamp.bind(this); this.debouncedUpdateTimestamp = debounce( - this.updateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); - this.debouncedTrackSliderChange = debounce( - this.trackSliderChange.bind(this), TIMELINE_DEBOUNCE_INTERVAL); + this.instantUpdateTimestamp.bind(this), TIMELINE_DEBOUNCE_INTERVAL); } componentWillReceiveProps(props) { @@ -51,53 +53,59 @@ class TimeTravel extends React.Component { this.props.resumeTime(); } - handleSliderChange(timestamp) { - if (!timestamp.isSame(this.props.pausedAt)) { - this.travelTo(timestamp, true); - this.debouncedTrackSliderChange(); - } - } - handleInputChange(ev) { - let timestamp = moment(ev.target.value); + const timestamp = moment(ev.target.value); this.setState({ inputValue: ev.target.value }); if (timestamp.isValid()) { - timestamp = Math.min(timestamp, moment().valueOf()); - this.travelTo(timestamp); - - trackMixpanelEvent('scope.time.timestamp.edit', { - layout: this.props.topologyViewMode, - topologyId: this.props.currentTopology.get('id'), - parentTopologyId: this.props.currentTopology.get('parentId'), - }); + const clampedTimestamp = clampToNowInSecondsPrecision(timestamp); + this.instantUpdateTimestamp(clampedTimestamp, this.trackTimestampEdit); } } - // TODO: Redo - handleJumpClick(millisecondsDelta) { - let timestamp = this.state.sliderValue + millisecondsDelta; - timestamp = Math.min(timestamp, moment().valueOf()); - this.travelTo(timestamp, true); + handleTimelinePan(timestamp) { + this.setState(getTimestampStates(timestamp)); + this.debouncedUpdateTimestamp(timestamp); } - updateTimestamp(timestamp) { - this.props.jumpToTime(moment(timestamp)); + handleTimelinePanEnd(timestamp) { + this.instantUpdateTimestamp(timestamp, this.trackTimelinePan); } - travelTo(timestamp, debounced = false) { - this.props.timeTravelStartTransition(); - this.setState(getTimestampStates(timestamp)); - if (debounced) { - this.debouncedUpdateTimestamp(timestamp); - } else { + handleInstantJump(timestamp) { + this.instantUpdateTimestamp(timestamp, this.trackTimelineClick); + } + + instantUpdateTimestamp(timestamp, callback) { + if (!timestamp.isSame(this.props.pausedAt)) { this.debouncedUpdateTimestamp.cancel(); - this.updateTimestamp(timestamp); + this.setState(getTimestampStates(timestamp)); + this.props.timeTravelStartTransition(); + this.props.jumpToTime(moment(timestamp)); + + // Used for tracking. + if (callback) callback(); } } - trackSliderChange() { - trackMixpanelEvent('scope.time.slider.change', { + trackTimestampEdit() { + trackMixpanelEvent('scope.time.timestamp.edit', { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } + + trackTimelineClick() { + trackMixpanelEvent('scope.time.timeline.click', { + layout: this.props.topologyViewMode, + topologyId: this.props.currentTopology.get('id'), + parentTopologyId: this.props.currentTopology.get('parentId'), + }); + } + + trackTimelinePan() { + trackMixpanelEvent('scope.time.timeline.pan', { layout: this.props.topologyViewMode, topologyId: this.props.currentTopology.get('id'), parentTopologyId: this.props.currentTopology.get('parentId'), @@ -109,7 +117,8 @@ class TimeTravel extends React.Component {
{ + it('records how long a function takes to execute', () => { + const add100k = (number) => { + for (let i = 0; i < 100000; i += 1) { + number += 1; + } + return number; + }; + + const timedFn = timer(add100k); + const result = timedFn(70); + expect(result).toEqual(100070); + expect(timedFn.time).toBeA('number'); + }); +}); diff --git a/client/app/scripts/utils/time-utils.js b/client/app/scripts/utils/time-utils.js new file mode 100644 index 0000000000..c93e424154 --- /dev/null +++ b/client/app/scripts/utils/time-utils.js @@ -0,0 +1,26 @@ +import moment from 'moment'; + +// Replacement for timely dependency +export function timer(fn) { + const timedFn = (...args) => { + const start = new Date(); + const result = fn.apply(fn, args); + timedFn.time = new Date() - start; + return result; + }; + return timedFn; +} + +export function nowInSecondsPrecision() { + return moment().startOf('second'); +} + +export function clampToNowInSecondsPrecision(timestamp) { + const now = nowInSecondsPrecision(); + return timestamp.isAfter(now) ? now : timestamp; +} + +// This is unfortunately not there in moment.js +export function scaleDuration(duration, scale) { + return moment.duration(duration.asMilliseconds() * scale); +} diff --git a/client/app/scripts/utils/tracking-utils.js b/client/app/scripts/utils/tracking-utils.js index 94f5b73253..47b0508b59 100644 --- a/client/app/scripts/utils/tracking-utils.js +++ b/client/app/scripts/utils/tracking-utils.js @@ -1,9 +1,10 @@ import debug from 'debug'; -const log = debug('service:tracking'); +const log = debug('scope:tracking'); // Track mixpanel events only if Scope is running inside of Weave Cloud. export function trackMixpanelEvent(name, props) { + log('trackMixpanelEvent', name, props); if (window.mixpanel && process.env.WEAVE_CLOUD) { window.mixpanel.track(name, props); } else { diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 4e05b602e3..ba147dbb97 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -255,8 +255,6 @@ } .time-travel { - // align-items: center; - // display: flex; position: relative; margin-bottom: 15px; z-index: 2001; @@ -290,7 +288,7 @@ width: 100%; height: 100%; - &.dragging { @extend .grabbing; } + &.panning { @extend .grabbing; } .available-range { fill: #888; @@ -318,6 +316,7 @@ background-color: red; height: 70px; width: 3px; + margin-left: -1px; } } @@ -346,7 +345,6 @@ background-color: #ccc; height: 9px; width: 1px; - left: 1px } } } From 9bbb2618ca9fe184ca8ccafa1cabf197d40d1f1e Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 25 Jul 2017 13:23:40 +0200 Subject: [PATCH 22/31] More cleaning up. --- .../components/time-travel-timeline.js | 153 ++++++++++-------- client/app/scripts/components/time-travel.js | 3 +- client/app/styles/_base.scss | 2 +- client/app/styles/_variables.scss | 2 +- 4 files changed, 87 insertions(+), 73 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 37ca5aaf80..4cac121b36 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -16,36 +16,42 @@ import { import { TIMELINE_TICK_INTERVAL } from '../constants/timer'; -const TICK_ROWS = { - minute: { - format: 'HH:mm', - intervals: [ - moment.duration(1, 'minute'), - moment.duration(5, 'minutes'), - moment.duration(15, 'minutes'), - moment.duration(1, 'hour'), - moment.duration(3, 'hours'), - moment.duration(6, 'hours'), - ], - }, - day: { - format: 'Do', +const TICK_SETTINGS_PER_PERIOD = { + year: { + format: 'YYYY', + childPeriod: 'month', intervals: [ - moment.duration(1, 'day'), - moment.duration(1, 'week'), + moment.duration(1, 'year'), ], }, month: { format: 'MMMM', + parentPeriod: 'year', + childPeriod: 'day', intervals: [ moment.duration(1, 'month'), moment.duration(3, 'months'), ], }, - year: { - format: 'YYYY', + day: { + format: 'Do', + parentPeriod: 'month', + childPeriod: 'minute', intervals: [ - moment.duration(1, 'year'), + moment.duration(1, 'day'), + moment.duration(1, 'week'), + ], + }, + minute: { + format: 'HH:mm', + parentPeriod: 'day', + intervals: [ + moment.duration(1, 'minute'), + moment.duration(5, 'minutes'), + moment.duration(15, 'minutes'), + moment.duration(1, 'hour'), + moment.duration(3, 'hours'), + moment.duration(6, 'hours'), ], }, }; @@ -59,33 +65,6 @@ const ZOOM_SENSITIVITY = 1.0015; const FADE_OUT_FACTOR = 1.4; -function yFunc(currentDuration, fadedInDuration) { - const durationLog = d => Math.log(d.asMilliseconds()); - const fadedOutDuration = scaleDuration(fadedInDuration, FADE_OUT_FACTOR); - const transitionFactor = durationLog(fadedOutDuration) - durationLog(currentDuration); - const transitionLength = durationLog(fadedOutDuration) - durationLog(fadedInDuration); - return clamp(transitionFactor / transitionLength, 0, 1); -} - -function getFadeInFactor(period, duration) { - const durationMultiplier = 1 / MAX_TICK_SPACING_PX; - const minuteShift = yFunc(duration, scaleDuration(moment.duration(1, 'day'), durationMultiplier)); - const dayShift = yFunc(duration, scaleDuration(moment.duration(1, 'month'), durationMultiplier)); - const monthShift = yFunc(duration, scaleDuration(moment.duration(1, 'year'), durationMultiplier)); - const yearShift = 1; - - let result = 0; - switch (period) { - case 'year': result = yearShift + monthShift + dayShift + minuteShift; break; - case 'month': result = monthShift + dayShift + minuteShift; break; - case 'day': result = dayShift + minuteShift; break; - case 'minute': result = minuteShift; break; - default: result = 0; break; - } - return result; -} - - class TimeTravelTimeline extends React.Component { constructor(props, context) { super(props, context); @@ -95,21 +74,20 @@ class TimeTravelTimeline extends React.Component { focusedTimestamp: nowInSecondsPrecision(), durationPerPixel: INIT_DURATION_PER_PX, boundingRect: { width: 0, height: 0 }, - isDragging: false, + isPanning: false, }; - this.saveSvgRef = this.saveSvgRef.bind(this); this.jumpRelativePixels = this.jumpRelativePixels.bind(this); this.jumpForward = this.jumpForward.bind(this); this.jumpBackward = this.jumpBackward.bind(this); this.jumpTo = this.jumpTo.bind(this); - this.findOptimalDuration = this.findOptimalDuration.bind(this); - this.handleZoom = this.handleZoom.bind(this); this.handlePanStart = this.handlePanStart.bind(this); this.handlePanEnd = this.handlePanEnd.bind(this); this.handlePan = this.handlePan.bind(this); + + this.saveSvgRef = this.saveSvgRef.bind(this); } componentDidMount() { @@ -131,9 +109,11 @@ class TimeTravelTimeline extends React.Component { } componentWillReceiveProps(nextProps) { + // Don't update the focused timestamp if we're not paused (so the timeline is hidden). if (nextProps.pausedAt) { this.setState({ focusedTimestamp: nextProps.pausedAt }); } + // Always update the timeline dimension information. this.setState({ boundingRect: this.svgRef.getBoundingClientRect() }); } @@ -186,6 +166,12 @@ class TimeTravelTimeline extends React.Component { this.jumpRelativePixels(-this.state.boundingRect.width / 4); } + findOptimalDuration(durations) { + const { durationPerPixel } = this.state; + const minimalDuration = scaleDuration(durationPerPixel, 1.1 * MIN_TICK_SPACING_PX); + return find(durations, d => d >= minimalDuration); + } + getTimeScale() { const { durationPerPixel, focusedTimestamp } = this.state; const roundedTimestamp = moment(focusedTimestamp).utc().startOf('second'); @@ -196,16 +182,36 @@ class TimeTravelTimeline extends React.Component { .range([-1, 1]); } - findOptimalDuration(durations) { - const { durationPerPixel } = this.state; - const minimalDuration = scaleDuration(durationPerPixel, 1.1 * MIN_TICK_SPACING_PX); - return find(durations, d => d >= minimalDuration); + getVerticalShiftForPeriod(period) { + const { childPeriod, parentPeriod } = TICK_SETTINGS_PER_PERIOD[period]; + const currentDuration = this.state.durationPerPixel; + + let shift = 1; + if (parentPeriod) { + const durationMultiplier = 1 / MAX_TICK_SPACING_PX; + const parentPeriodStartInterval = TICK_SETTINGS_PER_PERIOD[parentPeriod].intervals[0]; + const fadedInDuration = scaleDuration(parentPeriodStartInterval, durationMultiplier); + const fadedOutDuration = scaleDuration(fadedInDuration, FADE_OUT_FACTOR); + + const durationLog = d => Math.log(d.asMilliseconds()); + const transitionFactor = durationLog(fadedOutDuration) - durationLog(currentDuration); + const transitionLength = durationLog(fadedOutDuration) - durationLog(fadedInDuration); + + shift = clamp(transitionFactor / transitionLength, 0, 1); + } + + if (childPeriod) { + shift += this.getVerticalShiftForPeriod(childPeriod, currentDuration); + } + + return shift; } - getTicks(period, parentPeriod) { + getTicksForPeriod(period) { // 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); + const { parentPeriod, intervals } = TICK_SETTINGS_PER_PERIOD[period]; + const duration = this.findOptimalDuration(intervals); if (!duration) return []; // Get the boundary values for the displayed part of the timeline. @@ -259,13 +265,14 @@ class TimeTravelTimeline extends React.Component { } renderTimestampTick({ timestamp, position, isBehind }, periodFormat, opacity) { - const { timestampNow } = this.state; - const disabled = timestamp.isAfter(timestampNow) || opacity < 0.2; + // Ticks are disabled if they are in the future or if they are too transparent. + const disabled = timestamp.isAfter(this.state.timestampNow) || opacity < 0.2; const handleClick = () => this.jumpTo(timestamp); return ( {!isBehind && } + Jump to {timestamp.utc().format()} {isBehind && '←'}{timestamp.utc().format(periodFormat)} @@ -275,16 +282,16 @@ class TimeTravelTimeline extends React.Component { ); } - renderPeriodBar(period, parentPeriod) { - const ticks = this.getTicks(period, parentPeriod); + renderPeriodTicks(period) { + const periodFormat = TICK_SETTINGS_PER_PERIOD[period].format; + const ticks = this.getTicksForPeriod(period); - const periodFormat = TICK_ROWS[period].format; - const p = getFadeInFactor(period, this.state.durationPerPixel); - const verticalPosition = 60 - (p * 15); - const opacity = Math.min(p, 1); + const verticalShift = this.getVerticalShiftForPeriod(period); + const transform = `translate(0, ${60 - (verticalShift * 15)})`; + const opacity = clamp(verticalShift, 0, 1); return ( - + {map(ticks, tick => this.renderTimestampTick(tick, periodFormat, opacity))} ); @@ -305,14 +312,20 @@ class TimeTravelTimeline extends React.Component { } renderAxis() { + const { width, height } = this.state.boundingRect; return ( + {this.renderDisabledShadow()} - {this.renderPeriodBar('year')} - {this.renderPeriodBar('month', 'year')} - {this.renderPeriodBar('day', 'month')} - {this.renderPeriodBar('minute', 'day')} + {this.renderPeriodTicks('year')} + {this.renderPeriodTicks('month')} + {this.renderPeriodTicks('day')} + {this.renderPeriodTicks('minute')} ); @@ -329,6 +342,7 @@ class TimeTravelTimeline extends React.Component { + Scroll to zoom, drag to pan {this.renderAxis()} @@ -343,6 +357,7 @@ class TimeTravelTimeline extends React.Component { function mapStateToProps(state) { return { + // Used only to trigger recalculations on window resize. viewportWidth: state.getIn(['viewport', 'width']), pausedAt: state.get('pausedAt'), }; diff --git a/client/app/scripts/components/time-travel.js b/client/app/scripts/components/time-travel.js index ee1fbc9366..c1f424f154 100644 --- a/client/app/scripts/components/time-travel.js +++ b/client/app/scripts/components/time-travel.js @@ -1,5 +1,4 @@ import React from 'react'; -// import Slider from 'rc-slider'; import moment from 'moment'; import classNames from 'classnames'; import { connect } from 'react-redux'; @@ -49,7 +48,7 @@ class TimeTravel extends React.Component { } componentWillUnmount() { - // TODO: Causing bug? + // TODO: Get rid of this somehow. See: https://github.com/weaveworks/service-ui/issues/814 this.props.resumeTime(); } diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index ba147dbb97..8d043dd145 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -189,7 +189,7 @@ &.time-travel-open { .details-wrapper { - margin-top: 65px; + margin-top: 85px; } } } diff --git a/client/app/styles/_variables.scss b/client/app/styles/_variables.scss index 801f8738f4..52d352f670 100644 --- a/client/app/styles/_variables.scss +++ b/client/app/styles/_variables.scss @@ -26,7 +26,7 @@ $text-darker-color: $primary-color; $white: $background-lighter-color; $details-window-width: 420px; -$details-window-padding-left: 36px; +$details-window-padding-left: 30px; $border-radius: 4px; $terminal-header-height: 44px; From d104a68b639530537a130cd7358ed65d646e1cc2 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 25 Jul 2017 13:47:03 +0200 Subject: [PATCH 23/31] Styled the timestamp input. --- client/app/styles/_base.scss | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 8d043dd145..a68fd70942 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -321,19 +321,20 @@ } &-timestamp { + background-color: $background-lighter-color; border: 1px solid #ccc; border-radius: 4px; padding: 2px 8px; pointer-events: all; margin: 8px auto 25px; - width: 225px; + opacity: 0.8; + width: 215px; input { border: 0; background-color: transparent; - font-family: $mono-font; - font-size: 0.875rem; - margin-right: 2px; + font-size: 1rem; + width: 167px; outline: 0; } From 85ac85b525f62bc19c311320e76cadf271589aa2 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 25 Jul 2017 13:52:19 +0200 Subject: [PATCH 24/31] Final cleanup. --- client/app/scripts/utils/tracking-utils.js | 3 +-- client/package.json | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/client/app/scripts/utils/tracking-utils.js b/client/app/scripts/utils/tracking-utils.js index 47b0508b59..94f5b73253 100644 --- a/client/app/scripts/utils/tracking-utils.js +++ b/client/app/scripts/utils/tracking-utils.js @@ -1,10 +1,9 @@ import debug from 'debug'; -const log = debug('scope:tracking'); +const log = debug('service:tracking'); // Track mixpanel events only if Scope is running inside of Weave Cloud. export function trackMixpanelEvent(name, props) { - log('trackMixpanelEvent', name, props); if (window.mixpanel && process.env.WEAVE_CLOUD) { window.mixpanel.track(name, props); } else { diff --git a/client/package.json b/client/package.json index 3bad883a0b..5d971a1c91 100644 --- a/client/package.json +++ b/client/package.json @@ -11,14 +11,12 @@ "babel-polyfill": "6.23.0", "classnames": "2.2.5", "d3-array": "1.2.0", - "d3-axis": "^1.0.8", "d3-color": "1.0.3", "d3-drag": "1.0.4", "d3-format": "1.2.0", "d3-scale": "1.0.5", "d3-selection": "1.0.5", "d3-shape": "1.0.6", - "d3-time": "^1.0.7", "d3-time-format": "2.0.5", "d3-transition": "1.0.4", "d3-zoom": "1.1.4", From 74d0915edcb59bb7a8cf51033e6c5dd54607d5c8 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 25 Jul 2017 13:54:36 +0200 Subject: [PATCH 25/31] Update yarn.lock --- client/yarn.lock | 8 -------- 1 file changed, 8 deletions(-) diff --git a/client/yarn.lock b/client/yarn.lock index 72b28de39f..8518dc6ffd 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1666,10 +1666,6 @@ d3-array@1, d3-array@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.0.tgz#147d269720e174c4057a7f42be8b0f3f2ba53108" -d3-axis@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa" - d3-collection@1: version "1.0.3" resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.3.tgz#00bdea94fbc1628d435abbae2f4dc2164e37dd34" @@ -1739,10 +1735,6 @@ d3-time@1: version "1.0.6" resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.6.tgz#a55b13d7d15d3a160ae91708232e0835f1d5e945" -d3-time@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.7.tgz#94caf6edbb7879bb809d0d1f7572bc48482f7270" - d3-timer@1: version "1.0.5" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.5.tgz#b266d476c71b0d269e7ac5f352b410a3b6fe6ef0" From 846e7d5e5401143aa2d6c92d3c752f6308129990 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 25 Jul 2017 14:04:19 +0200 Subject: [PATCH 26/31] Zoom tracking. --- client/app/scripts/components/time-travel-timeline.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 4cac121b36..6763ef52c6 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -7,6 +7,7 @@ import { drag } from 'd3-drag'; import { scaleUtc } from 'd3-scale'; import { event as d3Event, select } from 'd3-selection'; +import { trackMixpanelEvent } from '../utils/tracking-utils'; import { nowInSecondsPrecision, clampToNowInSecondsPrecision, @@ -143,6 +144,7 @@ class TimeTravelTimeline extends React.Component { 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; + trackMixpanelEvent('scope.time.timeline.zoom', { scale }); this.setState({ durationPerPixel }); } From 8e16491885acf422fc5f64d7672c93f1726054fa Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 25 Jul 2017 17:37:27 +0200 Subject: [PATCH 27/31] Animate timeline translations. --- .../components/time-travel-timeline.js | 46 ++++++++++++------- client/app/scripts/constants/animation.js | 1 + 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index 6763ef52c6..e7df0015f3 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; import { drag } from 'd3-drag'; import { scaleUtc } from 'd3-scale'; import { event as d3Event, select } from 'd3-selection'; +import { Motion, spring } from 'react-motion'; import { trackMixpanelEvent } from '../utils/tracking-utils'; import { @@ -14,6 +15,7 @@ import { scaleDuration, } from '../utils/time-utils'; +import { NODES_SPRING_FAST_ANIMATION_CONFIG } from '../constants/animation'; import { TIMELINE_TICK_INTERVAL } from '../constants/timer'; @@ -174,11 +176,10 @@ class TimeTravelTimeline extends React.Component { return find(durations, d => d >= minimalDuration); } - getTimeScale() { - const { durationPerPixel, focusedTimestamp } = this.state; + getTimeScale(focusedTimestamp) { const roundedTimestamp = moment(focusedTimestamp).utc().startOf('second'); - const startDate = moment(roundedTimestamp).subtract(durationPerPixel); - const endDate = moment(roundedTimestamp).add(durationPerPixel); + const startDate = moment(roundedTimestamp).subtract(this.state.durationPerPixel); + const endDate = moment(roundedTimestamp).add(this.state.durationPerPixel); return scaleUtc() .domain([startDate, endDate]) .range([-1, 1]); @@ -209,7 +210,7 @@ class TimeTravelTimeline extends React.Component { return shift; } - getTicksForPeriod(period) { + getTicksForPeriod(period, focusedTimestamp) { // 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 { parentPeriod, intervals } = TICK_SETTINGS_PER_PERIOD[period]; @@ -217,7 +218,7 @@ class TimeTravelTimeline extends React.Component { if (!duration) return []; // Get the boundary values for the displayed part of the timeline. - const timeScale = this.getTimeScale(); + const timeScale = this.getTimeScale(focusedTimestamp); const startPosition = -this.state.boundingRect.width / 2; const endPosition = this.state.boundingRect.width / 2; const startDate = moment(timeScale.invert(startPosition)); @@ -284,9 +285,9 @@ class TimeTravelTimeline extends React.Component { ); } - renderPeriodTicks(period) { + renderPeriodTicks(period, focusedTimestamp) { const periodFormat = TICK_SETTINGS_PER_PERIOD[period].format; - const ticks = this.getTicksForPeriod(period); + const ticks = this.getTicksForPeriod(period, focusedTimestamp); const verticalShift = this.getVerticalShiftForPeriod(period); const transform = `translate(0, ${60 - (verticalShift * 15)})`; @@ -299,8 +300,8 @@ class TimeTravelTimeline extends React.Component { ); } - renderDisabledShadow() { - const timeScale = this.getTimeScale(); + renderDisabledShadow(focusedTimestamp) { + const timeScale = this.getTimeScale(focusedTimestamp); const nowShift = timeScale(this.state.timestampNow); const { width, height } = this.state.boundingRect; @@ -313,8 +314,9 @@ class TimeTravelTimeline extends React.Component { ); } - renderAxis() { + renderAxis(focusedTimestamp) { const { width, height } = this.state.boundingRect; + return ( - {this.renderDisabledShadow()} + {this.renderDisabledShadow(focusedTimestamp)} - {this.renderPeriodTicks('year')} - {this.renderPeriodTicks('month')} - {this.renderPeriodTicks('day')} - {this.renderPeriodTicks('minute')} + {this.renderPeriodTicks('year', focusedTimestamp)} + {this.renderPeriodTicks('month', focusedTimestamp)} + {this.renderPeriodTicks('day', focusedTimestamp)} + {this.renderPeriodTicks('minute', focusedTimestamp)} ); } + renderAnimatedContent() { + const timestamp = this.state.focusedTimestamp.valueOf(); + + return ( + + {interpolated => this.renderAxis(moment(interpolated.timestamp))} + + ); + } + render() { const className = classNames({ panning: this.state.isPanning }); const halfWidth = this.state.boundingRect.width / 2; @@ -345,7 +357,7 @@ class TimeTravelTimeline extends React.Component { Scroll to zoom, drag to pan - {this.renderAxis()} + {this.renderAnimatedContent()} diff --git a/client/app/scripts/constants/animation.js b/client/app/scripts/constants/animation.js index e24d707705..6382ab47cc 100644 --- a/client/app/scripts/constants/animation.js +++ b/client/app/scripts/constants/animation.js @@ -1,2 +1,3 @@ export const NODES_SPRING_ANIMATION_CONFIG = { stiffness: 80, damping: 20, precision: 0.1 }; +export const NODES_SPRING_FAST_ANIMATION_CONFIG = { stiffness: 800, damping: 50, precision: 0.1 }; From e80fe6bb3ae7ad9710ea2fa5c5dfc6ed917a94df Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Tue, 25 Jul 2017 17:58:24 +0200 Subject: [PATCH 28/31] Fixed the PAUSE button glitch and updating the time control info. --- client/app/scripts/actions/app-actions.js | 12 ++++++++++-- client/app/scripts/components/time-control.js | 11 +++++++++++ client/app/scripts/constants/animation.js | 2 +- client/app/scripts/reducers/root.js | 3 ++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 047a7f485b..cecbefab66 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -338,8 +338,16 @@ export function clickNode(nodeId, label, origin, topologyId = null) { } export function pauseTimeAtNow() { - return { - type: ActionTypes.PAUSE_TIME_AT_NOW + return (dispatch, getState) => { + dispatch({ + type: ActionTypes.PAUSE_TIME_AT_NOW + }); + if (!getState().get('nodesLoaded')) { + getNodes(getState, dispatch); + if (isResourceViewModeSelector(getState())) { + getResourceViewNodesSnapshot(getState(), dispatch); + } + } }; } diff --git a/client/app/scripts/components/time-control.js b/client/app/scripts/components/time-control.js index 2cfe27dd7b..de50001254 100644 --- a/client/app/scripts/components/time-control.js +++ b/client/app/scripts/components/time-control.js @@ -8,6 +8,8 @@ import TimeTravelButton from './time-travel-button'; import { trackMixpanelEvent } from '../utils/tracking-utils'; import { pauseTimeAtNow, resumeTime, startTimeTravel } from '../actions/app-actions'; +import { TIMELINE_TICK_INTERVAL } from '../constants/timer'; + const className = isSelected => ( classNames('time-control-action', { 'time-control-action-selected': isSelected }) @@ -23,6 +25,15 @@ class TimeControl extends React.Component { this.getTrackingMetadata = this.getTrackingMetadata.bind(this); } + componentDidMount() { + // Force periodic for the paused info. + this.timer = setInterval(() => { this.forceUpdate(); }, TIMELINE_TICK_INTERVAL); + } + + componentWillUnmount() { + clearInterval(this.timer); + } + getTrackingMetadata(data = {}) { const { currentTopology } = this.props; return { diff --git a/client/app/scripts/constants/animation.js b/client/app/scripts/constants/animation.js index 6382ab47cc..895b5f5f47 100644 --- a/client/app/scripts/constants/animation.js +++ b/client/app/scripts/constants/animation.js @@ -1,3 +1,3 @@ export const NODES_SPRING_ANIMATION_CONFIG = { stiffness: 80, damping: 20, precision: 0.1 }; -export const NODES_SPRING_FAST_ANIMATION_CONFIG = { stiffness: 800, damping: 50, precision: 0.1 }; +export const NODES_SPRING_FAST_ANIMATION_CONFIG = { stiffness: 800, damping: 50, precision: 1 }; diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 778a91ba02..880a6b56ee 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -366,12 +366,13 @@ export function rootReducer(state = initialState, action) { case ActionTypes.PAUSE_TIME_AT_NOW: { state = state.set('showingTimeTravel', false); + state = state.set('timeTravelTransitioning', false); return state.set('pausedAt', nowInSecondsPrecision()); } case ActionTypes.START_TIME_TRAVEL: { - state = state.set('timeTravelTransitioning', false); state = state.set('showingTimeTravel', true); + state = state.set('timeTravelTransitioning', false); return state.set('pausedAt', nowInSecondsPrecision()); } From 5e3a0a1b7b587f9427d87e5e98897d3dff2c9715 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Thu, 27 Jul 2017 12:12:06 +0200 Subject: [PATCH 29/31] Opacity fix and timeline arrows removed. --- .../scripts/components/time-travel-timeline.js | 15 +++++++++------ client/app/styles/_base.scss | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index e7df0015f3..d81e0bbdfa 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -62,7 +62,7 @@ const TICK_SETTINGS_PER_PERIOD = { const MIN_DURATION_PER_PX = moment.duration(250, 'milliseconds'); const INIT_DURATION_PER_PX = moment.duration(1, 'minute'); const MAX_DURATION_PER_PX = moment.duration(3, 'days'); -const MIN_TICK_SPACING_PX = 80; +const MIN_TICK_SPACING_PX = 70; const MAX_TICK_SPACING_PX = 415; const ZOOM_SENSITIVITY = 1.0015; const FADE_OUT_FACTOR = 1.4; @@ -269,16 +269,16 @@ class TimeTravelTimeline extends React.Component { renderTimestampTick({ timestamp, position, isBehind }, periodFormat, opacity) { // Ticks are disabled if they are in the future or if they are too transparent. - const disabled = timestamp.isAfter(this.state.timestampNow) || opacity < 0.2; + const disabled = timestamp.isAfter(this.state.timestampNow) || opacity < 0.4; const handleClick = () => this.jumpTo(timestamp); return ( {!isBehind && } - Jump to {timestamp.utc().format()} + {!disabled && Jump to {timestamp.utc().format()}} - {isBehind && '←'}{timestamp.utc().format(periodFormat)} + {timestamp.utc().format(periodFormat)} @@ -290,8 +290,11 @@ class TimeTravelTimeline extends React.Component { const ticks = this.getTicksForPeriod(period, focusedTimestamp); const verticalShift = this.getVerticalShiftForPeriod(period); - const transform = `translate(0, ${60 - (verticalShift * 15)})`; - const opacity = clamp(verticalShift, 0, 1); + const transform = `translate(0, ${62 - (verticalShift * 15)})`; + + // Ticks quickly fade in from the bottom and then slowly + // start fading out as they are being pushed to the top. + const opacity = verticalShift > 1 ? (6 - verticalShift) / 5 : verticalShift; return ( diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index a68fd70942..91eeef054c 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -296,6 +296,7 @@ } .timestamp-label { + margin-left: 2px; padding: 3px; &[disabled] { From 07aff0577f26cf48011f6c825649e964aa768c0f Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Thu, 27 Jul 2017 14:05:34 +0200 Subject: [PATCH 30/31] Fixed the red vertical bar. --- client/app/styles/_base.scss | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 91eeef054c..c3d4be41a1 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -306,18 +306,28 @@ } } - &:after { + &:before, &:after { content: ''; position: absolute; display: block; left: 50%; - top: 0; border: 1px solid white; border-top: 0; + border-bottom: 0; background-color: red; - height: 70px; - width: 3px; margin-left: -1px; + width: 3px; + } + + &:before { + top: 0; + height: 70px; + } + + &:after { + top: 70px; + height: 9px; + opacity: 0.15; } } @@ -338,16 +348,6 @@ width: 167px; outline: 0; } - - &:before { - content: ''; - position: relative; - display: block; - margin: -12px auto 3px; - background-color: #ccc; - height: 9px; - width: 1px; - } } } From 4b1921693b38b538fa0fb0d88b3a62054c6eabb3 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Thu, 27 Jul 2017 14:06:28 +0200 Subject: [PATCH 31/31] Use preventDefault() on timeline scrolling. --- client/app/scripts/components/time-travel-timeline.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/app/scripts/components/time-travel-timeline.js b/client/app/scripts/components/time-travel-timeline.js index d81e0bbdfa..aca28888fa 100644 --- a/client/app/scripts/components/time-travel-timeline.js +++ b/client/app/scripts/components/time-travel-timeline.js @@ -141,13 +141,15 @@ class TimeTravelTimeline extends React.Component { this.setState({ focusedTimestamp }); } - handleZoom(e) { - const scale = Math.pow(ZOOM_SENSITIVITY, e.deltaY); + handleZoom(ev) { + const scale = Math.pow(ZOOM_SENSITIVITY, ev.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; + trackMixpanelEvent('scope.time.timeline.zoom', { scale }); this.setState({ durationPerPixel }); + ev.preventDefault(); } jumpTo(timestamp) {