Configuring Equipment Schedule Time Snap in Ignition

Tom Garrett7 min read
HMI / SCADAOther ManufacturerTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

In this installation, the Perspective Equipment Schedule returns drag and resize results on a continuous millisecond timeline. A drop can land at 13:01:18 even when the view is zoomed to 15-minute divisions. Snapping appeared in a Designer preview session, disappeared after preview mode was stopped and restarted, and never appeared in a Chrome session. That pattern points to an inconsistency inside the component, not a missing setting.

The fix that holds in every environment is to quantize timestamps in the edit handler. Subtract the modulus of the epoch milliseconds for sub-day intervals, use midnight() for day boundaries, and write the snapped values back so the bar jumps visibly onto the grid. The same method corrects Vision 8.1.44 day-granularity events whose dragged end lands a few minutes past midnight.

Snap Behavior Across Designer, Preview Restart, and Browser Sessions

One quantity decides the case: the remainder of the stored timestamp against the interval, ms % interval_ms. If that remainder is non-zero after an edit, the component handed back a raw pointer position. Whatever the bar appeared to do on screen, nothing downstream quantized the value. Treat this as a logic fault in the data path, not a rendering or performance problem. Redraw speed, client load, and browser caching do not move a timestamp off the grid.

Environment Observed drag result What it tells you
Perspective, Designer preview (first run) Snaps to zoom-level boundaries The component has a snapping path, but it is not applied consistently
Perspective, Designer preview after stop/start Continuous timeline, no snap Snap state is not persistent across preview sessions; behaves like a defect
Perspective, Chrome session No snap at all (e.g. 13:01:18 at 15-min zoom) Production users get raw times; the handler must quantize
Vision 8.1.44, events forced to 00:00:00 to 23:59:59 Dragged end lands a few minutes past midnight, rolling into the next day Pointer resolution at coarse zoom exceeds the tolerance of an unsnapped day boundary

Because the Designer result cannot be reproduced reliably, test snapping only in a real browser session. Do not accept a Designer preview as proof.

Pixel-to-Time Resolution Behind Off-Grid Drops

A drag converts pointer position to time roughly as t = view_start + x × (visible_span_ms / width_px). Each pixel therefore carries visible_span_ms / width_px milliseconds. Without a quantization step, the component returns that raw product. The seconds field ends up holding whatever the mouse happened to cover.

That is the size of the "over midnight by a few minutes" error seen on Vision day-zoom drags. At 15-minute zoom the per-pixel resolution is much finer, so the error shows up as odd seconds (13:01:18) instead of a day rollover. Read your own figure from the schedule's visible start/end and the component's rendered width.

Quantity Target / limit Where to read it
ms % interval_ms on snapped start/end 0 Log the value in the edit handler
ms per pixel at current zoom Compare with half the snap interval Visible span divided by component width
Day-event start vs midnight(start) Equal Handler log or the schedule's event data
Day-event end 23:59:59 (closed convention) or next midnight (half-open) Event data after write-back

Epoch-Modulus Snap for Second, Minute, and Hour Intervals

Convert the date to UTC epoch milliseconds, then subtract the remainder against the interval. Flooring alone biases every drop backward. For drag input, add half an interval first so the value rounds to the nearest boundary.

  1. Convert the incoming start and end to milliseconds.
  2. Add interval_ms / 2 for nearest-boundary rounding, or skip it for floor rounding.
  3. Subtract ms % interval_ms and convert back to a date.
  4. Write the result back to the event data before any downstream logic consumes it.

Check the arithmetic on the observed value.

Day-Boundary Snap with midnight() and the End-of-Day Convention

Usemidnight() for day boundaries instead. It returns local midnight for the date passed in.

def snapDay(d):
    m = system.date.midnight(d)
    nxt = system.date.addDays(m, 1)
    # round to whichever local midnight is closer
    if system.date.millisBetween(m, d) >= system.date.millisBetween(d, nxt):
        return nxt
    return m

start = snapDay(rawStart)
endBoundary = snapDay(rawEnd)          # exclusive end (half-open)
end = system.date.addSeconds(endBoundary, -1)  # 23:59:59 closed convention

