Timeline Views and Zoom

ApexGantt renders tasks on a scrollable horizontal timeline where the zoom level determines how many pixels represent one calendar day. That single number, pixelsPerDay, controls how much time fits in the visible area: a high value zooms in to show a few days in detail, a low value zooms out to show months or years at a glance. The header tiers (year, quarter, month, week, day, halfday, hour, minute) are picked automatically from the current density.

Timeline density with pixelsPerDay

pixelsPerDay is a continuous number: any value between roughly 0.25 and 1280 is accepted. Rather than a fixed set of named modes, you set the exact density you want and the header re-tiers itself to match.

These calibrated reference points map the familiar named scales to a pixelsPerDay value, so you can reach for a preset density or pick anything in between:

Named scalepixelsPerDayApproximate time on screenTypical use
Year0.5Multi-yearExecutive overviews
Quarter1.61-2 yearsRoadmaps, long-term planning
Month4.93-6 monthsMost projects
Week25.71-3 monthsSprint planning, short projects
Day801-2 weeksDetailed scheduling, hour-level work

Setting the zoom at construction

Pass pixelsPerDay in the options object. If omitted, the chart auto-fits the data span (see below).

import ApexGantt from 'apexgantt'

ApexGantt.setLicense('YOUR-LICENSE-KEY')

const gantt = new ApexGantt(document.getElementById('gantt'), {
  series: [
    { id: '1', name: 'Discovery',   startTime: '2024-04-01', endTime: '2024-04-14' },
    { id: '2', name: 'Design',      startTime: '2024-04-15', endTime: '2024-04-30' },
    { id: '3', name: 'Development', startTime: '2024-05-01', endTime: '2024-06-15' },
    { id: '4', name: 'Testing',     startTime: '2024-06-10', endTime: '2024-06-30' },
  ],
  pixelsPerDay: 25.7, // week-density timeline
})

gantt.render()

Changing the zoom at runtime

Call update() with a new pixelsPerDay. The timeline re-renders immediately without discarding task data.

// Zoom out to a quarterly overview
gantt.update({ pixelsPerDay: 1.6 })

// Zoom back in to week-level detail
gantt.update({ pixelsPerDay: 25.7 })

// Any value in between works too, for a density between week and day
gantt.update({ pixelsPerDay: 40 })

update() accepts a partial options object, so you can change only pixelsPerDay while leaving all other configuration untouched.

Auto-fit zoom

When you omit pixelsPerDay, ApexGantt calculates a value that fits the full span of your task data into the available container width on first render. This is the default behavior. It is useful when you do not know the date range of the data ahead of time and want the chart to always start fully visible without horizontal scrolling.

Once the user scrolls or a programmatic zoom is applied, the auto-fit value is no longer re-calculated.

Programmatic zoom buttons

zoomIn() and zoomOut() step through the preset densities. Each call moves one level in the direction indicated. The underlying pixelsPerDay is updated and the header tiers adjust automatically.

  • zoomIn() moves toward finer detail: Year → Quarter → Month → Week → Day. Calling zoomIn() when already at Day has no effect.
  • zoomOut() moves toward coarser overview: Day → Week → Month → Quarter → Year. Calling zoomOut() when already at Year has no effect.

A minimal toolbar using plain buttons:

<div id="gantt" style="height: 400px;"></div>
<div class="zoom-controls">
  <button id="zoom-in">Zoom in</button>
  <button id="zoom-out">Zoom out</button>
</div>
import ApexGantt from 'apexgantt'

ApexGantt.setLicense('YOUR-LICENSE-KEY')

const gantt = new ApexGantt(document.getElementById('gantt'), {
  series: [
    { id: '1', name: 'Planning',    startTime: '2024-01-01', endTime: '2024-03-31' },
    { id: '2', name: 'Execution',   startTime: '2024-04-01', endTime: '2024-09-30' },
    { id: '3', name: 'Review',      startTime: '2024-10-01', endTime: '2024-12-31' },
  ],
  pixelsPerDay: 4.9, // month-density timeline
})

gantt.render()

document.querySelector('#zoom-in').addEventListener('click', () => gantt.zoomIn())
document.querySelector('#zoom-out').addEventListener('click', () => gantt.zoomOut())

The built-in toolbar also includes zoom controls automatically. If you prefer to rely on those instead of custom buttons, set toolbarItems to include the zoom items or leave the toolbar at its default configuration.

React

In React, bind pixelsPerDay to component state. The react-apexgantt wrapper diffs props on every render and calls update() internally only when a prop changes, so you do not need to manage the chart instance manually.

import { useState } from 'react'
import { ApexGanttChart } from 'react-apexgantt'

const tasks = [
  { id: '1', name: 'Planning',  startTime: '2024-01-01', endTime: '2024-03-31' },
  { id: '2', name: 'Execution', startTime: '2024-04-01', endTime: '2024-09-30' },
  { id: '3', name: 'Review',    startTime: '2024-10-01', endTime: '2024-12-31' },
]

// Preset densities, labelled for a simple selector.
const PRESETS = [
  { label: 'Day', value: 80 },
  { label: 'Week', value: 25.7 },
  { label: 'Month', value: 4.9 },
  { label: 'Quarter', value: 1.6 },
  { label: 'Year', value: 0.5 },
]

export default function GanttWithZoomSelector() {
  const [pixelsPerDay, setPixelsPerDay] = useState(4.9)

  return (
    <div>
      <div style={{ marginBottom: 12, display: 'flex', gap: 8 }}>
        {PRESETS.map((p) => (
          <button
            key={p.label}
            style={{ fontWeight: pixelsPerDay === p.value ? 'bold' : 'normal' }}
            onClick={() => setPixelsPerDay(p.value)}
          >
            {p.label}
          </button>
        ))}
      </div>
      <ApexGanttChart tasks={tasks} pixelsPerDay={pixelsPerDay} height="400px" />
    </div>
  )
}

