@@ -81,6 +81,7 @@ protected function initChartObjectData($dataTable, $visualization)
8181 // collect series data to show. each row-to-display/column-to-display permutation creates a series.
8282 $ allSeriesData = [];
8383 $ allSeriesDataAvailability = [];
84+ $ allSeriesAllowsDownwardForecast = [];
8485 foreach ($ rowsToDisplay as $ rowIdentifier ) {
8586 $ rowLabel = $ rowIdentifier ;
8687
@@ -93,10 +94,32 @@ protected function initChartObjectData($dataTable, $visualization)
9394 }
9495
9596 foreach ($ columnsToDisplay as $ columnName ) {
97+ $ columnAllowsDownwardForecast = $ this ->columnAllowsDownwardForecast (
98+ $ columnName ,
99+ $ units [$ columnName ] ?? false
100+ );
101+
96102 if (!$ this ->isComparing ) {
97- $ this ->setNonComparisonSeriesData ($ allSeriesData , $ allSeriesDataAvailability , $ rowLabel , $ columnName , $ dataTable );
103+ $ this ->setNonComparisonSeriesData (
104+ $ allSeriesData ,
105+ $ allSeriesDataAvailability ,
106+ $ allSeriesAllowsDownwardForecast ,
107+ $ rowLabel ,
108+ $ columnName ,
109+ $ columnAllowsDownwardForecast ,
110+ $ dataTable
111+ );
98112 } else {
99- $ this ->setComparisonSeriesData ($ allSeriesData , $ allSeriesDataAvailability , $ seriesLabels , $ rowLabel , $ columnName , $ dataTable );
113+ $ this ->setComparisonSeriesData (
114+ $ allSeriesData ,
115+ $ allSeriesDataAvailability ,
116+ $ allSeriesAllowsDownwardForecast ,
117+ $ seriesLabels ,
118+ $ rowLabel ,
119+ $ columnName ,
120+ $ columnAllowsDownwardForecast ,
121+ $ dataTable
122+ );
100123 }
101124 }
102125 }
@@ -144,7 +167,14 @@ protected function initChartObjectData($dataTable, $visualization)
144167
145168 $ dataStates = $ this ->setDataStates ($ visualization , $ dataTables );
146169 $ visualization ->setForecastData (
147- $ this ->buildForecastData ($ allSeriesData , $ dataTables , $ dataStates , $ seriesUnits , $ allSeriesDataAvailability )
170+ $ this ->buildForecastData (
171+ $ allSeriesData ,
172+ $ dataTables ,
173+ $ dataStates ,
174+ $ seriesUnits ,
175+ $ allSeriesDataAvailability ,
176+ $ allSeriesAllowsDownwardForecast
177+ )
148178 );
149179 }
150180
@@ -154,14 +184,16 @@ protected function initChartObjectData($dataTable, $visualization)
154184 * @param array<int, string> $dataStates
155185 * @param array<string, string|false> $seriesUnits
156186 * @param array<string, array<int, bool>> $allSeriesDataAvailability
187+ * @param array<string, bool> $allSeriesAllowsDownwardForecast
157188 * @return array<int, array<int, float|null>>
158189 */
159190 protected function buildForecastData (
160191 array $ allSeriesData ,
161192 array $ dataTables ,
162193 array $ dataStates ,
163194 array $ seriesUnits ,
164- array $ allSeriesDataAvailability
195+ array $ allSeriesDataAvailability ,
196+ array $ allSeriesAllowsDownwardForecast
165197 ): array {
166198 if (empty ($ this ->properties ['show_forecast ' ]) || $ this ->isComparing ) {
167199 return [];
@@ -176,7 +208,33 @@ protected function buildForecastData(
176208 }
177209 }
178210
179- return (new ForecastBuilder ())->build ($ allSeriesData , $ dataTables , $ dataStates , $ seriesUnits , $ allSeriesDataAvailability );
211+ return (new ForecastBuilder ())->build (
212+ $ allSeriesData ,
213+ $ dataTables ,
214+ $ dataStates ,
215+ $ seriesUnits ,
216+ $ allSeriesDataAvailability ,
217+ $ allSeriesAllowsDownwardForecast
218+ );
219+ }
220+
221+ /**
222+ * Whether forecasts for a given column may legitimately fall below the current partial value.
223+ *
224+ * Counts (visits, conversions, etc.) only grow within an incomplete period, so their forecast
225+ * is gated by the "forecast >= current" rule. Ratios and lower-is-better metrics (bounce rate,
226+ * exit rate, average page generation time, position) can move either way during the period
227+ * and need the gate lifted, otherwise valid downward trends are silently suppressed.
228+ *
229+ * @param string|false $columnUnit
230+ */
231+ private function columnAllowsDownwardForecast (string $ columnName , $ columnUnit ): bool
232+ {
233+ if ($ columnUnit === '% ' ) {
234+ return true ;
235+ }
236+
237+ return Metrics::isLowerValueBetter ($ columnName );
180238 }
181239
182240 private function getSeriesData ($ rowLabel , $ columnName , DataTable \Map $ dataTable , &$ seriesDataAvailability )
@@ -295,17 +353,33 @@ protected function addSelectedSeriesXLabels(array &$xLabels, array $dataTables)
295353 }
296354 }
297355
298- private function setNonComparisonSeriesData (array &$ allSeriesData , array &$ allSeriesDataAvailability , $ rowLabel , $ columnName , DataTable \Map $ dataTable )
299- {
356+ private function setNonComparisonSeriesData (
357+ array &$ allSeriesData ,
358+ array &$ allSeriesDataAvailability ,
359+ array &$ allSeriesAllowsDownwardForecast ,
360+ $ rowLabel ,
361+ $ columnName ,
362+ bool $ columnAllowsDownwardForecast ,
363+ DataTable \Map $ dataTable
364+ ) {
300365 $ seriesLabel = $ this ->getSeriesLabel ($ rowLabel , $ columnName );
301366
302367 $ seriesData = $ this ->getSeriesData ($ rowLabel , $ columnName , $ dataTable , $ seriesDataAvailability );
303368 $ allSeriesData [$ seriesLabel ] = $ seriesData ;
304369 $ allSeriesDataAvailability [$ seriesLabel ] = $ seriesDataAvailability ;
370+ $ allSeriesAllowsDownwardForecast [$ seriesLabel ] = $ columnAllowsDownwardForecast ;
305371 }
306372
307- private function setComparisonSeriesData (array &$ allSeriesData , array &$ allSeriesDataAvailability , array $ seriesLabels , $ rowLabel , $ columnName , DataTable \Map $ dataTable )
308- {
373+ private function setComparisonSeriesData (
374+ array &$ allSeriesData ,
375+ array &$ allSeriesDataAvailability ,
376+ array &$ allSeriesAllowsDownwardForecast ,
377+ array $ seriesLabels ,
378+ $ rowLabel ,
379+ $ columnName ,
380+ bool $ columnAllowsDownwardForecast ,
381+ DataTable \Map $ dataTable
382+ ) {
309383 foreach ($ dataTable ->getDataTables () as $ label => $ childTable ) {
310384 // get the row for this label (use the first if $rowLabel is false)
311385 if ($ rowLabel === false ) {
@@ -322,6 +396,7 @@ private function setComparisonSeriesData(array &$allSeriesData, array &$allSerie
322396 $ wholeSeriesLabel = $ this ->getComparisonSeriesLabelFromCompareSeries ($ seriesLabelPrefix , $ columnName , $ rowLabel );
323397 $ allSeriesData [$ wholeSeriesLabel ][] = 0 ;
324398 $ allSeriesDataAvailability [$ wholeSeriesLabel ][] = false ;
399+ $ allSeriesAllowsDownwardForecast [$ wholeSeriesLabel ] = $ columnAllowsDownwardForecast ;
325400 }
326401
327402 continue ;
@@ -334,6 +409,7 @@ private function setComparisonSeriesData(array &$allSeriesData, array &$allSerie
334409 $ value = $ compareRow ->getColumn ($ columnName );
335410 $ allSeriesData [$ seriesLabel ][] = $ value ;
336411 $ allSeriesDataAvailability [$ seriesLabel ][] = $ this ->hasColumnValue ($ value );
412+ $ allSeriesAllowsDownwardForecast [$ seriesLabel ] = $ columnAllowsDownwardForecast ;
337413 }
338414
339415 $ totalsRow = $ comparisonTable ->getTotalsRow ();
@@ -342,6 +418,7 @@ private function setComparisonSeriesData(array &$allSeriesData, array &$allSerie
342418 $ value = $ totalsRow ->getColumn ($ columnName );
343419 $ allSeriesData [$ seriesLabel ][] = $ value ;
344420 $ allSeriesDataAvailability [$ seriesLabel ][] = $ this ->hasColumnValue ($ value );
421+ $ allSeriesAllowsDownwardForecast [$ seriesLabel ] = $ columnAllowsDownwardForecast ;
345422 }
346423 }
347424 }
@@ -487,6 +564,7 @@ public function precomputeForecast($dataTable): array
487564
488565 $ allSeriesData = [];
489566 $ allSeriesDataAvailability = [];
567+ $ allSeriesAllowsDownwardForecast = [];
490568 foreach ($ rowsToDisplay as $ rowIdentifier ) {
491569 $ rowLabel = $ rowIdentifier ;
492570
@@ -499,7 +577,20 @@ public function precomputeForecast($dataTable): array
499577 }
500578
501579 foreach ($ columnsToDisplay as $ columnName ) {
502- $ this ->setNonComparisonSeriesData ($ allSeriesData , $ allSeriesDataAvailability , $ rowLabel , $ columnName , $ dataTable );
580+ $ columnAllowsDownwardForecast = $ this ->columnAllowsDownwardForecast (
581+ $ columnName ,
582+ $ units [$ columnName ] ?? false
583+ );
584+
585+ $ this ->setNonComparisonSeriesData (
586+ $ allSeriesData ,
587+ $ allSeriesDataAvailability ,
588+ $ allSeriesAllowsDownwardForecast ,
589+ $ rowLabel ,
590+ $ columnName ,
591+ $ columnAllowsDownwardForecast ,
592+ $ dataTable
593+ );
503594 }
504595 }
505596
@@ -508,7 +599,8 @@ public function precomputeForecast($dataTable): array
508599 $ dataTables ,
509600 $ dataStates ,
510601 $ seriesUnits ,
511- $ allSeriesDataAvailability
602+ $ allSeriesDataAvailability ,
603+ $ allSeriesAllowsDownwardForecast
512604 );
513605 }
514606}
0 commit comments