From 07eae83a00d860e149359fae67d549488403d88b Mon Sep 17 00:00:00 2001
From: Benjamin Bellamy <ben@podlibre.org>
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  | 128 +++++++++++++-----
 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, 705 insertions(+), 192 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 b580e3f4ed..878e0cc80a 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 1e68a84437..2cf43a4736 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 40c0d6004b..229e383a47 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 8f6a9d6030..de0a9b768e 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 344d60fb2b..4a8e75acb0 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 5827731b8f..18ff9526a7 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 c654e32ad9..5e3e8879d6 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 3d15320696..9dae71b3c8 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 8aabc24724..2469f6fbb2 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 7645be0d00..ea48383746 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 753dd7835e..053b4b02f6 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 2dc3e0ee19..fce1544546 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 dfe3e4938c..f6491c2aee 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 d384011640..45edbc2413 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 6bd1cb56cc..04518027a7 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 dd4cb0afee..afaeded0f4 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 61acd36168..b8d112077c 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 0000000000..37b526afae
--- /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 d68082f6bf..7398d818de 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 5ecb203113..07a88fd9a1 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 1f49578316..9d10d229fe 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 c09619f234..0000000000
--- 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 0000000000..f604d17536
--- /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 0000000000..d4620e3a81
--- /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 0000000000..2a5fa59dcd
--- /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 0000000000..8803a54a73
--- /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 0000000000..9b0a014225
--- /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() ?>
-- 
GitLab