From d7712a5395a844ed722584bff6e9645dbf67b4fb Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Tue, 1 Apr 2025 14:44:43 +0200 Subject: [PATCH 01/10] Make a simple demo for further test use --- demo/src/App.tsx | 107 +++++++++++---------- demo/src/demo-custom/generate-fake-data.js | 41 ++++++++ demo/src/demo-custom/groups.json | 70 ++++++++++++++ demo/src/demo-custom/index.jsx | 106 ++++++++++++++++++++ demo/src/demo-main/index.jsx | 26 ++--- demo/src/generate-fake-data.ts | 69 ++++++------- 6 files changed, 314 insertions(+), 105 deletions(-) create mode 100644 demo/src/demo-custom/generate-fake-data.js create mode 100644 demo/src/demo-custom/groups.json create mode 100644 demo/src/demo-custom/index.jsx diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 469555d4..908e9670 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[] = [ { @@ -27,54 +23,59 @@ const routes: RouteObject[] = [ loader, }, { - path: '/DemoPerformance', - Component: withLayout(DemoPerformance), + path: '/Custom', + Component: withLayout(DemoCustom), loader, }, - { - path: '/DemoTreePGroups', - Component: withLayout(DemoTreePGroups), - }, - { - path: '/LinkedTimelines', - Component: withLayout(LinkedTimelines), - }, - { - path: '/ElementResize', - Component: withLayout(ElementResize), - }, - { - path: '/Renderers', - Component: withLayout(Renderers), - }, - { - path: '/VerticalClasses', - Component: withLayout(VerticalClasses), - }, - { - path: '/CustomItems', - Component: withLayout(CustomItems), - }, - { - path: '/CustomHeaders', - Component: withLayout(CustomHeaders), - }, - { - path: '/CustomInfoLabel', - Component: withLayout(CustomInfoLabel), - }, - { - path: '/ControledSelect', - Component: withLayout(ControledSelect), - }, - { - path: '/ControlledScrolling', - Component: withLayout(ControlledScrolling), - }, - { - path: "/ExternalDrop", - Component: withLayout(ExternalDrop), - } + // { + // path: '/DemoPerformance', + // Component: withLayout(DemoPerformance), + // loader, + // }, + // { + // path: '/DemoTreePGroups', + // Component: withLayout(DemoTreePGroups), + // }, + // { + // path: '/LinkedTimelines', + // Component: withLayout(LinkedTimelines), + // }, + // { + // path: '/ElementResize', + // Component: withLayout(ElementResize), + // }, + // { + // path: '/Renderers', + // Component: withLayout(Renderers), + // }, + // { + // path: '/VerticalClasses', + // Component: withLayout(VerticalClasses), + // }, + // { + // path: '/CustomItems', + // Component: withLayout(CustomItems), + // }, + // { + // path: '/CustomHeaders', + // Component: withLayout(CustomHeaders), + // }, + // { + // path: '/CustomInfoLabel', + // Component: withLayout(CustomInfoLabel), + // }, + // { + // path: '/ControledSelect', + // Component: withLayout(ControledSelect), + // }, + // { + // path: '/ControlledScrolling', + // Component: withLayout(ControlledScrolling), + // }, + // { + // 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..16f1eff6 --- /dev/null +++ b/demo/src/demo-custom/index.jsx @@ -0,0 +1,106 @@ +import React from 'react' +import { Component } from 'react' +import dayjs from 'dayjs' + +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" +}; + +export default class App extends Component { + constructor(props) { + super(props); + + const { groups, items } = generateFakeData(3, 6, 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() + + 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); + }; + + render() { + const { groups, items, defaultTimeStart, defaultTimeEnd } = this.state; + + return ( + + ); + } +} 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 } } From 822da38d5330ee47300350dfd1c0b36ede9aae6d Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Tue, 8 Apr 2025 15:16:40 +0200 Subject: [PATCH 02/10] Use directly number type instead of dateType --- src/lib/Timeline.tsx | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lib/Timeline.tsx b/src/lib/Timeline.tsx index 5d15c787..ac3ebc77 100644 --- a/src/lib/Timeline.tsx +++ b/src/lib/Timeline.tsx @@ -46,17 +46,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 +74,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 @@ -138,8 +138,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 +224,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) }, @@ -494,8 +494,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, From 5a13abf9768a28ee7682533d98a2dc898f782360 Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Wed, 9 Apr 2025 12:38:07 +0200 Subject: [PATCH 03/10] Added custom date type! Wooo! Not too bad. --- src/lib/columns/Columns.tsx | 17 ++++++----- src/lib/default-config.ts | 7 +++++ src/lib/headers/CustomHeader.tsx | 12 ++++---- src/lib/headers/DateHeader.tsx | 25 ++++++++------- src/lib/types/main.ts | 3 +- src/lib/utility/calendar.tsx | 52 +++++++++++++++++++++----------- 6 files changed, 72 insertions(+), 44 deletions(-) diff --git a/src/lib/columns/Columns.tsx b/src/lib/columns/Columns.tsx index 2159e1d9..ae67bcdb 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 dayjs from 'dayjs' import { TimelineStateConsumer } from '../timeline/TimelineStateContext' import { TimelineTimeSteps } from '../types/main' @@ -48,15 +49,15 @@ class Columns extends Component { const lines: React.JSX.Element[] = [] - iterateTimes(canvasTimeStart, canvasTimeEnd, minUnit, timeSteps, (time, nextTime) => { - const minUnitValue = time.get(minUnit === 'day' ? 'date' : minUnit) + iterateTimes(canvasTimeStart, canvasTimeEnd, minUnit, timeSteps, (time: number, nextTime: number) => { + const minUnitValue = minUnit === 'blocks5' ? minUnit : dayjs(time).get(minUnit === 'day' ? 'date' : minUnit) const firstOfType = minUnitValue === (minUnit === 'day' ? 1 : 0) 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 +65,14 @@ class Columns extends Component { const classNames = 'rct-vl' + (firstOfType ? ' rct-vl-first' : '') + - (minUnit === 'day' || minUnit === 'hour' || minUnit === 'minute' ? ` rct-day-${time.day()} ` : ' ') + + (minUnit === 'day' || minUnit === 'hour' || minUnit === 'minute' ? ` 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(
= { + blocks5: { + long: '--', + mediumLong: '--', + medium: '--', + short: '--', + }, year: { long: 'YYYY', mediumLong: 'YYYY', diff --git a/src/lib/headers/CustomHeader.tsx b/src/lib/headers/CustomHeader.tsx index 7d9c9813..8e93a78b 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, { Dayjs } from 'dayjs' import { CustomDateHeaderProps } from './CustomDateHeader' import isEqual from 'lodash/isEqual' import { GetIntervalPropsType } from './types' @@ -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..bc7d47db 100644 --- a/src/lib/headers/DateHeader.tsx +++ b/src/lib/headers/DateHeader.tsx @@ -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' | 'blocks5' | 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' | 'blocks5' | 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,12 @@ const formatLabel:FormatLabelFunction = ( } else { format = formatOptions[unit]['short'] } + + if (unit === 'blocks5') { + console.log('timestart: ', timeStart.valueOf()) + const timeLeft = timeStart.valueOf() - timeStart.startOf(getNextUnit(unit)).valueOf() + return 'b5: ' + Math.floor(timeLeft / 20) + } return timeStart.format(format) } diff --git a/src/lib/types/main.ts b/src/lib/types/main.ts index 80818c1b..6d40d217 100644 --- a/src/lib/types/main.ts +++ b/src/lib/types/main.ts @@ -124,6 +124,7 @@ export interface TimelineKeys { export type dateType = number //| undefined; export interface TimelineTimeSteps { + blocks5: number second: number minute: number hour: number @@ -182,7 +183,7 @@ export interface SidebarHeaderProps { export class SidebarHeader extends Component> {} -export type Unit = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'isoWeek' | 'month' | 'year' +export type Unit = 'blocks5' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'isoWeek' | 'month' | 'year' export interface IntervalContext { interval: Interval diff --git a/src/lib/utility/calendar.tsx b/src/lib/utility/calendar.tsx index c922c10f..bf9112be 100644 --- a/src/lib/utility/calendar.tsx +++ b/src/lib/utility/calendar.tsx @@ -73,22 +73,35 @@ export function iterateTimes( 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 (unit === 'blocks5') { + // 5 msec blocks or future 5 blocks (where each block will represent 16nanoseconds) + // this is used for the timeline to render the blocks + const blockTime = 20 + let time = Math.floor(start / blockTime) * blockTime + + while (time < end) { + const nextTime = Math.floor((time + blockTime) / blockTime) * blockTime + callback(time, nextTime) + time = nextTime + } + } else { + let time = dayjs(start).startOf(unit) - 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(unit) + time = time.set(unit, 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(unit) - callback(time, nextTime) - time = nextTime + callback(time.valueOf(), nextTime.valueOf()) + time = nextTime + } } } @@ -115,7 +128,8 @@ export function getMinUnit(zoom: number, width: number, timeSteps: TimelineTimeS // 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, + blocks5: 20, + second: 50, minute: 60, hour: 60, day: 24, @@ -159,10 +173,12 @@ export function getMinUnit(zoom: number, width: number, timeSteps: TimelineTimeS return minUnit } -export type SelectUnits = 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' -export type SelectUnitsRes = Exclude +export type SelectUnits = 'blocks5' | 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' + +export type SelectUnitsRes = Exclude export const NEXT_UNITS: Record = { + blocks5: 'second', second: 'minute', minute: 'hour', hour: 'day', @@ -305,8 +321,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 +689,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] } From 7746cbb737b48603731b972c9f4e5f0a08dd2407 Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Wed, 16 Apr 2025 16:56:22 +0200 Subject: [PATCH 04/10] Use number type instead of dayjs in showPeriod for simplicity --- src/lib/Timeline.tsx | 6 +++--- src/lib/headers/CustomDateHeader.tsx | 4 ++-- src/lib/headers/CustomHeader.tsx | 2 +- src/lib/headers/Interval.tsx | 12 ++++++------ src/lib/timeline/TimelineStateContext.tsx | 5 ++--- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/lib/Timeline.tsx b/src/lib/Timeline.tsx index ac3ebc77..1108df97 100644 --- a/src/lib/Timeline.tsx +++ b/src/lib/Timeline.tsx @@ -532,9 +532,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 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 8e93a78b..a8ef51bf 100644 --- a/src/lib/headers/CustomHeader.tsx +++ b/src/lib/headers/CustomHeader.tsx @@ -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 diff --git a/src/lib/headers/Interval.tsx b/src/lib/headers/Interval.tsx index fdd12040..384f29ec 100644 --- a/src/lib/headers/Interval.tsx +++ b/src/lib/headers/Interval.tsx @@ -9,7 +9,7 @@ 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,12 @@ 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) + 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 = { From 428014138d55f274e4afa7213e7743d8aa2ce06f Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Wed, 16 Apr 2025 16:58:49 +0200 Subject: [PATCH 05/10] Customize demo example and formatting in Timeline.tsx --- demo/src/demo-custom/index.jsx | 74 ++++++++++++++++++++++++++++++++-- src/lib/Timeline.tsx | 39 +++++++++--------- 2 files changed, 89 insertions(+), 24 deletions(-) diff --git a/demo/src/demo-custom/index.jsx b/demo/src/demo-custom/index.jsx index 16f1eff6..265f93b8 100644 --- a/demo/src/demo-custom/index.jsx +++ b/demo/src/demo-custom/index.jsx @@ -1,6 +1,7 @@ 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' @@ -19,13 +20,71 @@ var keys = { 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 defaultTimeStart = dayjs(items[0].start_time).startOf('day').toDate().valueOf() - const defaultTimeEnd = dayjs(items[0].end_time).startOf('day').add(1, 'day').toDate().valueOf() + // 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, @@ -90,6 +149,9 @@ export default class App extends Component { keys={keys} fullUpdate itemTouchSendsClick={false} + minZoom={5} + maxZoom={16 * 6 * 5 * 4 * 5} + dragSnap={1000} stackItems itemHeightRatio={0.75} canMove={true} @@ -100,6 +162,12 @@ export default class App extends Component { onItemResize={this.handleItemResize} onItemClick={this.handleItemClick} onItemSelect={this.handleItemSelect} + onTimeChange={(visibleStartTime, visibleEndTime, updateScrollCanvas) => { + if (visibleStartTime > defaultTimeStart) { + updateScrollCanvas(visibleStartTime, visibleEndTime); + } + } + } /> ); } diff --git a/src/lib/Timeline.tsx b/src/lib/Timeline.tsx index 1108df97..6a2c5e9f 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,17 @@ import { calculateScrollCanvas, getCanvasBoundariesFromVisibleTime, getCanvasWidth, - stackTimelineItems, coordinateToTimeRatio, + stackTimelineItems, + coordinateToTimeRatio, } 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, @@ -966,10 +966,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 +976,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() { From a9d0b0acb317567c8341391014fa90624ca3a9c5 Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Wed, 16 Apr 2025 18:10:52 +0200 Subject: [PATCH 06/10] Add multiple custom types --- src/lib/Timeline.tsx | 2 +- src/lib/columns/Columns.tsx | 18 +++++-- src/lib/default-config.ts | 55 ++++++++++++++++++---- src/lib/headers/CustomHeader.tsx | 2 +- src/lib/headers/DateHeader.tsx | 14 +++--- src/lib/headers/Interval.tsx | 15 ++++-- src/lib/types/main.ts | 14 ++++-- src/lib/utility/calendar.tsx | 80 +++++++++++++++++++++++--------- 8 files changed, 148 insertions(+), 52 deletions(-) diff --git a/src/lib/Timeline.tsx b/src/lib/Timeline.tsx index 6a2c5e9f..2c294963 100644 --- a/src/lib/Timeline.tsx +++ b/src/lib/Timeline.tsx @@ -37,7 +37,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' diff --git a/src/lib/columns/Columns.tsx b/src/lib/columns/Columns.tsx index ae67bcdb..6b5a90fb 100644 --- a/src/lib/columns/Columns.tsx +++ b/src/lib/columns/Columns.tsx @@ -1,7 +1,7 @@ import React, { Component, FC } from 'react' -import { iterateTimes } from '../utility/calendar' -import dayjs from 'dayjs' +import { isCustomUnit, iterateTimes } from '../utility/calendar' +import dayjs, { UnitType } from 'dayjs' import { TimelineStateConsumer } from '../timeline/TimelineStateContext' import { TimelineTimeSteps } from '../types/main' @@ -50,8 +50,16 @@ class Columns extends Component { const lines: React.JSX.Element[] = [] iterateTimes(canvasTimeStart, canvasTimeEnd, minUnit, timeSteps, (time: number, nextTime: number) => { - const minUnitValue = minUnit === 'blocks5' ? minUnit : dayjs(time).get(minUnit === 'day' ? 'date' : minUnit) - const firstOfType = minUnitValue === (minUnit === 'day' ? 1 : 0) + // 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) { @@ -65,7 +73,7 @@ class Columns extends Component { const classNames = 'rct-vl' + (firstOfType ? ' rct-vl-first' : '') + - (minUnit === 'day' || minUnit === 'hour' || minUnit === 'minute' ? ` rct-day-${dayjs(time).day()} ` : ' ') + + (originalCheck ? ` rct-day-${dayjs(time).day()} ` : ' ') + classNamesForTime.join(' ') const left = getLeftOffsetFromDate(time) diff --git a/src/lib/default-config.ts b/src/lib/default-config.ts index 482af547..b9f4b83c 100644 --- a/src/lib/default-config.ts +++ b/src/lib/default-config.ts @@ -11,8 +11,12 @@ export const defaultKeys = { itemTimeEndKey: 'end_time', } -export const defaultTimeSteps = { - blocks5: 1, +const customDefaultTimeSteps = { + blocks1: 1, + blocks2: 1, +} + +const originalDefaultTimeSteps = { second: 1, minute: 1, hour: 1, @@ -21,19 +25,49 @@ export const defaultTimeSteps = { year: 1, } +export const defaultTimeSteps = { ...customDefaultTimeSteps, ...originalDefaultTimeSteps } + type UnitValue = { long: string mediumLong: string medium: string short: string } -export const defaultHeaderFormats: Record = { - blocks5: { - long: '--', - mediumLong: '--', - medium: '--', - short: '--', + +const customDefaultHeaderFormats: Record = { + blocks1: { + long: '16 nsec', + mediumLong: '16 nsec', + medium: '16', + short: '16', + }, + blocks2: { + long: '96 nsec', + mediumLong: '96 nsec', + medium: '96', + short: '96', }, + // blocks3: { + // long: '480 nsec', + // mediumLong: '480 nsec', + // medium: '480', + // short: '480', + // }, + // blocks4: { + // long: '1.92 usec', + // mediumLong: '1.92 usec', + // medium: '1.9', + // short: '1.9', + // }, + // blocks5: { + // long: '9.6 usec', + // mediumLong: '9.6 usec', + // medium: '9.6', + // short: '9.6', + // }, +} + +const originalDefaultHeaderFormats: Record = { year: { long: 'YYYY', mediumLong: 'YYYY', @@ -77,3 +111,8 @@ export const defaultHeaderFormats: Record = { short: 'ss', }, } + +export const defaultHeaderFormats: Record = { + ...customDefaultHeaderFormats, + ...originalDefaultHeaderFormats, +} diff --git a/src/lib/headers/CustomHeader.tsx b/src/lib/headers/CustomHeader.tsx index a8ef51bf..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, { Dayjs } from 'dayjs' +import dayjs from 'dayjs' import { CustomDateHeaderProps } from './CustomDateHeader' import isEqual from 'lodash/isEqual' import { GetIntervalPropsType } from './types' diff --git a/src/lib/headers/DateHeader.tsx b/src/lib/headers/DateHeader.tsx index bc7d47db..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' | 'blocks5' | 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' | 'blocks5' | undefined + unitProp: UnitType | 'primaryHeader' | CustomUnit | undefined headerData: Data } export interface DateHeaderProps { @@ -161,10 +161,8 @@ const formatLabel: FormatLabelFunction = ( format = formatOptions[unit]['short'] } - if (unit === 'blocks5') { - console.log('timestart: ', timeStart.valueOf()) - const timeLeft = timeStart.valueOf() - timeStart.startOf(getNextUnit(unit)).valueOf() - return 'b5: ' + Math.floor(timeLeft / 20) + 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 384f29ec..ecdefdad 100644 --- a/src/lib/headers/Interval.tsx +++ b/src/lib/headers/Interval.tsx @@ -1,9 +1,9 @@ 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 @@ -21,10 +21,19 @@ class Interval extends React.PureComponent> { onIntervalClick = () => { const { primaryHeader, interval, unit, showPeriod } = this.props if (primaryHeader) { + 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.valueOf(), interval.endTime.valueOf()) } diff --git a/src/lib/types/main.ts b/src/lib/types/main.ts index 6d40d217..fe9f2f1a 100644 --- a/src/lib/types/main.ts +++ b/src/lib/types/main.ts @@ -123,8 +123,12 @@ export interface TimelineKeys { export type dateType = number //| undefined; -export interface TimelineTimeSteps { - blocks5: number +interface CustomTimelineTimeSteps { + blocks1: number + blocks2: number +} + +interface OriginalTimelineTimeSteps { second: number minute: number hour: number @@ -133,6 +137,8 @@ export interface TimelineTimeSteps { year: number } +export interface TimelineTimeSteps extends CustomTimelineTimeSteps, OriginalTimelineTimeSteps {} + export class TimelineMarkers extends Component {} export interface CustomMarkerChildrenProps { @@ -183,7 +189,9 @@ export interface SidebarHeaderProps { export class SidebarHeader extends Component> {} -export type Unit = 'blocks5' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'isoWeek' | 'month' | 'year' +export type CustomUnit = 'blocks1' | 'blocks2' +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 bf9112be..c2e1fa9e 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,6 +69,26 @@ export function calculateTimeForXPosition( return timeFromCanvasTimeStart + canvasTimeStart } +const customPrevUnits: Record = { + 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'] + return customTypes.includes(unit as CustomUnit) +} + export function iterateTimes( start: number, end: number, @@ -75,10 +96,8 @@ export function iterateTimes( timeSteps: TimelineTimeSteps, callback: (time: number, nextTime: number) => void, ) { - if (unit === 'blocks5') { - // 5 msec blocks or future 5 blocks (where each block will represent 16nanoseconds) - // this is used for the timeline to render the blocks - const blockTime = 20 + if (isCustomUnit(unit)) { + const blockTime = getPrevFactor(unit as CustomUnit) let time = Math.floor(start / blockTime) * blockTime while (time < end) { @@ -87,17 +106,18 @@ export function iterateTimes( time = nextTime } } else { - let time = dayjs(start).startOf(unit) + 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])) + 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) + .startOf(dayjsUnit) callback(time.valueOf(), nextTime.valueOf()) time = nextTime @@ -122,20 +142,23 @@ export function iterateTimes( // i think this is the distance between cell lines export const minCellWidth = 17 +export const defaultTimeDividers: Record = { + blocks1: 8, + blocks2: 5, + second: 25, + 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 = { - blocks5: 20, - second: 50, - minute: 60, - hour: 60, - day: 24, - month: 30, - year: 12, - } + const timeDividers: Record = defaultTimeDividers let minUnit: keyof TimelineTimeSteps = 'year' @@ -170,15 +193,21 @@ export function getMinUnit(zoom: number, width: number, timeSteps: TimelineTimeS } }) - return minUnit + return minUnit as SelectUnits } -export type SelectUnits = 'blocks5' | 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' +type CustomSelectUnits = 'blocks1' | 'blocks2' +type OriginalSelectUnits = 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' +export type SelectUnits = CustomSelectUnits | OriginalSelectUnits -export type SelectUnitsRes = Exclude +export type SelectUnitsRes = Exclude -export const NEXT_UNITS: Record = { - blocks5: 'second', +const customNextUnits: Record = { + blocks1: 'blocks2', + blocks2: 'second', +} + +const originalNextUnits: Record = { second: 'minute', minute: 'hour', hour: 'day', @@ -187,6 +216,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`) From 964052bdbc6cae6c7eac947eb4ea67e0c70f8b3f Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Thu, 17 Apr 2025 18:41:34 +0200 Subject: [PATCH 07/10] Introduce custom made types from nsec to hour --- demo/src/demo-custom/index.jsx | 4 +- src/lib/default-config.ts | 79 +++++++++++++++++++++++----------- src/lib/types/main.ts | 18 +++++++- src/lib/utility/calendar.tsx | 53 ++++++++++++++++++++--- 4 files changed, 122 insertions(+), 32 deletions(-) diff --git a/demo/src/demo-custom/index.jsx b/demo/src/demo-custom/index.jsx index 265f93b8..a11178da 100644 --- a/demo/src/demo-custom/index.jsx +++ b/demo/src/demo-custom/index.jsx @@ -150,8 +150,8 @@ export default class App extends Component { fullUpdate itemTouchSendsClick={false} minZoom={5} - maxZoom={16 * 6 * 5 * 4 * 5} - dragSnap={1000} + maxZoom={8 * 2 * 50 * 25 * 50 * 25} + dragSnap={10} stackItems itemHeightRatio={0.75} canMove={true} diff --git a/src/lib/default-config.ts b/src/lib/default-config.ts index b9f4b83c..5f12bf1c 100644 --- a/src/lib/default-config.ts +++ b/src/lib/default-config.ts @@ -14,6 +14,13 @@ export const defaultKeys = { const customDefaultTimeSteps = { blocks1: 1, blocks2: 1, + blocks3: 1, + blocks4: 1, + blocks5: 1, + blocks6: 1, + blocks7: 1, + blocks8: 1, + blocks9: 1, } const originalDefaultTimeSteps = { @@ -36,35 +43,59 @@ type UnitValue = { 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', }, - blocks2: { - long: '96 nsec', - mediumLong: '96 nsec', - medium: '96', - short: '96', - }, - // blocks3: { - // long: '480 nsec', - // mediumLong: '480 nsec', - // medium: '480', - // short: '480', - // }, - // blocks4: { - // long: '1.92 usec', - // mediumLong: '1.92 usec', - // medium: '1.9', - // short: '1.9', - // }, - // blocks5: { - // long: '9.6 usec', - // mediumLong: '9.6 usec', - // medium: '9.6', - // short: '9.6', - // }, + 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 = { diff --git a/src/lib/types/main.ts b/src/lib/types/main.ts index fe9f2f1a..67da5fe2 100644 --- a/src/lib/types/main.ts +++ b/src/lib/types/main.ts @@ -126,6 +126,13 @@ export type dateType = number //| undefined; interface CustomTimelineTimeSteps { blocks1: number blocks2: number + blocks3: number + blocks4: number + blocks5: number + blocks6: number + blocks7: number + blocks8: number + blocks9: number } interface OriginalTimelineTimeSteps { @@ -189,7 +196,16 @@ export interface SidebarHeaderProps { export class SidebarHeader extends Component> {} -export type CustomUnit = 'blocks1' | 'blocks2' +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 diff --git a/src/lib/utility/calendar.tsx b/src/lib/utility/calendar.tsx index c2e1fa9e..34bf489f 100644 --- a/src/lib/utility/calendar.tsx +++ b/src/lib/utility/calendar.tsx @@ -70,6 +70,13 @@ export function calculateTimeForXPosition( } const customPrevUnits: Record = { + blocks9: 'blocks8', + blocks8: 'blocks7', + blocks7: 'blocks6', + blocks6: 'blocks5', + blocks5: 'blocks4', + blocks4: 'blocks3', + blocks3: 'blocks2', blocks2: 'blocks1', blocks1: 'blocks1', } @@ -85,7 +92,17 @@ export function getPrevFactor(unit: CustomUnit): number { } export function isCustomUnit(unit: keyof TimelineTimeSteps): boolean { - const customTypes: CustomUnit[] = ['blocks1', 'blocks2'] + const customTypes: CustomUnit[] = [ + 'blocks1', + 'blocks2', + 'blocks3', + 'blocks4', + 'blocks5', + 'blocks6', + 'blocks7', + 'blocks8', + 'blocks9', + ] return customTypes.includes(unit as CustomUnit) } @@ -144,8 +161,16 @@ export const minCellWidth = 17 export const defaultTimeDividers: Record = { blocks1: 8, - blocks2: 5, - second: 25, + 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, @@ -196,7 +221,16 @@ export function getMinUnit(zoom: number, width: number, timeSteps: TimelineTimeS return minUnit as SelectUnits } -type CustomSelectUnits = 'blocks1' | 'blocks2' +type CustomSelectUnits = + | 'blocks1' + | 'blocks2' + | 'blocks3' + | 'blocks4' + | 'blocks5' + | 'blocks6' + | 'blocks7' + | 'blocks8' + | 'blocks9' type OriginalSelectUnits = 'second' | 'minute' | 'hour' | 'day' | 'month' | 'year' export type SelectUnits = CustomSelectUnits | OriginalSelectUnits @@ -204,7 +238,16 @@ export type SelectUnitsRes = Exclude const customNextUnits: Record = { blocks1: 'blocks2', - blocks2: 'second', + 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', } const originalNextUnits: Record = { From 7ea79bce700f9f6d412a38d0e05c7d1de8c2f9b4 Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Fri, 18 Apr 2025 12:10:27 +0200 Subject: [PATCH 08/10] Add cursors to demo --- demo/src/demo-custom/index.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/demo/src/demo-custom/index.jsx b/demo/src/demo-custom/index.jsx index a11178da..49140e5e 100644 --- a/demo/src/demo-custom/index.jsx +++ b/demo/src/demo-custom/index.jsx @@ -151,7 +151,7 @@ export default class App extends Component { itemTouchSendsClick={false} minZoom={5} maxZoom={8 * 2 * 50 * 25 * 50 * 25} - dragSnap={10} + dragSnap={16} stackItems itemHeightRatio={0.75} canMove={true} @@ -168,7 +168,12 @@ export default class App extends Component { } } } - /> + > + + + + + ); } } From 722f0d8a30b6918273de2f866685da159fbaeafe Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Fri, 18 Apr 2025 12:25:27 +0200 Subject: [PATCH 09/10] Extend onZoom to return time dividers --- demo/src/demo-custom/index.jsx | 5 +++++ src/lib/Timeline.tsx | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/demo/src/demo-custom/index.jsx b/demo/src/demo-custom/index.jsx index 49140e5e..ee854ea6 100644 --- a/demo/src/demo-custom/index.jsx +++ b/demo/src/demo-custom/index.jsx @@ -139,6 +139,10 @@ export default class App extends Component { console.log("Resized", itemId, time, edge); }; + handleZoom = (timelineContext, unit, dividers) => { + console.log("Unit: ", unit, dividers[unit]); + } + render() { const { groups, items, defaultTimeStart, defaultTimeEnd } = this.state; @@ -168,6 +172,7 @@ export default class App extends Component { } } } + onZoom={this.handleZoom} > diff --git a/src/lib/Timeline.tsx b/src/lib/Timeline.tsx index 2c294963..d09fa504 100644 --- a/src/lib/Timeline.tsx +++ b/src/lib/Timeline.tsx @@ -15,6 +15,7 @@ import { getCanvasWidth, stackTimelineItems, coordinateToTimeRatio, + defaultTimeDividers, } from './utility/calendar' import { _get } from './utility/generic' import { defaultKeys, defaultTimeSteps } from './default-config' @@ -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 @@ -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! From f63d36c831adb259594c77335948884272536f8d Mon Sep 17 00:00:00 2001 From: Valentin Vasiliu Date: Thu, 22 May 2025 10:52:19 +0200 Subject: [PATCH 10/10] Uncomment demo examples in App.tsx --- demo/src/App.tsx | 100 +++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/demo/src/App.tsx b/demo/src/App.tsx index 908e9670..32d0c86a 100644 --- a/demo/src/App.tsx +++ b/demo/src/App.tsx @@ -23,59 +23,59 @@ const routes: RouteObject[] = [ loader, }, { - path: '/Custom', + path: '/CustomTimescale', Component: withLayout(DemoCustom), loader, }, - // { - // path: '/DemoPerformance', - // Component: withLayout(DemoPerformance), - // loader, - // }, - // { - // path: '/DemoTreePGroups', - // Component: withLayout(DemoTreePGroups), - // }, - // { - // path: '/LinkedTimelines', - // Component: withLayout(LinkedTimelines), - // }, - // { - // path: '/ElementResize', - // Component: withLayout(ElementResize), - // }, - // { - // path: '/Renderers', - // Component: withLayout(Renderers), - // }, - // { - // path: '/VerticalClasses', - // Component: withLayout(VerticalClasses), - // }, - // { - // path: '/CustomItems', - // Component: withLayout(CustomItems), - // }, - // { - // path: '/CustomHeaders', - // Component: withLayout(CustomHeaders), - // }, - // { - // path: '/CustomInfoLabel', - // Component: withLayout(CustomInfoLabel), - // }, - // { - // path: '/ControledSelect', - // Component: withLayout(ControledSelect), - // }, - // { - // path: '/ControlledScrolling', - // Component: withLayout(ControlledScrolling), - // }, - // { - // path: '/ExternalDrop', - // Component: withLayout(ExternalDrop), - // }, + { + path: '/DemoPerformance', + Component: withLayout(DemoPerformance), + loader, + }, + { + path: '/DemoTreePGroups', + Component: withLayout(DemoTreePGroups), + }, + { + path: '/LinkedTimelines', + Component: withLayout(LinkedTimelines), + }, + { + path: '/ElementResize', + Component: withLayout(ElementResize), + }, + { + path: '/Renderers', + Component: withLayout(Renderers), + }, + { + path: '/VerticalClasses', + Component: withLayout(VerticalClasses), + }, + { + path: '/CustomItems', + Component: withLayout(CustomItems), + }, + { + path: '/CustomHeaders', + Component: withLayout(CustomHeaders), + }, + { + path: '/CustomInfoLabel', + Component: withLayout(CustomInfoLabel), + }, + { + path: '/ControledSelect', + Component: withLayout(ControledSelect), + }, + { + path: '/ControlledScrolling', + Component: withLayout(ControlledScrolling), + }, + { + path: '/ExternalDrop', + Component: withLayout(ExternalDrop), + }, ] function Menu() {