GLQL: Add showTrends support (for stats and tables)

Add showTrends configuration support to GLQL, enabling percentage change indicators in both stat and table displays.

This is a GLQL-level visualization feature that works across multiple display types (stat, table) and any data type once their aggregation engines support time-series data.

Part 6 of 6 visualization types needed for SDLC dashboard migration:

  1. Tables (#592262 (closed)) - 18.11 GA
  2. Stats (#592780 (moved)) - Post-18.11
  3. Sparklines (#592781 (moved)) - Post-18.11
  4. Bar Charts (#592782 (moved)) - Post-18.11
  5. Column Charts (#592784 (moved)) - Post-18.11
  6. Area Charts (#592783 (moved)) - Post-18.11
  7. showTrends (this issue) - Post-18.11 - Works with stats and tables

Aligned with research issue #589575 (closed) for GLQL visualization syntax.

SDLC Dashboard use cases (CodeSuggestion):

  • Single stat showing total suggestions with % change vs previous period
  • Table showing acceptance rate by language with % change column
  • Any metric display that benefits from trend comparison

Overview

showTrends is a boolean configuration option that adds percentage change indicators:

  • In stats: Shows "▲ 12.5%" or "▼ 8.3%" below the main value
  • In tables: Adds a "Change %" column showing trend for each row

Frontend calculates the percentage change by comparing the last two periods in the time-series data.

Acceptance Criteria

General

  • displayConfig.showTrends: true is supported in both stat and table display types
  • Requires time dimension using timeSegment() in aggregate
  • Frontend calculates % change from time-series data (comparing last two periods)
  • Up arrow (▲) shown for increases, down arrow (▼) for decreases
  • Feature flag glql_trends_display_type gates functionality using a gitlab_com_derisk flag
  • When flag is disabled but showTrends requested, returns error
  • Works with any GLQL data type that has time-series data
  • No JavaScript console errors

For Stat Display

  • Trend shows below main value as "▲ 12.5%" or "▼ 8.3%"
  • Query validation: Must have 1 metric and 1 time dimension
  • Frontend extracts last two data points from time-series
  • Calculates: (current - previous) / previous * 100

For Table Display

  • Adds implicit "Change %" column after all metrics
  • Shows trend for each row comparing first period to last period
  • Query validation: Must have time dimension in aggregate
  • Each row shows "▲ X.X%" or "▼ X.X%" based on that row's trend

Testing

  • Frontend tests cover showTrends in stat display
  • Frontend tests cover showTrends in table display
  • Error handling when showTrends used without time dimension

Example GLQL Queries

Stat with showTrends

type: CodeSuggestion
mode: analytics
query: timestamp >= -60d
display: stat
displayConfig:
  showTrends: true
aggregate:
  dimensions: timeSegment(1m) on timestamp
  metrics: totalCount as 'Total Suggestions'

Expected output:

┌─────────────────────┐
│ Total Suggestions   │
│                     │
│       1,234        │
│    ▲ 12.5%         │
└─────────────────────┘

Frontend calculation logic:

  • Query returns: [{month: 1, value: 1100}, {month: 2, value: 1234}]
  • Frontend shows last value: 1,234
  • Frontend calculates: (1234 - 1100) / 1100 * 100 = 12.18%
  • Displays: ▲ 12.5% (rounded)

Table with showTrends

type: CodeSuggestion
mode: analytics
query: timestamp >= -60d
display: table
displayConfig:
  showTrends: true
aggregate:
  dimensions: language as 'Language', timeSegment(1m) on timestamp
  metrics: totalCount as 'Total', acceptanceRate as 'Acceptance Rate'
sort: totalCount desc

Expected output:

| Language   | Total | Acceptance Rate | Change % |
| ---------- | ----- | --------------- | -------- |
| Ruby       | 1,234 | 77.6%           | ▲ 12.5%  |
| JavaScript | 2,456 | 68.4%           | ▼ 3.2%   |
| Python     | 987   | 81.2%           | ▲ 8.1%   |

Frontend calculation logic:

  • For each row (e.g., Ruby):
    • Extract time-series: [{month: 1, total: 1100}, {month: 2, total: 1234}]
    • Compare first to last: (1234 - 1100) / 1100 * 100 = 12.18%
    • Display: ▲ 12.5%

Invalid Example

type: CodeSuggestion
mode: analytics
display: stat
displayConfig:
  showTrends: true
aggregate:
  metrics: totalCount as 'Total'
# Error: "showTrends requires a time dimension using timeSegment()"

Implementation Notes

Files to modify:

  • app/assets/javascripts/glql/ - Add showTrends renderer for both stat and table

Cross-display type feature:

  • This is the first GLQL feature that works across multiple display types
  • Shared logic should be extracted to common utility
  • Both stat and table renderers will use the same calculation logic

Frontend calculation (shared):

function calculateTrend(timeSeriesData) {
  if (timeSeriesData.length < 2) return null;
  
  const previous = timeSeriesData[timeSeriesData.length - 2].value;
  const current = timeSeriesData[timeSeriesData.length - 1].value;
  
  const percentChange = ((current - previous) / previous) * 100;
  const arrow = percentChange >= 0 ? '' : '';
  const formatted = Math.abs(percentChange).toFixed(1);
  
  return `${arrow} ${formatted}%`;
}

Stat-specific rendering:

  • Trend appears below the main value
  • Centered alignment
  • Slightly smaller font than main value
  • Green color for ▲, red color for ▼

Table-specific rendering:

  • Adds "Change %" column after all metrics
  • Each row calculates trend independently
  • Uses same color scheme (green ▲, red ▼)
  • Column ordering: dimensions, metrics, sparklines, trends (FIFO)

Time dimension requirement:

  • Must use timeSegment() function syntax
  • Examples: timeSegment(1m), timeSegment(1w), timeSegment(1d)
  • Frontend identifies time-series data by this dimension

Validation:

  • Error if showTrends: true but no time dimension present
  • Error message: "showTrends requires a time dimension using timeSegment()"

Feature flag behavior:

  • glql_trends_display_type gates this functionality
  • When disabled and showTrends is requested, return error message

Comparison logic:

  • Always compares last two periods in the time-series
  • User controls the comparison by choosing the time range in the query
  • Example: Query with -60d and timeSegment(1m) compares last 2 months
  • Example: Query with -14d and timeSegment(1d) compares last 2 days

Future enhancements (document but don't implement):

  1. Custom comparison periods:
displayConfig:
  showTrends: true
  trendComparison: -7d  # Compare to specific period instead of previous
  1. Configurable positive direction:
displayConfig:
  showTrends: true
  positiveDirection: down  # For error rates where down is good
  1. Multiple trend columns in tables:
displayConfig:
  trends:
    - column: '7d Change'
      compareWith: -7d
    - column: '30d Change'
      compareWith: -30d