tonkean-react-calendar-timeline
Version:
react calendar timeline
365 lines (301 loc) • 10.6 kB
JavaScript
import moment from 'moment'
import { _get } from './generic'
export function coordinateToTimeRatio(
canvasTimeStart,
canvasTimeEnd,
canvasWidth
) {
return (canvasTimeEnd - canvasTimeStart) / canvasWidth
}
export function calculateXPositionForTime(
canvasTimeStart,
canvasTimeEnd,
canvasWidth,
time
) {
const widthToZoomRatio = canvasWidth / (canvasTimeEnd - canvasTimeStart)
const timeOffset = time - canvasTimeStart
return timeOffset * widthToZoomRatio
}
export function iterateTimes(start, end, unit, timeSteps, callback) {
let time = moment(start).startOf(unit)
if (timeSteps[unit] && timeSteps[unit] > 1) {
let value = time.get(unit)
time.set(unit, value - value % timeSteps[unit])
}
while (time.valueOf() < end) {
let nextTime = moment(time).add(timeSteps[unit] || 1, `${unit}s`)
callback(time, nextTime)
time = nextTime
}
}
// this function is VERY HOT as its used in Timeline.js render function
// TODO: check if there are performance implications here
// when "weeks" feature is implemented, this function will be modified heavily
/** determine the current rendered time unit based on timeline time span
*
* zoom: (in milliseconds) difference between time start and time end of timeline canvas
* width: (in pixels) pixel width of timeline canvas
* timeSteps: map of timeDividers with number to indicate step of each divider
*/
// the smallest cell we want to render is 17px
// this can be manipulated to make the breakpoints change more/less
// i.e. on zoom how often do we switch to the next unit of time
// i think this is the distance between cell lines
export const minCellWidth = 17
export function getMinUnit(zoom, width, timeSteps) {
// for supporting weeks, its important to remember that each of these
// units has a national progression to the other. i.e. a year is 12 months
// a month is 24 days, a day is 24 hours.
// with weeks this isnt the case so weeks needs to be handled specially
let timeDividers = {
second: 1000,
minute: 60,
hour: 60,
day: 24,
month: 30,
year: 12
}
let minUnit = 'year'
// this timespan is in ms initially
let nextTimeSpanInUnitContext = zoom
Object.keys(timeDividers).some(unit => {
// converts previous time span to current unit
// (e.g. milliseconds to seconds, seconds to minutes, etc)
nextTimeSpanInUnitContext = nextTimeSpanInUnitContext / timeDividers[unit]
// timeSteps is "
// With what step to display different units. E.g. 15 for minute means only minutes 0, 15, 30 and 45 will be shown."
// how many cells would be rendered given this time span, for this unit?
// e.g. for time span of 60 minutes, and time step of 1, we would render 60 cells
const cellsToBeRenderedForCurrentUnit =
nextTimeSpanInUnitContext / timeSteps[unit]
// what is happening here? why 3 if time steps are greater than 1??
const cellWidthToUse =
timeSteps[unit] && timeSteps[unit] > 1 ? 3 * minCellWidth : minCellWidth
// for the minWidth of a cell, how many cells would be rendered given
// the current pixel width
// i.e. f
const minimumCellsToRenderUnit = width / cellWidthToUse
if (cellsToBeRenderedForCurrentUnit < minimumCellsToRenderUnit) {
// for the current zoom, the number of cells we'd need to render all parts of this unit
// is less than the minimum number of cells needed at minimum cell width
minUnit = unit
return true
}
})
return minUnit
}
export function getNextUnit(unit) {
let nextUnits = {
second: 'minute',
minute: 'hour',
hour: 'day',
day: 'month',
month: 'year'
}
return nextUnits[unit] || ''
}
export function calculateDimensions({
itemTimeStart,
itemTimeEnd,
isDragging,
isResizing,
canvasTimeStart,
canvasTimeEnd,
canvasWidth,
dragTime,
resizingEdge,
resizeTime
}) {
const itemStart =
isResizing && resizingEdge === 'left' ? resizeTime : itemTimeStart
const itemEnd =
isResizing && resizingEdge === 'right' ? resizeTime : itemTimeEnd
const itemTimeRange = itemEnd - itemStart
let newItemStart = isDragging ? dragTime : itemStart
const ratio =
1 / coordinateToTimeRatio(canvasTimeStart, canvasTimeEnd, canvasWidth)
// restrict startTime and endTime to be bounded by canvasTimeStart and canasTimeEnd
const effectiveStartTime = Math.max(itemStart, canvasTimeStart)
const effectiveEndTime = Math.min(itemEnd, canvasTimeEnd)
const itemWidth = (effectiveEndTime - effectiveStartTime) * ratio
const dimensions = {
left: Math.max(newItemStart - canvasTimeStart, 0) * ratio,
width: Math.max(itemWidth, 3),
collisionLeft: newItemStart,
collisionWidth: itemTimeRange
}
return dimensions
}
export function getGroupOrders(groups, keys) {
const { groupIdKey } = keys
let groupOrders = {}
for (let i = 0; i < groups.length; i++) {
groupOrders[_get(groups[i], groupIdKey)] = i
}
return groupOrders
}
export function getGroupedItems(items, groupOrders) {
var arr = []
// Initialize with empty arrays for each group
for (let i = 0; i < Object.keys(groupOrders).length; i++) {
arr[i] = []
}
// Populate groups
for (let i = 0; i < items.length; i++) {
if (items[i].dimensions.order !== undefined) {
arr[items[i].dimensions.order].push(items[i])
}
}
return arr
}
export function getVisibleItems(items, canvasTimeStart, canvasTimeEnd, keys) {
const { itemTimeStartKey, itemTimeEndKey } = keys
return items.filter(item => {
return (
_get(item, itemTimeStartKey) <= canvasTimeEnd &&
_get(item, itemTimeEndKey) >= canvasTimeStart
)
})
}
const EPSILON = 0.001
export function collision(a, b, lineHeight, collisionPadding = EPSILON) {
// 2d collisions detection - https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
var verticalMargin = 0
return (
a.collisionLeft + collisionPadding < b.collisionLeft + b.collisionWidth &&
a.collisionLeft + a.collisionWidth - collisionPadding > b.collisionLeft &&
a.top - verticalMargin + collisionPadding < b.top + b.height &&
a.top + a.height + verticalMargin - collisionPadding > b.top
)
}
export function stack(items, groupOrders, lineHeight, force, minGroupHeight) {
minGroupHeight = minGroupHeight || 0;
var i, iMax
var totalHeight = 0
var groupHeights = []
var groupTops = []
var groupedItems = getGroupedItems(items, groupOrders)
if (force) {
// reset top position of all items
for (i = 0, iMax = items.length; i < iMax; i++) {
items[i].dimensions.top = null
}
}
groupedItems.forEach(function(group) {
// calculate new, non-overlapping positions
groupTops.push(totalHeight)
var groupHeight = 0
var verticalMargin = 0
for (i = 0, iMax = group.length; i < iMax; i++) {
var item = group[i]
verticalMargin = lineHeight - item.dimensions.height
if (item.dimensions.stack && item.dimensions.top === null) {
item.dimensions.top = totalHeight + verticalMargin
groupHeight = Math.max(groupHeight, lineHeight)
do {
var collidingItem = null
for (var j = 0, jj = group.length; j < jj; j++) {
var other = group[j]
if (
other.dimensions.top !== null &&
other !== item &&
other.dimensions.stack &&
collision(item.dimensions, other.dimensions, lineHeight)
) {
collidingItem = other
break
} else {
// console.log('dont test', other.top !== null, other !== item, other.stack);
}
}
if (collidingItem != null) {
// There is a collision. Reposition the items above the colliding element
item.dimensions.top = collidingItem.dimensions.top + lineHeight
groupHeight = Math.max(
groupHeight,
item.dimensions.top + item.dimensions.height - totalHeight
)
}
} while (collidingItem)
}
}
let addedHeight = Math.max(groupHeight + verticalMargin, lineHeight, minGroupHeight);
groupHeights.push(addedHeight);
totalHeight += addedHeight;
})
return {
height: totalHeight,
groupHeights,
groupTops
}
}
export function forcestack(items, groupOrders, lineHeight, force, minGroupHeight) {
minGroupHeight = minGroupHeight || 0;
var i, iMax;
var totalHeight = 0;
var groupHeights = [];
var groupTops = [];
var groupedItems = getGroupedItems(items, groupOrders);
if (force) {
// reset top position of all items
for (i = 0, iMax = items.length; i < iMax; i++) {
items[i].dimensions.top = null
}
}
// stack all the items on top of eachother taking vertical margin into account.
groupedItems.forEach(function(group) {
groupTops.push(totalHeight);
var groupHeight = 0;
var verticalMargin = 0;
for (i = 0, iMax = group.length; i < iMax; i++) {
var item = group[i];
verticalMargin = lineHeight - item.dimensions.height;
item.dimensions.top = totalHeight + groupHeight + verticalMargin;
groupHeight += lineHeight;
}
let addedHeight = Math.max(groupHeight + verticalMargin, lineHeight, minGroupHeight);
groupHeights.push(addedHeight);
totalHeight += addedHeight;
});
return {
height: totalHeight,
groupHeights,
groupTops
}
}
export function nostack(items, groupOrders, lineHeight, force, minGroupHeight) {
minGroupHeight = minGroupHeight || 0;
var i, iMax
var totalHeight = 0
var groupHeights = []
var groupTops = []
var groupedItems = getGroupedItems(items, groupOrders)
if (force) {
// reset top position of all items
for (i = 0, iMax = items.length; i < iMax; i++) {
items[i].dimensions.top = null
}
}
groupedItems.forEach(function(group) {
// calculate new, non-overlapping positions
groupTops.push(totalHeight)
var groupHeight = 0
for (i = 0, iMax = group.length; i < iMax; i++) {
var item = group[i]
var verticalMargin = (lineHeight - item.dimensions.height) / 2
if (item.dimensions.top === null) {
item.dimensions.top = totalHeight + verticalMargin
groupHeight = Math.max(groupHeight, lineHeight)
}
}
let addedHeight = Math.max(groupHeight + verticalMargin, lineHeight, minGroupHeight);
groupHeights.push(addedHeight);
totalHeight += addedHeight;
})
return {
height: totalHeight,
groupHeights,
groupTops
}
}