Chart date ranges: 7D · 30D · 90D · 1Y · All
Asked for by Leong Sen Fong, 2026-09-28 10:53: apply these ranges to every chart, investigate first, design before any code. No code has been changed. This page is the investigation (what each chart does today, what breaks) and the UI design. Mock-ups use made-up numbers; the shapes, labels and layout are the proposal. Drawn by Tora Master.

The proposal in one screen

One shared switch on every chart, on its own row under the title. The bars stay readable in every range because the bar size follows the range:

RangeOne bar =BarsSwipe?
7Da day7no
30Da day30yes, opens on today (as now)
90Da week13no (today: 90 daily bars with swiping)
1Ya month12no
Allchosen by length: ≤31 days → day, ≤ ~6 months → week, longer → monthvariesno
Everything below the switch on that card follows the range — numbers, chart and lists — so the card never shows two time spans at once.

1 · The switch

Today there are two different switches:

7D30D90D
Invite Performance today
7 days30 days90 days
Admission History today

Proposed — one component, five equal buttons, full width under the title (dark and light theme):

Invite Performance
7D30D90D1YAll
Admission History
7D30D90D1YAll

Why its own row: at phone width the title already wraps next to three buttons; five would squeeze it further.

2 · One chart, five ranges

Invite Performance as the example. Every other chart behaves the same way.

Invite Performance
7D30D90D1YAll
024622Sep232425262728Today
Mon 28 Sep5 created1 joined
ⓘ Tap any day for details
7D
Invite Performance
7D30D90D1YAll
024630Aug311Sep2345678910111213141516171819202122232425262728Today
Mon 28 Sep5 created1 joined
ⓘ Tap any day for details · swipe for earlier days
30D
Invite Performance
7D30D90D1YAll
04812166Jul1320273Aug101724317Sep142128This wk
Week of 28 Sep · so far6 created2 joined
ⓘ One bar per week · tap any week
90D
Invite Performance
7D30D90D1YAll
010203040Oct2025NovDecJan2026FebMarAprMayJunJulAugSepSo far
Sep 2026 · so far31 created7 joined
ⓘ One bar per month · tap any month
1Y
Invite Performance
7D30D90D1YAll
0369123Aug101724317Sep142128This wk
Week of 28 Sep · so far6 created2 joined
ⓘ Since 5 Aug 2026, when invite codes started · one bar per week
All

Labels under the bars

Bar sizeUnder each barSecond lineDetail row
Dayday number (as now)month on the first bar and on the 1st; "Today" on the lastMon 28 Sep
Weekday the week startsmonth on the first bar and when it changes; "This wk" on the lastWeek of 21 Sep · or "(so far)"
Monthmonth (Sep)year on the first bar and on January; "So far" on the lastSeptember 2026 · or "(so far)"

3 · Where the switch goes, and what follows it

ChartRange todayOpens onWhat follows the switch
Admin dashboard → Invite Performance7D · 30D · 90D (switch exists)7DEverything in the section: the three numbers, the daily chart, "Where people drop out", "Which channel brings people in". The Expand page gets the same switch and opens on the range the card is showing.
Admin dashboard → Active usersnone (fixed 7 days)7DThe chart only. DAU / WAU / MAU tiles above it stay as they are — they are already "today / this week / this month".
Admission History7 / 30 / 90 days (switch exists, different style)7DThe overview (Joined / Invited / Walked in and its bar), the chart, and the day-by-day list.
Daily Limitnone (fixed 7 days)7DThe chart and the "limit reached on N days" line. "Used today" and the limit control are about today and stay as they are.
Invites Per Usernone (fixed 30 days)7DEverything on the page: the top numbers, the chart and the "who is inviting" list (decided by Leong 09-28 — today the numbers and the list are all-time and only the chart is 30 days).

4 · Problems and edge cases found

From reading each chart's code and the backend behind it. Red = must be fixed first, amber = design decision, green = handled in the design.

1. The backend only allows up to 90 days, and one endpoint silently ignores other values.
/performance accepts 7 and 90; anything else, including 365, quietly becomes 30. /daily-history and the Invites Per User series fall back to their default above 90. The dashboard series has no range at all. So today, asking for 1Y would show 30 days with no error — the worst kind of wrong. Every endpoint needs a range parameter that rejects values it does not know.
2. Long ranges will be slow as the code stands.
Most series run a separate count for every day, and the tables have no index on their date columns (invite_codes.created_at / used_at, users.created_at, messages.created_at). 1Y means about 365 counts per chart, each over a whole table. The dashboard endpoint already takes 3.6–7 s in production for 7 days. Fix: add the date indexes (one migration) and count per week / month with a single grouped query, not per day. The Active users chart moves to its own endpoint, off the slow dashboard one.
3. 365 daily bars do not fit, so the bar size changes with the range.
At one bar per day, 1Y would be about 9,500 px of swiping and a label under every bar. Proposal: 7D and 30D stay one bar per day (as today); 90D becomes one bar per week (the last 13 whole weeks, Monday to Sunday — 91 days; no swiping — this changes today's 90D, which is 90 daily bars); 1Y is one bar per month (the last 12 calendar months, this month last and marked "so far"). The detail row says what one bar is ("Week of 21 Sep", "September 2026").
4. "All" means a different start date on each chart.
Invite codes began on 5 Aug 2026, user sign-ups on 9 Jun 2026, and the daily-limit counter only on 15 Sep 2026. So "All" is 8 weeks on one chart and 2 weeks on another. Proposal: "All" starts where that chart's data starts, says so under the chart ("since 5 Aug 2026"), and picks the bar size from the length: up to 31 days → days, up to about 6 months → weeks, longer → months.
5. Some numbers cannot simply be added up into a week or a month.
Active users: someone active on Monday and Tuesday is one active user that week, not two. Weekly and monthly bars must count distinct people on the server, not add up the days. Daily Limit: the limit is per day, so a weekly bar cannot show "the limit". Proposal: for weeks and months the bar shows everyone who walked in, and the detail row says "limit reached on 2 of 7 days".
6. The last bar is not finished yet.
In 90D, 1Y and All the last week or month is still in progress, so it will look low. It is labelled "This wk" / "So far" and the detail row says "(so far)", so nobody reads a half month as a drop.
7. Two charts count days in different time zones.
Active users is bucketed in UTC; every other chart uses Malaysia time. A day there starts at 08:00 Malaysia time. Proposal: move it to Malaysia time as part of this work, so the same date means the same day on every chart.
8. Data before a feature existed.
Admission History counts everyone who joined before invite codes existed (before 5 Aug) as "walked in", because there was no code to count. On 1Y / All that shows as a large walked-in block in June–July. Proposal: those bars are drawn but marked "before invite codes", rather than hidden.
9. Two different switches today.
Invite Performance uses a small dark "7D 30D 90D" pill; Admission History uses large green "7 days / 30 days / 90 days" tabs. This work replaces both with one shared switch, like the charts are one shared chart.
10. Five buttons do not fit next to the title on a phone.
On Leong's web screenshot "Invite Performance" already wraps onto two lines with three buttons beside it. The switch therefore goes on its own row, full width, under the title — on every chart.
11. Switching range while data loads, and remembering the choice.
While a range loads, the old bars stay and the switch shows the new selection (no flash of empty). If loading fails, the old range stays with a "couldn't load — retry" line. Nothing is remembered: every chart opens on 7D each time the page opens (decided by Leong 09-28).

5 · How the build would be split (after your go)

#Subtask
1Backend: one range parameter (7d | 30d | 90d | 1y | all) on every chart endpoint; unknown values return 400, not a silent default. Each response says its bar size (day / week / month) and where "All" starts.
2Backend: grouped weekly / monthly counts; distinct active users per week / month; Active users moves to its own endpoint and to Malaysia time. Date indexes dropped (2026-09-28): measured on production, a full year of Active users takes 123 ms and 1Y / All read every message anyway, so an index cannot speed them up; the migration numbers 464–468 are also held by unshipped branches.
3App: one shared range switch; the shared chart learns week and month bars (labels, detail row, "so far").
4App: put the switch on the five charts, one page per subtask, with each page's numbers following the range as in the table above.
5Tests + screenshots of every chart in every range, the guard extended, docs, web version.

1 and 2 come first: without them the switch would show wrong data with no error (problem 1).

6 · Decisions (Leong Sen Fong, 2026-09-28)

"all looks good, all opens on 7d with default" · 11:38 MYT "follow your suggestion. Remember update the plan."
#QuestionDecided
190D as 13 weekly bars instead of 90 daily barsYes — as proposed
2"All" starts where each chart's data starts, labelled "since …"Yes — as proposed
3Daily Limit on week / month bars: walked in + "limit reached on N days"Yes — as proposed
4Invites Per User: do the top numbers and the list follow the range?Yes, they follow the range (Tora's suggestion, chosen by Leong) — the page never shows two time spans at once
5Default range when a chart opens7D on every chart (changed from the proposal, which kept today's defaults)
6Remember the last range on the device, or always open on 7D?Always open on 7D — even if 1Y was picked last time
7Week start, 1Y span, Active users time zoneAs Tora suggested: weeks start Monday (90D = last 13 whole weeks, 91 days) · 1Y = last 12 calendar months, this month marked "so far" · Active users moves from UTC to Malaysia time (its 7D bars shift slightly once — that is the fix)

11:44 MYT "go with a), the three things follow your suggestions." — building started, in the order of section 5.

Later the same day (v2.761): Leong removed the 1Y button (“remove the 1y filter from all the charts”). The switch is now 7D · 30D · 90D · All; month bars remain for a long All.

Shipped 2026-09-28 as v2.758: backend live (?range= on every chart endpoint, /active-series, codes_since); app on the web version (hoop-web.pages.dev) and in the next TestFlight build. One change from section 5: no date-index migration (measured on production, see subtask 2).

Appendix · what each chart reads today

ChartEndpointRange limit todayData sinceDay in
Invite Performance/v1/admin/invites/performance?days=7 / 30 / 90; others → 30 silently5 Aug 2026 (invite codes)Malaysia
Active users/v1/admin/invites/dashboard (series_7d)fixed 79 Jun 2026 (messages)UTC
Admission History/v1/admin/invites/daily-history?days=1–90; above → 79 Jun 2026 (sign-ups); invites from 5 AugMalaysia
Daily Limitsame endpoint, 7 days1–9015 Sep 2026 (limit counter + history)Malaysia
Invites Per User/v1/admin/invites/invitersfixed 30 in the handler5 Aug 2026Malaysia

Nothing deletes old rows from these tables, except disappearing messages that expire (they can lower past active-user counts slightly).