@@ -87,27 +87,60 @@ defmodule Plausible.Stats.QueryRunner do
8787 end
8888 end
8989
90- defp get_time_lookup ( query , comparison_query ) do
91- if Time . time_dimension ( query ) && comparison_query do
92- Enum . zip (
93- Time . time_labels ( query ) ,
94- Time . time_labels ( comparison_query )
95- )
96- |> Map . new ( )
97- else
98- % { }
90+ # Assembles the final results, optionally attaching comparison data.
91+ #
92+ # Without a comparison, main results are returned as-is and comparison_results
93+ # is nil.
94+ #
95+ # With comparisons, timeseries and non-time-dimension breakdowns are handled
96+ # separately because they have fundamentally different shapes:
97+ #
98+ # - Non-time breakdowns (e.g. by page, source) return one row per dimension
99+ # group. The comparison query is filtered to the same set of dimension
100+ # values as the main query, so every comparison result is guaranteed to
101+ # have a matching main result. Comparison data is merged inline into each
102+ # result row; comparison_results is nil.
103+ #
104+ # - Timeseries (single "time:*" dimension) keep results and comparison_results
105+ # as separate lists of only non-empty rows. Each comparison row carries a
106+ # `change` field computed against the positionally-aligned original bucket
107+ # (or nil when there is no corresponding original bucket).
108+ defp build_results_list ( % __MODULE__ { main_query: query , main_results: main_results } = runner ) do
109+ case { query . include . compare , query . dimensions } do
110+ { nil , _dimensions } ->
111+ struct! ( runner ,
112+ results: main_results ,
113+ comparison_results: nil
114+ )
115+
116+ { _non_nil_compare , [ "time:" <> _ ] } ->
117+ struct! ( runner ,
118+ results: main_results ,
119+ comparison_results: build_comparison_results ( runner )
120+ )
121+
122+ { _non_nil_compare , _dimensions } ->
123+ struct! ( runner ,
124+ results: merge_with_comparison_results ( main_results , runner ) ,
125+ comparison_results: nil
126+ )
99127 end
100128 end
101129
102- defp build_results_list ( % __MODULE__ { main_query: query , main_results: main_results } = runner ) do
103- results =
104- case query . dimensions do
105- [ "time:" <> _ ] -> main_results |> add_empty_timeseries_rows ( runner )
106- _ -> main_results
107- end
108- |> merge_with_comparison_results ( runner )
109-
110- struct! ( runner , results: results )
130+ defp build_comparison_results ( % __MODULE__ { main_query: query } = runner ) do
131+ main_map = index_by_dimensions ( runner . main_results )
132+
133+ comp_label_to_main_label =
134+ Enum . zip ( Time . time_labels ( runner . comparison_query ) , Time . time_labels ( query ) )
135+ |> Map . new ( )
136+
137+ Enum . map ( runner . comparison_results , fn % { dimensions: [ comp_label ] } = comp_row ->
138+ main_label = Map . get ( comp_label_to_main_label , comp_label )
139+ main_metrics = main_label && Map . get ( main_map , [ main_label ] )
140+ change = calculate_metric_changes ( query , main_metrics , comp_row . metrics )
141+
142+ Map . put ( comp_row , :change , change )
143+ end )
111144 end
112145
113146 defp execute_query ( query , site ) do
@@ -181,82 +214,52 @@ defmodule Plausible.Stats.QueryRunner do
181214 |> Enum . at ( goal_index - 1 )
182215 end
183216
184- # Special case: If comparison and single time dimension, add 0 rows - otherwise
185- # comparisons would not be shown for timeseries with 0 values.
186- defp add_empty_timeseries_rows ( results_list , % __MODULE__ { main_query: query } )
187- when not is_nil ( query . include . compare ) do
188- indexed_results = index_by_dimensions ( results_list )
189-
190- empty_timeseries_rows =
191- Time . time_labels ( query )
192- |> Enum . reject ( fn dimension_value -> Map . has_key? ( indexed_results , [ dimension_value ] ) end )
193- |> Enum . map ( fn dimension_value ->
194- % {
195- metrics: empty_metrics ( query , [ dimension_value ] ) ,
196- dimensions: [ dimension_value ]
197- }
198- end )
199-
200- results_list ++ empty_timeseries_rows
201- end
202-
203- defp add_empty_timeseries_rows ( results_list , _ ) , do: results_list
204-
205217 defp merge_with_comparison_results ( results_list , runner ) do
206- comparison_map = ( runner . comparison_results || [ ] ) |> index_by_dimensions ( )
207- time_lookup = get_time_lookup ( runner . main_query , runner . comparison_query )
208-
209- Enum . map (
210- results_list ,
211- & add_comparison_results ( & 1 , runner . main_query , comparison_map , time_lookup )
212- )
218+ comparison_map = index_by_dimensions ( runner . comparison_results )
219+ Enum . map ( results_list , & add_comparison_results ( & 1 , runner . main_query , comparison_map ) )
213220 end
214221
215- defp add_comparison_results ( row , query , comparison_map , time_lookup )
216- when not is_nil ( query . include . compare ) do
217- dimensions = get_comparison_dimensions ( row . dimensions , query , time_lookup )
218- comparison_metrics = get_comparison_metrics ( comparison_map , dimensions , query )
222+ defp add_comparison_results ( row , query , comparison_map ) do
223+ comparison_metrics = metrics_for_dimension_group ( comparison_map , row . dimensions , query )
219224
220225 change =
221226 Enum . zip ( [ query . metrics , row . metrics , comparison_metrics ] )
222- |> Enum . map ( fn { metric , metric_value , comparison_value } ->
223- Compare . calculate_change ( metric , comparison_value , metric_value )
227+ |> Enum . map ( fn { metric , main_value , comp_value } ->
228+ Compare . calculate_change ( metric , comp_value , main_value )
224229 end )
225230
226231 Map . merge ( row , % {
227232 comparison: % {
228- dimensions: dimensions ,
233+ dimensions: row . dimensions ,
229234 metrics: comparison_metrics ,
230235 change: change
231236 }
232237 } )
233238 end
234239
235- defp add_comparison_results ( row , _ , _ , _ ) , do: row
236-
237- defp get_comparison_dimensions ( dimensions , query , time_lookup ) do
238- query . dimensions
239- |> Enum . zip ( dimensions )
240- |> Enum . map ( fn
241- { "time:" <> _ , value } -> time_lookup [ value ]
242- { _ , value } -> value
243- end )
244- end
245-
246240 defp index_by_dimensions ( results_list ) do
247241 results_list
248242 |> Map . new ( fn entry -> { entry . dimensions , entry . metrics } end )
249243 end
250244
251- defp get_comparison_metrics ( comparison_map , dimensions , query ) do
252- Map . get_lazy ( comparison_map , dimensions , fn -> empty_metrics ( query , dimensions ) end )
245+ defp metrics_for_dimension_group ( lookup_map , dimensions , query ) do
246+ Map . get_lazy ( lookup_map , dimensions , fn -> empty_metrics ( query , dimensions ) end )
253247 end
254248
255249 defp empty_metrics ( query , dimensions ) do
256250 query . metrics
257251 |> Enum . map ( fn metric -> Metrics . default_value ( metric , query , dimensions ) end )
258252 end
259253
254+ defp calculate_metric_changes ( query , main_metrics , comparison_metrics ) do
255+ if main_metrics do
256+ Enum . zip ( [ query . metrics , main_metrics , comparison_metrics ] )
257+ |> Enum . map ( fn { metric , main_value , comp_value } ->
258+ Compare . calculate_change ( metric , comp_value , main_value )
259+ end )
260+ end
261+ end
262+
260263 defp total_rows ( [ ] ) , do: 0
261264 defp total_rows ( [ first_row | _rest ] ) , do: first_row . total_rows
262265end
0 commit comments