npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mq-timeline-calendar

v0.5.0

Published

A flexible, framework-agnostic timeline/calendar component with smooth scrolling and zooming

Downloads

586

Readme

mq-timeline-calendar

A flexible, headless timeline/calendar component for React with smooth scrolling, zooming, and drag-and-drop capabilities.

npm version License: MIT

Features

  • 🎯 Headless by default - Full control over styling and rendering
  • 📅 Smooth scrolling - Navigate through time seamlessly with mouse wheel
  • 🔍 Continuous zoom - Smooth, cursor-relative zoom (Ctrl/Cmd + wheel)
  • 🎨 Built-in themes - Light and dark themes included
  • 🖱️ Drag & Drop - Move items horizontally (time) and vertically (rows)
  • 📍 Pinpoint markers - Display events and milestones with automatic clustering
  • 📊 Row grouping - Organize timeline items with collapsible row groups
  • 🌍 Localization - Support for 13+ European languages
  • Availability overlay - Show working hours and available time periods
  • 📊 Smart aggregation - Auto-aggregate items when zoomed out for better performance
  • ⚛️ React first - Optimized React components with hooks
  • 📦 TypeScript - Full type safety and IntelliSense support
  • 🚀 Framework-agnostic core - Core engine can be used with any framework

Installation

npm install mq-timeline-calendar

Live Demo

Try out the timeline calendar in StackBlitz:

Open in StackBlitz

Open Demo →

Quick Start

import { TimelineCalendar, TimelineItem, TimelineRow, TimelineRowGroup } from 'mq-timeline-calendar/react';

function App() {
  return (
    <TimelineCalendar
      startDate={new Date('2025-01-01')}
      endDate={new Date('2025-12-31')}
      height="600px"
      showCurrentTime={true}
      theme="light"
    >
      <TimelineRowGroup>
        <TimelineRow id="row-1" label="Production Line A" rowCount={2} collapsible={true}>
          <TimelineItem
            startTime="2025-03-15"
            duration="1 week"
            row={0}
          >
            <div style={{ background: '#3b82f6', padding: '8px', color: 'white' }}>
              Order #1234
            </div>
          </TimelineItem>

          <TimelineItem
            startTime="2025-03-25"
            duration="5 days"
            row={1}
          >
            <div style={{ background: '#10b981', padding: '8px', color: 'white' }}>
              Order #1235
            </div>
          </TimelineItem>
        </TimelineRow>
      </TimelineRowGroup>
    </TimelineCalendar>
  );
}

Core Concepts

TimelineCalendar

The main container component that manages the timeline viewport, zoom level, and scrolling.

<TimelineCalendar
  startDate={new Date('2024-01-01')}
  endDate={new Date('2026-12-31')}
  width="100%"
  height="600px"
  minZoom="1000 years"
  maxZoom="100 milliseconds"
  showNavigation={false}
  showCurrentTime={true}
  locale={fiFI}
  theme="dark"
  onViewportChange={(start, end) => console.log('Viewport:', start, end)}
  onZoomChange={(pixelsPerMs) => console.log('Zoom:', pixelsPerMs)}
>
  {/* Timeline items and rows */}
</TimelineCalendar>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | startDate | TimeValue | required | Start date of the timeline | | endDate | TimeValue | required | End date of the timeline | | width | string \| number | "100%" | Width of the container | | height | string \| number | "600px" | Height of the container | | minZoom | number \| string | - | Minimum zoom level (e.g., "5 years") | | maxZoom | number \| string | - | Maximum zoom level (e.g., "1 hour") | | showNavigation | boolean | false | Show navigation buttons | | showCurrentTime | boolean | false | Show current time indicator | | currentTimeLineWidth | number | 2 | Width of current time line in pixels | | locale | CalendarLocale | - | Locale for date formatting | | theme | 'light' \| 'dark' \| TimelineTheme | 'light' | Theme preset or custom theme object | | availability | AvailabilityConfig | - | Availability/working hours configuration | | onViewportChange | (start: Date, end: Date) => void | - | Called when viewport changes | | onZoomChange | (pixelsPerMs: number) => void | - | Called when zoom level changes |

