From 07eae83a00d860e149359fae67d549488403d88b Mon Sep 17 00:00:00 2001 From: Benjamin Bellamy Date: Wed, 14 Oct 2020 10:38:48 +0000 Subject: [PATCH] feat: add map analytics, add episodes analytics, clean analytics page layout, translate countries --- app/Config/Routes.php | 44 ++++- app/Controllers/Admin/Podcast.php | 36 +++- app/Entities/AnalyticsPodcastsByCountry.php | 5 + app/Entities/AnalyticsPodcastsByRegion.php | 5 + app/Entities/AnalyticsWebsiteByEntryPage.php | 6 + app/Language/en/Breadcrumb.php | 4 + app/Language/en/Charts.php | 18 +- app/Language/en/PodcastNavigation.php | 6 +- app/Language/fr/Charts.php | 8 +- app/Language/fr/PodcastNavigation.php | 4 +- app/Models/AnalyticsPodcastByCountryModel.php | 45 ++++- app/Models/AnalyticsPodcastByEpisodeModel.php | 66 +++++-- app/Models/AnalyticsPodcastByPlayerModel.php | 170 ++++++++++++------ app/Models/AnalyticsPodcastByRegionModel.php | 13 +- app/Models/AnalyticsPodcastModel.php | 5 +- app/Models/AnalyticsWebsiteByRefererModel.php | 39 +++- app/Views/_assets/modules/Charts.ts | 79 +++++--- app/Views/_assets/styles/charts.css | 15 ++ app/Views/_assets/styles/index.css | 1 + app/Views/admin/episode/view.php | 27 ++- app/Views/admin/podcast/_sidebar.php | 8 +- app/Views/admin/podcast/analytics.php | 84 --------- app/Views/admin/podcast/analytics/index.php | 43 +++++ .../admin/podcast/analytics/locations.php | 46 +++++ app/Views/admin/podcast/analytics/players.php | 67 +++++++ .../podcast/analytics/unique_listeners.php | 34 ++++ .../admin/podcast/analytics/webpages.php | 61 +++++++ 27 files changed, 726 insertions(+), 213 deletions(-) create mode 100644 app/Views/_assets/styles/charts.css delete mode 100644 app/Views/admin/podcast/analytics.php create mode 100644 app/Views/admin/podcast/analytics/index.php create mode 100644 app/Views/admin/podcast/analytics/locations.php create mode 100644 app/Views/admin/podcast/analytics/players.php create mode 100644 app/Views/admin/podcast/analytics/unique_listeners.php create mode 100644 app/Views/admin/podcast/analytics/webpages.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index b580e3f4..878e0cc8 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -123,10 +123,46 @@ $routes->group( 'as' => 'podcast-delete', 'filter' => 'permission:podcasts-delete', ]); - $routes->get('analytics', 'Podcast::analytics/$1', [ - 'as' => 'podcast-analytics', - 'filter' => 'permission:podcasts-view,podcast-view', - ]); + + $routes->group('analytics', function ($routes) { + $routes->get('/', 'Podcast::viewAnalytics/$1', [ + 'as' => 'podcast-analytics', + 'filter' => 'permission:podcasts-view,podcast-view', + ]); + $routes->get( + 'webpages', + 'Podcast::viewAnalyticsWebpages/$1', + [ + 'as' => 'podcast-analytics-webpages', + 'filter' => 'permission:podcasts-view,podcast-view', + ] + ); + $routes->get( + 'locations', + 'Podcast::viewAnalyticsLocations/$1', + [ + 'as' => 'podcast-analytics-locations', + 'filter' => 'permission:podcasts-view,podcast-view', + ] + ); + $routes->get( + 'unique-listeners', + 'Podcast::viewAnalyticsUniqueListeners/$1', + [ + 'as' => 'podcast-analytics-unique-listeners', + 'filter' => 'permission:podcasts-view,podcast-view', + ] + ); + $routes->get( + 'players', + 'Podcast::viewAnalyticsPlayers/$1', + [ + 'as' => 'podcast-analytics-players', + 'filter' => 'permission:podcasts-view,podcast-view', + ] + ); + }); + $routes->get( 'analytics-data/(:segment)', 'AnalyticsData::getData/$1/$2', diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index 1e68a844..2cf43a47 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -58,12 +58,44 @@ class Podcast extends BaseController return view('admin/podcast/view', $data); } - public function analytics() + public function viewAnalytics() { $data = ['podcast' => $this->podcast]; replace_breadcrumb_params([0 => $this->podcast->title]); - return view('admin/podcast/analytics', $data); + return view('admin/podcast/analytics/index', $data); + } + + public function viewAnalyticsWebpages() + { + $data = ['podcast' => $this->podcast]; + + replace_breadcrumb_params([0 => $this->podcast->title]); + return view('admin/podcast/analytics/webpages', $data); + } + + public function viewAnalyticsLocations() + { + $data = ['podcast' => $this->podcast]; + + replace_breadcrumb_params([0 => $this->podcast->title]); + return view('admin/podcast/analytics/locations', $data); + } + + public function viewAnalyticsUniqueListeners() + { + $data = ['podcast' => $this->podcast]; + + replace_breadcrumb_params([0 => $this->podcast->title]); + return view('admin/podcast/analytics/unique_listeners', $data); + } + + public function viewAnalyticsPlayers() + { + $data = ['podcast' => $this->podcast]; + + replace_breadcrumb_params([0 => $this->podcast->title]); + return view('admin/podcast/analytics/players', $data); } public function create() diff --git a/app/Entities/AnalyticsPodcastsByCountry.php b/app/Entities/AnalyticsPodcastsByCountry.php index 40c0d600..229e383a 100644 --- a/app/Entities/AnalyticsPodcastsByCountry.php +++ b/app/Entities/AnalyticsPodcastsByCountry.php @@ -20,4 +20,9 @@ class AnalyticsPodcastsByCountry extends Entity 'date' => 'datetime', 'hits' => 'integer', ]; + + public function getLabels() + { + return lang('Countries.' . $this->attributes['labels']); + } } diff --git a/app/Entities/AnalyticsPodcastsByRegion.php b/app/Entities/AnalyticsPodcastsByRegion.php index 8f6a9d60..de0a9b76 100644 --- a/app/Entities/AnalyticsPodcastsByRegion.php +++ b/app/Entities/AnalyticsPodcastsByRegion.php @@ -23,4 +23,9 @@ class AnalyticsPodcastsByRegion extends Entity 'date' => 'datetime', 'hits' => 'integer', ]; + + public function getCountryCode() + { + return lang('Countries.' . $this->attributes['country_code']); + } } diff --git a/app/Entities/AnalyticsWebsiteByEntryPage.php b/app/Entities/AnalyticsWebsiteByEntryPage.php index 344d60fb..4a8e75ac 100644 --- a/app/Entities/AnalyticsWebsiteByEntryPage.php +++ b/app/Entities/AnalyticsWebsiteByEntryPage.php @@ -20,4 +20,10 @@ class AnalyticsWebsiteByEntryPage extends Entity 'date' => 'datetime', 'hits' => 'integer', ]; + + public function getLabels() + { + $split = explode('/', $this->attributes['labels']); + return $split[count($split) - 1]; + } } diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php index 5827731b..18ff9526 100644 --- a/app/Language/en/Breadcrumb.php +++ b/app/Language/en/Breadcrumb.php @@ -23,4 +23,8 @@ return [ 'settings' => 'settings', 'platforms' => 'platforms', 'analytics' => 'Analytics', + 'locations' => 'Locations', + 'website' => 'Website', + 'unique-listeners' => 'Unique listeners', + 'players' => 'Players', ]; diff --git a/app/Language/en/Charts.php b/app/Language/en/Charts.php index c654e32a..5e3e8879 100644 --- a/app/Language/en/Charts.php +++ b/app/Language/en/Charts.php @@ -7,14 +7,24 @@ */ return [ - 'by_player' => 'Podcast downloads by player (for the past week)', + 'by_player_weekly' => 'Podcast downloads by player (for the past week)', + 'by_player_yearly' => 'Podcast downloads by player (for the past year)', + 'by_device_weekly' => 'Podcast downloads by device (for the past week)', + 'by_os_weekly' => 'Podcast downloads by O.S. (for the past week)', + 'podcast_by_region' => 'Podcast downloads by region (for the past week)', 'unique_daily_listeners' => 'Daily unique listeners', 'unique_monthly_listeners' => 'Monthly unique listeners', - 'by_browser' => 'Website usage by browser (for the past week)', + 'by_browser' => 'Web pages usage by browser (for the past week)', 'podcast_by_day' => 'Podcast daily downloads', 'podcast_by_month' => 'Podcast monthly downloads', + 'episode_by_day' => 'Episode daily downloads (first 60 days)', + 'episode_by_month' => 'Episode monthly downloads', 'episodes_by_day' => '5 latest episodes downloads (during their first 60 days)', - 'by_country' => 'Podcast downloads by country (for the past week)', - 'by_domain' => 'Website visits by origin (for the past week)', + 'by_country_weekly' => 'Podcast downloads by country (for the past week)', + 'by_country_yearly' => 'Podcast downloads by country (for the past year)', + 'by_domain_weekly' => 'Web pages visits by source (for the past week)', + 'by_domain_yearly' => 'Web pages visits by source (for the past year)', + 'by_entry_page' => 'Web pages visits by landing page (for the past week)', + 'podcast_bots' => 'Bots (crawlers)', ]; diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php index 3d153206..9dae71b3 100644 --- a/app/Language/en/PodcastNavigation.php +++ b/app/Language/en/PodcastNavigation.php @@ -20,5 +20,9 @@ return [ 'contributor-add' => 'Add contributor', 'settings' => 'Settings', 'platforms' => 'Podcast platforms', - 'podcast-analytics' => 'Audiences Overview', + 'podcast-analytics' => 'Audience overview', + 'podcast-analytics-webpages' => 'Web pages visits', + 'podcast-analytics-locations' => 'Locations', + 'podcast-analytics-unique-listeners' => 'Unique listeners', + 'podcast-analytics-players' => 'Players', ]; diff --git a/app/Language/fr/Charts.php b/app/Language/fr/Charts.php index 8aabc247..2469f6fb 100644 --- a/app/Language/fr/Charts.php +++ b/app/Language/fr/Charts.php @@ -20,7 +20,7 @@ return [ 'unique_daily_listeners' => 'Auditeurs uniques quotidiens', 'unique_monthly_listeners' => 'Auditeurs uniques mensuels', 'by_browser' => - 'Fréquentation du site par navigateur (sur la dernière semaine)', + 'Fréquentation des pages web par navigateur (sur la dernière semaine)', 'podcast_by_day' => 'Téléchargements quotidiens de podcasts', 'podcast_by_month' => 'Téléchargements mensuels de podcasts', 'episode_by_day' => @@ -33,10 +33,10 @@ return [ 'by_country_yearly' => 'Téléchargement de podcasts par pays (sur la dernière année)', 'by_domain_weekly' => - 'Fréquentation du site par origine (sur la dernière semaine)', + 'Fréquentation des pages web par origine (sur la dernière semaine)', 'by_domain_yearly' => - 'Fréquentation du site par origine (sur la dernière année)', + 'Fréquentation des pages web par origine (sur la dernière année)', 'by_entry_page' => - 'Fréquentation du site par page d’entrée (sur la dernière semaine)', + 'Fréquentation des pages web par page d’entrée (sur la dernière semaine)', 'podcast_bots' => 'Robots (bots)', ]; diff --git a/app/Language/fr/PodcastNavigation.php b/app/Language/fr/PodcastNavigation.php index 7645be0d..ea483837 100644 --- a/app/Language/fr/PodcastNavigation.php +++ b/app/Language/fr/PodcastNavigation.php @@ -20,8 +20,8 @@ return [ 'contributor-add' => 'Ajouter un contributeur', 'settings' => 'Paramètres', 'platforms' => 'Plateformes du podcast', - 'podcast-analytics' => 'Mesures d’audience', - 'podcast-analytics-website' => 'Visites du site web', + 'podcast-analytics' => 'Vue d’ensemble', + 'podcast-analytics-webpages' => 'Visites des pages web', 'podcast-analytics-locations' => 'Localisations', 'podcast-analytics-unique-listeners' => 'Auditeurs uniques', 'podcast-analytics-players' => 'Lecteurs', diff --git a/app/Models/AnalyticsPodcastByCountryModel.php b/app/Models/AnalyticsPodcastByCountryModel.php index 753dd783..053b4b02 100644 --- a/app/Models/AnalyticsPodcastByCountryModel.php +++ b/app/Models/AnalyticsPodcastByCountryModel.php @@ -30,9 +30,14 @@ class AnalyticsPodcastByCountryModel extends Model * * @return array */ - public function getData(int $podcastId): array + public function getDataWeekly(int $podcastId): array { - if (!($found = cache("{$podcastId}_analytics_podcast_by_country"))) { + $locale = service('request')->getLocale(); + if ( + !($found = cache( + "{$podcastId}_analytics_podcast_by_country_weekly_{$locale}" + )) + ) { $found = $this->select('`country_code` as `labels`') ->selectSum('`hits`', '`values`') ->where([ @@ -44,7 +49,41 @@ class AnalyticsPodcastByCountryModel extends Model ->findAll(10); cache()->save( - "{$podcastId}_analytics_podcast_by_country", + "{$podcastId}_analytics_podcast_by_country_weekly_{$locale}", + $found, + 600 + ); + } + return $found; + } + + /** + * Gets country data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataYearly(int $podcastId): array + { + $locale = service('request')->getLocale(); + if ( + !($found = cache( + "{$podcastId}_analytics_podcast_by_country_yearly_{$locale}" + )) + ) { + $found = $this->select('`country_code` as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 year')), + ]) + ->groupBy('`labels`') + ->orderBy('`values`', 'DESC') + ->findAll(10); + + cache()->save( + "{$podcastId}_analytics_podcast_by_country_yearly_{$locale}", $found, 600 ); diff --git a/app/Models/AnalyticsPodcastByEpisodeModel.php b/app/Models/AnalyticsPodcastByEpisodeModel.php index 2dc3e0ee..fce15445 100644 --- a/app/Models/AnalyticsPodcastByEpisodeModel.php +++ b/app/Models/AnalyticsPodcastByEpisodeModel.php @@ -37,12 +37,12 @@ class AnalyticsPodcastByEpisodeModel extends Model )) ) { $lastEpisodes = (new EpisodeModel()) - ->select('id, season_number, number, title') - ->orderBy('id', 'DESC') - ->where(['podcast_id' => $podcastId]) + ->select('`id`, `season_number`, `number`, `title`') + ->orderBy('`id`', 'DESC') + ->where(['`podcast_id`' => $podcastId]) ->findAll(5); - $found = $this->select('age AS X'); + $found = $this->select('`age` AS `X`'); $letter = 97; foreach ($lastEpisodes as $episode) { @@ -51,7 +51,7 @@ class AnalyticsPodcastByEpisodeModel extends Model '(CASE WHEN `episode_id`=' . $episode->id . ' THEN `hits` END)', - chr($letter) . 'Y' + '`' . chr($letter) . 'Y`' ) ->select( '"' . @@ -62,20 +62,20 @@ class AnalyticsPodcastByEpisodeModel extends Model ? '' : '-' . $episode->number . '/ ') . $episode->title . - '" AS ' . + '" AS `' . chr($letter) . - 'Value' + 'Value`' ); $letter++; } $found = $found ->where([ - 'podcast_id' => $podcastId, - 'age <' => 60, + '`podcast_id`' => $podcastId, + '`age` <' => 60, ]) - ->groupBy('X') - ->orderBy('X', 'ASC') + ->groupBy('`X`') + ->orderBy('`X`', 'ASC') ->findAll(); cache()->save( @@ -91,14 +91,15 @@ class AnalyticsPodcastByEpisodeModel extends Model "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day" )) ) { - $found = $this->select('date as labels') - ->selectSum('hits', 'values') + $found = $this->select('`date as `labels`') + ->selectSum('`hits`', '`values`') ->where([ - 'episode_id' => $episodeId, - 'podcast_id' => $podcastId, + '`episode_id`' => $episodeId, + '`podcast_id`' => $podcastId, + '`age` <' => 60, ]) - ->groupBy('labels') - ->orderBy('labels', 'ASC') + ->groupBy('`labels`') + ->orderBy('`labels`', 'ASC') ->findAll(); cache()->save( @@ -110,4 +111,35 @@ class AnalyticsPodcastByEpisodeModel extends Model return $found; } } + + /** + * @param int $podcastId, $episodeId + * + * @return array + */ + public function getDataByMonth(int $podcastId, int $episodeId = null): array + { + if ( + !($found = cache( + "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_month" + )) + ) { + $found = $this->select('DATE_FORMAT(`date`,"%Y-%m-01") as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + 'episode_id' => $episodeId, + 'podcast_id' => $podcastId, + ]) + ->groupBy('`labels`') + ->orderBy('`labels`', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_month", + $found, + 600 + ); + } + return $found; + } } diff --git a/app/Models/AnalyticsPodcastByPlayerModel.php b/app/Models/AnalyticsPodcastByPlayerModel.php index dfe3e493..f6491c2a 100644 --- a/app/Models/AnalyticsPodcastByPlayerModel.php +++ b/app/Models/AnalyticsPodcastByPlayerModel.php @@ -30,11 +30,11 @@ class AnalyticsPodcastByPlayerModel extends Model * * @return array */ - public function getDataByApp(int $podcastId): array + public function getDataByAppWeekly(int $podcastId): array { if ( !($found = cache( - "{$podcastId}_analytics_podcasts_by_player_by_app" + "{$podcastId}_analytics_podcasts_by_player_by_app_weekly" )) ) { $found = $this->select('`app` as `labels`') @@ -50,92 +50,148 @@ class AnalyticsPodcastByPlayerModel extends Model ->findAll(10); cache()->save( - "{$podcastId}_analytics_podcasts_by_player_by_app", + "{$podcastId}_analytics_podcasts_by_player_by_app_weekly", $found, 600 ); } - return $found; } /** - * Gets device data for a podcast + * Gets player data for a podcast * * @param int $podcastId * * @return array */ - public function getDataByDevice(int $podcastId): array + public function getDataByAppYearly(int $podcastId): array { if ( !($found = cache( - "{$podcastId}_analytics_podcasts_by_player_by_device" + "{$podcastId}_analytics_podcasts_by_player_by_app_yearly" )) ) { - $foundApp = $this->select( - 'CONCAT_WS("/", `device`, `os`, `app`) as `ids`, `app` as `labels`, CONCAT_WS("/", `device`, `os`) as `parents`' - ) + $found = $this->select('`app` as `labels`') ->selectSum('`hits`', '`values`') ->where([ '`podcast_id`' => $podcastId, - '`app` !=' => null, + '`app` !=' => '', '`bot`' => 0, - '`date` >' => date('Y-m-d', strtotime('-1 week')), + '`date` >' => date('Y-m-d', strtotime('-1 year')), ]) - ->groupBy('`ids`') + ->groupBy('`labels`') ->orderBy('`values`', 'DESC') - ->findAll(); + ->findAll(10); - $foundOs = $this->select( - 'CONCAT_WS("/", `device`, `os`) as `ids`, `os` as `labels`, `device` as `parents`' - ) - ->selectSum('`hits`', '`values`') - ->where([ - '`podcast_id`' => $podcastId, - '`os` !=' => null, - '`bot`' => 0, - '`date` >' => date('Y-m-d', strtotime('-1 week')), - ]) - ->groupBy('`ids`') - ->orderBy('`values`', 'DESC') - ->findAll(); - - $foundDevice = $this->select( - '`device` as `ids`, `device` as `labels`, "" as `parents`' - ) - ->selectSum('`hits`', '`values`') - ->where([ - '`podcast_id`' => $podcastId, - '`device` !=' => null, - '`bot`' => 0, - '`date` >' => date('Y-m-d', strtotime('-1 week')), - ]) - ->groupBy('`ids`') - ->orderBy('`values`', 'DESC') - ->findAll(); - - $foundBot = $this->select( - '"bots" as `ids`, "Bots" as `labels`, "" as `parents`' - ) - ->selectSum('`hits`', '`values`') - ->where([ - '`podcast_id`' => $podcastId, - '`bot`' => 1, - '`date` >' => date('Y-m-d', strtotime('-1 week')), - ]) - ->groupBy('`ids`') - ->orderBy('`values`', 'DESC') - ->findAll(); - - $found = array_merge($foundApp, $foundOs, $foundDevice, $foundBot); cache()->save( - "{$podcastId}_analytics_podcasts_by_player_by_device", + "{$podcastId}_analytics_podcasts_by_player_by_app_yearly", $found, 600 ); } + return $found; + } + /** + * Gets os data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByOsWeekly(int $podcastId): array + { + if ( + !($found = cache( + "{$podcastId}_analytics_podcasts_by_player_by_os_weekly" + )) + ) { + $found = $this->select('`os` as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`app` !=' => '', + '`bot`' => 0, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->groupBy('`labels`') + ->orderBy('`values`', 'DESC') + ->findAll(10); + + cache()->save( + "{$podcastId}_analytics_podcasts_by_player_by_os_weekly", + $found, + 600 + ); + } + return $found; + } + + /** + * Gets player data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByDeviceWeekly(int $podcastId): array + { + if ( + !($found = cache( + "{$podcastId}_analytics_podcasts_by_player_by_device_weekly" + )) + ) { + $found = $this->select('`device` as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`device` !=' => '', + '`bot`' => 0, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->groupBy('`labels`') + ->orderBy('`values`', 'DESC') + ->findAll(10); + + cache()->save( + "{$podcastId}_analytics_podcasts_by_player_by_device_weekly", + $found, + 600 + ); + } + return $found; + } + + /** + * Gets bots data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataBots(int $podcastId): array + { + if ( + !($found = cache("{$podcastId}_analytics_podcasts_by_player_bots")) + ) { + $found = $this->select('DATE_FORMAT(`date`,"%Y-%m-01") as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`bot`' => 1, + '`date` >' => date('Y-m-d', strtotime('-1 year')), + ]) + ->groupBy('`labels`') + ->orderBy('`labels`', 'ASC') + ->findAll(10); + + cache()->save( + "{$podcastId}_analytics_podcasts_by_player_bots", + $found, + 600 + ); + } return $found; } } diff --git a/app/Models/AnalyticsPodcastByRegionModel.php b/app/Models/AnalyticsPodcastByRegionModel.php index d3840116..45edbc24 100644 --- a/app/Models/AnalyticsPodcastByRegionModel.php +++ b/app/Models/AnalyticsPodcastByRegionModel.php @@ -32,11 +32,16 @@ class AnalyticsPodcastByRegionModel extends Model */ public function getData(int $podcastId): array { - if (!($found = cache("{$podcastId}_analytics_podcast_by_region"))) { + $locale = service('request')->getLocale(); + if ( + !($found = cache( + "{$podcastId}_analytics_podcast_by_region_{$locale}" + )) + ) { $found = $this->select( '`country_code`, `region_code`, `latitude`, `longitude`' ) - ->selectSum('`hits`', '`values`') + ->selectSum('`hits`', '`value`') ->groupBy( '`country_code`, `region_code`, `latitude`, `longitude`' ) @@ -44,11 +49,11 @@ class AnalyticsPodcastByRegionModel extends Model '`podcast_id`' => $podcastId, '`date` >' => date('Y-m-d', strtotime('-1 week')), ]) - ->orderBy('`values`', 'DESC') + ->orderBy('`value`', 'DESC') ->findAll(); cache()->save( - "{$podcastId}_analytics_podcast_by_region", + "{$podcastId}_analytics_podcast_by_region_{$locale}", $found, 600 ); diff --git a/app/Models/AnalyticsPodcastModel.php b/app/Models/AnalyticsPodcastModel.php index 6bd1cb56..04518027 100644 --- a/app/Models/AnalyticsPodcastModel.php +++ b/app/Models/AnalyticsPodcastModel.php @@ -36,7 +36,7 @@ class AnalyticsPodcastModel extends Model $found = $this->select('`date` as `labels`, `hits` as `values`') ->where([ '`podcast_id`' => $podcastId, - '`date` >' => date('Y-m-d', strtotime('-1 year')), + '`date` >' => date('Y-m-d', strtotime('-60 days')), ]) ->orderBy('`labels`', 'ASC') ->findAll(); @@ -60,7 +60,6 @@ class AnalyticsPodcastModel extends Model ->selectSum('`hits`', '`values`') ->where([ '`podcast_id`' => $podcastId, - '`date` >' => date('Y-m-d', strtotime('-1 year')), ]) ->groupBy('`labels`') ->orderBy('`labels`', 'ASC') @@ -94,7 +93,7 @@ class AnalyticsPodcastModel extends Model ) ->where([ '`podcast_id`' => $podcastId, - '`date` >' => date('Y-m-d', strtotime('-1 year')), + '`date` >' => date('Y-m-d', strtotime('-60 days')), ]) ->orderBy('`labels`', 'ASC') ->findAll(); diff --git a/app/Models/AnalyticsWebsiteByRefererModel.php b/app/Models/AnalyticsWebsiteByRefererModel.php index dd4cb0af..afaeded0 100644 --- a/app/Models/AnalyticsWebsiteByRefererModel.php +++ b/app/Models/AnalyticsWebsiteByRefererModel.php @@ -59,9 +59,11 @@ class AnalyticsWebsiteByRefererModel extends Model * * @return array */ - public function getDataByDomain(int $podcastId): array + public function getDataByDomainWeekly(int $podcastId): array { - if (!($found = cache("{$podcastId}_analytics_website_by_domain"))) { + if ( + !($found = cache("{$podcastId}_analytics_website_by_domain_weekly")) + ) { $found = $this->select('`domain` as `labels`') ->selectSum('`hits`', '`values`') ->where([ @@ -73,7 +75,38 @@ class AnalyticsWebsiteByRefererModel extends Model ->findAll(10); cache()->save( - "{$podcastId}_analytics_website_by_domain", + "{$podcastId}_analytics_website_by_domain_weekly", + $found, + 600 + ); + } + return $found; + } + + /** + * Gets domain data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByDomainYearly(int $podcastId): array + { + if ( + !($found = cache("{$podcastId}_analytics_website_by_domain_yearly")) + ) { + $found = $this->select('`domain` as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 year')), + ]) + ->groupBy('`labels`') + ->orderBy('`values`', 'DESC') + ->findAll(10); + + cache()->save( + "{$podcastId}_analytics_website_by_domain_yearly", $found, 600 ); diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts index 61acd361..b8d11207 100644 --- a/app/Views/_assets/modules/Charts.ts +++ b/app/Views/_assets/modules/Charts.ts @@ -1,33 +1,29 @@ // Import modules +import am4geodata_worldLow from "@amcharts/amcharts4-geodata/worldLow"; import * as am4charts from "@amcharts/amcharts4/charts"; import * as am4core from "@amcharts/amcharts4/core"; +import * as am4maps from "@amcharts/amcharts4/maps"; import am4themes_material from "@amcharts/amcharts4/themes/material"; const drawPieChart = (chartDivId: string, dataUrl: string | null): void => { // Create chart instance const chart = am4core.create(chartDivId, am4charts.PieChart); am4core.percent(100); - // Set theme am4core.useTheme(am4themes_material); - chart.innerRadius = am4core.percent(10); - // Add data chart.dataSource.url = dataUrl || ""; chart.dataSource.parser.options.emptyAs = 0; - // Add and configure Series const pieSeries = chart.series.push(new am4charts.PieSeries()); pieSeries.dataFields.value = "values"; pieSeries.dataFields.category = "labels"; - pieSeries.slices.template.stroke = am4core.color("#ffffff"); pieSeries.slices.template.strokeWidth = 1; pieSeries.slices.template.strokeOpacity = 1; pieSeries.labels.template.disabled = true; pieSeries.ticks.template.disabled = true; - chart.legend = new am4charts.Legend(); chart.legend.position = "right"; chart.legend.scrollable = true; @@ -37,32 +33,32 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => { // Create chart instance const chart = am4core.create(chartDivId, am4charts.XYChart); am4core.percent(100); - // Set theme am4core.useTheme(am4themes_material); - // Create axes const dateAxis = chart.xAxes.push(new am4charts.DateAxis()); dateAxis.renderer.minGridDistance = 60; - chart.yAxes.push(new am4charts.ValueAxis()); - // Add data chart.dataSource.url = dataUrl || ""; chart.dataSource.parser.options.emptyAs = 0; - // Create series const series = chart.series.push(new am4charts.LineSeries()); series.dataFields.valueY = "values"; series.dataFields.dateX = "labels"; - series.tooltipText = "{valueY} downloads"; - + series.tooltipText = "{valueY} hits"; + series.strokeWidth = 2; + // Make bullets grow on hover + const bullet = series.bullets.push(new am4charts.CircleBullet()); + bullet.circle.strokeWidth = 2; + bullet.circle.radius = 4; + bullet.circle.fill = am4core.color("#fff"); + const bullethover = bullet.states.create("hover"); + bullethover.properties.scale = 1.3; series.tooltip.pointerOrientation = "vertical"; - chart.cursor = new am4charts.XYCursor(); chart.cursor.snapToSeries = series; chart.cursor.xAxis = dateAxis; - chart.scrollbarX = new am4core.Scrollbar(); }; @@ -73,40 +69,74 @@ const drawXYSeriesChart = ( // Create chart instance const chart = am4core.create(chartDivId, am4charts.XYChart); am4core.percent(100); - // Set theme am4core.useTheme(am4themes_material); - // Create axes chart.xAxes.push(new am4charts.ValueAxis()); chart.yAxes.push(new am4charts.ValueAxis()); - // Add data chart.dataSource.url = dataUrl || ""; chart.dataSource.parser.options.emptyAs = 0; - // Create series const series1 = chart.series.push(new am4charts.LineSeries()); series1.dataFields.valueX = "X"; series1.dataFields.valueY = "aY"; - const series2 = chart.series.push(new am4charts.LineSeries()); series2.dataFields.valueX = "X"; series2.dataFields.valueY = "bY"; - const series3 = chart.series.push(new am4charts.LineSeries()); series3.dataFields.valueX = "X"; series3.dataFields.valueY = "cY"; - const series4 = chart.series.push(new am4charts.LineSeries()); series4.dataFields.valueX = "X"; series4.dataFields.valueY = "dY"; - const series5 = chart.series.push(new am4charts.LineSeries()); series5.dataFields.valueX = "X"; series5.dataFields.valueY = "eY"; }; +const drawMapChart = (chartDivId: string, dataUrl: string | null): void => { + // Create map instance + const chart = am4core.create(chartDivId, am4maps.MapChart); + am4core.percent(100); + // Set theme + am4core.useTheme(am4themes_material); + // Set map definition + chart.geodata = am4geodata_worldLow; + // Set projection + chart.projection = new am4maps.projections.Miller(); + // Create map polygon series + const polygonSeries = chart.series.push(new am4maps.MapPolygonSeries()); + // Exclude Antartica + polygonSeries.exclude = ["AQ"]; + // Make map load polygon (like country names) data from GeoJSON + polygonSeries.useGeodata = true; + // Configure series + const polygonTemplate = polygonSeries.mapPolygons.template; + polygonTemplate.tooltipText = "{name}"; + polygonTemplate.polygon.fillOpacity = 0.6; + // Create hover state and set alternative fill color + const hs = polygonTemplate.states.create("hover"); + hs.properties.fill = chart.colors.getIndex(0); + // Add image series + const imageSeries = chart.series.push(new am4maps.MapImageSeries()); + imageSeries.dataSource.url = dataUrl || ""; + imageSeries.mapImages.template.propertyFields.longitude = "longitude"; + imageSeries.mapImages.template.propertyFields.latitude = "latitude"; + imageSeries.mapImages.template.tooltipText = + "{country_code}, {region_code}:\n[bold]{value}[/] hits"; + const circle = imageSeries.mapImages.template.createChild(am4core.Circle); + circle.radius = 1; + circle.fill = am4core.color("#60f"); + imageSeries.heatRules.push({ + target: circle, + property: "radius", + min: 0.5, + max: 3, + dataField: "value", + }); +}; + const DrawCharts = (): void => { const chartDivs: NodeListOf = document.querySelectorAll( "div[data-chart-type]" @@ -125,6 +155,9 @@ const DrawCharts = (): void => { case "xy-series-chart": drawXYSeriesChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); break; + case "map-chart": + drawMapChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); + break; default: console.error("Unknown chart type:" + chartType); } diff --git a/app/Views/_assets/styles/charts.css b/app/Views/_assets/styles/charts.css new file mode 100644 index 00000000..37b526af --- /dev/null +++ b/app/Views/_assets/styles/charts.css @@ -0,0 +1,15 @@ +.chart-map { + height: 800px; + border: solid 10px #eee; +} +.chart-pie { + height: 400px; + width: 100%; + border: solid 1px #eee; +} +.chart-xy { + height: 500px; + width: 100%; + border: solid 1px #eee; + border: solid 3px #eee; +} diff --git a/app/Views/_assets/styles/index.css b/app/Views/_assets/styles/index.css index d68082f6..7398d818 100644 --- a/app/Views/_assets/styles/index.css +++ b/app/Views/_assets/styles/index.css @@ -5,3 +5,4 @@ @import "./radioBtn.css"; @import "./switch.css"; @import "./enclosureInput.css"; +@import "./charts.css"; diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php index 5ecb2031..07a88fd9 100644 --- a/app/Views/admin/episode/view.php +++ b/app/Views/admin/episode/view.php @@ -9,7 +9,7 @@ endSection() ?> section('content') ?> - +
+ +
+

+
+
+ +
+

+
+
+ + + endSection() ?> diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php index 1f495783..9d10d229 100644 --- a/app/Views/admin/podcast/_sidebar.php +++ b/app/Views/admin/podcast/_sidebar.php @@ -10,7 +10,13 @@ $podcastNavigation = [ ], 'analytics' => [ 'icon' => 'line-chart', - 'items' => ['podcast-analytics'], + 'items' => [ + 'podcast-analytics', + 'podcast-analytics-unique-listeners', + 'podcast-analytics-players', + 'podcast-analytics-locations', + 'podcast-analytics-webpages', + ], ], 'contributors' => [ 'icon' => 'group', diff --git a/app/Views/admin/podcast/analytics.php b/app/Views/admin/podcast/analytics.php deleted file mode 100644 index c09619f2..00000000 --- a/app/Views/admin/podcast/analytics.php +++ /dev/null @@ -1,84 +0,0 @@ -extend('admin/_layout') ?> - -section('title') ?> -title ?> -endSection() ?> - -section('pageTitle') ?> -title ?> -endSection() ?> - -section('content') ?> - -

-
- -

-
- -

-
- -

-
- -

-
- -

-
- -

-
- -

-
- -

-
- - -endSection() ?> diff --git a/app/Views/admin/podcast/analytics/index.php b/app/Views/admin/podcast/analytics/index.php new file mode 100644 index 00000000..f604d175 --- /dev/null +++ b/app/Views/admin/podcast/analytics/index.php @@ -0,0 +1,43 @@ +extend('admin/_layout') ?> + +section('title') ?> +title ?> +endSection() ?> + +section('pageTitle') ?> +title ?> +endSection() ?> + +section('content') ?> +
+

+
+
+ +
+

+
+
+ +
+

+
+
+ + +endSection() ?> diff --git a/app/Views/admin/podcast/analytics/locations.php b/app/Views/admin/podcast/analytics/locations.php new file mode 100644 index 00000000..d4620e3a --- /dev/null +++ b/app/Views/admin/podcast/analytics/locations.php @@ -0,0 +1,46 @@ +extend('admin/_layout') ?> + +section('title') ?> +title ?> +endSection() ?> + +section('pageTitle') ?> +title ?> +endSection() ?> + +section('content') ?> + +
+
+

+
+
+ +
+

+
+
+
+ +
+

+
+
+ + + +endSection() ?> diff --git a/app/Views/admin/podcast/analytics/players.php b/app/Views/admin/podcast/analytics/players.php new file mode 100644 index 00000000..2a5fa59d --- /dev/null +++ b/app/Views/admin/podcast/analytics/players.php @@ -0,0 +1,67 @@ +extend('admin/_layout') ?> + +section('title') ?> +title ?> +endSection() ?> + +section('pageTitle') ?> +title ?> +endSection() ?> + +section('content') ?> + +
+
+

+
+
+ + +
+

+
+
+ +
+

+
+
+ +
+

+
+
+
+ +
+

+
+
+ + +endSection() ?> diff --git a/app/Views/admin/podcast/analytics/unique_listeners.php b/app/Views/admin/podcast/analytics/unique_listeners.php new file mode 100644 index 00000000..8803a54a --- /dev/null +++ b/app/Views/admin/podcast/analytics/unique_listeners.php @@ -0,0 +1,34 @@ +extend('admin/_layout') ?> + +section('title') ?> +title ?> +endSection() ?> + +section('pageTitle') ?> +title ?> +endSection() ?> + +section('content') ?> + +
+

+
+
+ +
+

+
+
+ + +endSection() ?> diff --git a/app/Views/admin/podcast/analytics/webpages.php b/app/Views/admin/podcast/analytics/webpages.php new file mode 100644 index 00000000..9b0a0142 --- /dev/null +++ b/app/Views/admin/podcast/analytics/webpages.php @@ -0,0 +1,61 @@ +extend('admin/_layout') ?> + +section('title') ?> +title ?> +endSection() ?> + +section('pageTitle') ?> +title ?> +endSection() ?> + +section('content') ?> + +
+ +
+

+
+
+ + +
+

+
+
+ + +
+

+
+
+ + +
+

+
+
+ +
+ + + + +endSection() ?>