app/TimeHistogram.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QDate> | ||
| 4 | #include <QList> | ||
| 5 | #include <QLocale> | ||
| 6 | #include <QMap> | ||
| 7 | #include <QString> | ||
| 8 | #include <QVector> | ||
| 9 | #include <functional> | ||
| 10 | |||
| 11 | // Sprint 101 S101.1.4 (D-7, D-8): the pure calendar arithmetic behind the | ||
| 12 | // search timeline. No Qt Quick, no SQL, no clock: every function maps its | ||
| 13 | // arguments to a result, so the chart, the breadcrumb, the date chip and the | ||
| 14 | // drill-down share one definition of "the bucket" (rule 8 of SPRINT-101). | ||
| 15 | // | ||
| 16 | // Ranges are inclusive local calendar days. Counts arrive once per query as | ||
| 17 | // counts per local day (plus the 24 hours of a single-day range) and are | ||
| 18 | // re-bucketed here; zooming therefore never needs a new SQL aggregate. | ||
| 19 | namespace TimeHistogram { | ||
| 20 | |||
| 21 | enum class Unit { Year, Month, Week, Day, Hour }; | ||
| 22 | |||
| 23 | struct Range { | ||
| 24 | QDate first; | ||
| 25 | QDate last; | ||
| 26 |
4/6✓ Branch 3 → 4 taken 67032 times.
✓ Branch 3 → 10 taken 3104 times.
✓ Branch 5 → 6 taken 67032 times.
✗ Branch 5 → 10 not taken.
✓ Branch 8 → 9 taken 67032 times.
✗ Branch 8 → 10 not taken.
|
70136 | bool isValid() const { return first.isValid() && last.isValid() && first <= last; } |
| 27 |
2/2✓ Branch 3 → 4 taken 2215 times.
✓ Branch 3 → 6 taken 2 times.
|
2217 | qint64 days() const { return isValid() ? first.daysTo(last) + 1 : 0; } |
| 28 |
5/6✓ Branch 3 → 4 taken 33021 times.
✗ Branch 3 → 11 not taken.
✓ Branch 6 → 7 taken 32332 times.
✓ Branch 6 → 11 taken 689 times.
✓ Branch 9 → 10 taken 5023 times.
✓ Branch 9 → 11 taken 27309 times.
|
33021 | bool contains(const QDate &date) const { return isValid() && date >= first && date <= last; } |
| 29 |
2/4✓ Branch 3 → 4 taken 397 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 397 times.
✗ Branch 5 → 7 not taken.
|
397 | bool operator==(const Range &other) const { return first == other.first && last == other.last; } |
| 30 | bool operator!=(const Range &other) const { return !(*this == other); } | ||
| 31 | }; | ||
| 32 | |||
| 33 | struct Bucket { | ||
| 34 | Range range; // the days the bar covers, clipped to the shown range | ||
| 35 | int hour = -1; // 0..23 for Unit::Hour, else -1 | ||
| 36 | qint64 count = 0; | ||
| 37 | }; | ||
| 38 | |||
| 39 | // A breadcrumb level: "All time" (invalid range) or a calendar range. | ||
| 40 | struct Level { | ||
| 41 | Range range; | ||
| 42 | QString label; | ||
| 43 | }; | ||
| 44 | |||
| 45 | // An axis label at a bucket boundary. | ||
| 46 | struct Tick { | ||
| 47 | int bucket = 0; // index of the bucket the label starts at | ||
| 48 | QString label; | ||
| 49 | }; | ||
| 50 | |||
| 51 | // Readability cap: a timeline never shows more bars than this, whatever the | ||
| 52 | // width (P-45: all time → years, a year → months, a month → days, a day → | ||
| 53 | // hours). Narrow plots lower it further through maxBucketsForWidth(). | ||
| 54 | constexpr int MaximumBuckets = 40; | ||
| 55 | // D-7: one bar needs at least this many pixels (bar + gap). | ||
| 56 | constexpr int MinimumBarPixels = 6; | ||
| 57 | |||
| 58 | int maxBucketsForWidth(qreal plotWidth); | ||
| 59 | |||
| 60 | // Number of buckets of `unit` the range spans (weeks start on weekStart). | ||
| 61 | int bucketCount(const Range &range, Unit unit, Qt::DayOfWeek weekStart); | ||
| 62 | |||
| 63 | // The finest unit whose bucket count fits maxBuckets. Hours only for a | ||
| 64 | // single-day range. Falls back to years. | ||
| 65 | Unit chooseUnit(const Range &range, int maxBuckets, Qt::DayOfWeek weekStart); | ||
| 66 | |||
| 67 | // The calendar unit containing `date`: its year, month, week or day. | ||
| 68 | Range unitRange(const QDate &date, Unit unit, Qt::DayOfWeek weekStart); | ||
| 69 | |||
| 70 | // The shown range for hits between first and last (no date constraint): | ||
| 71 | // extended to whole units of the chosen unit so bars are complete. | ||
| 72 | Range spanForHits(const QDate &firstHit, const QDate &lastHit, int maxBuckets, Qt::DayOfWeek weekStart); | ||
| 73 | |||
| 74 | // Day counts → one bucket per unit inside `range` (empty buckets included, | ||
| 75 | // clipped to the range). The sum of the bucket counts equals the sum of the | ||
| 76 | // day counts inside the range. | ||
| 77 | QList<Bucket> buckets(const QMap<QDate, qint64> &dayCounts, const Range &range, Unit unit, | ||
| 78 | Qt::DayOfWeek weekStart); | ||
| 79 | // The 24 hour buckets of one day. | ||
| 80 | QList<Bucket> hourBuckets(const QDate &day, const QVector<qint64> &hourCounts); | ||
| 81 | |||
| 82 | qint64 total(const QList<Bucket> &buckets); | ||
| 83 | |||
| 84 | // The range a drag from bucket a to bucket b covers (either order). | ||
| 85 | Range brush(const QList<Bucket> &buckets, int a, int b); | ||
| 86 | |||
| 87 | // D-8 zoom out: the enclosing calendar level of a range — a day or week → | ||
| 88 | // its month, a month → its year, a year or a range across years → all time | ||
| 89 | // (invalid range). A range inside one month goes to that month, inside one | ||
| 90 | // year to that year. | ||
| 91 | Range parentRange(const Range &range); | ||
| 92 | |||
| 93 | // The breadcrumb from "All time" down to `range` (the last level is range). | ||
| 94 | QList<Level> breadcrumb(const Range &range, const QLocale &locale, const QString &allTimeLabel); | ||
| 95 | |||
| 96 | // Human labels (never unit names): "2025", "March 2025", "10–16 March 2025", | ||
| 97 | // "Fri 14 March 2025", "14:00" — in the locale's formats. | ||
| 98 | QString rangeLabel(const Range &range, const QLocale &locale); | ||
| 99 | QString bucketLabel(const Bucket &bucket, Unit unit, const QLocale &locale); | ||
| 100 | // The short axis label of a bucket ("2025", "Mar", "10", "14"). | ||
| 101 | QString axisLabel(const Bucket &bucket, Unit unit, const QLocale &locale); | ||
| 102 | |||
| 103 | // Collision-free axis labels: walks the buckets left to right and keeps a | ||
| 104 | // label only when it starts after the previous label ended (plus a gap). | ||
| 105 | // Month labels name the year on the first tick and on January. | ||
| 106 | // `textWidth` measures a label in pixels; `bucketWidth` is the slot width. | ||
| 107 | QList<Tick> ticks(const QList<Bucket> &buckets, Unit unit, const QLocale &locale, qreal bucketWidth, | ||
| 108 | const std::function<qreal(const QString &)> &textWidth, qreal gap = 8); | ||
| 109 | |||
| 110 | } // namespace TimeHistogram | ||
| 111 |