Skip to main content

Scales

This guide covers the two scales you configure most often: the time axis on day, week, resources, and timeline views - which hours appear, how tall each slot is, where dragged events snap - and the unit scale that maps events onto resource columns or rows, including events assigned to several resources at once.

How the time axis works

Time-based views (day, week, resources, timeline) build their grid from a time scale on one of the section axes. The scale is just configuration: a window between two hours, divided into slots of a fixed length, with an optional snap step for drag-to-create and resize.

Three numbers shape it:

  • startHour / endHour - the visible window. Defaults are 8 and 18. Events outside this range stay in the data but don't render in this view.
  • step - minutes per slot. Default 60. Each slot is one row of header cells and one drop target.
  • snapStep - minutes per snap increment when the user drags or resizes. Defaults to step. Set false to disable snapping; events land exactly where the pointer releases.

The defaults live inside each view's section definition. To change them, pass a ViewConfig object in views and deep-merge overrides under sections.timeGrid.yScale - or xScale for timeline, where the time axis runs horizontally. Only the keys you set are replaced; format, ui, and other defaults stay intact.

Slot pixel size is a separate setting. It lives under the same scale's ui.minUnitHeight (vertical time axes) or ui.minUnitWidth (horizontal ones), not under sections.ui.

Setting visible hours

Restrict the day to working hours by overriding startHour and endHour on the timeGrid section's yScale:

<Calendar
:events="data"
view="day"
:views="[
{
id: 'day',
sections: {
timeGrid: {
yScale: { startHour: 9, endHour: 17 },
},
},
},
'week',
'month',
]"
/>

The same override shape works for week. For timeline, the time axis is xScale instead - replace yScale with xScale in the override.

Adjusting slot size

step controls how many minutes one row covers. Two-hour rows give a sparser grid; 30-minute rows give a denser one. Pair step with a different snap if you want sub-row precision:

<Calendar
:events="data"
view="week"
:views="[
{
id: 'week',
sections: {
timeGrid: {
yScale: {
startHour: 8,
endHour: 20,
step: 120,
snapStep: 30,
},
},
},
},
]"
/>

The user sees one row per two hours, but dragging snaps every 30 minutes - so a 9:30-10:00 event is still creatable without cluttering the grid.

Controlling slot height

Row height is read from yScale.ui.minUnitHeight (in pixels). Increase it for a roomier grid; decrease it to fit more hours on screen:

<Calendar
:events="data"
view="day"
:views="[
{
id: 'day',
sections: {
timeGrid: {
yScale: {
startHour: 0,
endHour: 24,
step: 60,
ui: { minUnitHeight: 40 },
},
},
},
},
]"
/>

A full 24-hour day with 40px rows scrolls comfortably; the default 100 would stretch it across a much longer page. The same key drives row height in resources. For timeline the time axis is horizontal, so use minUnitWidth instead.

Snap granularity

Snap only affects pointer-driven actions: create-by-drag, move, and resize. It doesn't change the rendered slot boundaries. Use it to decouple visual density from edit precision:

<Calendar
:events="data"
view="day"
:views="[
{
id: 'day',
sections: {
timeGrid: {
yScale: { snapStep: 15 },
},
},
},
]"
/>

Set snapStep: false to let users place events at arbitrary minute offsets. Omit snapStep to inherit step.

Switching configurations at runtime

views is a regular prop, so you can derive it from app state. The calendar picks up the new configuration when the array changes:

<script setup>
import { ref, computed } from "vue";

const mode = ref("work");

const views = computed(() => [
{
id: "day",
sections: {
timeGrid: {
yScale: {
startHour: mode.value === "work" ? 8 : 0,
endHour: mode.value === "work" ? 18 : 24,
step: mode.value === "work" ? 60 : 120,
ui: { minUnitHeight: 40 },
},
ui: { nowLine: true },
},
},
},
]);
</script>

<template>
<Calendar :events="events" view="day" :views="views" />
</template>

Use the same pattern to toggle full-day vs. working-hour layouts, swap step sizes per user preference, or shrink the grid for compact modes.

Unit scales

Resource columns and timeline rows come from a unit scale instead of a time or date one. It takes the list of items to render and an accessor that tells the store which item an event belongs to:

const scale = {
type: "unit",
items: [
{ id: "room-a", label: "Room A" },
{ id: "room-b", label: "Room B" },
],
accessor: "roomId",
};

A string accessor is a field name - it reads event.roomId to place the event and writes the same field back when the event is dropped into another unit. When the value doesn't map to an id directly, pass an object with an explicit get/set pair instead.

Where the scale lives depends on the axis the units occupy: sections.timeGrid.xScale in resources (units as columns), sections.timeGrid.yScale in timeline (units as rows), or the nested inner/outer scale of a combined scale.

Multi-unit events

By default one event belongs to one unit. Set multiple: true on the unit scale and the accessor may return an array, so a single stored event can occupy several resources - a meeting with two hosts, a shift covering two rooms:

const scale = {
type: "unit",
items: rooms,
accessor: "roomId",
multiple: true,
};

const events = [
{ id: 1, text: "Joint standup", start, end, roomId: ["room-a", "room-b"] },
{ id: 2, text: "Design sync", start, end, roomId: "room-b" },
];

The option changes both what users see and what an edit writes back:

  • Rendering. The event is drawn once per valid, unique id in the array - unknown ids and duplicates are dropped. There is still only one stored record behind those copies, so eventCss, eventContent, tooltips, and click handlers all receive the same event.
  • Editing. Each rendered copy carries its source unit inside the rendered id, so dragging one copy into another unit dispatches move-event and replaces only that assignment, keeping the rest of the array and deduplicating the result. The other copies stay where they are. An update-event that carries the whole field - from your own code or from an editor item - replaces the array as given; the built-in editor has no unit field, so a form that manages assignments is yours to add.

A scalar value keeps working while multiple: true is on, which is what makes migration painless: switch the option on, and move records to arrays whenever it suits you.

The option belongs to the unit scale itself, so it works in the resources and timeline views on its own - a combined scale is not required. Inside a combined scale only one nested unit scale may use it, because a render id carries a single source unit.