Picking up where the last post left off
A previous post walked through how the grocery price tracker came together over three weekends: a SharePoint list for structured data, an Azure Function to expose it as JSON, and a static web app to render it as a searchable, sortable table. That version worked well and answered the core question the project was built to answer—what am I actually paying for groceries, and where.
But a flat table only tells you where things stand today. It does not tell you where things are heading. This session picked up exactly there: turning historical price observations into trend lines and comparisons, then spending the back half of the morning tracking down why the new charts did not always behave the way they should.
Architecture, unchanged
SharePoint List
↓
Azure Function (/api/prices)
↓
Static Web App (HTML/CSS/JS)
↓
Filtered Table + Charts
Nothing about the underlying architecture changed this session. That was deliberate. The backend was already proven stable from the original build, so the entire effort could go into the presentation layer without touching authentication, permissions, or the Microsoft Graph query logic at all.
Part 1 (≈ 2 hours): Turning rows into trends
The frontend already tracked everything needed for time-based analysis—every row carried a DateObserved
field—but nothing on screen used it beyond a "most recent" summary card. The first stretch of the morning was
about putting that field to work.
What got added
- Date range filters (start/end) that combine with the existing search, store, and category filters
- Quick-range buttons for Last 7 / 30 / 90 / 365 days
- An item price trend line chart, driven by a dropdown of tracked items
- An average price by store bar chart
- A category price change percentage chart, comparing the oldest half of observations against the newest half
Chart.js was pulled in via CDN rather than an npm build step, which kept the static web app exactly that—static. No bundler, no build pipeline changes, just one additional script tag and three canvas elements.
Every chart reads from the same filtered dataset as the table. There is no separate charting data path to keep in sync—filter once, and the table and the charts update together.
Part 2 (≈ 90 minutes): When "it works" isn't the same as "it's reliable"
The charts rendered. The demo looked good. Then came the real test: actually using it the way an end user would— picking random date ranges, switching stores mid-session, clearing filters, changing them again. That is where the cracks appeared.
Bug 1 — the item dropdown didn't know about filters
The dropdown that drives the price trend chart was populated once, at page load, from the entire dataset. Once a store or category filter was applied, the dropdown kept listing items that had zero matching observations left. Selecting one of those produced a chart that looked broken, when really it was just being asked to plot nothing.
Bug 2 — silent empty charts
Only the price trend chart had a message explaining an empty state. The store comparison and category change charts had none, so a narrow filter could leave a blank canvas with no explanation at all.
Bug 3 — date comparisons were timezone-sensitive
Filtering compared JavaScript Date objects directly, which is sensitive to how the browser resolves
UTC versus local time. Rows sitting right at the edge of a selected range could unpredictably fall in or out
depending on that resolution—a classic off-by-one-day bug that only shows up intermittently.
Bug 4 — no visibility into what the charts were actually showing
Nothing on the page told you how much data, or what date span, was currently feeding the charts. Filters could return three rows or three hundred, and the dashboard looked the same either way until you scrolled down and squinted at a chart.
None of these were Chart.js problems. They were state-synchronization and feedback problems—the kind of bugs that only surface once a static demo becomes something you actually navigate.
What fixed it
- The item dropdown now rebuilds from the currently filtered rows on every filter change, with observation counts shown directly in each option
- All three charts now show an explicit, specific "no data" message instead of rendering blank
- Date filtering was rewritten to compare plain
YYYY-MM-DDstrings instead of Date objects, removing the timezone ambiguity entirely - A live summary line now sits above the charts, stating exactly how many records, stores, and categories are feeding them at that moment
- A "data available" hint above the date pickers shows the real span of the dataset, with a one-click "Full History" reset
Development disciplines encountered
- Data Visualization – translating flat rows into trend lines and comparative charts
- State Management – keeping a dropdown, a table, and three charts all in sync with one filter state
- Date/Time Handling – avoiding timezone drift in comparisons without touching the source data
- UX Feedback Design – making an empty result state as clear as a populated one
- Regression Thinking – treating a "working" feature as unfinished until it survives real navigation, not just a happy-path demo
As with the original build, none of these were planned as separate workstreams. They surfaced naturally once the dashboard moved from "renders correctly once" to "holds up under actual use."
The real takeaway
Adding the charts took a couple of hours. Making them trustworthy took just as long—and mattered more.
- Visualization libraries are the easy part; wiring them to live, filterable state is where the real work is
- An empty result and a broken result look identical to a user unless the interface says otherwise
- Date logic deserves the same scrutiny as authentication logic—both fail quietly and inconsistently if you let them
Final thought
Working Demo
→ Real Navigation
→ Discovered Edge Cases
→ Reliable Product
One more focused morning was enough to take this dashboard from "displays data" to "explains its own data." That second half of the work—the part that never shows up in a screenshot—is usually the difference between a project that looks finished and one that actually is.