Skip to content

perf(llc/frontend): Gantt axis ticks are generated for the whole range with no viewport culling #14769

Closed

Description

@mrveiss

Problem

axisTicks emits one tick for every step across the entire data range with no
viewport culling.

autobot-frontend/src/views/llc/GanttTimelineView.vue:284-298

const axisTicks = computed(() => {
  const ticks: { x: number; label: string }[] = []
  const step = zoom.value === 'day' ? 1 : zoom.value === 'week' ? 7 : zoom.value === 'month' ? 30 : 91
  for (let ms = rangeStart.value; ms <= rangeEnd.value; ms += step * DAY_MS) {
    ...
    ticks.push({ x: xForMs(ms), label })
  }
  return ticks
})

At zoom: 'day' the step is 1 day and ZOOM_PX_PER_DAY.day is 40 (line 161),
so a multi-year range produces thousands of SVG tick elements, nearly all of
them outside the visible scroll window. Each carries a toLocaleDateString
call as well.

Proposed fix

Clamp the loop to the visible horizontal window (scroll offset + client width,
converted back to ms via the inverse of xForMs) plus a small overscan, and
recompute on scroll. Label formatting then only runs for ticks that render.

Acceptance criteria

  • Tick count is bounded by the viewport width divided by tick spacing, not
    by the data range.
  • Ticks still appear correctly when scrolling to either end of the range at
    every zoom level (day / week / month / quarter).
  • Test asserts tick count stays bounded for a multi-year range at
    zoom: 'day'.

Source

Found while reading our own Gantt grid code during
docs/research/canvas-grid-rendering-review.md Phase 2. Recorded per the
report-and-fix-every-discovered-problem rule rather than folded into an
unrelated change. Sibling finding in the same file: #14767.

Activity

  1. mrveiss commented on Aug 23, 2026

    @mrveiss
    OwnerAuthor

    Closed — verified in base

    Delivered by #14796, merged into Dev_new_gui as d88b6d8aa.

    Closed by hand — PRs here target Dev_new_gui, not the default branch, so the
    Closes #... keyword never fires. Evidence read from origin/Dev_new_gui, not
    from the PR diff.

    Axis ticks are culled to the visible window (TICK_OVERSCAN_PX, 400px either
    side), driven by a ResizeObserver attached from a watch on the element ref
    — .gantt-scroll sits behind v-else and does not exist until a timeline
    loads, so an onMounted-only hookup would have observed nothing and left the
    fallback permanently engaged while appearing to work.

    Two invariants matter more than the culling itself, and both are in base:

    The interaction with #14767 is the part worth remembering. Culling is
    suspended during a PNG export, or the export would have silently shipped a
    chart missing every gridline and date label outside the viewport — a worse
    defect than the 1x softness being fixed, and invisible in review, because a
    complete PNG and a gutted one both look like "an export happened". There is a
    test that counts gridlines at serialisation time via a stubbed
    XMLSerializer, and another asserting culling is restored after a failed
    export.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions