Skip to content

feat(home/chart): add per-service savings-range bar chart (closes #765) - #766

Merged
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
feat/765-per-service-range-bars
May 27, 2026
Merged

feat(home/chart): add per-service savings-range bar chart (closes #765)#766
cristim merged 3 commits into
feat/multicloud-web-frontendfrom
feat/765-per-service-range-bars

Conversation

@cristim

@cristim cristim commented May 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds a stacked vertical bar chart below the cumulative savings line chart on the Home page, one bar per service sorted by max savings descending.
  • Each bar splits into two datasets: a solid "Floor" (min savings across the selected window) and a semi-transparent "Range" (max minus min, the upside). Shares the existing getSavingsAnalytics() response so no extra fetch is needed.
  • Hover tooltip shows service / min / median / max / sample count / % of total. Caps at 10 services with a "+N more" heading annotation when truncated. Empty state mirrors the existing line-chart pattern.

Design

The chart is wired into the existing loadSavingsTrendChart() call, so it automatically honours the same account/provider/timeframe filters and re-renders on every range-button click or filter-chip change. The computeServiceStats() helper is a pure function (exported for testing) that iterates data_points[].by_service to produce per-service min/max/sum/count stats.

Test plan

  • computeServiceStats accumulates min/max/sum/count correctly for single and multiple services, skips data points with no by_service.
  • Empty/all-zero data points show empty state (canvas hidden, #savings-by-service-empty visible).
  • Two-service fixture produces exactly two bars with correct labels.
  • Floor dataset = min, Range dataset = max - min.
  • Sort order: service with largest max appears first.
  • Re-render destroys the previous Chart instance before creating a new one.
  • Missing canvas in DOM is a silent no-op (no throw).
  • Filter-driven re-fetch via loadSavingsTrendChart() re-renders bars with fresh data.
  • Filter-driven re-fetch with empty data_points restores empty state on bar chart.
  • npx tsc --noEmit passes with no errors.
  • All 52 existing dashboard tests continue to pass (no regressions).

Summary by CodeRabbit

  • New Features
    • Added "Savings range by service" chart to visualize savings data broken down by individual services, with intelligent empty-state handling when analytics data is unavailable.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a06004a9-a1ee-4866-9f24-aed997b6dbf9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements the "Savings range by service" feature (Issue #765), adding a new stacked bar chart to the dashboard that visualizes per-service savings with Floor (minimum) and Range (maximum minus minimum) datasets. It includes data aggregation, chart rendering, HTML markup, loader integration across all paths, and comprehensive test coverage.

Changes

Per-service savings range chart

Layer / File(s) Summary
Per-service savings feature implementation
frontend/src/index.html, frontend/src/dashboard.ts
Adds HTML markup with canvas and empty-state elements (savings-by-service-chart, savings-by-service-empty); defines ServiceSavingsStats interface and computeServiceStats() to aggregate per-service min/max/sum/count; implements renderSavingsByService() to build a stacked bar chart with Floor and Range datasets, sort services by maximum savings descending, truncate to max count, and toggle empty state visibility based on positive max savings; configures chart state and service color palette; exports computeServiceStats in __test__ helper.
Loader integration across all paths
frontend/src/dashboard.ts
Wires renderSavingsByService() into loadSavingsTrendChart to degrade the per-service bar chart to empty state when analytics data is missing, render the chart with data.data_points when available, and degrade to empty state on catch/error path.
Test suite
frontend/src/__tests__/dashboard.test.ts
Adds unit tests validating computeServiceStats handles empty input, ignores points without by_service, and correctly aggregates single and multiple services; adds DOM behavior tests for renderSavingsByService covering empty-state vs canvas visibility, label filtering to positive-savings services, Floor (min) and Range (max–min) dataset derivation, descending service sort by max, prior Chart.js instance cleanup, and graceful no-throw when canvas is absent; adds integration tests confirming per-service bar chart re-renders on analytics filter changes and empty-state toggles correctly.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • LeanerCloud/CUDly#765: Directly implements the per-service savings-range chart feature with matching aggregation and rendering helpers that address the issue objectives.

Possibly related PRs

  • LeanerCloud/CUDly#747: Also modifies loadSavingsTrendChart empty-state and re-render behavior, so the per-service chart integration is adjacent to the retrieved PR's savings-trend handling.

Suggested labels

effort/m, type/feat

Poem

🐰 A chart appears with colors bright,
Per-service savings in the light,
Floor and Range dance side by side,
Empty states and stats collide—
One more metric to confide! 📊

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a per-service savings-range bar chart feature. It is specific, concise, and directly reflects the primary functionality introduced across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/765-per-service-range-bars

Comment @coderabbitai help to get the list of available commands and usage tips.

@cristim cristim added priority/p2 Backlog-worthy severity/low Minor harm urgency/this-sprint Within the current sprint impact/all-users Affects every user enhancement New feature or request triaged Item has been triaged labels May 27, 2026
@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/dashboard.ts`:
- Around line 861-863: The per-service chart is missing the provider filter
because loadSavingsTrendChart() calls only forward account_ids; update the call
sites that now reuse loadSavingsTrendChart() (the places that call
renderSavingsByService([]) then return and the other similar call) to pass the
selected provider filter through (e.g., provider_ids or providerIds) into
loadSavingsTrendChart(), and update loadSavingsTrendChart()'s request-building
logic to include provider_ids in the analytics fetch payload so the backend
query is filtered by provider when a provider chip is selected.
- Around line 774-787: The tooltip currently labels `Median` but calculates
`s.sum / s.count` (the mean) in the `label` function (see `label: (ctx) => { ...
}`, `stats`, `totalSavings`, `s.sum`, `s.count`). Fix by either (A) renaming the
line to "Mean" (replace `Median: $${Number(median)...}` with `Mean: ...`) to
reflect the existing calculation, or (B) compute the true median by ensuring
`stats` stores the raw sample values (e.g., `s.samples`), sorting that array and
taking the middle value (or average of two middle values for even counts) and
using that value for the displayed `Median`; then remove the `s.sum / s.count`
usage for the median display. Ensure the formatted label string and variable
names match the chosen approach.
- Around line 707-724: When positive.length === 0 the function returns early but
doesn't reset the section heading, leaving a previous "(+N more)" suffix
visible; update the early-return branch to locate the heading (use
section?.querySelector('h3') or the same selector used later) and set
heading.textContent = 'Savings range by service' (the default) before returning
so the title is cleared when the chart is empty; keep the existing
canvas.classList/emptyEl toggles.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e8325905-ed8f-45b4-ad9f-a2ba9d2c80a2

📥 Commits

Reviewing files that changed from the base of the PR and between b5d3d00 and 9b4a48b.

📒 Files selected for processing (3)
  • frontend/src/__tests__/dashboard.test.ts
  • frontend/src/dashboard.ts
  • frontend/src/index.html

Comment thread frontend/src/dashboard.ts
Comment thread frontend/src/dashboard.ts
Comment thread frontend/src/dashboard.ts
@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

cristim added a commit that referenced this pull request May 27, 2026
- Finding 1: move heading query above empty-state guard and reset
  heading.textContent to default on early return, preventing stale
  "+N more" suffix after dataset empties
- Finding 2: replace mean (sum/count) mislabeled as Median with true
  median via medianOf(samples); add samples[] field to
  ServiceSavingsStats and populate it in computeServiceStats
- Finding 3: forward currentProvider to getSavingsAnalytics so the
  per-service bar chart honours the provider chip filter end-to-end
- Tests: extend dashboard.test.ts with three targeted assertions for
  each finding (heading reset, samples accumulation, provider forwarding)
@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented May 27, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

cristim added a commit that referenced this pull request May 27, 2026
…uffix

PR #766 inadvertently appended the unit suffix to Period Savings:

    periodSavingsEl.textContent = formatCurrency(displayTotal) + ' ' + suffix;

This contradicted the inline comment one line above ("Period Savings is
the cumulative total ... no per-unit rate suffix -- it is already a
dollar total") and broke 4 tests in savings-history.test.ts that pin
the expected output as `$5.00K`, `$500.00`, etc. (no suffix).

Period Savings is a cumulative dollar total over the selected date
range, not a rate -- adding "/mo" / "/hr" / "/yr" implies it is a
per-unit value, which it is not. The avg / peak rows correctly retain
the suffix because those ARE rates.

Drop the suffix concat. The 4 failing tests now pass without
modification, restoring the contract the comment documented.
cristim added 3 commits May 27, 2026 23:55
Add a stacked vertical bar chart below the cumulative savings line chart
on the Home page. One bar per service, split into two stacked datasets:

- Floor (solid): height = min savings observed across the selected window.
- Range (35% opacity): height = max - min (upside above the floor).

Services are sorted by max savings descending; at most 10 are shown with
a "+N more" note in the heading when truncated.

Hover tooltip shows: service / min / median / max / sample count / % of total.

The chart shares the existing getSavingsAnalytics() response consumed by
the trend line chart, so no extra fetch is needed. It honours the same
account/provider/timeframe filters because it is rendered from within
loadSavingsTrendChart() on every re-fetch.

Empty state: when no service has positive savings (no data points, all
zeros, or on API error), the canvas is hidden and a descriptive paragraph
is shown, matching the existing line-chart empty-state pattern.

Changes:
- frontend/src/index.html: new <section id="savings-by-service-section">
  with canvas + hidden empty-state paragraph, inserted above the existing
  "Potential Savings by Service" section.
- frontend/src/dashboard.ts: new SERVICE_BAR_COLORS palette constant,
  savingsByServiceChart instance variable, exported computeServiceStats()
  helper, exported renderSavingsByService() function, wired into the data
  / empty / error paths of loadSavingsTrendChart().
- frontend/src/__tests__/dashboard.test.ts: 15 new tests covering
  computeServiceStats accumulation, empty/zero state, two-service geometry,
  floor/range dataset values, sort order, chart destroy-before-recreate,
  missing-canvas no-op, and filter-driven re-render.
- Finding 1: move heading query above empty-state guard and reset
  heading.textContent to default on early return, preventing stale
  "+N more" suffix after dataset empties
- Finding 2: replace mean (sum/count) mislabeled as Median with true
  median via medianOf(samples); add samples[] field to
  ServiceSavingsStats and populate it in computeServiceStats
- Finding 3: forward currentProvider to getSavingsAnalytics so the
  per-service bar chart honours the provider chip filter end-to-end
- Tests: extend dashboard.test.ts with three targeted assertions for
  each finding (heading reset, samples accumulation, provider forwarding)
…uffix

PR #766 inadvertently appended the unit suffix to Period Savings:

    periodSavingsEl.textContent = formatCurrency(displayTotal) + ' ' + suffix;

This contradicted the inline comment one line above ("Period Savings is
the cumulative total ... no per-unit rate suffix -- it is already a
dollar total") and broke 4 tests in savings-history.test.ts that pin
the expected output as `$5.00K`, `$500.00`, etc. (no suffix).

Period Savings is a cumulative dollar total over the selected date
range, not a rate -- adding "/mo" / "/hr" / "/yr" implies it is a
per-unit value, which it is not. The avg / peak rows correctly retain
the suffix because those ARE rates.

Drop the suffix concat. The 4 failing tests now pass without
modification, restoring the contract the comment documented.
@cristim
cristim force-pushed the feat/765-per-service-range-bars branch from a741b11 to 8ed66a1 Compare May 27, 2026 21:58
@cristim
cristim merged commit 51bdab8 into feat/multicloud-web-frontend May 27, 2026
6 checks passed
@cristim
cristim deleted the feat/765-per-service-range-bars branch June 3, 2026 21:53
cristim added a commit that referenced this pull request Jun 5, 2026
Replace mis-citations of #769 (button-disable bug) in the
renderSavingsByService code path with the correct issues #908/#766
(savings-by-service chart feature). Four sites updated:
- dashboard.ts: two JSDoc comments
- styles/charts.css: one CSS comment
- dashboard.test.ts: two comments (describe label + inline note)

Genuine #769 references in recommendations.ts and
recommendations.test.ts are untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request impact/all-users Affects every user priority/p2 Backlog-worthy severity/low Minor harm triaged Item has been triaged urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant