src/components/timebar.js
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import moment from 'moment';
import {intToPix} from '../utils/commonUtils';
import {timebarFormat as defaultTimebarFormat} from '../consts/timebarConsts';
/**
* Timebar component - displays the current time on top of the timeline
*/
export default class Timebar extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.guessResolution = this.guessResolution.bind(this);
this.renderBar = this.renderBar.bind(this);
this.renderTopBar = this.renderTopBar.bind(this);
this.renderBottomBar = this.renderBottomBar.bind(this);
}
componentWillMount() {
this.guessResolution();
}
/**
* On new props we check if a resolution is given, and if not we guess one
* @param {Object} nextProps Props coming in
*/
componentWillReceiveProps(nextProps) {
if (nextProps.top_resolution && nextProps.bottom_resolution) {
this.setState({resolution: {top: nextProps.top_resolution, bottom: nextProps.bottom_resolution}});
} else {
this.guessResolution(nextProps.start, nextProps.end);
}
}
/**
* Attempts to guess the resolution of the top and bottom halves of the timebar based on the viewable date range.
* Sets resolution to state.
* @param {moment} start Start date for the timebar
* @param {moment} end End date for the timebar
*/
guessResolution(start, end) {
if (!start || !end) {
start = this.props.start;
end = this.props.end;
}
const durationSecs = end.diff(start, 'seconds');
// -> 1h
if (durationSecs <= 60 * 60) this.setState({resolution: {top: 'hour', bottom: 'minute'}});
// 1h -> 3d
else if (durationSecs <= 24 * 60 * 60 * 3) this.setState({resolution: {top: 'day', bottom: 'hour'}});
// 1d -> 30d
else if (durationSecs <= 30 * 24 * 60 * 60) this.setState({resolution: {top: 'month', bottom: 'day'}});
//30d -> 1y
else if (durationSecs <= 365 * 24 * 60 * 60) this.setState({resolution: {top: 'year', bottom: 'month'}});
// 1y ->
else this.setState({resolution: {top: 'year', bottom: 'year'}});
}
/**
* Renderer for top bar.
* @returns {Object} JSX for top menu bar - based of time format & resolution
*/
renderTopBar() {
let res = this.state.resolution.top;
return this.renderBar({format: this.props.timeFormats.majorLabels[res], type: res});
}
/**
* Renderer for bottom bar.
* @returns {Object} JSX for bottom menu bar - based of time format & resolution
*/
renderBottomBar() {
let res = this.state.resolution.bottom;
return this.renderBar({format: this.props.timeFormats.minorLabels[res], type: res});
}
/**
* Gets the number of pixels per segment of the timebar section (using the resolution)
* @param {moment} date The date being rendered. This is used to figure out how many days are in the month
* @param {string} resolutionType Timebar section resolution [Year; Month...]
* @returns {number} The number of pixels per segment
*/
getPixelIncrement(date, resolutionType, offset = 0) {
const {start, end} = this.props;
const width = this.props.width - this.props.leftOffset;
const start_end_min = end.diff(start, 'minutes');
const pixels_per_min = width / start_end_min;
function isLeapYear(year) {
return year % 400 === 0 || (year % 100 !== 0 && year % 4 === 0);
}
const daysInYear = isLeapYear(date.year()) ? 366 : 365;
let inc = width;
switch (resolutionType) {
case 'year':
inc = pixels_per_min * 60 * 24 * (daysInYear - offset);
break;
case 'month':
inc = pixels_per_min * 60 * 24 * (date.daysInMonth() - offset);
break;
case 'day':
inc = pixels_per_min * 60 * (24 - offset);
break;
case 'hour':
inc = pixels_per_min * (60 - offset);
break;
case 'minute':
inc = pixels_per_min - offset;
break;
default:
break;
}
return Math.min(inc, width);
}
/**
* Renders an entire segment of the timebar (top or bottom)
* @param {string} resolution The resolution to render at [Year; Month...]
* @returns {Object[]} A list of sections (making up a segment) to be rendered
* @property {string} label The text displayed in the section (usually the date/time)
* @property {boolean} isSelected Whether the section is being 'touched' when dragging/resizing
* @property {number} size The number of pixels the segment will take up
* @property {number|string} key Key for react
*/
renderBar(resolution) {
const {start, end, selectedRanges} = this.props;
const width = this.props.width - this.props.leftOffset;
let currentDate = start.clone();
let timeIncrements = [];
let pixelsLeft = width;
let labelSizeLimit = 60;
function _addTimeIncrement(initialOffset, offsetType, stepFunc) {
let offset = null;
while (currentDate.isBefore(end) && pixelsLeft > 0) {
// if this is the first 'block' it may be cut off at the start
if (pixelsLeft === width) {
offset = initialOffset;
} else {
offset = moment.duration(0);
}
let pixelIncrements = Math.min(
this.getPixelIncrement(currentDate, resolution.type, offset.as(offsetType)),
pixelsLeft
);
const labelSize = pixelIncrements < labelSizeLimit ? 'short' : 'long';
let label = currentDate.format(resolution.format[labelSize]);
let isSelected = _.some(selectedRanges, s => {
return (
currentDate.isSameOrAfter(s.start.clone().startOf(resolution.type)) &&
currentDate.isSameOrBefore(s.end.clone().startOf(resolution.type))
);
});
timeIncrements.push({label, isSelected, size: pixelIncrements, key: pixelsLeft});
stepFunc(currentDate, offset);
pixelsLeft -= pixelIncrements;
}
}
const addTimeIncrement = _addTimeIncrement.bind(this);
if (resolution.type === 'year') {
const offset = moment.duration(currentDate.diff(currentDate.clone().startOf('year')));
addTimeIncrement(offset, 'months', (currentDt, offst) => currentDt.subtract(offst).add(1, 'year'));
} else if (resolution.type === 'month') {
const offset = moment.duration(currentDate.diff(currentDate.clone().startOf('month')));
addTimeIncrement(offset, 'days', (currentDt, offst) => currentDt.subtract(offst).add(1, 'month'));
} else if (resolution.type === 'day') {
const offset = moment.duration(currentDate.diff(currentDate.clone().startOf('day')));
addTimeIncrement(offset, 'hours', (currentDt, offst) => currentDt.subtract(offst).add(1, 'days'));
} else if (resolution.type === 'hour') {
const offset = moment.duration(currentDate.diff(currentDate.clone().startOf('hour')));
addTimeIncrement(offset, 'minutes', (currentDt, offst) => currentDt.subtract(offst).add(1, 'hours'));
} else if (resolution.type === 'minute') {
addTimeIncrement(moment.duration(0), 'minutes', (currentDt, offst) => currentDt.add(1, 'minutes'));
}
return timeIncrements;
}
/**
* Renders the timebar
* @returns {Object} Timebar component
*/
render() {
const {cursorTime} = this.props;
const topBarComponent = this.renderTopBar();
const bottomBarComponent = this.renderBottomBar();
const GroupTitleRenderer = this.props.groupTitleRenderer;
// Only show the cursor on 1 of the top bar segments
// Pick the segment that has the biggest size
let topBarCursorKey = null;
if (topBarComponent.length > 1 && topBarComponent[1].size > topBarComponent[0].size)
topBarCursorKey = topBarComponent[1].key;
else if (topBarComponent.length > 0) topBarCursorKey = topBarComponent[0].key;
return (
<div className="rct9k-timebar">
<div className="rct9k-timebar-group-title" style={{width: this.props.leftOffset}}>
<GroupTitleRenderer />
</div>
<div className="rct9k-timebar-outer" style={{width: this.props.width, paddingLeft: this.props.leftOffset}}>
<div className="rct9k-timebar-inner rct9k-timebar-inner-top">
{_.map(topBarComponent, i => {
let topLabel = i.label;
if (cursorTime && i.key === topBarCursorKey) {
topLabel += ` [${cursorTime}]`;
}
let className = 'rct9k-timebar-item';
if (i.isSelected) className += ' rct9k-timebar-item-selected';
return (
<span className={className} key={i.key} style={{width: intToPix(i.size)}}>
{topLabel}
</span>
);
})}
</div>
<div className="rct9k-timebar-inner rct9k-timebar-inner-bottom">
{_.map(bottomBarComponent, i => {
let className = 'rct9k-timebar-item';
if (i.isSelected) className += ' rct9k-timebar-item-selected';
return (
<span className={className} key={i.key} style={{width: intToPix(i.size)}}>
{i.label}
</span>
);
})}
</div>
</div>
</div>
);
}
}
Timebar.propTypes = {
cursorTime: PropTypes.any,
groupTitleRenderer: PropTypes.func,
start: PropTypes.object.isRequired, //moment
end: PropTypes.object.isRequired, //moment
width: PropTypes.number.isRequired,
leftOffset: PropTypes.number,
top_resolution: PropTypes.string,
bottom_resolution: PropTypes.string,
selectedRanges: PropTypes.arrayOf(PropTypes.object), // [start: moment ,end: moment (end)]
timeFormats: PropTypes.object
};
Timebar.defaultProps = {
selectedRanges: [],
groupTitleRenderer: () => <div />,
leftOffset: 0,
timeFormats: defaultTimebarFormat
};