TimelineItem

Individual items placed on the timeline at specific times.

<TimelineItem
  startTime="2025-03-15"
  duration="1 week"
  row={0}
  draggable={true}
  allowRowChange={true}
  onDragStart={(timestamp, row, rowGroupId) => console.log('Drag started')}
  onDrag={(timestamp, row, rowGroupId) => console.log('Dragging')}
  onDragEnd={(newTime, oldTime, newRow, oldRow, newGroupId, oldGroupId) => {
    console.log('Dropped at:', new Date(newTime), 'row:', newRow);
  }}
>
  <div>My Event</div>
</TimelineItem>

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | startTime | TimeValue | required | Start time (Date, timestamp, or ISO string) | | duration | DurationValue | required | Duration (ms or human-readable like "1 week") | | endTime | TimeValue | - | Alternative to duration - specify end time directly | | row | number | 0 | Row/lane for vertical positioning | | draggable | boolean | false | Enable drag and drop | | allowRowChange | boolean | false | Allow dragging between rows (vertical) | | onDragStart | (timestamp, row, rowGroupId?) => void | - | Called when drag starts | | onDrag | (timestamp, row, rowGroupId?) => void | - | Called during drag | | onRowChange | (newRow, oldRow, newGroupId?, oldGroupId?) => void | - | Called when row changes | | onDragEnd | (newTime, oldTime, newRow, oldRow, newGroupId?, oldGroupId?) => void | - | Called when drag ends | | className | string | - | CSS class name | | style | CSSProperties | - | Inline styles |

TimelineRow & TimelineRowGroup

Organize timeline items into rows with optional collapsible headers.

<TimelineRowGroup>
  <TimelineRow
    id="line-a"
    label="Production Line A"
    rowCount={2}
    collapsible={true}
    defaultExpanded={true}
  >
    <TimelineItem startTime="2025-03-15" duration="1 week" row={0}>
      <div>Order #1</div>
    </TimelineItem>
    <TimelineItem startTime="2025-03-20" duration="3 days" row={1}>
      <div>Order #2</div>
    </TimelineItem>
  </TimelineRow>

  <TimelineRow id="line-b" label="Production Line B" rowCount={1} collapsible={true}>
    <TimelineItem startTime="2025-03-18" duration="5 days" row={0}>
      <div>Order #3</div>
    </TimelineItem>
  </TimelineRow>
</TimelineRowGroup>

TimelineRow Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | id | string | auto-generated | Unique identifier | | label | string | - | Label text for header | | rowCount | number | 1 | Number of rows this component occupies | | collapsible | boolean | false | Whether row can be collapsed | | showHeader | boolean | true | Show header (only if collapsible=true) | | defaultExpanded | boolean | true | Initial expanded state | | headerClassName | string | - | CSS class for header | | headerStyle | CSSProperties | - | Inline styles for header | | aggregation | AggregationConfig | - | Aggregation configuration (see Aggregation section) | | renderAggregatedPeriod | (params) => ReactNode | - | Custom renderer for aggregated periods | | getAggregatedTypeStyle | (type: string) => StyleObject | - | Function to get style for aggregated bar segments |

Drag and Drop

Horizontal Dragging (Time)

Move items forward or backward in time:

const [itemTime, setItemTime] = useState(new Date('2025-03-15'));

<TimelineItem
  startTime={itemTime}
  duration="5 days"
  row={0}
  draggable={true}
  onDragEnd={(newTimestamp) => {
    setItemTime(new Date(newTimestamp));
  }}
>
  <div>Drag me horizontally!</div>
</TimelineItem>

Vertical Dragging (Rows)

Move items between rows within the same TimelineRow group:

const [itemRow, setItemRow] = useState(0);

