diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 469555d4..32d0c86a 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -1,12 +1,8 @@ import { ComponentType } from 'react' -import { - createBrowserRouter, - Link, - RouteObject, - RouterProvider, -} from 'react-router-dom' +import { createBrowserRouter, Link, RouteObject, RouterProvider } from 'react-router-dom' import DemoMain from './demo-main' +import DemoCustom from './demo-custom' import DemoPerformance from './demo-performance' import DemoTreePGroups from './demo-tree-groups' import LinkedTimelines from './demo-linked-timelines' @@ -18,7 +14,7 @@ import CustomHeaders from './demo-headers' import CustomInfoLabel from './demo-custom-info-label' import ControledSelect from './demo-controlled-select' import ControlledScrolling from './demo-controlled-scrolling' -import ExternalDrop from "./demo-external" +import ExternalDrop from './demo-external' const loader = () => 'loading' const routes: RouteObject[] = [ { @@ -26,6 +22,11 @@ const routes: RouteObject[] = [ Component: withLayout(DemoMain), loader, }, + { + path: '/CustomTimescale', + Component: withLayout(DemoCustom), + loader, + }, { path: '/DemoPerformance', Component: withLayout(DemoPerformance), @@ -72,9 +73,9 @@ const routes: RouteObject[] = [ Component: withLayout(ControlledScrolling), }, { - path: "/ExternalDrop", + path: '/ExternalDrop', Component: withLayout(ExternalDrop), - } + }, ] function Menu() { diff --git a/demo/src/demo-custom/generate-fake-data.js b/demo/src/demo-custom/generate-fake-data.js new file mode 100644 index 00000000..105472a5 --- /dev/null +++ b/demo/src/demo-custom/generate-fake-data.js @@ -0,0 +1,41 @@ +import { faker } from '@faker-js/faker' +import randomColor from "randomcolor"; +import dayjs from 'dayjs'; + +export default function (groupCount = 30, itemCount = 1000, daysInPast = 30) { + let randomSeed = Math.floor(Math.random() * 1000); + let groups = []; + for (let i = 0; i < groupCount; i++) { + groups.push({ + id: `${i + 1}`, + title: faker.name.firstName(), + rightTitle: faker.name.lastName(), + bgColor: randomColor({ luminosity: "light", seed: randomSeed + i }) + }); + } + + let items = []; + for (let i = 0; i < itemCount; i++) { + const startDate = faker.date.recent({ days: daysInPast }).valueOf() + daysInPast * 0.3 * 86400 * 1000 + const startValue = Math.floor(dayjs(startDate).valueOf() / 10000000) * 10000000 + const endValue = dayjs(startDate + faker.number.int({ min: 2, max: 20 }) * 15 * 60 * 1000).valueOf() + + items.push({ + id: i + "", + group: faker.number.int({ min: 1, max: groups.length }) + '', + title: faker.hacker.phrase(), + start: startValue, + end: endValue, + // canMove: startValue > new Date().getTime(), + // canResize: 'both', + className: dayjs(startDate).day() === 6 || dayjs(startDate).day() === 0 ? "item-weekend" : "", + itemProps: { + "data-tip": faker.hacker.phrase() + } + }); + } + + items = items.sort((a, b) => b - a); + + return { groups, items }; +} diff --git a/demo/src/demo-custom/groups.json b/demo/src/demo-custom/groups.json new file mode 100644 index 00000000..3a18a49d --- /dev/null +++ b/demo/src/demo-custom/groups.json @@ -0,0 +1,70 @@ +[ + { + "id": 10, + "title": "U.S. President" + }, + { + "id": 20, + "title": "Windows" + }, + { + "id": 21, + "title": "Android" + }, + { + "id": 22, + "title": "RPG Maker" + }, + { + "id": 40, + "title": "Pokemon game" + }, + { + "id": 30, + "title": "Nintendo console" + }, + { + "id": 31, + "title": "Nintendo handheld" + }, + { + "id": 32, + "title": "Sony console" + }, + { + "id": 33, + "title": "Sony handheld" + }, + { + "id": 34, + "title": "Microsoft console" + }, + { + "id": 35, + "title": "Sega console" + }, + { + "id": 41, + "title": "Mario game" + }, + { + "id": 42, + "title": "Zelda game" + }, + { + "id": 49, + "title": "GTA game" + }, + { + "id": 50, + "title": "Best Picture" + }, + { + "id": 51, + "title": "Star Wars movies" + }, + { + "id": 52, + "title": "Marvel movies (MCU)" + } +] diff --git a/demo/src/demo-custom/index.jsx b/demo/src/demo-custom/index.jsx new file mode 100644 index 00000000..ee854ea6 --- /dev/null +++ b/demo/src/demo-custom/index.jsx @@ -0,0 +1,184 @@ +import React from 'react' +import { Component } from 'react' +import dayjs from 'dayjs' +import randomColor from "randomcolor"; + +import Timeline, { TimelineMarkers, TodayMarker, CustomMarker, CursorMarker } from '../../../src/index' + +import generateFakeData from './generate-fake-data' + +var keys = { + groupIdKey: "id", + groupTitleKey: "title", + groupRightTitleKey: "rightTitle", + itemIdKey: "id", + itemTitleKey: "title", + itemDivTitleKey: "title", + itemGroupKey: "group", + itemTimeStartKey: "start", + itemTimeEndKey: "end", + groupLabelKey: "title" +}; + +function customData() { + let randomSeed = Math.floor(Math.random() * 1000); + let groups = [ + { id: 1, title: "G1", rightTitle: "Right Title 1", bgColor: randomColor({ luminosity: "light", seed: randomSeed + 0 }) }, + { id: 2, title: "G2", rightTitle: "Right Title 2", bgColor: randomColor({ luminosity: "light", seed: randomSeed + 1 }) }, + { id: 3, title: "G3", rightTitle: "Right Title 3", bgColor: randomColor({ luminosity: "light", seed: randomSeed + 2 }) }, + ] + + let items = []; + items.push({ + id: "0", + group: "1", + title: "Hercules", + start: 0, + end: 16 * 10, + canChangeGroup: false, + className: "", + itemProps: { "data-tip": "GO" } + }); + items.push({ + id: "1", + group: "1", + title: "Ares", + start: 16 * 12, + end: 16 * 30, + canChangeGroup: false, + className: "", + itemProps: { "data-tip": "GO" } + }); + items.push({ + id: "2", + group: "2", + title: "Artemis", + start: 16 * 4, + end: 16 * 124, + canMove: true, + canChangeGroup: false, + className: "", + itemProps: { "data-tip": "GO" } + }); + items.push({ + id: "3", + group: "3", + title: "Athena", + start: 16 * 1, + end: 16 * 60, + canChangeGroup: false, + canResize: "both", + className: "", + itemProps: { "data-tip": "GO" } + }); + + return { groups, items }; +} + +export default class App extends Component { + constructor(props) { + super(props); + + // const { groups, items } = generateFakeData(3, 6, 1); + const { groups, items } = customData(); + // const defaultTimeStart = dayjs(items[0].start_time).startOf('day').toDate().valueOf() + const defaultTimeStart = 1 + // const defaultTimeEnd = dayjs(items[0].end_time).startOf('day').add(1, 'day').toDate().valueOf() + const defaultTimeEnd = 10000 + + this.state = { + groups, + items, + defaultTimeStart, + defaultTimeEnd + }; + } + + handleItemClick = (itemId, _, time) => { + console.log('Clicked: ' + itemId, dayjs(time).format()) + } + + handleItemSelect = (itemId, _, time) => { + console.log('Selected: ' + itemId, dayjs(time).format()) + } + + handleItemMove = (itemId, dragTime, newGroupOrder) => { + const { items, groups } = this.state; + + const group = groups[newGroupOrder]; + + this.setState({ + items: items.map(item => + item.id === itemId + ? Object.assign({}, item, { + start: dragTime, + end: dragTime + (item.end - item.start), + group: group.id + }) + : item + ) + }); + + console.log("Moved", itemId, dragTime, newGroupOrder); + }; + + handleItemResize = (itemId, time, edge) => { + const { items } = this.state; + + this.setState({ + items: items.map(item => + item.id === itemId + ? Object.assign({}, item, { + start: edge === "left" ? time : item.start, + end: edge === "left" ? item.end : time + }) + : item + ) + }); + + console.log("Resized", itemId, time, edge); + }; + + handleZoom = (timelineContext, unit, dividers) => { + console.log("Unit: ", unit, dividers[unit]); + } + + render() { + const { groups, items, defaultTimeStart, defaultTimeEnd } = this.state; + + return ( + { + if (visibleStartTime > defaultTimeStart) { + updateScrollCanvas(visibleStartTime, visibleEndTime); + } + } + } + onZoom={this.handleZoom} + > + + + + + + ); + } +} diff --git a/demo/src/demo-main/index.jsx b/demo/src/demo-main/index.jsx index 90475de1..3df7cb94 100644 --- a/demo/src/demo-main/index.jsx +++ b/demo/src/demo-main/index.jsx @@ -25,7 +25,7 @@ export default class App extends Component { constructor(props) { super(props) - const { groups, items } = generateFakeData() + const { groups, items } = generateFakeData(3, 4, 1) const defaultTimeStart = dayjs(items[0].start_time).startOf('day').toDate().valueOf() const defaultTimeEnd = dayjs(items[0].end_time).startOf('day').add(1, 'day').toDate().valueOf() @@ -80,10 +80,10 @@ export default class App extends Component { items: items.map((item) => item.id === itemId ? Object.assign({}, item, { - start: dragTime, - end: dragTime + (item.end - item.start), - group: group.id, - }) + start: dragTime, + end: dragTime + (item.end - item.start), + group: group.id, + }) : item, ), }) @@ -98,9 +98,9 @@ export default class App extends Component { items: items.map((item) => item.id === itemId ? Object.assign({}, item, { - start: edge === 'left' ? time : item.start, - end: edge === 'left' ? item.end : time, - }) + start: edge === 'left' ? time : item.start, + end: edge === 'left' ? item.end : time, + }) : item, ), }) @@ -144,8 +144,8 @@ export default class App extends Component { keys={keys} sidebarWidth={150} sidebarContent={
Above The Left
} - canMove - canResize="right" + canMove={true} + canResize={"both"} canSelect itemsSorted itemTouchSendsClick={false} @@ -164,8 +164,8 @@ export default class App extends Component { onZoom={this.handleZoom} moveResizeValidator={this.moveResizeValidator} buffer={3} - minZoom={60 * 60 * 1000} // 1 year - maxZoom={365*24 * 86400 * 1000 * 20} // 20 years + minZoom={60 * 60 * 1000} // 1 hour + maxZoom={365 * 24 * 86400 * 1000 * 20} // 20 years defaultTimeStart={defaultTimeStart} defaultTimeEnd={defaultTimeEnd} > @@ -178,7 +178,7 @@ export default class App extends Component { return
}} - + ) diff --git a/demo/src/generate-fake-data.ts b/demo/src/generate-fake-data.ts index fde35871..5d2486a5 100644 --- a/demo/src/generate-fake-data.ts +++ b/demo/src/generate-fake-data.ts @@ -1,11 +1,11 @@ -import {faker} from '@faker-js/faker' +import { faker } from '@faker-js/faker' import randomColor from 'randomcolor' import dayjs from 'dayjs' -import {HTMLProps} from "react"; -import {TimelineGroupBase, TimelineItemBase} from "react-calendar-timeline"; +import { HTMLProps } from 'react' +import { TimelineGroupBase, TimelineItemBase } from 'react-calendar-timeline' -export type FakeGroup = TimelineGroupBase & { - id:string; +export type FakeGroup = TimelineGroupBase & { + id: string label: string bgColor: string } @@ -18,56 +18,48 @@ export type FakeDataItem = TimelineItemBase & { canMove?: boolean canResize?: false | 'left' | 'right' | 'both' className?: string - bgColor?:string, - selectedBgColor?:string - color?:string + bgColor?: string + selectedBgColor?: string + color?: string itemProps?: HTMLProps } export default function (groupCount = 30, itemCount = 1000, daysInPast = 30) { let randomSeed = Math.floor(Math.random() * 1000) - let groups :FakeGroup[]= [] + let groups: FakeGroup[] = [] for (let i = 0; i < groupCount; i++) { groups.push({ - id:`${i + 1}`, + id: `${i + 1}`, title: faker.person.firstName(), rightTitle: faker.person.lastName(), label: `Label ${faker.person.firstName()}`, - bgColor: randomColor({luminosity: 'light', seed: randomSeed + i}), + bgColor: randomColor({ luminosity: 'light', seed: randomSeed + i }), }) } - let items:FakeDataItem[] = [] + let items: FakeDataItem[] = [] for (let i = 0; i < itemCount; i++) { - const startDate = - faker.date.recent({days: daysInPast}).valueOf() + - daysInPast * 0.3 * 86400 * 1000 - const startValue = - Math.floor(dayjs(startDate).valueOf() / 10000000) * 10000000 - const endValue = dayjs( - startDate + faker.number.int({min: 2, max: 20}) * 15 * 60 * 1000, - ).valueOf() + const startDate = faker.date.recent({ days: daysInPast }).valueOf() + daysInPast * 0.3 * 86400 * 1000 + const startValue = Math.floor(dayjs(startDate).valueOf() / 10000000) * 10000000 + const endValue = dayjs(startDate + faker.number.int({ min: 2, max: 20 }) * 15 * 60 * 1000).valueOf() - const itemProps: HTMLProps = { style: {} } - itemProps['data-tip']= faker.hacker.phrase(); + const itemProps: HTMLProps = { style: {} } + itemProps['data-tip'] = faker.hacker.phrase() items.push({ id: i, - group: faker.number.int({min: 1, max: groups.length}) + '', + group: faker.number.int({ min: 1, max: groups.length }) + '', title: faker.hacker.phrase(), start_time: startValue, end_time: endValue, - canMove: startValue > new Date().getTime(), - canResize: - startValue > new Date().getTime() - ? endValue > new Date().getTime() - ? 'both' - : 'left' - : endValue > new Date().getTime() - ? 'right' - : false, - className: - dayjs(startDate).day() === 6 || dayjs(startDate).day() === 0 - ? 'item-weekend' - : '', + // canMove: startValue > new Date().getTime(), + // canResize: + // startValue > new Date().getTime() + // ? endValue > new Date().getTime() + // ? 'both' + // : 'left' + // : endValue > new Date().getTime() + // ? 'right' + // : false, + className: dayjs(startDate).day() === 6 || dayjs(startDate).day() === 0 ? 'item-weekend' : '', bgColor: randomColor({ luminosity: 'light', seed: randomSeed + i, @@ -80,13 +72,12 @@ export default function (groupCount = 30, itemCount = 1000, daysInPast = 30) { format: 'rgba', alpha: 1, }), - color: randomColor({luminosity: 'dark', seed: randomSeed + i}), + color: randomColor({ luminosity: 'dark', seed: randomSeed + i }), // itemProps:itemProps - }) } items = items.sort((a, b) => b.start_time - a.start_time) - return {groups, items} + return { groups, items } } diff --git a/src/lib/Timeline.tsx b/src/lib/Timeline.tsx index 5d15c787..d09fa504 100644 --- a/src/lib/Timeline.tsx +++ b/src/lib/Timeline.tsx @@ -1,9 +1,9 @@ -import React, {Component, MouseEvent} from 'react' -import {getSumOffset, getSumScroll} from './utility/dom-helpers' -import Items, {CanResize} from './items/Items' +import React, { Component, MouseEvent } from 'react' +import { getSumOffset, getSumScroll } from './utility/dom-helpers' +import Items, { CanResize } from './items/Items' import Sidebar from './layout/Sidebar' import Columns from './columns/Columns' -import GroupRows, {RowClickEvent} from './row/GroupRows' +import GroupRows, { RowClickEvent } from './row/GroupRows' import ScrollElement from './scroll/ScrollElement' import MarkerCanvas from './markers/MarkerCanvas' import windowResizeDetector from '../resize-detector/window' @@ -13,17 +13,18 @@ import { calculateScrollCanvas, getCanvasBoundariesFromVisibleTime, getCanvasWidth, - stackTimelineItems, coordinateToTimeRatio, + stackTimelineItems, + coordinateToTimeRatio, + defaultTimeDividers, } from './utility/calendar' -import {_get} from './utility/generic' -import {defaultKeys, defaultTimeSteps} from './default-config' -import {TimelineStateProvider} from './timeline/TimelineStateContext' -import {TimelineMarkersProvider} from './markers/TimelineMarkersContext' -import {TimelineHeadersProvider} from './headers/HeadersContext' +import { _get } from './utility/generic' +import { defaultKeys, defaultTimeSteps } from './default-config' +import { TimelineStateProvider } from './timeline/TimelineStateContext' +import { TimelineMarkersProvider } from './markers/TimelineMarkersContext' +import { TimelineHeadersProvider } from './headers/HeadersContext' import TimelineHeaders from './headers/TimelineHeaders' -import {DateHeader} from './headers/DateHeader' +import { DateHeader } from './headers/DateHeader' import { - dateType, ElementWithSecret, Id, OnItemDragObjectMove, @@ -37,7 +38,7 @@ import { Unit, } from './types/main' import { ItemDimension } from './types/dimension' -import dayjs, { Dayjs } from 'dayjs' +import dayjs from 'dayjs' import { ItemProps, ResizeEdge } from './items/Item' // import './Timeline.scss' import localizedFormat from 'dayjs/plugin/localizedFormat' @@ -46,17 +47,17 @@ dayjs.extend(localizedFormat) export interface ReactCalendarTimelineRef { // Add any methods or properties you want to expose - getBoundingClientRect(): DOMRect; + getBoundingClientRect(): DOMRect - calculateDropCoordinatesToTimeAndGroup(x: number, y: number): { time: number, groupIndex: number }; + calculateDropCoordinatesToTimeAndGroup(x: number, y: number): { time: number; groupIndex: number } } export type OnTimeChange = ( visibleTimeStart: number, visibleTimeEnd: number, updateScrollCanvas: ( - start: dateType, - end: dateType, + start: number, + end: number, forceUpdateDimensions?: boolean, items?: CustomItem[], groups?: CustomGroup[], @@ -74,8 +75,8 @@ export type ReactCalendarTimelineProps< keys: TimelineKeys defaultTimeStart: number defaultTimeEnd: number - visibleTimeStart?: dateType - visibleTimeEnd?: dateType + visibleTimeStart?: number + visibleTimeEnd?: number selected?: Id[] | undefined sidebarWidth: number sidebarContent?: React.ReactNode | undefined @@ -109,7 +110,7 @@ export type ReactCalendarTimelineProps< onCanvasClick?(groupId: Id, time: number, e: React.SyntheticEvent): void onCanvasDoubleClick?(groupId: Id, time: number, e: React.SyntheticEvent): void onCanvasContextMenu?(groupId: Id, time: number, e: React.SyntheticEvent): void - onZoom?(timelineContext: TimelineContext, unit: Unit): void + onZoom?(timelineContext: TimelineContext, unit: Unit, dividers: Record): void moveResizeValidator?: ItemProps['moveResizeValidator'] onTimeChange?: OnTimeChange onBoundsChange?(canvasTimeStart: number, canvasTimeEnd: number): any @@ -138,8 +139,8 @@ export type ReactCalendarTimelineState< width: number visibleTimeStart: number visibleTimeEnd: number - canvasTimeStart: dateType - canvasTimeEnd: dateType + canvasTimeStart: number + canvasTimeEnd: number selectedItem: Id | null dragTime: number | null dragGroupTitle: string | null @@ -224,9 +225,9 @@ export default class ReactCalendarTimeline< visibleTimeStart: null, visibleTimeEnd: null, onTimeChange: function ( - visibleTimeStart: dateType, - visibleTimeEnd: dateType, - updateScrollCanvas: (visibleTimeStart: dateType, visibleTimeEnd: dateType) => void, + visibleTimeStart: number, + visibleTimeEnd: number, + updateScrollCanvas: (visibleTimeStart: number, visibleTimeEnd: number) => void, ) { updateScrollCanvas(visibleTimeStart, visibleTimeEnd) }, @@ -408,7 +409,7 @@ export default class ReactCalendarTimeline< // are we changing zoom? Report it! if (this.props.onZoom && newZoom !== oldZoom) { - this.props.onZoom(this.getTimelineContext(), this.getTimelineUnit()) + this.props.onZoom(this.getTimelineContext(), this.getTimelineUnit(), defaultTimeDividers) } // The bounds have changed? Report it! @@ -494,8 +495,8 @@ export default class ReactCalendarTimeline< // called when the visible time changes updateScrollCanvas = ( - visibleTimeStart: dateType, - visibleTimeEnd: dateType, + visibleTimeStart: number, + visibleTimeEnd: number, forceUpdateDimensions: boolean = false, items = this.props.items, groups = this.props.groups, @@ -532,9 +533,9 @@ export default class ReactCalendarTimeline< } } - showPeriod = (from: Dayjs, to: Dayjs) => { - const visibleTimeStart = from.valueOf() - const visibleTimeEnd = to.valueOf() + showPeriod = (from: number, to: number) => { + const visibleTimeStart = from + const visibleTimeEnd = to const zoom = visibleTimeEnd - visibleTimeStart // can't zoom in more than to show one hour @@ -966,10 +967,9 @@ export default class ReactCalendarTimeline< const offset = getSumOffset(this.scrollComponent!).offsetLeft const scrolls = getSumScroll(this.scrollComponent!) - const dragTime = (x - offset + scrolls.scrollLeft) * ratio + this.state.canvasTimeStart; - let groupDelta = 0; + const dragTime = (x - offset + scrolls.scrollLeft) * ratio + this.state.canvasTimeStart + let groupDelta = 0 for (const key of this.state.groupTops) { - if (y > Number(key)) { groupDelta = this.state.groupTops.indexOf(key) } else { @@ -977,15 +977,13 @@ export default class ReactCalendarTimeline< } } - if (!this.props.dragSnap) return {time: dragTime, groupIndex: groupDelta}; + if (!this.props.dragSnap) return { time: dragTime, groupIndex: groupDelta } - const consideredOffset = dayjs().utcOffset() * 60 * 1000; + const consideredOffset = dayjs().utcOffset() * 60 * 1000 return { - time: Math.round(dragTime / this.props.dragSnap) * this.props.dragSnap - (consideredOffset % this.props.dragSnap) - , groupIndex: groupDelta + time: Math.round(dragTime / this.props.dragSnap) * this.props.dragSnap - (consideredOffset % this.props.dragSnap), + groupIndex: groupDelta, } - - } render() { diff --git a/src/lib/columns/Columns.tsx b/src/lib/columns/Columns.tsx index 2159e1d9..6b5a90fb 100644 --- a/src/lib/columns/Columns.tsx +++ b/src/lib/columns/Columns.tsx @@ -1,6 +1,7 @@ import React, { Component, FC } from 'react' -import { iterateTimes } from '../utility/calendar' +import { isCustomUnit, iterateTimes } from '../utility/calendar' +import dayjs, { UnitType } from 'dayjs' import { TimelineStateConsumer } from '../timeline/TimelineStateContext' import { TimelineTimeSteps } from '../types/main' @@ -48,15 +49,23 @@ class Columns extends Component { const lines: React.JSX.Element[] = [] - iterateTimes(canvasTimeStart, canvasTimeEnd, minUnit, timeSteps, (time, nextTime) => { - const minUnitValue = time.get(minUnit === 'day' ? 'date' : minUnit) - const firstOfType = minUnitValue === (minUnit === 'day' ? 1 : 0) + iterateTimes(canvasTimeStart, canvasTimeEnd, minUnit, timeSteps, (time: number, nextTime: number) => { + // TODO: bypasses first line for custom timeline steps, missing slight css perk + let minUnitValue = 0 + let firstOfType = false + let originalCheck = false + if (!isCustomUnit(minUnit)) { + const originalMinUnit = minUnit as UnitType + minUnitValue = dayjs(time).get(originalMinUnit === 'day' ? 'date' : originalMinUnit) + firstOfType = minUnitValue === (originalMinUnit === 'day' ? 1 : 0) + originalCheck = originalMinUnit === 'day' || originalMinUnit === 'hour' || originalMinUnit === 'minute' + } let classNamesForTime: string[] = [] if (verticalLineClassNamesForTime) { classNamesForTime = verticalLineClassNamesForTime( - time.unix() * 1000, // turn into ms, which is what verticalLineClassNamesForTime expects - nextTime.unix() * 1000 - 1, + time * 1000, // turn into ms, which is what verticalLineClassNamesForTime expects + nextTime * 1000 - 1, ) } @@ -64,14 +73,14 @@ class Columns extends Component { const classNames = 'rct-vl' + (firstOfType ? ' rct-vl-first' : '') + - (minUnit === 'day' || minUnit === 'hour' || minUnit === 'minute' ? ` rct-day-${time.day()} ` : ' ') + + (originalCheck ? ` rct-day-${dayjs(time).day()} ` : ' ') + classNamesForTime.join(' ') - const left = getLeftOffsetFromDate(time.valueOf()) - const right = getLeftOffsetFromDate(nextTime.valueOf()) + const left = getLeftOffsetFromDate(time) + const right = getLeftOffsetFromDate(nextTime) lines.push(
= { + +const customDefaultHeaderFormats: Record = { + blocks1: { + long: '8 nsec', + mediumLong: '8 nsec', + medium: '8ns', + short: '8ns', + }, + blocks2: { + long: '16 nsec', + mediumLong: '16 nsec', + medium: '16', + short: '16', + }, + blocks3: { + long: '0.8 usec', + mediumLong: '0.8 usec', + medium: '.8us', + short: '.8us', + }, + blocks4: { + long: '20 usec', + mediumLong: '20 usec', + medium: '20', + short: '20', + }, + blocks5: { + long: '1 msec', + mediumLong: '1 msec', + medium: '1ms', + short: '1ms', + }, + blocks6: { + long: '25 msec', + mediumLong: '25 msec', + medium: '25', + short: '25', + }, + blocks7: { + long: '1 sec', + mediumLong: '1 sec', + medium: '1s', + short: '1s', + }, + blocks8: { + long: '1 min', + mediumLong: '1 min', + medium: '1m', + short: '1m', + }, + blocks9: { + long: '1 hour', + mediumLong: '1 hour', + medium: '1h', + short: '1h', + }, +} + +const originalDefaultHeaderFormats: Record = { year: { long: 'YYYY', mediumLong: 'YYYY', @@ -70,3 +142,8 @@ export const defaultHeaderFormats: Record = { short: 'ss', }, } + +export const defaultHeaderFormats: Record = { + ...customDefaultHeaderFormats, + ...originalDefaultHeaderFormats, +} diff --git a/src/lib/headers/CustomDateHeader.tsx b/src/lib/headers/CustomDateHeader.tsx index 93a40e36..ec6db860 100644 --- a/src/lib/headers/CustomDateHeader.tsx +++ b/src/lib/headers/CustomDateHeader.tsx @@ -1,4 +1,4 @@ -import React, {HTMLProps} from 'react' +import React, { HTMLProps } from 'react' import Interval from './Interval' import { Interval as IntervalType, IntervalRenderer } from '../types/main' import { Dayjs } from 'dayjs' @@ -12,7 +12,7 @@ export interface CustomDateHeaderProps { } getRootProps: (props?: { style?: React.CSSProperties }) => HTMLProps getIntervalProps: GetIntervalPropsType - showPeriod: (start: Dayjs, end: Dayjs) => void + showPeriod: (start: number, end: number) => void data: { style: React.CSSProperties intervalRenderer: (props: IntervalRenderer) => React.ReactNode diff --git a/src/lib/headers/CustomHeader.tsx b/src/lib/headers/CustomHeader.tsx index 7d9c9813..4a293842 100644 --- a/src/lib/headers/CustomHeader.tsx +++ b/src/lib/headers/CustomHeader.tsx @@ -3,7 +3,7 @@ import { useTimelineHeadersContext } from './HeadersContext' import { useTimelineState } from '../timeline/TimelineStateContext' import { iterateTimes } from '../utility/calendar' import { Interval, TimelineTimeSteps } from '../types/main' -import { Dayjs } from 'dayjs' +import dayjs from 'dayjs' import { CustomDateHeaderProps } from './CustomDateHeader' import isEqual from 'lodash/isEqual' import { GetIntervalPropsType } from './types' @@ -17,7 +17,7 @@ export type CustomHeaderProps = { canvasTimeStart: number canvasTimeEnd: number canvasWidth: number - showPeriod: (start: Dayjs, end: Dayjs) => void + showPeriod: (start: number, end: number) => void headerData?: Data getLeftOffsetFromDate: (date: any) => number height: number @@ -107,13 +107,13 @@ class CustomHeader extends React.Component, State> getLeftOffsetFromDate, }) => { const intervals: Interval[] = [] - iterateTimes(canvasTimeStart, canvasTimeEnd, unit, timeSteps, (startTime, endTime) => { - const left = getLeftOffsetFromDate(startTime.valueOf()) - const right = getLeftOffsetFromDate(endTime.valueOf()) + iterateTimes(canvasTimeStart, canvasTimeEnd, unit, timeSteps, (startTime: number, endTime: number) => { + const left = getLeftOffsetFromDate(startTime) + const right = getLeftOffsetFromDate(endTime) const width = right - left intervals.push({ - startTime, - endTime, + startTime: dayjs(startTime), + endTime: dayjs(endTime), labelWidth: width, left, }) diff --git a/src/lib/headers/DateHeader.tsx b/src/lib/headers/DateHeader.tsx index 21ecb894..5066f832 100644 --- a/src/lib/headers/DateHeader.tsx +++ b/src/lib/headers/DateHeader.tsx @@ -1,11 +1,11 @@ import React, { CSSProperties, ReactNode } from 'react' import { TimelineStateConsumer } from '../timeline/TimelineStateContext' import CustomHeader from './CustomHeader' -import { getNextUnit, SelectUnits } from '../utility/calendar' +import { getNextUnit, isCustomUnit, SelectUnits } from '../utility/calendar' import { defaultHeaderFormats } from '../default-config' import memoize from 'memoize-one' import { CustomDateHeader } from './CustomDateHeader' -import { IntervalRenderer, SidebarHeaderChildrenFnProps, TimelineTimeSteps } from '../types/main' +import { CustomUnit, IntervalRenderer, SidebarHeaderChildrenFnProps, TimelineTimeSteps } from '../types/main' import { Dayjs, UnitType } from 'dayjs' type GetHeaderData = ( @@ -13,14 +13,14 @@ type GetHeaderData = ( style: React.CSSProperties, className: string | undefined, getLabelFormat: (interval: [Dayjs, Dayjs], unit: keyof typeof defaultHeaderFormats, labelWidth: number) => string, - unitProp: UnitType | 'primaryHeader' | undefined, + unitProp: UnitType | 'primaryHeader' | CustomUnit | undefined, headerData: Data | undefined, ) => { intervalRenderer?: IntervalRenderer style: React.CSSProperties className: string getLabelFormat: (interval: [Dayjs, Dayjs], unit: keyof typeof defaultHeaderFormats, labelWidth: number) => string - unitProp: UnitType | 'primaryHeader' | undefined + unitProp: UnitType | 'primaryHeader' | CustomUnit | undefined headerData: Data } export interface DateHeaderProps { @@ -28,10 +28,7 @@ export interface DateHeaderProps { className?: string | undefined unit?: keyof TimelineTimeSteps | 'primaryHeader' | undefined timelineUnit: SelectUnits - labelFormat?: - | string - | FormatLabelFunction - | undefined + labelFormat?: string | FormatLabelFunction | undefined intervalRenderer?: (props: IntervalRenderer) => ReactNode headerData?: Data | undefined children?: ((props: SidebarHeaderChildrenFnProps) => ReactNode) | undefined @@ -144,15 +141,15 @@ type FormatLabelFunction = ( timeRange: [Dayjs, Dayjs], unit: keyof typeof defaultHeaderFormats, labelWidth?: number, - formatOptions?: typeof defaultHeaderFormats -) => string; + formatOptions?: typeof defaultHeaderFormats, +) => string -const formatLabel:FormatLabelFunction = ( +const formatLabel: FormatLabelFunction = ( [timeStart], unit, - labelWidth =150, + labelWidth = 150, formatOptions = defaultHeaderFormats, -) =>{ +) => { let format if (labelWidth >= 150) { format = formatOptions[unit]['long'] @@ -163,6 +160,10 @@ const formatLabel:FormatLabelFunction = ( } else { format = formatOptions[unit]['short'] } + + if (unit in defaultHeaderFormats && isCustomUnit(unit as keyof TimelineTimeSteps)) { + return format + } return timeStart.format(format) } diff --git a/src/lib/headers/Interval.tsx b/src/lib/headers/Interval.tsx index fdd12040..ecdefdad 100644 --- a/src/lib/headers/Interval.tsx +++ b/src/lib/headers/Interval.tsx @@ -1,15 +1,15 @@ import React, { HTMLAttributes, ReactNode } from 'react' -import { getNextUnit, SelectUnits } from '../utility/calendar' +import { getNextUnit, SelectUnits, getPrevFactor, isCustomUnit } from '../utility/calendar' import { composeEvents } from '../utility/events' -import { Dayjs } from 'dayjs' -import { IntervalRenderer, Interval as IntervalType, GetIntervalProps } from '../types/main' +import { IntervalRenderer, Interval as IntervalType, GetIntervalProps, CustomUnit } from '../types/main' import { GetIntervalPropsType } from './types' +import { UnitType } from 'dayjs' export type IntervalProps = { intervalRenderer: (p: IntervalRenderer) => ReactNode unit: SelectUnits interval: IntervalType - showPeriod: (startTime: Dayjs, endTime: Dayjs) => void + showPeriod: (startTime: number, endTime: number) => void intervalText: string primaryHeader: boolean getIntervalProps: GetIntervalPropsType @@ -21,12 +21,21 @@ class Interval extends React.PureComponent> { onIntervalClick = () => { const { primaryHeader, interval, unit, showPeriod } = this.props if (primaryHeader) { - const nextUnit = getNextUnit(unit) - const newStartTime = interval.startTime.clone().startOf(nextUnit) - const newEndTime = interval.startTime.clone().endOf(nextUnit) - showPeriod(newStartTime, newEndTime) + if (isCustomUnit(unit)) { + const nextUnit = getNextUnit(unit) as CustomUnit + const startTimeValue = interval.startTime.valueOf() + const blockSize = getPrevFactor(nextUnit) + const newStartTimeValue = Math.floor(startTimeValue / blockSize) * blockSize + const newEndTimeValue = Math.floor(startTimeValue / blockSize) * blockSize + blockSize - 1 + showPeriod(newStartTimeValue, newEndTimeValue) + } else { + const nextUnit = getNextUnit(unit) as UnitType + const newStartTime = interval.startTime.clone().startOf(nextUnit) + const newEndTime = interval.startTime.clone().endOf(nextUnit) + showPeriod(newStartTime.valueOf(), newEndTime.valueOf()) + } } else { - showPeriod(interval.startTime, interval.endTime) + showPeriod(interval.startTime.valueOf(), interval.endTime.valueOf()) } } diff --git a/src/lib/timeline/TimelineStateContext.tsx b/src/lib/timeline/TimelineStateContext.tsx index 68d0f285..b55a6141 100644 --- a/src/lib/timeline/TimelineStateContext.tsx +++ b/src/lib/timeline/TimelineStateContext.tsx @@ -2,7 +2,6 @@ import React, { PropsWithChildren, useContext } from 'react' import { calculateXPositionForTime, calculateTimeForXPosition, SelectUnits } from '../utility/calendar' import { TimelineContext as TimelineContextValue } from '../types/main' -import { Dayjs } from 'dayjs' /* this context will hold all information regarding timeline state: 1. timeline width @@ -40,7 +39,7 @@ type TimelineStartProps = { canvasTimeStart: number canvasTimeEnd: number canvasWidth: number - showPeriod: (from: Dayjs, to: Dayjs) => void + showPeriod: (from: number, to: number) => void timelineUnit: SelectUnits timelineWidth: number } @@ -48,7 +47,7 @@ export type TimelineContextType = { getTimelineState: () => TimelineContextValue getLeftOffsetFromDate: (date: number) => number getDateFromLeftOffsetPosition: (leftOffset: number) => number - showPeriod: (from: Dayjs, to: Dayjs) => void + showPeriod: (from: number, to: number) => void } type TimelineState = { diff --git a/src/lib/types/main.ts b/src/lib/types/main.ts index 80818c1b..67da5fe2 100644 --- a/src/lib/types/main.ts +++ b/src/lib/types/main.ts @@ -123,7 +123,19 @@ export interface TimelineKeys { export type dateType = number //| undefined; -export interface TimelineTimeSteps { +interface CustomTimelineTimeSteps { + blocks1: number + blocks2: number + blocks3: number + blocks4: number + blocks5: number + blocks6: number + blocks7: number + blocks8: number + blocks9: number +} + +interface OriginalTimelineTimeSteps { second: number minute: number hour: number @@ -132,6 +144,8 @@ export interface TimelineTimeSteps { year: number } +export interface TimelineTimeSteps extends CustomTimelineTimeSteps, OriginalTimelineTimeSteps {} + export class TimelineMarkers extends Component {} export interface CustomMarkerChildrenProps { @@ -182,7 +196,18 @@ export interface SidebarHeaderProps { export class SidebarHeader extends Component> {} -export type Unit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'isoWeek' | 'month' | 'year' +export type CustomUnit = + | 'blocks1' + | 'blocks2' + | 'blocks3' + | 'blocks4' + | 'blocks5' + | 'blocks6' + | 'blocks7' + | 'blocks8' + | 'blocks9' +type OriginalUnit = 'second' | 'minute' | 'hour' | 'day' | 'isoWeek' | 'month' | 'year' +export type Unit = CustomUnit | OriginalUnit export interface IntervalContext { interval: Interval diff --git a/src/lib/utility/calendar.tsx b/src/lib/utility/calendar.tsx index c922c10f..34bf489f 100644 --- a/src/lib/utility/calendar.tsx +++ b/src/lib/utility/calendar.tsx @@ -1,8 +1,9 @@ /* eslint-disable no-var */ -import dayjs, { Dayjs } from 'dayjs' +import dayjs, { UnitType } from 'dayjs' import { _get } from './generic' import { Dimension, ItemDimension } from '../types/dimension' import { + CustomUnit, GroupedItem, GroupOrders, GroupStack, @@ -68,27 +69,76 @@ export function calculateTimeForXPosition( return timeFromCanvasTimeStart + canvasTimeStart } +const customPrevUnits: Record = { + blocks9: 'blocks8', + blocks8: 'blocks7', + blocks7: 'blocks6', + blocks6: 'blocks5', + blocks5: 'blocks4', + blocks4: 'blocks3', + blocks3: 'blocks2', + blocks2: 'blocks1', + blocks1: 'blocks1', +} + +export function getPrevFactor(unit: CustomUnit): number { + let currentUnit = unit + let factor = 1 + while (currentUnit !== customPrevUnits[currentUnit]) { + factor *= defaultTimeDividers[currentUnit] + currentUnit = customPrevUnits[currentUnit] + } + return factor * defaultTimeDividers[currentUnit] +} + +export function isCustomUnit(unit: keyof TimelineTimeSteps): boolean { + const customTypes: CustomUnit[] = [ + 'blocks1', + 'blocks2', + 'blocks3', + 'blocks4', + 'blocks5', + 'blocks6', + 'blocks7', + 'blocks8', + 'blocks9', + ] + return customTypes.includes(unit as CustomUnit) +} + export function iterateTimes( start: number, end: number, unit: keyof TimelineTimeSteps, timeSteps: TimelineTimeSteps, - callback: (time: Dayjs, nextTime: Dayjs) => void, + callback: (time: number, nextTime: number) => void, ) { - let time = dayjs(start).startOf(unit) + if (isCustomUnit(unit)) { + const blockTime = getPrevFactor(unit as CustomUnit) + let time = Math.floor(start / blockTime) * blockTime + + while (time < end) { + const nextTime = Math.floor((time + blockTime) / blockTime) * blockTime + callback(time, nextTime) + time = nextTime + } + } else { + const dayjsUnit = unit as UnitType + let time = dayjs(start).startOf(dayjsUnit) - if (timeSteps[unit] && timeSteps[unit] > 1) { - const value = time.get(unit) - time = time.set(unit, value - (value % timeSteps[unit])) - } + if (timeSteps[unit] && timeSteps[unit] > 1) { + const value = time.get(dayjsUnit) + time = time.set(dayjsUnit, value - (value % timeSteps[unit])) + } - while (time.valueOf() < end) { - const nextTime = dayjs(time) - .add(timeSteps[unit] || 1, unit as dayjs.ManipulateType) - .startOf(unit) + while (time.valueOf() < end) { + const nextTime = dayjs(time) + .add(timeSteps[unit] || 1, unit as dayjs.ManipulateType) + .startOf(dayjsUnit) - callback(time, nextTime) - time = nextTime + callback(time.valueOf(), nextTime.valueOf()) + time = nextTime + } } } @@ -109,19 +159,31 @@ export function iterateTimes( // i think this is the distance between cell lines export const minCellWidth = 17 +export const defaultTimeDividers: Record = { + blocks1: 8, + blocks2: 2, // 16 + blocks3: 50, // 800 - 0.8 usec + blocks4: 25, // 20 usec + blocks5: 50, // 1 msec + blocks6: 25, // 25 msec + blocks7: 50, // 1 sec + blocks8: 60, // 1 min + blocks9: 60, // 1 hour + // original time dividers + second: 1000, + minute: 60, + hour: 60, + day: 24, + month: 30, + year: 12, +} + export function getMinUnit(zoom: number, width: number, timeSteps: TimelineTimeSteps) { // for supporting weeks, its important to remember that each of these // units has a natural 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 - const timeDividers: Record = { - second: 1000, - minute: 60, - hour: 60, - day: 24, - month: 30, - year: 12, - } + const timeDividers: Record = defaultTimeDividers let minUnit: keyof TimelineTimeSteps = 'year' @@ -156,13 +218,39 @@ export function getMinUnit(zoom: number, width: number, timeSteps: TimelineTimeS } }) - return minUnit + return minUnit as SelectUnits } -export type SelectUnits = 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' -export type SelectUnitsRes = Exclude +type CustomSelectUnits = + | 'blocks1' + | 'blocks2' + | 'blocks3' + | 'blocks4' + | 'blocks5' + | 'blocks6' + | 'blocks7' + | 'blocks8' + | 'blocks9' +type OriginalSelectUnits = 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' +export type SelectUnits = CustomSelectUnits | OriginalSelectUnits + +export type SelectUnitsRes = Exclude + +const customNextUnits: Record = { + blocks1: 'blocks2', + blocks2: 'blocks3', + blocks3: 'blocks4', + blocks4: 'blocks5', + blocks5: 'blocks6', + blocks6: 'blocks7', + blocks7: 'blocks8', + blocks8: 'blocks9', + // Purely for continuity -- meaningless. The zoom will be bounded before reaching the original unit. + // TODO: check if it can be removed safely + blocks9: 'second', +} -export const NEXT_UNITS: Record = { +const originalNextUnits: Record = { second: 'minute', minute: 'hour', hour: 'day', @@ -171,6 +259,11 @@ export const NEXT_UNITS: Record = { year: 'year', } +export const NEXT_UNITS: Record = { + ...(customNextUnits as Record), + ...(originalNextUnits as Record), +} + export function getNextUnit(unit: SelectUnits): SelectUnitsRes { if (!NEXT_UNITS[unit]) { throw new Error(`unit ${unit} is not acceptable`) @@ -305,8 +398,8 @@ export function getVisibleItems< const { itemTimeStartKey, itemTimeEndKey } = keys return items.filter((item) => { - const afterStart = dayjs(_get(item, itemTimeStartKey)).valueOf() <= canvasTimeEnd - const beforeEnd = dayjs(_get(item, itemTimeEndKey)).valueOf() >= canvasTimeStart + const afterStart = _get(item, itemTimeStartKey) <= canvasTimeEnd + const beforeEnd = _get(item, itemTimeEndKey) >= canvasTimeStart return afterStart && beforeEnd }) @@ -673,7 +766,7 @@ export function getItemWithInteractions< export function getCanvasBoundariesFromVisibleTime(visibleTimeStart: number, visibleTimeEnd: number, buffer: number) { const zoom = visibleTimeEnd - visibleTimeStart // buffer - 1 (1 is visible area) divided by 2 (2 is the buffer split on the right and left of the timeline) - const canvasTimeStart = visibleTimeStart - zoom * (buffer - 1) / 2 + const canvasTimeStart = visibleTimeStart - (zoom * (buffer - 1)) / 2 const canvasTimeEnd = canvasTimeStart + zoom * buffer return [canvasTimeStart, canvasTimeEnd] }