From 0226303084f4c5aca826266bad89f0cf9b32e2e6 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 | 88 ++++++++--------- inc/class-statify-evaluation.php | 45 ++++----- js/dashboard.js | 141 ++++++++++++++++++++++++++- tests/test-evaluation.php | 2 +- views/view-dashboard.php | 162 ++++++++++++++++++++++++++++++- 6 files changed, 367 insertions(+), 74 deletions(-) diff --git a/css/dashboard.css b/css/dashboard.css index 25660cda..985ae539 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 f624d71c..b5a7c613 100644 --- a/inc/class-statify-api.php +++ b/inc/class-statify-api.php @@ -183,64 +183,60 @@ public static function get_extended( $request ) { ); } + // 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 ) { if ( 'year' === $scope ) { - if ( ! $post ) { - $stats = self::from_cache( $scope ); - } - - if ( ! $stats ) { - $stats = Statify_Evaluation::get_views_for_all_years( $post ); - } + $stats = Statify_Evaluation::get_views_for_all_years( $post ); } elseif ( 'month' === $scope ) { - if ( ! $post ) { - $stats = self::from_cache( $scope ); - } - - if ( ! $stats ) { - $visits = Statify_Evaluation::get_views_for_all_months( $post ); - $stats = array( 'visits' => array() ); - $last_ym = 0; - foreach ( $visits as $ym => $v ) { - $ym = explode( '-', $ym ); - $year = intval( $ym[0] ); - $month = intval( $ym[1] ); - $year_month = $year * 12 + $month; - for ( $ym = $last_ym + 1; $last_ym > 0 && $ym < $year_month; $ym ++ ) { - // Fill gaps. - $y = intval( $ym / 12 ); - if ( ! isset( $stats['visits'][ $y ] ) ) { - $stats['visits'][ $y ] = array(); - } - $stats['visits'][ $y ][ $ym % 12 ] = 0; - } - if ( ! isset( $stats['visits'][ $year ] ) ) { - $stats['visits'][ $year ] = array(); + $visits = Statify_Evaluation::get_views_for_all_months( $post ); + $stats = array( 'visits' => array() ); + $last_ym = 0; + foreach ( $visits as $ym => $v ) { + $ym = explode( '-', $ym ); + $year = intval( $ym[0] ); + $month = intval( $ym[1] ); + $year_month = $year * 12 + $month; + for ( $ym = $last_ym + 1; $last_ym > 0 && $ym < $year_month; $ym ++ ) { + // Fill gaps. + $y = intval( $ym / 12 ); + if ( ! isset( $stats['visits'][ $y ] ) ) { + $stats['visits'][ $y ] = array(); } - $stats['visits'][ $year ][ $month ] = $v; - $last_ym = $year_month; + $stats['visits'][ $y ][ $ym % 12 ] = 0; + } + if ( ! isset( $stats['visits'][ $year ] ) ) { + $stats['visits'][ $year ] = array(); } + $stats['visits'][ $year ][ $month ] = $v; + $last_ym = $year_month; } } elseif ( 'day' === $scope ) { - if ( ! $post ) { - $stats = self::from_cache( $scope ); - } - - if ( ! $stats ) { - $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 ); } } @@ -251,22 +247,24 @@ public static function get_extended( $request ) { * 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( $scope ) { - return get_transient( 'statify_data_' . $scope ); + private static function from_cache( $scope, $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( $scope, $data ) { + private static function update_cache( $scope, $index, $data ) { 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 ad2ecdec..fd96545c 100644 --- a/inc/class-statify-evaluation.php +++ b/inc/class-statify-evaluation.php @@ -126,36 +126,33 @@ public static function get_years() { * 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( $post_url = '' ) { + public static function get_views_for_all_days( $single_year = 0, $post_url = '' ) { 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`'; + + $results = $wpdb->get_results($wpdb->prepare($query, $args), 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 d00a26b7..eacaee13 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 cd6f2bbe..38c70ab6 100644 --- a/tests/test-evaluation.php +++ b/tests/test-evaluation.php @@ -90,7 +90,7 @@ public function test_get_views_for_all_days() { '2023-03-23' => 1, '2023-03-25' => 2, ), - Statify_Evaluation::get_views_for_all_days( '/test/' ) + Statify_Evaluation::get_views_for_all_days( -1, '/test/' ) ); } diff --git a/views/view-dashboard.php b/views/view-dashboard.php index 65929bd4..d4026d02 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 @@       -   +  
+ + + +
+

+ +

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