<TimelineRow id="group-1" rowCount={3} collapsible={true}>
  <TimelineItem
    startTime="2025-03-15"
    duration="5 days"
    row={itemRow}
    draggable={true}
    allowRowChange={true}
    onDragEnd={(newTime, oldTime, newRow) => {
      setItemRow(newRow);
    }}
  >
    <div>Drag me vertically!</div>
  </TimelineItem>
</TimelineRow>

Drag Behavior

  • Direction-locked: Once drag starts, it's locked to either horizontal (time) or vertical (row) based on initial movement
  • Snapping: Time changes snap to 15-minute intervals by default
  • Threshold: 5-pixel movement required before drag activates (prevents accidental drags)
  • Row constraints: Vertical dragging is constrained within the TimelineRow's rowCount

Time Formats

The component accepts multiple time formats:

// Date object
startTime={new Date('2025-03-15')}

// ISO string
startTime="2025-03-15T10:00:00Z"

// Timestamp (milliseconds)
startTime={1710504000000}

// Duration can be:
duration={7 * 24 * 60 * 60 * 1000} // milliseconds
duration="1 week"                    // human-readable
duration="3 days"
duration="2 hours"
duration="30 minutes"

Themes

Built-in Themes

// Light theme (default)
<TimelineCalendar theme="light">

// Dark theme
<TimelineCalendar theme="dark">

Custom Theme

<TimelineCalendar
  theme={{
    colors: {
      background: '#1a1a1a',
      gridLine: '#333333',
      headerBackground: '#2a2a2a',
      headerText: '#ffffff',
      currentTimeLine: '#3b82f6'
    },
    spacing: {
      headerHeight: 100,
      rowHeight: 60
    }
  }}
>

Availability / Working Hours

Show available and unavailable time periods:

<TimelineCalendar
  availability={{
    // Weekly pattern: define working hours for each day
    weekly: {
      1: [{ start: '08:00', end: '17:30' }], // Monday
      2: [{ start: '09:00', end: '16:00' }], // Tuesday
      3: [{ start: '08:00', end: '17:30' }], // Wednesday
      4: [{ start: '09:00', end: '16:00' }], // Thursday
      5: [{ start: '08:00', end: '15:00' }], // Friday
    },
    unavailableStyle: {
      backgroundColor: 'rgba(156, 163, 175, 0.12)',
    }
  }}
>

Day Calendar (vertical day view)

<DayCalendar> is a separate component that renders a traditional day-planner view: time runs on the Y-axis (constrained to opening hours derived from AvailabilityConfig), and a list of mechanics (or any resources) is rendered as columns. Use this when <TimelineCalendar>'s horizontal layout isn't a fit — e.g., shop-floor "who's working on what today".

Basic Usage

import { DayCalendar } from 'mq-timeline-calendar/react';

const mechanics = [
  { id: 'm1', name: 'Mikko' },
  { id: 'm2', name: 'Pekka' },
  { id: 'm3', name: 'Anna' },
];

const events = [
  {
    id: 'e1',
    mechanicId: 'm1',
    startTime: '2026-05-11T09:00:00',
    endTime:   '2026-05-11T10:30:00',
    title: 'Öljynvaihto',
    color: '#3b82f6',
  },
  {
    id: 'e2',
    mechanicId: 'm2',
    startTime: '2026-05-11T10:00:00',
    endTime:   '2026-05-11T12:00:00',
    title: 'Jarruhuolto',
  },
];

<DayCalendar
  date={new Date('2026-05-11')}
  mechanics={mechanics}
  events={events}
  availability={{
    weekly: {
      1: [{ start: '08:00', end: '17:30' }],
      2: [{ start: '08:00', end: '17:30' }],
      3: [{ start: '08:00', end: '17:30' }],
      4: [{ start: '08:00', end: '17:30' }],
      5: [{ start: '08:00', end: '15:00' }],
    },
  }}
  slotMinutes={30}
  height="600px"
  showCurrentTime
  onSlotClick={(mechanicId, datetime) => console.log('new slot:', mechanicId, datetime)}
  onEventClick={(eventId, event) => console.log('event clicked:', eventId, event)}
