diff --git a/app/Config/Routes.php b/app/Config/Routes.php index b580e3f4edb79e8de24c4a7e9cde794c61b4ecc5..878e0cc80a99ccd56346a7eea903f3016017ab70 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 1e68a844372f24477e2c6a78e477ce4a13cada87..2cf43a4736140e26604999dfc36a63836fe9e3e1 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 40c0d6004b4cc9f362c979cb9c4b7d14b3431dc4..229e383a47eda672c3951186b864979558d2a1fd 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 8f6a9d603056f4c5d697e3569c0b3337de031e98..de0a9b768ea8668f136b796a001d87700aaafc2f 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 344d60fb2bc4dd136977c656d84b715a015c5a96..4a8e75acb09e831b33a5e742d0ff43407be961c7 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 5827731b8f23af69c99a1b97620dfdf8a2eec995..18ff9526a7b7c65271753fbf8c1c6218edd73a93 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 c654e32ad921415928c2cf201d50d6925a5f3af0..5e3e8879d6e8707970fdf8c23b9db3e1c5089fa4 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 3d15320696645e456adab4ab314b9631a4e8102d..9dae71b3c8349030facf852be33dc7509df839a1 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 8aabc24724508f202bebb5af1aef4af10778cb17..2469f6fbb2b92f318c2715519cd9d232a4dc6189 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 7645be0d006b3764b1d50d32fc3c44f38d4381c3..ea4838374670172f89dbb4d071b474dc28e751d7 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 753dd7835eee05e130bf88c755c53bd7c5a1bae4..053b4b02f656cb4e395ceaa82c0f06d8e624b3c8 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 2dc3e0ee1905457b815c1bfbd0607a6e891df5da..fce1544546bf690aeecc7bec00ec1788b7355712 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 dfe3e4938c791aaa554fe863e0541b6167382116..f6491c2aee4478191601d4dc04634c69cfda172d 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`' - ) + cache()->save( + "{$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, - '`os` !=' => null, + '`app` !=' => '', '`bot`' => 0, '`date` >' => date('Y-m-d', strtotime('-1 week')), ]) - ->groupBy('`ids`') + ->groupBy('`labels`') ->orderBy('`values`', 'DESC') - ->findAll(); + ->findAll(10); + + cache()->save( + "{$podcastId}_analytics_podcasts_by_player_by_os_weekly", + $found, + 600 + ); + } + return $found; + } - $foundDevice = $this->select( - '`device` as `ids`, `device` as `labels`, "" as `parents`' - ) + /** + * 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` !=' => null, + '`device` !=' => '', '`bot`' => 0, '`date` >' => date('Y-m-d', strtotime('-1 week')), ]) - ->groupBy('`ids`') + ->groupBy('`labels`') ->orderBy('`values`', 'DESC') - ->findAll(); + ->findAll(10); + + cache()->save( + "{$podcastId}_analytics_podcasts_by_player_by_device_weekly", + $found, + 600 + ); + } + return $found; + } - $foundBot = $this->select( - '"bots" as `ids`, "Bots" as `labels`, "" as `parents`' - ) + /** + * 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 week')), + '`date` >' => date('Y-m-d', strtotime('-1 year')), ]) - ->groupBy('`ids`') - ->orderBy('`values`', 'DESC') - ->findAll(); + ->groupBy('`labels`') + ->orderBy('`labels`', 'ASC') + ->findAll(10); - $found = array_merge($foundApp, $foundOs, $foundDevice, $foundBot); cache()->save( - "{$podcastId}_analytics_podcasts_by_player_by_device", + "{$podcastId}_analytics_podcasts_by_player_bots", $found, 600 ); } - return $found; } } diff --git a/app/Models/AnalyticsPodcastByRegionModel.php b/app/Models/AnalyticsPodcastByRegionModel.php index d384011640e5bce1fb5b15bd0b0247f83dc8ca4d..45edbc2413d37e4b612a58b0e98e60d2fb60b51b 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 6bd1cb56ccd75186a35d3a216d6418bb12068c77..04518027a7d07f32da04b77569f32df39874bd41 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 dd4cb0afee8c042d6546f14b7419e7067e902cd6..afaeded0f4c7b350fefe6b61054f5438365f908e 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 61acd36168b1336ddbc24620be86a98a8e7fb03a..b8d112077c2e683be49f4fdf443404c469c9ab1b 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<HTMLDivElement> = 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 0000000000000000000000000000000000000000..37b526afae0a6495bb4d98610ec85a31b88a921d --- /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 d68082f6bf61fa02c596f13af0c9918d7cb89f6e..7398d818de5963a11c26a22714204463dc3003f0 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 5ecb2031131198e69bab7af8b640125e43d2a536..07a88fd9a1b3281f8305436509643bbf60db48d4 100644 --- a/app/Views/admin/episode/view.php +++ b/app/Views/admin/episode/view.php @@ -9,7 +9,7 @@ <?= $this->endSection() ?> <?= $this->section('content') ?> - + <div class="flex flex-wrap"> <div class="w-full max-w-sm mb-6 md:mr-4"> <img @@ -46,4 +46,29 @@ </section> </div> + + <div class="mb-12 text-center"> + <h2><?= lang('Charts.episode_by_day') ?></h2> + <div class="chart-xy" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-filtered-data', + $podcast->id, + 'PodcastByEpisode', + 'ByDay', + $episode->id + ) ?>"></div> + </div> + + <div class="mb-12 text-center"> + <h2><?= lang('Charts.episode_by_month') ?></h2> + <div class="chart-xy" id="by-month-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-filtered-data', + $podcast->id, + 'PodcastByEpisode', + 'ByMonth', + $episode->id + ) ?>"></div> + </div> + + +<script src="/assets/charts.js" type="module"></script> <?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php index 1f4957831605f52f777e0f03f99c434eb3bdda10..9d10d229fe4019c2dd73641780396f36fa7a37e4 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 c09619f23448c7139dea16c2650137f38163709a..0000000000000000000000000000000000000000 --- a/app/Views/admin/podcast/analytics.php +++ /dev/null @@ -1,84 +0,0 @@ -<?= $this->extend('admin/_layout') ?> - -<?= $this->section('title') ?> -<?= $podcast->title ?> -<?= $this->endSection() ?> - -<?= $this->section('pageTitle') ?> -<?= $podcast->title ?> -<?= $this->endSection() ?> - -<?= $this->section('content') ?> - -<h2><?= lang('Charts.podcast_by_day') ?></h2> -<div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( - 'analytics-data', - $podcast->id, - 'Podcast', - 'ByDay' -) ?>"></div> - -<h2><?= lang('Charts.podcast_by_month') ?></h2> -<div class="h-64" id="by-month-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( - 'analytics-data', - $podcast->id, - 'Podcast', - 'ByMonth' -) ?>"></div> - -<h2><?= lang('Charts.unique_daily_listeners') ?></h2> -<div class="h-64" id="by-day-listeners-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( - 'analytics-data', - $podcast->id, - 'Podcast', - 'UniqueListenersByDay' -) ?>"></div> - -<h2><?= lang('Charts.unique_monthly_listeners') ?></h2> -<div class="h-64" id="by-month-listeners-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( - 'analytics-data', - $podcast->id, - 'Podcast', - 'UniqueListenersByMonth' -) ?>"></div> - -<h2><?= lang('Charts.episodes_by_day') ?></h2> -<div class="h-64" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to( - 'analytics-data', - $podcast->id, - 'PodcastByEpisode', - 'ByDay' -) ?>"></div> - -<h2><?= lang('Charts.by_player') ?></h2> -<div class="h-64" id="by-app-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( - 'analytics-data', - $podcast->id, - 'PodcastByPlayer', - 'ByApp' -) ?>"></div> - -<h2><?= lang('Charts.by_browser') ?></h2> -<div class="h-64" id="by-browser-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( - 'analytics-full-data', - $podcast->id, - 'WebsiteByBrowser' -) ?>"></div> - -<h2><?= lang('Charts.by_country') ?></h2> -<div class="h-64" id="by-country-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( - 'analytics-full-data', - $podcast->id, - 'PodcastByCountry' -) ?>"></div> - -<h2><?= lang('Charts.by_domain') ?></h2> -<div class="h-64" id="by-domain-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( - 'analytics-data', - $podcast->id, - 'WebsiteByReferer', - 'ByDomain' -) ?>"></div> - -<script src="/assets/charts.js" type="module"></script> -<?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/analytics/index.php b/app/Views/admin/podcast/analytics/index.php new file mode 100644 index 0000000000000000000000000000000000000000..f604d1753659e86e4e56f2ba42bcb98c16e6ddc4 --- /dev/null +++ b/app/Views/admin/podcast/analytics/index.php @@ -0,0 +1,43 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> +<div class="mb-12 text-center"> +<h2><?= lang('Charts.podcast_by_day') ?></h2> +<div class="chart-xy" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'Podcast', + 'ByDay' +) ?>"></div> +</div> + +<div class="mb-12 text-center"> +<h2><?= lang('Charts.podcast_by_month') ?></h2> +<div class="chart-xy" id="by-month-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'Podcast', + 'ByMonth' +) ?>"></div> +</div> + +<div class="mb-12 text-center"> +<h2><?= lang('Charts.episodes_by_day') ?></h2> +<div class="chart-xy" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByEpisode', + 'ByDay' +) ?>"></div> +</div> + +<script src="/assets/charts.js" type="module"></script> +<?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/analytics/locations.php b/app/Views/admin/podcast/analytics/locations.php new file mode 100644 index 0000000000000000000000000000000000000000..d4620e3a814fb98be95b07c48d054136b126cdce --- /dev/null +++ b/app/Views/admin/podcast/analytics/locations.php @@ -0,0 +1,46 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> + +<div class="grid grid-cols-2 divide-x"> + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_country_weekly') ?></h2> + <div class="chart-pie" id="by-country-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByCountry', + 'Weekly' + ) ?>"></div> + </div> + + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_country_yearly') ?></h2> + <div class="chart-pie" id="by-country-by-year-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByCountry', + 'Yearly' + ) ?>"></div> + </div> +</div> + +<div class="mb-12 mr-6 text-center"> +<h2><?= lang('Charts.podcast_by_region') ?></h2> +<div class="chart-map" id="by-region-map" data-chart-type="map-chart" data-chart-url="<?= route_to( + 'analytics-full-data', + $podcast->id, + 'PodcastByRegion' +) ?>"></div> +</div> + + +<script src="/assets/charts.js" type="module"></script> +<?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/analytics/players.php b/app/Views/admin/podcast/analytics/players.php new file mode 100644 index 0000000000000000000000000000000000000000..2a5fa59dcde96678239e79efeaf829abf9fbc10c --- /dev/null +++ b/app/Views/admin/podcast/analytics/players.php @@ -0,0 +1,67 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> + +<div class="grid grid-cols-2 divide-x"> + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_player_weekly') ?></h2> + <div class="chart-pie" id="by-app-weekly-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByPlayer', + 'ByAppWeekly' + ) ?>"></div> + </div> + + + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_player_yearly') ?></h2> + <div class="chart-pie" id="by-app-yearly-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByPlayer', + 'ByAppYearly' + ) ?>"></div> + </div> + + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_device_weekly') ?></h2> + <div class="chart-pie" id="by-device-weekly-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByPlayer', + 'ByDeviceWeekly' + ) ?>"></div> + </div> + + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_os_weekly') ?></h2> + <div class="chart-pie" id="by-os-yearly-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByPlayer', + 'ByOsWeekly' + ) ?>"></div> + </div> +</div> + +<div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.podcast_bots') ?></h2> + <div class="chart-xy" id="bots-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastByPlayer', + 'Bots' + ) ?>"></div> +</div> + +<script src="/assets/charts.js" type="module"></script> +<?= $this->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 0000000000000000000000000000000000000000..8803a54a73caf7e320909f57580bba2118185e73 --- /dev/null +++ b/app/Views/admin/podcast/analytics/unique_listeners.php @@ -0,0 +1,34 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> + +<div class="mb-12 text-center"> +<h2><?= lang('Charts.unique_daily_listeners') ?></h2> +<div class="chart-xy" id="by-day-listeners-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'Podcast', + 'UniqueListenersByDay' +) ?>"></div> +</div> + +<div class="mb-12 text-center"> +<h2><?= lang('Charts.unique_monthly_listeners') ?></h2> +<div class="chart-xy" id="by-month-listeners-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'Podcast', + 'UniqueListenersByMonth' +) ?>"></div> +</div> + +<script src="/assets/charts.js" type="module"></script> +<?= $this->endSection() ?> diff --git a/app/Views/admin/podcast/analytics/webpages.php b/app/Views/admin/podcast/analytics/webpages.php new file mode 100644 index 0000000000000000000000000000000000000000..9b0a0142252363790753bd92e464af9dbfcba322 --- /dev/null +++ b/app/Views/admin/podcast/analytics/webpages.php @@ -0,0 +1,61 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> + +<div class="grid grid-cols-2 divide-x"> + + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_domain_weekly') ?></h2> + <div class="chart-pie" id="by-domain-weekly-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'WebsiteByReferer', + 'ByDomainWeekly' + ) ?>"></div> + </div> + + + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_domain_yearly') ?></h2> + <div class="chart-pie" id="by-domain-yearly-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'WebsiteByReferer', + 'ByDomainYearly' + ) ?>"></div> + </div> + + + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_entry_page') ?></h2> + <div class="chart-pie" id="by-entry-page-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-full-data', + $podcast->id, + 'WebsiteByEntryPage' + ) ?>"></div> + </div> + + + <div class="mb-12 mr-6 text-center"> + <h2><?= lang('Charts.by_browser') ?></h2> + <div class="chart-pie" id="by-browser-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-full-data', + $podcast->id, + 'WebsiteByBrowser' + ) ?>"></div> + </div> + +</div> + + + +<script src="/assets/charts.js" type="module"></script> +<?= $this->endSection() ?>