refactor(documents): rework timeline UX after live testing (#385)
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m29s
CI / OCR Service Tests (push) Successful in 48s
CI / Backend Unit Tests (push) Failing after 3m46s
CI / Unit & Component Tests (pull_request) Failing after 4m31s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m31s
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m29s
CI / OCR Service Tests (push) Successful in 48s
CI / Backend Unit Tests (push) Failing after 3m46s
CI / Unit & Component Tests (pull_request) Failing after 4m31s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m31s
Replaces the discrete zoom-in button with a Graylog-style drag-to-zoom range selector and adds X/Y axis labels so the chart is readable. Drag interaction - Pointerdown on a bar attaches document-level pointermove/pointerup/ pointercancel listeners; pointermove maps clientX to a bar index via the row's bounding rect, so the mint-bordered window expands smoothly even when the cursor leaves the bar or the chart entirely. - pointerup commits filter + zoom atomically. Same-bar release on a year bar (year-aggregated mode) zooms into that year's months; same-bar release on a month bar emits filter-only. - setPointerCapture removed — it was suppressing pointerenter on sibling bars and preventing the drag window from expanding. - Bar buttons are now h-full so the entire 80 px column is the hit target, not just the visible bar height. Axis labels - Y-axis: max-count and 0 labels left of the bar area. - X-axis: tickIndicesFor() picks decadal years for long ranges, evenly spaced months for short year-zoom views, January boundaries for multi-year month ranges. formatTickLabel() drops the year when the visible range is a single year so 12-month zooms read "Jan Feb Mär…". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -102,6 +102,64 @@ export function selectionBoundaryTo(label: string): string {
|
||||
return monthBoundaryTo(label);
|
||||
}
|
||||
|
||||
/**
|
||||
* Picks bucket indices that should get an X-axis tick label. The strategy adapts
|
||||
* to whether bars are years or months and how many are visible:
|
||||
* - Year bars: pick years divisible by a step that scales with range length
|
||||
* (every 25 yrs for >120 bars, every 20 / 10 / 5 / 1 below).
|
||||
* - Month bars: prefer January boundaries (year breaks). For ≤18 bars (e.g.
|
||||
* one year zoomed in to months), fall back to evenly spaced ticks so we
|
||||
* show ~6 labels even when no January boundary exists.
|
||||
*/
|
||||
export function tickIndicesFor(filled: MonthBucket[]): number[] {
|
||||
if (filled.length === 0) return [];
|
||||
const isYearMode = filled[0].month.length === 4;
|
||||
const indices: number[] = [];
|
||||
|
||||
if (isYearMode) {
|
||||
const years = filled.length;
|
||||
const step =
|
||||
years > 120 ? 25 : years > 60 ? 20 : years > 30 ? 10 : years > 12 ? 5 : years > 6 ? 2 : 1;
|
||||
for (let i = 0; i < filled.length; i++) {
|
||||
const year = parseInt(filled[i].month, 10);
|
||||
if (year % step === 0) indices.push(i);
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
if (filled.length <= 18) {
|
||||
const step = Math.max(1, Math.round(filled.length / 6));
|
||||
for (let i = 0; i < filled.length; i += step) indices.push(i);
|
||||
return indices;
|
||||
}
|
||||
|
||||
// Long month range — pick January boundaries (year breaks).
|
||||
for (let i = 0; i < filled.length; i++) {
|
||||
if (filled[i].month.endsWith('-01')) indices.push(i);
|
||||
}
|
||||
// Fallback if there's no January in the visible range (rare): even spacing.
|
||||
if (indices.length === 0) {
|
||||
const step = Math.max(1, Math.round(filled.length / 6));
|
||||
for (let i = 0; i < filled.length; i += step) indices.push(i);
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a bucket month label ("YYYY" or "YYYY-MM") for the X-axis. When
|
||||
* `omitYear` is true the year is dropped so a 12-month zoomed view reads as
|
||||
* "Jan", "Feb", … without repetition.
|
||||
*/
|
||||
export function formatTickLabel(label: string, locale?: string, omitYear = false): string {
|
||||
if (label.length === 4) return label;
|
||||
const [yearStr, monthStr] = label.split('-');
|
||||
const date = new Date(parseInt(yearStr, 10), parseInt(monthStr, 10) - 1, 1);
|
||||
const opts: Intl.DateTimeFormatOptions = omitYear
|
||||
? { month: 'short' }
|
||||
: { month: 'short', year: 'numeric' };
|
||||
return new Intl.DateTimeFormat(locale, opts).format(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* The subset of /documents URL params that should narrow the density chart.
|
||||
* Date bounds (`from`/`to`) are intentionally excluded — see
|
||||
|
||||
Reference in New Issue
Block a user