/>

Behavior

  • Visible time range is the union of all ranges for the target day across weekly, simple, and specific in availability. Falls back to 00:00–24:00 when no config is provided.
  • Slot grid is slotMinutes wide (default 30). Range bounds snap to slot boundaries.
  • Events are absolute-positioned inside their mechanic column. Events that span outside opening hours are clipped to the visible range. Events that fall entirely outside are not rendered.
  • Overlapping events in the same mechanic column lay out side-by-side using the same assignSubRows helper as <TimelineCalendar>.
  • Current-time line (when showCurrentTime) shows a horizontal line across all mechanic columns when the current time falls within the visible range.
  • Navigation (when showNavigation) renders a prev / today / next toolbar with a localized date label. <DayCalendar> is controlled — wire onDateChange to your date state.

DayCalendar Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | date | TimeValue | required | Target day to display | | mechanics | Mechanic[] | required | Columns. { id, name, color? } | | events | ScheduleEvent[] | [] | { id, mechanicId, startTime, endTime, title?, color?, data? } | | availability | AvailabilityConfig | - | Same shape as <TimelineCalendar> — drives the visible hour range | | slotMinutes | number | 30 | Slot granularity | | width | string \| number | '100%' | Container width | | height | string \| number | '600px' | Container height | | timeColumnWidth | number | 60 | Time column width in px | | slotHeight | number | 40 | Slot row height in px | | minColumnWidth | number | 100 | Min mechanic column width (triggers horizontal scroll) | | theme | 'light' \| 'dark' \| 'compact' \| 'compact-dark' \| TimelineTheme | 'light' | Reused from <TimelineCalendar> | | showCurrentTime | boolean | false | Show current-time line | | showNavigation | boolean | false | Render prev / today / next toolbar | | navigationLabels | { previous?, today?, next? } | - | Override toolbar button labels | | onDateChange | (newDate: Date) => void | - | Required when showNavigation — receives the new target day | | renderDateLabel | (date: Date) => ReactNode | - | Custom date label in the toolbar | | onSlotClick | (mechanicId, datetime) => void | - | Fired with the slot's start datetime | | onEventClick | (eventId, event) => void | - | Fired when an event is clicked | | renderEvent | (params) => ReactNode | - | Custom event renderer; receives { event, top, height, left, width } (left/width are percentages) | | renderMechanicHeader | (mechanic) => ReactNode | - | Custom mechanic header cell | | renderTimeLabel | (time: Date) => ReactNode | - | Custom time-column label |

Differences from <TimelineCalendar>

| | <TimelineCalendar> | <DayCalendar> | |--|--|--| | Time axis | Horizontal (X) | Vertical (Y) | | Zoom / scroll | Continuous (mouse wheel, pinch) | Native scroll, fixed slot size | | Range | Multi-day / multi-year | Single day | | Rows / columns | Many rows of any height | N mechanic columns | | Children API | <TimelineItem> children | events prop | | Drag & drop | Built-in | Not yet (planned) |

Not yet supported

  • Drag & drop (move event in time or between mechanics)
  • Week view
  • Resizing events

Pinpoint Markers

Display point-in-time markers (milestones, events, deadlines) with automatic clustering when markers are too close together.

Basic Usage

import { TimelinePinpoint, TimelinePinpointGroup, TimelineRow } from 'mq-timeline-calendar/react';

<TimelineCalendar
  startDate={new Date('2025-01-01')}
  endDate={new Date('2025-12-31')}
>
  <TimelineRow id="events" label="Events & Milestones" rowCount={1}>
    <TimelinePinpointGroup row={0} clusterDistance={30}>
      {/* Individual pinpoints - automatically cluster when too close */}
      <TimelinePinpoint
        time="2025-03-10T10:00:00"
        color="#10b981"
        data={{ type: 'inspection', name: 'Safety Check' }}
      >
        ✓
      </TimelinePinpoint>

      <TimelinePinpoint
        time="2025-03-10T14:00:00"
        color="#3b82f6"
        data={{ type: 'meeting', name: 'Team Sync' }}
      >
        👥
      </TimelinePinpoint>

      <TimelinePinpoint
        time="2025-03-15T09:00:00"
        color="#ef4444"
        data={{ type: 'deadline' }}
        onClick={(timestamp, data) => {
          console.log('Clicked:', new Date(timestamp), data);
        }}
      >
        !
      </TimelinePinpoint>
    </TimelinePinpointGroup>
  </TimelineRow>
</TimelineCalendar>

Customization

{/* Different sizes */}
<TimelinePinpoint
  time="2025-03-01T10:00:00"
  size={32}              // Circle size in pixels (default: 24)
  color="#10b981"
>
  ✓
</TimelinePinpoint>

{/* Different alignments */}
<TimelinePinpoint
  time="2025-03-05T10:00:00"
  alignment="top"        // 'top' (default), 'center', or 'bottom'
>
  📍
</TimelinePinpoint>

<TimelinePinpoint
  time="2025-03-10T10:00:00"
  alignment="center"     // Center alignment hides the vertical line
>
  ◆
</TimelinePinpoint>

{/* Custom line styles */}
<TimelinePinpoint
  time="2025-03-15T10:00:00"
  lineStyle="dashed"     // 'solid', 'dashed', or 'dotted'
  lineWidth={3}          // Line width in pixels (default: 2)
  lineLength={50}        // Line length in pixels (default: half row height)
>
  !
</TimelinePinpoint>

TimelinePinpoint Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | time | TimeValue | required | Time of the pinpoint (Date, timestamp, or ISO string) | | row | number | 0 | Row/lane for vertical positioning | | size | number | 24 | Circle marker size in pixels | | color | string | theme color | Color of marker and line | | alignment | 'top' \| 'center' \| 'bottom' | 'top' | Vertical position; 'center' hides line | | lineWidth | number | 2 | Width of vertical line in pixels | | lineLength | number | half row height | Length of vertical line in pixels | | lineStyle | 'solid' \| 'dashed' \| 'dotted' | 'solid' | Style of vertical line | | children | ReactNode | - | Icon/emoji to display in marker | | data | any | - | Custom data to associate with pinpoint | | onClick | (timestamp, data?) => void | - | Click handler |

TimelinePinpointGroup Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | row | number | 0 | Row/lane for vertical positioning | | clusterDistance | number | 30 | Pixel distance threshold for clustering | | clusterColor | string | theme color | Color for cluster markers | | clusterSize | number | 24 | Size of cluster circle marker in pixels | | pinpointSize | number | 24 | Default size for individual pinpoint markers in pixels | | pinpointLineWidth | number | 2 | Default line width for pinpoints in pixels | | pinpointLineLength | number | half row height | Default line length for pinpoints in pixels | | onClusterClick | (timestamp, items) => void | - | Custom cluster click handler (default: zoom in) |

Clustering Behavior

  • Pinpoints within clusterDistance pixels are automatically grouped into clusters
  • Cluster markers display the count of pinpoints
  • Clicking a cluster smoothly zooms in to separate the pinpoints
  • Custom cluster click handler can override default zoom behavior

Group-Level Defaults

Set default properties for all pinpoints in a group:

{/* All pinpoints inherit size and line properties */}
<TimelinePinpointGroup
  row={0}
  clusterDistance={30}
  clusterSize={32}           // Larger cluster markers
  clusterColor="#ec4899"
  pinpointSize={16}          // All pinpoints default to 16px
  pinpointLineWidth={3}      // Thicker lines for all
  pinpointLineLength={25}    // Shorter lines