To call zoomIn() and zoomOut() from buttons in React, use the imperative ref API. See React ApexGantt for the full ApexGanttHandle reference.

import { useRef } from 'react'
import { ApexGanttChart } from 'react-apexgantt'
import type { ApexGanttHandle } from 'react-apexgantt'

export default function GanttWithZoom() {
  const ganttRef = useRef<ApexGanttHandle>(null)

  return (
    <div>
      <div style={{ marginBottom: 12, display: 'flex', gap: 8 }}>
        <button onClick={() => ganttRef.current?.zoomIn()}>Zoom in</button>
        <button onClick={() => ganttRef.current?.zoomOut()}>Zoom out</button>
      </div>
      <ApexGanttChart ref={ganttRef} tasks={tasks} height="400px" />
    </div>
  )
}

Vue

In Vue, bind pixels-per-day to a ref. The vue-apexgantt wrapper handles the update() call when the prop changes.

<template>
  <div>
    <div style="margin-bottom: 12px; display: flex; gap: 8px;">
      <button
        v-for="p in presets"
        :key="p.label"
        :style="{ fontWeight: pixelsPerDay === p.value ? 'bold' : 'normal' }"
        @click="pixelsPerDay = p.value"
      >
        {{ p.label }}
      </button>
    </div>
    <ApexGanttChart :tasks="tasks" :pixels-per-day="pixelsPerDay" height="400px" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { ApexGanttChart } from 'vue-apexgantt'

const presets = [
  { label: 'Day', value: 80 },
  { label: 'Week', value: 25.7 },
  { label: 'Month', value: 4.9 },
  { label: 'Quarter', value: 1.6 },
  { label: 'Year', value: 0.5 },
]
const pixelsPerDay = ref(4.9)

const tasks = [
  { id: '1', name: 'Planning',  startTime: '2024-01-01', endTime: '2024-03-31' },
  { id: '2', name: 'Execution', startTime: '2024-04-01', endTime: '2024-09-30' },
  { id: '3', name: 'Review',    startTime: '2024-10-01', endTime: '2024-12-31' },
]
</script>

To call zoomIn() and zoomOut() from Vue, use a template ref. See Vue ApexGantt for the full template ref method list.

Crosshair

The crosshair is a vertical line that tracks the cursor across the timeline. It shows a date label at the top of the timeline as the user moves the mouse, making it easy to read off exact dates on dense charts.

Enable it with enableCrosshair: true:

const gantt = new ApexGantt(document.getElementById('gantt'), {
  series: tasks,
  enableCrosshair: true,
})

gantt.render()

Crosshair color

Change the line color with crosshairColor. Any CSS color value is accepted.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  series: tasks,
  enableCrosshair: true,
  crosshairColor: '#F59E0B',
})

gantt.render()

Custom crosshair label

The crosshairLabelFormat callback lets you control the text shown in the date label. It receives the Date under the cursor and a tier string that describes which calendar resolution is currently active at the cursor position.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  series: tasks,
  enableCrosshair: true,
  crosshairLabelFormat: (date, tier) => {
    // tier: 'minute' | 'hour' | 'halfday' | 'day' | 'week' | 'month' | 'quarter' | 'year'
    if (tier === 'day' || tier === 'halfday') {
      return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
    }
    if (tier === 'hour' || tier === 'minute') {
      return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
    }
    return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })
  },
})

gantt.render()

The tier value matches the current header resolution, so the label can stay contextually relevant as the user zooms in and out.

Snap behavior

When a user drags or resizes a task bar, ApexGantt snaps the task boundaries to the nearest unit. This prevents tasks from landing on arbitrary timestamps and keeps the schedule clean.

snapUnit sets the unit of the grid: 'day', 'hour', or 'minute'. snapValue sets how many of those units form one snap interval. The default is 1, meaning every day (or hour, or minute). Set it higher to enforce coarser alignment.

const gantt = new ApexGantt(document.getElementById('gantt'), {
  series: tasks,

  // Snap to the nearest half-day boundary
  snapUnit: 'hour',
  snapValue: 12,
})

gantt.render()

Common patterns:

// Daily snap (default)
{ snapUnit: 'day', snapValue: 1 }

// Weekly snap: tasks align to week boundaries
{ snapUnit: 'day', snapValue: 7 }

// Two-hour snap for sub-day scheduling
{ snapUnit: 'hour', snapValue: 2 }

// 15-minute snap for fine scheduling
{ snapUnit: 'minute', snapValue: 15 }

Snap only applies when enableTaskDrag or enableTaskResize is true. If both are false, the snap settings have no visible effect.

Scrolling to a task

On a long timeline a task's bar can sit well outside the visible window. Three mechanisms bring it back into view.

enableScrollButtons (default false since 3.15.0) shows a chevron at the edge of any row whose bar is scrolled out of the visible window; clicking it jumps that bar into view. Set it to true to opt in.

scrollToTaskOnRowClick (default false) reveals a task's bar when you click its row in the task list, using the same nearest-edge alignment. It composes with enableSelection, so a single click both selects the row and scrolls its bar into view. Clicks on the selection checkbox, the toggle chevron, and active inline-edit inputs are ignored, so those controls keep working.

new ApexGantt(el, { series: tasks, scrollToTaskOnRowClick: true })

scrollToTask(id) does the same programmatically, using nearest-edge alignment (the minimum scroll that reveals the bar) both horizontally and vertically. It works even when the target row is virtualized out of the DOM, is unaffected by the enableScrollButtons default, and returns whether the task was found and scrolled:

const found = gantt.scrollToTask('task-42')

Call it to locate a task from search results, an external list, or your own toolbar button.