tables = new Saddle_Analytics_Report_Table( $db ); $this->cards = new Saddle_Analytics_Summary( $db ); } /** @return array[] Rows: value + every metric, sorted by $metric. */ public function table( Saddle_Analytics_Dimension $d, string $metric, string $range, array $filters = array(), int $limit = 100, ?string $from = null, ?string $to = null ): array { return $this->tables->table( $d, $metric, $range, $filters, $limit, $from, $to ); } /** The KPI cards; with $compare, 'previous' and 'delta' are added. */ public function summary( string $range, ?string $from = null, ?string $to = null, bool $compare = false, array $filters = array() ): array { return $this->cards->summary( $range, $from, $to, $compare, $filters ); } /** * Visitors per bucket for the chart: hourly for a single day, daily * otherwise. Every bucket in the window is present (zero-filled), so the * chart always draws exactly as many points as the range has periods. * * With $compare, a 'previous' points array is added, bucketed over the * preceding window at the same interval and the same count so the chart can * align it index-to-index against the current series. * * @return array{interval:string, points:array, previous?:array} */ public function timeseries( string $range, ?string $from = null, ?string $to = null, bool $compare = false, array $filters = array() ): array { $now = time(); $window = Saddle_Analytics_Window::resolve( $range, $from, $to, $now ); $interval = Saddle_Analytics_Window::interval( $window ); $count = Saddle_Analytics_Window::buckets( $window, $interval, $now ); // 'all' has no start; the chart still caps at 90 days (its KPIs stay lifetime). if ( null === $window['start'] ) { $window['start'] = gmdate( 'Y-m-d 00:00:00', $now - 89 * DAY_IN_SECONDS ); } $series = array( 'interval' => $interval, 'points' => $this->buckets( $window, $interval, $count, $filters ), ); if ( $compare ) { // Same bucket count as the current series, deliberately: comparing // today's first 17 hours against yesterday's first 17 is like-for-like, // and the chart can align the two arrays index-to-index. $previous = Saddle_Analytics_Window::previous( $window ); $series['previous'] = null === $previous ? null : $this->buckets( $previous, $interval, $count, $filters ); } return $series; } /** One window's zero-filled chart buckets, narrowed by the visitor filters. */ private function buckets( array $window, string $interval, int $count, array $filters = array() ): array { $t = Saddle_Analytics_Install::tables(); $step = 'hour' === $interval ? HOUR_IN_SECONDS : DAY_IN_SECONDS; $anchor = (string) $window['start']; list( $bounds, $bound_params ) = Saddle_Analytics_Window::bounds( $window, 'ts' ); list( $scope, $scope_params ) = Saddle_Analytics_Dimensions::visitor_scope( $filters ); // Integer buckets as an offset from a prepared UTC anchor — same reason // as Saddle_Analytics_Realtime: DATE_FORMAT/HOUR() are not portable to the SQLite // driver, and both UNIX_TIMESTAMP terms cancel MySQL's session zone. $hits = (array) $this->db->get_results( $this->db->prepare( "SELECT FLOOR((UNIX_TIMESTAMP(ts) - UNIX_TIMESTAMP(%s)) / %d) AS bucket, COUNT(DISTINCT visitor_id) AS visitors, COUNT(*) AS pageviews FROM {$t['hits']} WHERE event_type = 1{$bounds}{$scope} GROUP BY bucket", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared $anchor, $step, ...array_merge( $bound_params, $scope_params ) ), ARRAY_A ); // Cast the bucket key: drivers may hand FLOOR() back as "3" or "3.0". $by_bucket = array(); foreach ( $hits as $row ) { $by_bucket[ (int) $row['bucket'] ] = $row; } $origin = (int) strtotime( $anchor . ' UTC' ); $points = array(); for ( $i = 0; $i < $count; $i++ ) { $points[] = array( 'ts' => $origin + $i * $step, 'visitors' => (int) ( $by_bucket[ $i ]['visitors'] ?? 0 ), 'pageviews' => (int) ( $by_bucket[ $i ]['pageviews'] ?? 0 ), ); } return $points; } /** Whole days a range spans; null = lifetime. */ public function range_days( string $range ): ?int { return Saddle_Analytics_Window::days( $range ); } /** Midnight of today−(N−1); null = lifetime. */ public function range_start( string $range ): ?string { return Saddle_Analytics_Window::range_start( $range, time() ); } }