From 1ad5cd61e1c68364d40c3160431cb3bd246c1127 Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Sun, 5 Nov 2023 19:01:10 +0100 Subject: [PATCH] introduce view tabs per year with daily/monthly data --- css/dashboard.css | 3 +- inc/class-statify-api.php | 30 ++++-- inc/class-statify-evaluation.php | 49 +++++----- js/dashboard.js | 141 ++++++++++++++++++++++++++- tests/test-evaluation.php | 25 ++++- views/view-dashboard.php | 162 ++++++++++++++++++++++++++++++- 6 files changed, 373 insertions(+), 37 deletions(-) diff --git a/css/dashboard.css b/css/dashboard.css index 25660cd..985ae53 100644 --- a/css/dashboard.css +++ b/css/dashboard.css @@ -61,7 +61,8 @@ body.rtl .statify-chart * { .statify-table th, -.statify-table td:last-child { +.statify-table tr.statify-table-sum td, +.statify-table td.statify-table-sum { font-weight: 700; } diff --git a/inc/class-statify-api.php b/inc/class-statify-api.php index 7e9f87b..991d798 100644 --- a/inc/class-statify-api.php +++ b/inc/class-statify-api.php @@ -183,11 +183,25 @@ public static function get_extended( WP_REST_Request $request ): WP_REST_Respons ); } + // Parse year, if provided. + $yr = $request->get_param( 'year' ); + if ( ! empty( $yr ) ) { + $yr = intval( $yr ); + if ( $yr <= 0 ) { + return new WP_REST_Response( + array( 'error' => 'invalid year' ), + 400 + ); + } + } else { + $yr = 0; + } + // Retrieve from cache, if data is not post-specific. $post = $request->get_param( 'post' ); $stats = false; if ( ! $post ) { - $stats = self::from_cache( $scope ); + $stats = self::from_cache( $scope, $yr ); } if ( ! $stats ) { @@ -217,12 +231,12 @@ public static function get_extended( WP_REST_Request $request ): WP_REST_Respons $last_ym = $year_month; } } elseif ( 'day' === $scope ) { - $stats = Statify_Evaluation::get_views_for_all_days( $post ); + $stats = Statify_Evaluation::get_views_for_all_days( $yr, $post ); } // Update cache, if data is not post-specific. if ( ! $post ) { - self::update_cache( $scope, $stats ); + self::update_cache( $scope, $yr, $stats ); } } @@ -233,22 +247,24 @@ public static function get_extended( WP_REST_Request $request ): WP_REST_Respons * Retrieve data from cache. * * @param string $scope Scope (year, month, day). + * @param int $index Optional index (e.g. year). * * @return array|false Transient data or FALSE. */ - private static function from_cache( string $scope ) { - return get_transient( 'statify_data_' . $scope ); + private static function from_cache( string $scope, int $index = 0 ) { + return get_transient( 'statify_data_' . $scope . ( $index > 0 ? '_' . $index : '' ) ); } /** * Update data cache. * * @param string $scope Scope (year, month, day). + * @param int $index Optional index (e.g. year). * @param array $data Data. */ - private static function update_cache( string $scope, array $data ): void { + private static function update_cache( string $scope, int $index, array $data ): void { set_transient( - 'statify_data_' . $scope, + 'statify_data_' . $scope . ( $index > 0 ? '_' . $index : '' ), $data, 30 * MINUTE_IN_SECONDS ); diff --git a/inc/class-statify-evaluation.php b/inc/class-statify-evaluation.php index 962d44d..81c5341 100644 --- a/inc/class-statify-evaluation.php +++ b/inc/class-statify-evaluation.php @@ -126,36 +126,37 @@ public static function get_years(): array { * Returns the views for all days. * If the given URL is not the empty string, the result is restricted to the given post. * - * @param string $post_url the URL of the post to select for (or the empty string for all posts). + * @param int $single_year single year. + * @param string $post_url the URL of the post to select for (or the empty string for all posts). * * @return array an array with date as key and views as value */ - public static function get_views_for_all_days( string $post_url = '' ): array { + public static function get_views_for_all_days( int $single_year, string $post_url = '' ): array { global $wpdb; - if ( empty( $post_url ) ) { - // For all posts. - $results = $wpdb->get_results( - 'SELECT `created` as `date`, COUNT(`created`) as `count`' . - " FROM `$wpdb->statify`" . - ' GROUP BY `created`' . - ' ORDER BY `created`', - ARRAY_A - ); - } else { - // Only for selected posts. - $results = $wpdb->get_results( - $wpdb->prepare( - 'SELECT `created` as `date`, COUNT(`created`) as `count`' . - " FROM `$wpdb->statify`" . - ' WHERE `target` = %s' . - ' GROUP BY `created`' . - ' ORDER BY `created`', - $post_url - ), - ARRAY_A - ); + $query = 'SELECT `created` as `date`, COUNT(`created`) as `count`' . + " FROM `$wpdb->statify`"; + $args = array(); + + if ( $single_year > 0 ) { + $query .= ' WHERE YEAR(`created`) = %d'; + $args[] = $single_year; + } + + if ( ! empty( $post_url ) ) { + $query .= ( $single_year > 0 ? ' AND' : ' WHERE' ) . ' `target` = %s'; + $args[] = $post_url; + } + + $query .= ' GROUP BY `created`' . + ' ORDER BY `created`'; + + if ( ! empty( $args ) ) { + $query = $wpdb->prepare( $query, $args ); } + + $results = $wpdb->get_results( $query, ARRAY_A ); + $views_for_all_days = array(); foreach ( $results as $result ) { $views_for_all_days[ $result['date'] ] = intval( $result['count'] ); diff --git a/js/dashboard.js b/js/dashboard.js index d00a26b..eacaee1 100644 --- a/js/dashboard.js +++ b/js/dashboard.js @@ -12,9 +12,11 @@ ); const refreshBtn = document.getElementById('statify_refresh'); + const chartElemDaily = document.getElementById('statify_chart_daily'); const chartElemMonthly = document.getElementById('statify_chart_monthly'); const chartElemYearly = document.getElementById('statify_chart_yearly'); const yearlyTable = document.getElementById('statify-table-yearly'); + const dailyTable = document.getElementById('statify-table-daily'); /** * Update the dashboard widget @@ -61,6 +63,22 @@ }); } + /** + * Render monthly statistics. + * + * @param {number} year Year to load data for. + * + * @return {Promise<{[key: string]: number}>} Data promise from API. + */ + function loadDaily(year) { + year = encodeURIComponent(year); + + // Load data from API. + return wp.apiFetch({ + path: `/statify/v1/stats/extended?scope=day&year=${year}`, + }); + } + /** * Render monthly statistics. * @@ -71,6 +89,19 @@ return wp.apiFetch({ path: '/statify/v1/stats/extended?scope=month' }); } + /** + * Render daily statistics. + * + * @param {HTMLElement} root Root element. + * @param {{[key: string]: number}} data Data from API. + */ + function renderDaily(root, data) { + const labels = Object.keys(data); + const values = Object.values(data); + + render(root, labels, values); + } + /** * Render monthly statistics. * @@ -308,6 +339,7 @@ } col = document.createElement('TD'); + col.classList.add('statify-table-sum'); col.innerText = sum; row.appendChild(col); @@ -315,6 +347,101 @@ } } + /** + * Render yearly table. + * + * @param {HTMLElement} table Root element. + * @param {any} data Data from API. + */ + function renderDailyTable(table, data) { + const rows = Array.from(table.querySelectorAll('tbody > tr')); + const cols = rows.map((row) => Array.from(row.querySelectorAll('td'))); + let out = cols.slice(0, 31); + + const sum = Array(12).fill(0); + const vls = Array(12).fill(0); + const min = Array(12).fill(Number.MAX_SAFE_INTEGER); + const max = Array(12).fill(0); + + for (const [day, count] of Object.entries(data)) { + const d = new Date(day); + const m = d.getMonth(); + sum[m] += count; + ++vls[m]; + min[m] = Math.min(min[m], count); + max[m] = Math.max(max[m], count); + out[d.getDate() - 1][m].innerText = count; + } + + out = + cols[ + rows.findIndex((row) => + row.classList.contains('statify-table-sum') + ) + ]; + const avg = + cols[ + rows.findIndex((row) => + row.classList.contains('statify-table-avg') + ) + ]; + for (const [m, s] of sum.entries()) { + if (vls[m] > 0) { + out[m].innerText = s; + avg[m].innerText = Math.round(s / vls[m]); + } else { + out[m].innerText = '-'; + avg[m].innerText = '-'; + } + } + + out = + cols[ + rows.findIndex((row) => + row.classList.contains('statify-table-min') + ) + ]; + for (const [m, s] of min.entries()) { + out[m].innerText = vls[m] > 0 ? s : '-'; + } + + out = + cols[ + rows.findIndex((row) => + row.classList.contains('statify-table-max') + ) + ]; + for (const [m, s] of max.entries()) { + out[m].innerText = vls[m] > 0 ? s : '-'; + } + + for (const row of rows) { + row.classList.remove('placeholder'); + } + } + + /** + * Convert daily to monthly data. + * + * @param {{[key: string]: number}} data Daily data. + * @return {{visits: {[key: string]: {[key: string]: number}}}} Monthly data. + */ + function dailyToMonthly(data) { + const monthly = { visits: {} }; + for (const [day, count] of Object.entries(data)) { + const date = new Date(day); + const year = date.getFullYear(); + const month = date.getMonth(); + + if (!(year in monthly.visits)) { + monthly.visits[year] = {}; + } + monthly.visits[year][month] = + count + (monthly.visits[year][month] || 0); + } + return monthly; + } + // Abort if config or target element is not present. if (typeof statifyDashboard !== 'undefined') { if (chartElem) { @@ -332,7 +459,19 @@ updateDashboard(false); } - if (chartElemMonthly) { + if (chartElemDaily) { + loadDaily(chartElemDaily.dataset.year).then((data) => { + renderDaily(chartElemDaily, data); + + if (chartElemMonthly) { + renderMonthly(chartElemMonthly, dailyToMonthly(data)); + } + + if (dailyTable) { + renderDailyTable(dailyTable, data); + } + }); + } else if (chartElemMonthly) { loadMonthly() .then((data) => { renderMonthly(chartElemMonthly, data); diff --git a/tests/test-evaluation.php b/tests/test-evaluation.php index cd6f2bb..78aa868 100644 --- a/tests/test-evaluation.php +++ b/tests/test-evaluation.php @@ -73,6 +73,7 @@ public function test_get_years() { * Test views for all days. */ public function test_get_views_for_all_days() { + $this->insert_test_data( '2022-10-20', '', '/test/' ); $this->insert_test_data( '2023-03-23', '', '/', 3 ); $this->insert_test_data( '2023-03-23', '', '/test/' ); $this->insert_test_data( '2023-03-25', '', '/' ); @@ -80,17 +81,37 @@ public function test_get_views_for_all_days() { self::assertSame( array( + '2022-10-20' => 1, '2023-03-23' => 4, '2023-03-25' => 3, ), - Statify_Evaluation::get_views_for_all_days() + Statify_Evaluation::get_views_for_all_days(), + 'unexpected results without anyfilter' ); self::assertSame( array( + '2022-10-20' => 1, '2023-03-23' => 1, '2023-03-25' => 2, ), - Statify_Evaluation::get_views_for_all_days( '/test/' ) + Statify_Evaluation::get_views_for_all_days( 0, '/test/' ), + 'unexpected results with post filter' + ); + self::assertSame( + array( + '2023-03-23' => 4, + '2023-03-25' => 3, + ), + Statify_Evaluation::get_views_for_all_days( 2023 ), + 'unexpected results with year filter' + ); + self::assertSame( + array( + '2023-03-23' => 1, + '2023-03-25' => 2, + ), + Statify_Evaluation::get_views_for_all_days( 2023, '/test/' ), + 'unexpected results with year and post filter' ); } diff --git a/views/view-dashboard.php b/views/view-dashboard.php index 65929bd..d4026d0 100644 --- a/views/view-dashboard.php +++ b/views/view-dashboard.php @@ -8,14 +8,67 @@ // Exit if accessed directly. defined( 'ABSPATH' ) || exit; + +if ( isset( $_GET['year'] ) ) { + $selected_year = intval( $_GET['year'] ); +} else { + $selected_year = null; +} +$years = Statify_Evaluation::get_years(); + ?>

+ +

+ +
+

+ +

+ +
+
+ +
+
+
+ +
-

+

+ +

@@ -24,6 +77,7 @@
+

@@ -61,9 +115,113 @@       -   +  
+ + + +
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
            
            
            
            
            
+
+