>
  <TimelinePinpoint time="2025-03-01T10:00:00" color="#10b981">✓</TimelinePinpoint>
  <TimelinePinpoint time="2025-03-01T11:00:00" color="#3b82f6">👥</TimelinePinpoint>
  <TimelinePinpoint time="2025-03-01T12:00:00" color="#f59e0b">🔧</TimelinePinpoint>
  {/* All inherit group defaults */}
</TimelinePinpointGroup>

{/* Individual pinpoints can override group defaults */}
<TimelinePinpointGroup
  row={1}
  pinpointSize={24}          // Default size for group
  pinpointLineWidth={2}
>
  <TimelinePinpoint time="2025-04-01T10:00:00" size={16}>✓</TimelinePinpoint>  {/* Overrides to 16px */}
  <TimelinePinpoint time="2025-04-05T10:00:00">👥</TimelinePinpoint>          {/* Uses group default 24px */}
</TimelinePinpointGroup>

Timeline Aggregation

When working with large datasets, the timeline can automatically aggregate items into grouped periods for better performance and visualization.

How It Works

When you zoom out far enough (beyond the configured threshold) and have many items, the timeline automatically switches from individual item rendering to an aggregated view. The aggregated view shows:

  • Stacked bar charts grouped by week or month
  • Occupancy percentage - how much of the available time is occupied
  • Item counts by type
  • Color-coded segments representing different item types

Configuration

Add the aggregation prop to TimelineRow:

<TimelineRow
  id="production-line"
  label="Production Line A"
  rowCount={2}
  aggregation={{
    enabled: true,
    threshold: "6 months",        // Switch to aggregated view when viewport > 6 months
    granularity: "dynamic",        // "week" | "month" | "dynamic"
    minItemsForAggregation: 50    // Only aggregate if row has 50+ items
  }}
  getAggregatedTypeStyle={(type) => ({
    backgroundColor: type === 'urgent' ? '#ef4444' : '#3b82f6',
    color: 'white'
  })}
  renderAggregatedPeriod={({ period, position, width, height }) => (
    <div style={{ width, height }}>
      Custom rendering for {period.start} - {period.end}
    </div>
  )}
>
  {/* Many TimelineItem components */}
</TimelineRow>

Aggregation Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | enabled | boolean | true | Enable/disable aggregation | | threshold | DurationValue | "6 months" | Viewport duration that triggers aggregation | | granularity | 'week' \| 'month' \| 'dynamic' | "dynamic" | How to group periods | | minItemsForAggregation | number | 50 | Minimum items needed to activate aggregation |

Custom Rendering

You can customize the aggregated view rendering:

<TimelineRow
  aggregation={{ enabled: true }}
  renderAggregatedPeriod={({ period, position, width, height }) => {
    const { occupancyPercent, byType } = period;

    return (
      <div
        style={{
          position: 'absolute',
          left: position,
          width,
          height,
          background: `linear-gradient(to top,
            rgba(59, 130, 246, ${occupancyPercent / 100}) 0%,
            rgba(59, 130, 246, 0.2) 100%)`
        }}
      >
        <span>{occupancyPercent.toFixed(0)}%</span>
      </div>
    );
  }}
>

Type-Based Styling

Customize the appearance of different item types in aggregated view:

<TimelineRow
  aggregation={{ enabled: true }}
  getAggregatedTypeStyle={(type) => {
    const styles = {
      'production': { backgroundColor: '#10b981', color: 'white' },
      'maintenance': { backgroundColor: '#f59e0b', color: 'white' },
      'downtime': { backgroundColor: '#ef4444', color: 'white' }
    };
    return styles[type] || { backgroundColor: '#6b7280', color: 'white' };
  }}
>

Granularity Options

  • week: Always group by ISO weeks (Monday-Sunday)
  • month: Always group by calendar months
  • dynamic: Automatically choose based on viewport:
    • Weeks for 6-12 month viewports
    • Months for >12 month viewports

Localization

import { deDE } from 'mq-timeline-calendar/react';

