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.
- Convert the incoming start and end to milliseconds.
- Add
interval_ms / 2for nearest-boundary rounding, or skip it for floor rounding. - Subtract
ms % interval_msand convert back to a date. - 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.
- Capture the original start and end from the event data before applying the edit.
- Compute
oldDur = oldEnd - oldStartandnewDur = newEnd - newStartin milliseconds. - If the durations are equal, treat it as a move. Snap the start only, then set
end = snappedStart + oldDur. - If the durations differ, treat it as a resize. Snap only the edge that changed and leave the other edge untouched.
- 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.
- Add a logger line in the handler that records raw and snapped values plus
ms % interval_msfor each edge.system.util.getLoggerwrites to the gateway log for Perspective and the client console for Vision. - 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.
- Confirm every logged snapped remainder is 0, or that
midnight(start) == startfor day events. - Confirm the bar visibly jumps to the boundary on release and that the stored event data matches the logged snapped value.
- Run move and resize tests separately. A move must preserve duration exactly; a resize must change only the dragged edge.
- 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.