diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb
index 2101ba228..b7ea15c28 100644
--- a/app/controllers/pages_controller.rb
+++ b/app/controllers/pages_controller.rb
@@ -508,15 +508,35 @@ class PagesController < ApplicationController
current_period: current_period,
previous_period: previous_period,
days: axis_days,
+ current_days: month_end.day,
axis_labels: spending_trend_axis_labels(month_start, previous_month_start, axis_days),
current: current_series,
previous: previous_series,
current_total: Money.new(current_total, currency),
previous_total: Money.new(previous_total, currency),
- delta: Money.new(current_total - previous_total, currency)
+ delta: Money.new(current_total - previous_total, currency),
+ previous_label: I18n.l(previous_month_start, format: :month_year).capitalize,
+ date_range_short: spending_trend_compact_date_range(current_period)
}
end
+ # Compact range for narrow viewports ("Sep 01 - 6, 2026"). The period
+ # never spans months, so the end date only needs its day.
+ def spending_trend_compact_date_range(period)
+ date_range = period.date_range
+
+ if date_range.begin == date_range.end
+ t("pages.dashboard.spending_trend.date_range_short_single",
+ date: I18n.l(date_range.begin, format: :short),
+ year: date_range.end.year)
+ else
+ t("pages.dashboard.spending_trend.date_range_short",
+ start_date: I18n.l(date_range.begin, format: :short),
+ end_day: date_range.end.day,
+ year: date_range.end.year)
+ end
+ end
+
# Localized tick labels, one per axis day. The selected month owns the
# axis up to its length; when the previous month is longer, its dates
# label the tail so a tick never rolls past month-end into the next month
diff --git a/app/javascript/controllers/spending_chart_controller.js b/app/javascript/controllers/spending_chart_controller.js
index cf9b263d3..6f1d761ca 100644
--- a/app/javascript/controllers/spending_chart_controller.js
+++ b/app/javascript/controllers/spending_chart_controller.js
@@ -48,6 +48,7 @@ export default class extends Controller {
const height = this.element.clientHeight;
const {
days = 30,
+ current_days: currentDays = days,
axis_labels: axisLabels = [],
current = [],
previous = [],
@@ -84,7 +85,7 @@ export default class extends Controller {
.range([innerHeight, 0]);
this._drawGridlines(group, y, innerWidth, innerHeight);
- this._drawXAxis(group, x, days, innerHeight, axisLabels);
+ this._drawXAxis(group, x, days, innerHeight, axisLabels, currentDays);
const line = d3
.line()
@@ -171,8 +172,10 @@ export default class extends Controller {
.style("font-weight", "500");
}
- _drawXAxis(group, x, days, innerHeight, axisLabels) {
- const tickDays = [...new Set([1, Math.round((1 + days) / 2), days])];
+ _drawXAxis(group, x, days, innerHeight, axisLabels, currentDays = days) {
+ const mobile = !window.matchMedia("(min-width: 640px)").matches;
+ const span = mobile ? Math.min(days, currentDays) : days;
+ const tickDays = [...new Set([1, Math.round((1 + span) / 2), span])];
group
.append("g")
diff --git a/app/views/pages/dashboard/_spending_trend.html.erb b/app/views/pages/dashboard/_spending_trend.html.erb
index cd94701ed..45b25038d 100644
--- a/app/views/pages/dashboard/_spending_trend.html.erb
+++ b/app/views/pages/dashboard/_spending_trend.html.erb
@@ -6,16 +6,22 @@
has_data = data[:current].any? { |p| p[:value].positive? } || data[:previous].any? { |p| p[:value].positive? }
chart_data = {
days: data[:days],
+ current_days: data[:current_days],
axis_labels: data[:axis_labels],
current: data[:current],
previous: data[:previous]
}
+ previous_label = data[:previous_label]
+ date_range_short = data[:date_range_short]
%>
-
+
<%= t(".date_range", start_date: l(period.date_range.begin, format: :long), end_date: l(period.date_range.end, format: :long)) %>
+
+ <%= date_range_short %>
+
<%= render DS::Tooltip.new(text: t(".period_scope_hint"), placement: "top-start") %>
@@ -45,9 +51,9 @@
<%= I18n.l(data[:month], format: :month_year) %>
-
- <%= format_money data[:current_total] %>
- ">
+
+ <%= format_money data[:current_total] %>
+ ">
<%= format_money data[:delta] %>
@@ -57,8 +63,8 @@
-
<%= t(".previous_month") %>
-
<%= format_money data[:previous_total] %>
+
<%= previous_label %>
+
<%= format_money data[:previous_total] %>
@@ -70,7 +76,7 @@
data-spending-chart-data-value="<%= chart_data.to_json %>"
data-spending-chart-currency-value="<%= Current.family.currency %>"
data-spending-chart-current-label-value="<%= I18n.l(data[:month], format: :month_year).capitalize %>"
- data-spending-chart-previous-label-value="<%= t(".previous_month") %>">
+ data-spending-chart-previous-label-value="<%= previous_label %>">
<% else %>
diff --git a/config/locales/views/pages/en.yml b/config/locales/views/pages/en.yml
index 80579c5dd..3ff81fca3 100644
--- a/config/locales/views/pages/en.yml
+++ b/config/locales/views/pages/en.yml
@@ -121,7 +121,8 @@ en:
spending_trend:
title: "Spending"
date_range: "%{start_date} to %{end_date}"
+ date_range_short: "%{start_date} - %{end_day}, %{year}"
+ date_range_short_single: "%{date}, %{year}"
month_picker_aria_label: "Select month"
period_scope_hint: "Shows the month you pick here — independent of the dashboard's time period at the top."
- previous_month: "Previous month"
no_data: "No spending data for this month"
diff --git a/test/controllers/pages_controller_test.rb b/test/controllers/pages_controller_test.rb
index f36cec015..8b307f6f3 100644
--- a/test/controllers/pages_controller_test.rb
+++ b/test/controllers/pages_controller_test.rb
@@ -382,6 +382,9 @@ class PagesControllerTest < ActionDispatch::IntegrationTest
assert_equal 75.0, current.last.fetch("value")
assert_equal 200.0, previous.last.fetch("value")
assert_equal [ selected_month.end_of_month.day, previous_month.end_of_month.day ].max, chart.fetch("days")
+ # The chart needs the selected month's own length to label only its days
+ # on narrow (mobile) widths.
+ assert_equal selected_month.end_of_month.day, chart.fetch("current_days")
end
test "dashboard spending trend widget caps an in-progress month at today" do
@@ -424,6 +427,40 @@ class PagesControllerTest < ActionDispatch::IntegrationTest
assert_equal I18n.l(previous_month.end_of_month, format: :short), labels.last
end
+ test "dashboard spending trend names the compared month in the comparison header" do
+ account = @family.accounts.create!(name: "Spending Trend Label Checking", currency: @family.currency, balance: 0, accountable: Depository.new)
+ selected_month = 2.months.ago.beginning_of_month.to_date
+ previous_month = 3.months.ago.beginning_of_month.to_date
+ create_transaction(account: account, name: "Spend", amount: 10, date: selected_month)
+
+ get root_path, params: { spending_month: selected_month.iso8601 }
+
+ assert_response :ok
+ # The real month name replaces the generic "Previous month" label, which
+ # truncated on mobile ("Previous mon…").
+ expected_label = I18n.l(previous_month, format: :month_year).capitalize
+ assert_select "#spending-trend-section p", text: expected_label
+ chart_element = css_select("[data-controller='spending-chart']").first
+ assert_equal expected_label, chart_element["data-spending-chart-previous-label-value"]
+ end
+
+ test "dashboard spending trend renders a compact date range for mobile" do
+ account = @family.accounts.create!(name: "Spending Trend Range Checking", currency: @family.currency, balance: 0, accountable: Depository.new)
+ selected_month = 2.months.ago.beginning_of_month.to_date
+ create_transaction(account: account, name: "Spend", amount: 10, date: selected_month)
+
+ get root_path, params: { spending_month: selected_month.iso8601 }
+
+ assert_response :ok
+ # Full form for wide viewports, compact form for narrow ones.
+ assert_select "p[class*='hidden sm:block']",
+ text: I18n.t("pages.dashboard.spending_trend.date_range",
+ start_date: I18n.l(selected_month, format: :long),
+ end_date: I18n.l(selected_month.end_of_month, format: :long))
+ assert_select "p[class*='sm:hidden']",
+ text: "#{I18n.l(selected_month, format: :short)} - #{selected_month.end_of_month.day}, #{selected_month.year}"
+ end
+
test "dashboard spending trend widget clamps invalid and future month params" do
account = @family.accounts.create!(name: "Spending Trend Clamp Checking", currency: @family.currency, balance: 0, accountable: Depository.new)
create_transaction(account: account, name: "Today", amount: 10, date: Date.current)