<TimelineCalendar locale={deDE}>

Available locales:

  • enUS - English (US) - Default
  • deDE - German (Germany)
  • frFR - French (France)
  • esES - Spanish (Spain)
  • itIT - Italian (Italy)
  • ptPT - Portuguese (Portugal)
  • nlNL - Dutch (Netherlands)
  • svSE - Swedish (Sweden)
  • noNO - Norwegian (Norway)
  • daDK - Danish (Denmark)
  • plPL - Polish (Poland)
  • ruRU - Russian (Russia)
  • fiFI - Finnish (Finland)

You can also create custom locales by implementing the CalendarLocale interface.

Keyboard & Mouse Controls

  • Horizontal Trackpad Swipe / Wheel: Scroll horizontally through time
  • Vertical Mouse Wheel (over header): Scroll horizontally through time
  • Vertical Trackpad Swipe / Mouse Wheel (over content): Scroll vertically through rows (when overflow exists)
  • Shift + Vertical Mouse Wheel: Scroll horizontally through time (works anywhere)
  • Ctrl/Cmd + Mouse Wheel: Zoom in/out (centered on cursor position)
  • Click header cells: Zoom to that time period
  • Drag items: Move items in time or between rows (if enabled)

TypeScript Support

The package is written in TypeScript and includes full type definitions:

import type {
  TimelineCalendarProps,
  TimelineItemProps,
  TimelineRowProps,
  TimelinePinpointProps,
  TimelinePinpointGroupProps,
  PinpointAlignment,
  TimeValue,
  DurationValue,
  CalendarLocale,
  TimelineTheme,
  AggregationConfig,
  AggregatedPeriod,
  AggregatedPeriodRenderParams
} from 'mq-timeline-calendar/react';

CSS Custom Properties

Customize appearance using CSS variables:

:root {
  --timeline-header-height: 100px;
  --timeline-header-row-height: 40px;
  --timeline-row-height: 60px;
  --timeline-header-bg: #ffffff;
  --timeline-header-text: #374151;
  --timeline-header-border: #d1d5db;
  --timeline-grid-line: #e5e7eb;
  --timeline-grid-line-primary: #d1d5db;
}

Framework-Agnostic Core

The core timeline engine can be used without React:

import { TimelineEngine } from 'mq-timeline-calendar/core';

const engine = new TimelineEngine(
  new Date('2025-01-01'),
  new Date('2025-12-31'),
  1000 // viewport width in pixels
);

// Convert time to pixel position
const pixelX = engine.timeToPixel(new Date('2025-06-15').getTime());

// Convert pixel to time
const timestamp = engine.pixelToTime(500);

// Zoom
engine.zoom(1.5, 500); // 1.5x zoom at pixel position 500

// Scroll
engine.scroll(100); // Scroll 100 pixels

Examples

Basic Timeline

import { TimelineCalendar, TimelineItem } from 'mq-timeline-calendar/react';

function BasicTimeline() {
  return (
    <TimelineCalendar
      startDate={new Date('2025-01-01')}
      endDate={new Date('2025-12-31')}
      height="400px"
    >
      <TimelineItem startTime="2025-03-15" duration="1 week" row={0}>
        <div>Event 1</div>
      </TimelineItem>
      <TimelineItem startTime="2025-06-20" duration="3 days" row={1}>
        <div>Event 2</div>
      </TimelineItem>
    </TimelineCalendar>
  );
}

Production Schedule

import { TimelineCalendar, TimelineItem, TimelineRow, TimelineRowGroup } from 'mq-timeline-calendar/react';

