diff --git a/app/javascript/controllers/time_series_chart_controller.js b/app/javascript/controllers/time_series_chart_controller.js index 3c358dd96..5a13d2c9c 100644 --- a/app/javascript/controllers/time_series_chart_controller.js +++ b/app/javascript/controllers/time_series_chart_controller.js @@ -408,11 +408,15 @@ export default class extends Controller { ${ - datum.trend.value === 0 + this._extractNumericValue(datum.trend.value) === 0 ? `` : ` - ${this._extractFormattedValue(datum.trend.value)} (${datum.trend.percent_formatted}) + ${this._extractFormattedValue(datum.trend.value)}${ + datum.trend.percent === null + ? "" + : ` (${datum.trend.percent_formatted})` + } ` } diff --git a/app/models/balance_sheet/net_worth_breakdown_series_builder.rb b/app/models/balance_sheet/net_worth_breakdown_series_builder.rb index 06397c4a5..6e21093f3 100644 --- a/app/models/balance_sheet/net_worth_breakdown_series_builder.rb +++ b/app/models/balance_sheet/net_worth_breakdown_series_builder.rb @@ -2,6 +2,7 @@ class BalanceSheet::NetWorthBreakdownSeriesBuilder # Monthly interval regardless of period length so the reports chart always # shows one point per month in the selected range. INTERVAL = "1 month" + CACHE_VERSION = "v2" def initialize(family, user: nil) @family = family @@ -41,18 +42,26 @@ class BalanceSheet::NetWorthBreakdownSeriesBuilder } end + # Built by hand rather than through `Series#as_json`, so it needs the same + # display rounding: the chart is drawn from the amount, and a sub-unit + # residue would plot as a visible move between two points that print the + # same value. The classification totals are left alone, since only their + # formatted string is rendered. + current = value.value.for_display + previous_value = (previous&.value || value.value).for_display + { date: value.date, date_formatted: value.date_formatted, - value: value.value, + value: current, # Month-over-month change between chart points. The trend on the raw # series value compares the underlying balance row's own start/end, # which at a monthly interval reflects only the last balance update # before the sample date. The first point has no prior month, so it # gets a flat trend. trend: Trend.new( - current: value.value, - previous: previous&.value || value.value, + current: current, + previous: previous_value, favorable_direction: "up" ), assets: classification_total(point_groups, "asset"), @@ -125,6 +134,7 @@ class BalanceSheet::NetWorthBreakdownSeriesBuilder shares_version = user ? AccountShare.where(user: user).maximum(:updated_at)&.to_i : nil key = [ "balance_sheet_net_worth_breakdown_series", + CACHE_VERSION, user&.id, shares_version, period.start_date, diff --git a/app/models/series.rb b/app/models/series.rb index 501d78fba..6c2800bf1 100644 --- a/app/models/series.rb +++ b/app/models/series.rb @@ -56,11 +56,14 @@ class Series @favorable_direction = favorable_direction end + # Rounded like the serialized values: this trend is only ever rendered next to + # them, so a change between two amounts that print identically has to read as + # no change here too. def trend return nil if values.blank? @trend ||= Trend.new( - current: values.last&.value, - previous: values.first&.value, + current: display_amount(values.last&.value), + previous: display_amount(values.first&.value), favorable_direction: favorable_direction ) end @@ -71,7 +74,27 @@ class Series end_date: end_date, interval: interval, trend: trend, - values: values.map { |v| { date: v.date, date_formatted: v.date_formatted, value: v.value, trend: v.trend } } + values: values.map do |v| + { date: v.date, date_formatted: v.date_formatted, value: display_amount(v.value), trend: display_trend(v.trend) } + end } end + + private + # A chart is drawn from these amounts, not from the formatted strings, so a + # sub-unit residue would plot as a visible move between two points that both + # print as the same value, and a change between them would read as non-zero. + def display_trend(trend) + return trend unless trend.is_a?(Trend) + + Trend.new( + current: display_amount(trend.current), + previous: display_amount(trend.previous), + favorable_direction: trend.favorable_direction + ) + end + + def display_amount(amount) + amount.is_a?(Money) ? amount.for_display : amount + end end diff --git a/app/views/accountable_sparklines/show.html.erb b/app/views/accountable_sparklines/show.html.erb index c090551a6..05b0339c0 100644 --- a/app/views/accountable_sparklines/show.html.erb +++ b/app/views/accountable_sparklines/show.html.erb @@ -4,8 +4,11 @@ <%= render "shared/sparkline", id: dom_id(@accountable, :sparkline_chart), series: @series %> - <%= tag.p @series.trend.percent_formatted, - style: "color: #{@series.trend.color}", - class: "font-mono text-right text-xs font-medium text-primary" %> + <%# A previous value of zero makes the percentage infinite, with nothing useful to print %> + <% if @series.trend.percent.finite? %> + <%= tag.p @series.trend.percent_formatted, + style: "color: #{@series.trend.color}", + class: "font-mono text-right text-xs font-medium text-primary" %> + <% end %> <% end %> diff --git a/app/views/accounts/sparkline.html.erb b/app/views/accounts/sparkline.html.erb index 0a07c653d..ba8339049 100644 --- a/app/views/accounts/sparkline.html.erb +++ b/app/views/accounts/sparkline.html.erb @@ -4,8 +4,11 @@ <%= render "shared/sparkline", id: dom_id(@account, :sparkline_chart), series: @sparkline_series %> - <%= tag.p @sparkline_series.trend.percent_formatted, - style: "color: #{@sparkline_series.trend.color}", - class: "font-mono text-right text-xs font-medium text-primary" %> + <%# A previous value of zero makes the percentage infinite, with nothing useful to print %> + <% if @sparkline_series.trend.percent.finite? %> + <%= tag.p @sparkline_series.trend.percent_formatted, + style: "color: #{@sparkline_series.trend.color}", + class: "font-mono text-right text-xs font-medium text-primary" %> + <% end %> <% end %> diff --git a/app/views/reports/_net_worth.html.erb b/app/views/reports/_net_worth.html.erb index 0d6bc74b0..6c87fafd6 100644 --- a/app/views/reports/_net_worth.html.erb +++ b/app/views/reports/_net_worth.html.erb @@ -17,9 +17,11 @@
<%= trend.value.format(signify_positive: true) %>
-- <%= trend.value >= 0 ? "+" : "" %><%= trend.percent_formatted %> -
+ <% if trend.percent.finite? %> ++ <%= trend.value >= 0 ? "+" : "" %><%= trend.percent_formatted %> +
+ <% end %> <% else %>--
<% end %> diff --git a/app/views/reports/print.html.erb b/app/views/reports/print.html.erb index d92698ab4..bfbe76ddc 100644 --- a/app/views/reports/print.html.erb +++ b/app/views/reports/print.html.erb @@ -82,9 +82,16 @@ <%= @net_worth_metrics[:current_net_worth].format %> <% if @net_worth_metrics[:trend] %> - - <%= @net_worth_metrics[:trend].value >= 0 ? "+" : "" %><%= @net_worth_metrics[:trend].value.format %> (<%= @net_worth_metrics[:trend].percent_formatted %>) <%= t("reports.print.net_worth.this_period") %> - + <% trend = @net_worth_metrics[:trend] %> + <% unless trend.direction.flat? %> + + <%= trend.value >= 0 ? "+" : "" %><%= trend.value.format %> + <% if trend.percent.finite? %> + (<%= trend.percent_formatted %>) + <% end %> + <%= t("reports.print.net_worth.this_period") %> + + <% end %> <% end %> <% if has_sparkline_data?(@trends_data) %>