fix(wallet): fill the dashboard's 7-day series window so the sparkline's x axis is time - #218
Merged
Merged
Conversation
…e's x axis is time
`GET /api/dashboard`'s `series` was a sparse day-bucket list: `GROUP BY
date(time)` emits a row only for days that actually have transactions, and
`ui/js/app.js::renderMonthChanges` feeds that list straight into
`sparkline()`, which places point i at an INDEX-based x
(`pad + i*(w - 2*pad)/(len - 1)`) rather than at its date. Days without
transactions were therefore collapsed instead of being shown as zero:
* a week with a single active day degenerated to a lone `M` command, i.e.
no line at all, while the number above it still rendered;
* a week with two active days was drawn as one straight line across the
full 7-day width, claiming a trend the data does not support.
Every sibling time series in this tree is zero-filled to its own window and
says why: `routes/ops.rs::runtime` fills hours 0..23 ('若不补零前端柱状图会
整体左移'), `app.js::dashTrendDays` and `app.js::txTrendDays` fill their day
buckets, and the guest branch of this very function always hands over 7
points. This was the only consumer that did not, so treat it as the omission
it is: `f525242` (#79) wired the sparse series up and the siblings were fixed
afterwards (#155/#156 and the ops PR6).
Fix: build the window with a recursive CTE (today plus the previous 6 days,
UTC) and a LEFT JOIN whose ON clause carries the user filter. The filter has
to stay in the JOIN condition: in WHERE it would drop exactly the empty days
this change exists to add. Only `src/routes/wallet.rs` changes; the window
semantics (a 7-day day-key window anchored on date('now', '-6 days'), UTC)
and the `month` / `net` windows of C2049 are untouched.
Evidence, all driven through the real router:
- `dashboard_series_is_a_zero_filled_seven_day_window`: 7 rows, ascending,
the date set compared against chrono (`Utc::now()`) as an independent
authority rather than derived from the CTE.
- `dashboard_series_zero_fills_inside_the_user_and_the_window`: per-day
values equal a plain per-day SUM for the same user; another user's row
inside the window and this user's row outside it never appear; a positive
control re-checks a zero-filled day after a later transaction lands on it.
- `wallet_summary_and_dashboard`'s `!series.is_empty()` (trivially true once
the window is filled) is rewritten into the shape assertion it meant.
- A/B, each mutation applied alone and reverted with the file's md5 restored:
the pre-change sparse query reddens 3 tests (including both new ones); an
off-by-one window (`-5 days`) reddens 2, a different set; carrying the user
filter in WHERE reddens those same 3; comparing a datetime column against a
date key reddens exactly 1 (the value test). Unmutated tree: 222 passed,
0 failed.
- jsdom end-to-end (real `ui/index.html` plus the four real scripts, only
`fetch` stubbed), fed the exact series the patched handler returns: 7
points / 6 line segments / 7 tooltips, against 1 point / 0 segments for the
pre-change shape. The 14-day bar chart on the same page keeps rendering 14
columns in every leg.
Display / API shape only: no ledger, balance or settlement change. No `ui/`
or i18n key touched, so the i18n gate counts are unchanged.
`cargo fmt --check` and `cargo clippy --all-targets -- -D warnings` are clean;
`cargo test` goes from 220 to 222.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GET /api/dashboard'sseriesis a sparse day-bucket list:GROUP BY date(time)emits a row only for days that actually have transactions.ui/js/app.js(renderMonthChanges) hands that list straight tosparkline(), which places pointiat an index-based x(
pad + i*(w - 2*pad)/(len - 1)) instead of at its date. Days withouttransactions were therefore collapsed rather than shown as zero.
Measured with jsdom (the real
ui/index.htmlplus the four real scripts, onlyfetchstubbed), driving the real card:M60.0 2.0M2.0 32.0 L21.3 32.0 ... L118.0 2.0M2.0 2.0 L118.0 32.0With a single active day the fill path has zero area and the stroke path is a
lone
Mcommand (per the SVG spec a subpath holding only a moveto is notstroked), so there is no line at all while the number above it still
renders. With two active days the two points are stretched across the full
width, drawing a week-long trend out of a one-day spike.
The convention this breaks is written down four times in this tree, each time
with its reason:
src/routes/ops.rs::runtimefills hours 0..23 — 「GROUP BY 会省略无调用的小时,若不补零前端柱状图会整体左移」;
ui/js/app.js::dashTrendDaysfills its day buckets — 「后端 GROUP BY 只返回『有交易』的日桶 … 缺行会让柱子左右移位」 (feat(ui): redesign dashboard, model market and sharing views (PR 4/6) #155/feat(ui): redesign wallet/transactions views on the landed component layer #156);
ui/js/app.js::txTrendDaysrepeats it for the transactions chart;The sparse series was wired up in
f525242(#79); every sibling was filledafterwards. This is the one consumer that was left out — an omission, not a
choice.
sparkline()has exactly two call sites (the guest branch's fixed7-point array and this one), and the tree's three time-bucketed aggregations
(
ops.rs::runtime, this endpoint,transactions_trend) now all fill theirwindow.
Related Issue
None (no issue exists for this; none was fabricated).
Changes
src/routes/wallet.rs—seriesnow builds its window with arecursive CTE (today plus the previous 6 days, UTC) and a
LEFT JOINwhose
ONclause carries the user filter, so every day of the windowgets exactly one row and days with no transactions get
0.The user filter has to stay in the
JOINcondition: inWHEREitwould discard precisely the empty days this change exists to add
(verified by mutation — see below).
src/routes/wallet.rs— the endpoint's doc comment now states the fill(the doc already contrasted the two windows after C2049).
src/routes/wallet.rs— the old assertion!v["series"].is_empty()is trivially true once the window is filled, so it is rewritten into the
shape assertion it meant (7 rows, ascending, ending today).
dashboard_series_is_a_zero_filled_seven_day_windowanddashboard_series_zero_fills_inside_the_user_and_the_window.Not touched, on purpose:
sparkline()'s per-pointtitlestructure (thatneeds a browser-capable probe, recorded separately), the guest branch's dead
code, and the front-end. Zero-filling on the client was rejected on purpose:
it would re-derive a window the server already owns — a second copy of
date('now','-6 days'), which is exactly what C2094 judged a defect when theweek bucket was defined twice.
The window semantics are unchanged: still
date(time)day keys, stillanchored on
date('now', '-6 days'), still UTC.month/net(theC2049 windows) are untouched.
Tests
cargo test— 222 passed (was 220).cargo fmt --check— clean.cargo clippy --all-targets -- -D warnings— clean.dashboard_series_is_a_zero_filled_seven_day_window— asserts 7 rows inascending order whose date set equals the window computed from
chrono::Utc::now()(an authority independent of the CTE undertest).
dashboard_series_zero_fills_inside_the_user_and_the_window— assertsthe per-day values, each compared against a plain per-day
SUMfor thesame user; another user's row inside the window and this user's row
outside it must not appear; then a positive control writes a new
transaction onto a day that was zero-filled and re-reads the endpoint, so
the fill cannot be hiding data.
restored (baseline
e6251ed2..., unmutated tree222 passed / 0 failed):- M1 the pre-change sparse query reddens 3:
dashboard_series_is_a_zero_filled_seven_day_window,dashboard_series_zero_fills_inside_the_user_and_the_window,wallet_summary_and_dashboard— i.e. the new tests reject thepre-change tree.
- M2 window off by one (
-5 days) reddens 2 — a different set — andincludes the shape test but not the value test.
- M3 user filter moved into
WHEREreddens the same 3 as M1 (movingit back into the join is the whole point of the
LEFT JOIN).- M4 joining a datetime column against a date key reddens exactly 1:
the value test, which is what makes it the guard for per-day values.
ui/index.htmlwith the unmodifiedapi.js/data.js/i18n.js/app.js, fed the exactseriesthe patched handler produces (captured from the router test): 7points, 6 line segments, 7 tooltips — against 1 point, 0 segments for the
pre-change shape.
Checklist
fix/).fix(wallet): ...).ui/or i18n key change, so the i18n gates are unaffected.