function ProductionSchedule() {
  return (
    <TimelineCalendar
      startDate={new Date('2025-01-01')}
      endDate={new Date('2025-12-31')}
      height="600px"
      showCurrentTime={true}
      theme="dark"
    >
      <TimelineRowGroup>
        <TimelineRow id="line-a" label="Production Line A" rowCount={2} collapsible={true}>
          <TimelineItem startTime="2025-03-10" duration="5 days" row={0}>
            <div className="order">Order #1234</div>
          </TimelineItem>
          <TimelineItem startTime="2025-03-18" duration="3 days" row={1}>
            <div className="order">Order #1235</div>
          </TimelineItem>
        </TimelineRow>

        <TimelineRow id="line-b" label="Production Line B" rowCount={1} collapsible={true}>
          <TimelineItem startTime="2025-03-12" duration="4 days" row={0}>
            <div className="order">Order #1240</div>
          </TimelineItem>
        </TimelineRow>
      </TimelineRowGroup>
    </TimelineCalendar>
  );
}

Draggable Items

function DraggableTimeline() {
  const [items, setItems] = useState([
    { id: 1, time: new Date('2025-03-15'), duration: '5 days', row: 0 },
    { id: 2, time: new Date('2025-03-20'), duration: '3 days', row: 1 },
  ]);

  const handleDragEnd = (itemId, newTime, newRow) => {
    setItems(prev => prev.map(item =>
      item.id === itemId
        ? { ...item, time: new Date(newTime), row: newRow }
        : item
    ));
  };

  return (
    <TimelineCalendar
      startDate={new Date('2025-01-01')}
      endDate={new Date('2025-12-31')}
      height="600px"
    >
      <TimelineRow id="tasks" rowCount={3} collapsible={false}>
        {items.map(item => (
          <TimelineItem
            key={item.id}
            startTime={item.time}
            duration={item.duration}
            row={item.row}
            draggable={true}
            allowRowChange={true}
            onDragEnd={(newTime, _, newRow) => handleDragEnd(item.id, newTime, newRow)}
          >
            <div>Task {item.id}</div>
          </TimelineItem>
        ))}
      </TimelineRow>
    </TimelineCalendar>
  );
}

Timeline with Aggregation

function LargeDatasetTimeline() {
  // Generate hundreds of items
  const items = useMemo(() => {
    const result = [];
    for (let i = 0; i < 200; i++) {
      const dayOffset = Math.floor(Math.random() * 365);
      const types = ['production', 'maintenance', 'testing', 'downtime'];
      result.push({
        id: i,
        startTime: addDays(new Date('2025-01-01'), dayOffset),
        duration: `${Math.floor(Math.random() * 5) + 1} days`,
        type: types[Math.floor(Math.random() * types.length)],
        row: Math.floor(Math.random() * 3)
      });
    }
    return result;
  }, []);

  return (
    <TimelineCalendar
      startDate={new Date('2025-01-01')}
      endDate={new Date('2025-12-31')}
      height="600px"
      showCurrentTime={true}
    >
      <TimelineRowGroup>
        <TimelineRow
          id="production"
          label="Production Line"
          rowCount={3}
          collapsible={true}
          aggregation={{
            enabled: true,
            threshold: "6 months",
            granularity: "dynamic",
            minItemsForAggregation: 50
          }}
          getAggregatedTypeStyle={(type) => {
            const styles = {
              production: { backgroundColor: '#10b981', color: 'white' },
              maintenance: { backgroundColor: '#f59e0b', color: 'white' },
              testing: { backgroundColor: '#3b82f6', color: 'white' },
              downtime: { backgroundColor: '#ef4444', color: 'white' }
            };
            return styles[type] || { backgroundColor: '#6b7280', color: 'white' };
          }}
        >
          {items.map(item => (
            <TimelineItem
              key={item.id}
              startTime={item.startTime}
              duration={item.duration}
              row={item.row}
              type={item.type}
            >
              <div style={{ padding: '4px', fontSize: '12px' }}>
                {item.type} #{item.id}
              </div>
            </TimelineItem>
          ))}
        </TimelineRow>
      </TimelineRowGroup>
    </TimelineCalendar>
  );
}

Browser Support

  • Chrome/Edge (latest)
  • Firefox (latest)
  • Safari (latest)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT © [Your Name]

Links