Pro modulesApplies toSuperScheduler Pro
Dependencies and links between events
Pass links as an array of { id, from, to, type } objects, where from and to are event ids; FinishToStart is the default type and curved lines are the default drawing. Style links with color, width, style and cssClass or the --super-scheduler-link tokens, and handle clicks with onLinkClick. The library draws links but does not enforce them: check dependencies in onEventMoving and onEventMove. Creating links by dragging between events is reserved and not implemented in 0.1.0.
Production steps, shoots and campaigns depend on each other: cutting starts when design ends, two crews start together. SuperScheduler draws those dependencies as links between events and keeps them attached while users scroll, zoom and drag. What a dependency means for scheduling (whether a move is allowed, whether successors shift) stays in your code, where your rules live.
Links need SuperScheduler Pro.
Draw links from data
A link is a plain object in the links prop. from and to are event ids (the id of EventData); id identifies the link itself.
import type { SuperScheduler } from 'super-scheduler'
// `from` and `to` are event ids. Links between ids that are not loaded are not drawn.
export const LINKS: SuperScheduler.LinkData[] = [
// Cutting can start once design has finished (the default type).
{ id: 'design-cut', from: 'design', to: 'cutting', type: 'FinishToStart' },
// Painting and drying start together.
{ id: 'paint-dry', from: 'painting', to: 'drying', type: 'StartToStart', style: 'dashed' },
// Inspection and packing finish together; drawn below the events, in a fixed color.
{
id: 'qa-pack',
from: 'inspection',
to: 'packing',
type: 'FinishToFinish',
color: '#7c3aed',
layer: 'Below',
},
// A heavier line, styled further by CSS through its class.
{ id: 'ship', from: 'packing', to: 'shipping', width: 3, cssClass: 'link--critical' },
]
export const LINK_OPTIONS = {
linkShape: 'Curved', // default; 'RightAngled' draws segments
linkBottomMargin: 12, // clearance for links that run below events (8 px minimum for curves)
linkLayer: 'Above', // default layer of links without their own `layer`
links: LINKS,
} satisfies SuperScheduler.SchedulerConfigYou should see four lines: a solid curve from design to cutting, a dashed one between painting and drying, a violet one under the inspection and packing events, and a thicker one into shipping.
Links come from your data model: a dependsOn column, a table of dependencies, or a rule such as "same order, next step". Derive the array with useMemo so its identity changes only when the data does. From code, control.links offers list, add(link), update(link), remove(link or id), find(id), findByFromTo(from, to) and findAllByFromTo(from, to); with React state, prefer updating your array.
Link types
type | Drawn from | To | Typical meaning |
|---|---|---|---|
'FinishToStart' (default) | the end of from | the start of to | to starts after from finishes |
'StartToStart' | the start of from | the start of to | both start together, or to after from starts |
'FinishToFinish' | the end of from | the end of to | both finish together |
'StartToFinish' | the start of from | the end of to | to finishes when from starts |
The type changes where the line attaches and how it is drawn. It does not change what users may do; see enforce dependencies.
Shape and routing
| Option | Default | Effect |
|---|---|---|
linkShape | 'Curved' | Smooth curves with a small ring at the origin and an arrow head. 'RightAngled' draws straight segments. |
linkBottomMargin | 17 | Clearance for links that pass below events (at least 8 px for curves) |
linkLayer | 'Above' | Default layer for links without their own layer: above or below the events |
linkWidth | 'Auto' | Default stroke width in pixels (curves are 2 px) |
Links between ends on the same side bow outward with a bounded reach, so links to distant rows do not create oversized loops. Backward links use the gap between rows, and run below both events when those rows overlap. The routes avoid the two linked events, not every event in between: this is not an obstacle-avoidance router.
Style links
Per link, color, width, style ('solid', 'dashed', 'dotted'), layer and cssClass change one link; an explicit width wins over the CSS width token. For the rest, use tokens on any ancestor or on a link class:
.planning {
--super-scheduler-link: #8b5cf6;
--super-scheduler-link-hover: #6d28d9;
--super-scheduler-link-width: 2;
}
.dark .planning {
--super-scheduler-link: #b89afa;
--super-scheduler-link-hover: #d8c6ff;
}
/* A class from your data or from onBeforeLinkRender */
.link--late {
--super-scheduler-link: var(--super-scheduler-danger);
}Links are SVG elements with the class super-scheduler__link and data-super-scheduler-part="link"; the arrow and origin ring take the link's color.
To style from the state of the linked events, use onBeforeLinkRender. It receives a copy of the link as args.data, plus the two events as args.from and args.to. Change color, width, style, cssClass, layer or type on the copy; the ends cannot change. The hook runs once per drawn link and again only when one of its events changes appearance.
When links are drawn
A link is drawn when both events are loaded and laid out in the same section of the grid (the scrolling rows, or the same frozen top or bottom rows). It is not drawn when:
- either id is not loaded, for example outside the range you fetched;
- an end is hidden by a filter or sits in a collapsed tree row;
- the zoom level draws an end as a thin overview bar;
- it lies entirely outside the rendered area (it reappears when scrolled into view).
While an event is dragged, its links follow the preview with the same routing.
Click, hover and menus
onLinkClickruns on a click withargs.link(aSuperScheduler.Link:data,id(),from(),to(),type()) andargs.originalEvent;args.preventDefault()skipsonLinkClicked, which runs after it.linkBubbleshows a link'sbubbleHtmlon hover.contextMenuLink, or a link's owncontextMenu, opens on right click.
Links take pointer input only when one of these is configured, so by default they never get in the way of event drags. Their invisible hit area is wider than the visible line.
Enforce dependencies in your application
Links are drawings. Nothing stops a user from moving cutting before design ends unless your code checks it. Write the rule once as a plain function:
import { SuperScheduler } from 'super-scheduler'
export interface Bounds {
/** Ticks of the civil wall clock, as SuperScheduler.Date.getTime() returns them. */
readonly start: number
/** Exclusive end. */
readonly end: number
}
export const boundsOf = (event: SuperScheduler.EventData): Bounds => ({
start: new SuperScheduler.Date(event.start).getTime(),
end: new SuperScheduler.Date(event.end).getTime(),
})
/**
* Finish-to-start rule: a task may not start before each of its predecessors ends.
* Returns the first link that `proposed` times for `taskId` would break, or null.
* Other link types need rules of their own; links only draw what the data says.
*/
export function brokenFinishToStart(
taskId: SuperScheduler.EventId,
proposed: Bounds,
links: readonly SuperScheduler.LinkData[],
find: (id: SuperScheduler.EventId) => Bounds | null,
): SuperScheduler.LinkData | null {
for (const link of links) {
if ((link.type ?? 'FinishToStart') !== 'FinishToStart') continue
if (link.to === taskId) {
const predecessor = find(link.from)
if (predecessor !== null && proposed.start < predecessor.end) return link
} else if (link.from === taskId) {
const successor = find(link.to)
if (successor !== null && successor.start < proposed.end) return link
}
}
return null
}
/** Ids of the links the current data already breaks, to draw them differently. */
export function lateLinks(
links: readonly SuperScheduler.LinkData[],
events: readonly SuperScheduler.EventData[],
): ReadonlySet<SuperScheduler.LinkData> {
const byId = new Map(events.map((event) => [event.id, boundsOf(event)]))
const late = new Set<SuperScheduler.LinkData>()
for (const link of links) {
const from = byId.get(link.from)
const to = byId.get(link.to)
if ((link.type ?? 'FinishToStart') === 'FinishToStart' && from && to && to.start < from.end)
late.add(link)
}
return late
}Then call it from the hooks that run during and before a change:
import { useCallback, useMemo, useState } from 'react'
import { SuperSchedulerComponent } from 'super-scheduler'
import type { SchedulerEventsChangeArgs, SchedulerProps, SuperScheduler } from 'super-scheduler'
import { boundsOf, brokenFinishToStart, lateLinks } from './finish-to-start'
import type { Bounds } from './finish-to-start'
export function ProductionPlan(props: {
resources: SuperScheduler.ResourceData[]
initialTasks: SuperScheduler.EventData[]
/** Dependencies come from your data: the library draws them, it does not invent them. */
initialLinks: SuperScheduler.LinkData[]
}) {
const [tasks, setTasks] = useState(props.initialTasks)
const [links, setLinks] = useState(props.initialLinks)
const owned = useMemo(() => tasks.slice(), [tasks])
// Links the data already breaks get a class; the color comes from CSS.
const drawnLinks = useMemo(() => {
const late = lateLinks(links, tasks)
return links.map((link) => (late.has(link) ? { ...link, cssClass: 'link--late' } : link))
}, [links, tasks])
const onEventsChange = useCallback(
(args: SchedulerEventsChangeArgs) => setTasks([...args.events]),
[],
)
const config = useMemo<SchedulerProps>(() => {
// The other tasks as the control holds them right now.
const finder =
(control: SuperScheduler.SchedulerApi) =>
(id: SuperScheduler.EventId): Bounds | null => {
const event = control.events.find(id)
return event === null ? null : boundsOf(event.data)
}
return {
linkShape: 'Curved',
// While dragging: refuse and explain, frame by frame.
onEventMoving(args) {
const proposed = { start: args.start.getTime(), end: args.end.getTime() }
const broken = brokenFinishToStart(args.e.id(), proposed, links, finder(this))
if (broken !== null) {
args.allowed = false
args.message = 'This breaks a finish-to-start dependency'
}
},
onEventResizing(args) {
const proposed = { start: args.start.getTime(), end: args.end.getTime() }
if (brokenFinishToStart(args.e.id(), proposed, links, finder(this)) !== null)
args.allowed = false
},
// Before the commit: also covers keyboard moves.
onEventMove(args) {
const proposed = { start: args.newStart.getTime(), end: args.newEnd.getTime() }
if (brokenFinishToStart(args.e.id(), proposed, links, finder(this)) !== null)
args.preventDefault()
},
// Clicking a link offers to remove the dependency.
onLinkClick(args) {
const link = args.link.data
void confirmWithUser('Remove this dependency?').then((ok) => {
if (ok) setLinks((current) => current.filter((item) => item.id !== link.id))
})
},
}
}, [links])
return (
<SuperSchedulerComponent
{...config}
events={owned}
links={drawnLinks}
onEventsChange={onEventsChange}
resources={props.resources}
startDate="2026-10-05"
days={14}
scale="Day"
/>
)
}You should see:
- while dragging a step before its predecessor ends, the drag card says "Not allowed · This breaks a finish-to-start dependency" and the drop is refused;
- links your data already breaks drawn in the danger color;
- a click on a link asking whether to remove that dependency.
onEventMoving and onEventResizing run on every frame of a drag, so keep the check cheap: look up only the links of the dragged event, as brokenFinishToStart does. onEventMove runs once before the commit and also covers keyboard moves and events dropped from outside.
Reserved: interactive link creation
To let users create dependencies today, add them from your own interface (for example "Add dependency" in an event's detail panel or context menu, picking the other event from a list or from the current selection) and append the link to your array.
Related
Manufacturing order schedulingMaintenance was brought forward. Move the order out of the way and keep its operations in sequence. Video production planningA shoot runs long. Move the edit that depended on it, see why, and take it back. Port berth planningA ship arrives twelve hours late. Move its berth window, then bring its tug and cranes along.
- Drag, resize and business rules for
onEventMoving,onEventMoveand refusal messages. - Undo, redo and history to make rescheduling undoable.
Related examples
- ForgeManufacturing order schedulingMaintenance was brought forward. Move the order out of the way and keep its operations in sequence.
- FramehouseVideo production planningA shoot runs long. Move the edit that depended on it, see why, and take it back.
- HarborworksPort berth planningA ship arrives twelve hours late. Move its berth window, then bring its tug and cranes along.
- Studio NorthCreative agency resource planningA designer is double-booked. Hand two tasks to a colleague in one drag, check what they unblock, and take it back.