Rounding the end to the nearest midnight absorbs the few-minutes-past-midnight error directly. It rounds back to that midnight, and the one-second subtraction yields 23:59:59 on the intended day. This replaces ad-hoc fixes such as pulling every end back by ten minutes. Those break as soon as the zoom changes and the per-pixel error grows past the fixed offset.

Choose one end convention and hold to it:

  • Half-open [start, end): store the end as the next midnight. Duration is an exact multiple of the interval, and adjacent events share a boundary without overlap or gap.
  • Closed [start, end]: store 23:59:59, as this Vision installation does. This is readable for operators, but every duration calculation must add the missing second back.

Move-Versus-Resize Detection in the Edit Handler

A drag either moves the whole bar or stretches one edge. Snapping both edges independently on a move can change the duration by one interval when the two edges round in opposite directions. The reliable discriminator is the one already used on this Vision schedule: compare the new duration with the old one.

  1. Capture the original start and end from the event data before applying the edit.
  2. Compute oldDur = oldEnd - oldStart and newDur = newEnd - newStart in milliseconds.
  3. If the durations are equal, treat it as a move. Snap the start only, then set end = snappedStart + oldDur.
  4. If the durations differ, treat it as a resize. Snap only the edge that changed and leave the other edge untouched.
  5. Enforce a minimum duration of one interval so a resize cannot collapse the event to zero length.

For day-granularity events, swap snapMillis for snapDay. Use addDays rather than addMillis to carry the duration, so DST days do not shift the end by an hour.

Confirming Snapped Timestamps in the Schedule Data

The UI gives no hint that 13:01:18 will be stored as 13:00:00. Write the snapped values back into the schedule's event data inside the same handler. The bar then redraws on the boundary, and the user sees the correction the moment the drag ends.

  1. Add a logger line in the handler that records raw and snapped values plus ms % interval_ms for each edge. system.util.getLogger writes to the gateway log for Perspective and the client console for Vision.
  2. Open a real browser session (Chrome, as in this case) instead of Designer preview. Drag an event to deliberately off-grid positions at each zoom level you support.
  3. Confirm every logged snapped remainder is 0, or that midnight(start) == start for day events.
  4. Confirm the bar visibly jumps to the boundary on release and that the stored event data matches the logged snapped value.
  5. Run move and resize tests separately. A move must preserve duration exactly; a resize must change only the dragged edge.
  6. Repeat after stopping and restarting preview and after a session reload. Snapped results must not depend on session state, which is the failure seen with the native behavior.

Time-Zone, DST, and Historian-Range Pitfalls

Pitfall Mechanism Correction
Hour snap lands on :30 or :45 local Epoch modulus snaps to UTC boundaries; zones with non-whole-hour offsets misalign Add the zone offset before the modulus and remove it after, or snap from midnight() plus N intervals
Day snap off by hours in Perspective Perspective scripts run on the gateway; midnight() uses the gateway's time zone, not the session's Compare gateway and session time zones; compute midnight in the session's zone when they differ

The historian correction is separate from the schedule's own end convention. Keep half-open boundaries in your logic. Subtract the single millisecond only at the point where you call the historian.

FAQ

Why does the Ignition Equipment Schedule snap in the Designer but not in the browser?

Snapping appeared in a first Designer preview, disappeared after preview mode was stopped and restarted, and never occurred in a Chrome session. That points to a component-side inconsistency. Quantize start and end in the edit handler so the stored values are correct regardless of session.

Why does my dragged Equipment Schedule event end a few minutes past midnight?

At day-level zoom one pixel can represent several minutes. Round the end to the nearestmidnight(), then subtract one second if you store 23:59:59 ends.

How do I round a timestamp to 15 minutes in Ignition scripting?

ms % 900000, and convert back to a date. This works for any interval that divides evenly into the next larger unit.

Why does hour snapping land on the half hour in some time zones?

Epoch-millisecond modulus aligns to UTC boundaries, so zones with a non-whole-hour offset see snapped times at :30 or :45 local. Apply the zone offset before the modulus, or build boundaries from midnight() plus whole intervals.

If the snapped values log correctly but the bar still redraws off-grid, or the native snap behavior differs between preview runs on your version, stop patching around it. Report the component version, zoom level, and a reproduction sequence (Designer preview, preview restart, browser session) to Inductive Automation support.

Back to blog