From 8ab313296bb4a254ab05e90b17d896039839b784 Mon Sep 17 00:00:00 2001
From: Benjamin Bellamy <ben@podlibre.org>
Date: Mon, 2 Nov 2020 18:15:19 +0000
Subject: [PATCH] feat(analytics): add weekday and hour bar charts

---
 app/Config/Routes.php                         |  8 +++
 app/Controllers/Admin/Podcast.php             |  8 +++
 app/Language/en/Breadcrumb.php                |  1 +
 app/Language/en/Charts.php                    |  3 +
 app/Language/en/PodcastNavigation.php         |  1 +
 app/Language/fr/Breadcrumb.php                |  1 +
 app/Language/fr/Charts.php                    |  3 +
 app/Language/fr/PodcastNavigation.php         |  1 +
 app/Models/AnalyticsPodcastByCountryModel.php | 42 ++++++++++++-
 app/Models/AnalyticsPodcastByHourModel.php    | 34 +++++++++++
 app/Models/AnalyticsPodcastModel.php          | 60 +++++++++++++++++++
 app/Views/_assets/modules/Charts.ts           | 34 ++++++++++-
 app/Views/admin/podcast/_sidebar.php          |  1 +
 app/Views/admin/podcast/analytics/index.php   | 10 ++++
 .../admin/podcast/analytics/time_periods.php  | 36 +++++++++++
 15 files changed, 239 insertions(+), 4 deletions(-)
 create mode 100644 app/Views/admin/podcast/analytics/time_periods.php

diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index f435cc7671..d578e387fb 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -158,6 +158,14 @@ $routes->group(
                             'filter' => 'permission:podcasts-view,podcast-view',
                         ]
                     );
+                    $routes->get(
+                        'time-periods',
+                        'Podcast::viewAnalyticsTimePeriods/$1',
+                        [
+                            'as' => 'podcast-analytics-time-periods',
+                            'filter' => 'permission:podcasts-view,podcast-view',
+                        ]
+                    );
                     $routes->get(
                         'players',
                         'Podcast::viewAnalyticsPlayers/$1',
diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php
index a6e9e2023b..1e51120e36 100644
--- a/app/Controllers/Admin/Podcast.php
+++ b/app/Controllers/Admin/Podcast.php
@@ -98,6 +98,14 @@ class Podcast extends BaseController
         return view('admin/podcast/analytics/listening_time', $data);
     }
 
+    public function viewAnalyticsTimePeriods()
+    {
+        $data = ['podcast' => $this->podcast];
+
+        replace_breadcrumb_params([0 => $this->podcast->title]);
+        return view('admin/podcast/analytics/time_periods', $data);
+    }
+
     public function viewAnalyticsPlayers()
     {
         $data = ['podcast' => $this->podcast];
diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php
index be49e1059e..a7b26ff798 100644
--- a/app/Language/en/Breadcrumb.php
+++ b/app/Language/en/Breadcrumb.php
@@ -28,4 +28,5 @@ return [
     'unique-listeners' => 'Unique listeners',
     'players' => 'Players',
     'listening-time' => 'Listening time',
+    'time-periods' => 'Time periods',
 ];
diff --git a/app/Language/en/Charts.php b/app/Language/en/Charts.php
index 206df55635..0b6078d1a1 100644
--- a/app/Language/en/Charts.php
+++ b/app/Language/en/Charts.php
@@ -30,4 +30,7 @@ return [
     'podcast_bots' => 'Bots (crawlers)',
     'daily_listening_time' => 'Daily cumulative listening time',
     'monthly_listening_time' => 'Monthly cumulative listening time',
+    'by_weekday' => 'By week day (for the past 60 days)',
+    'by_hour' => 'By time of day (for the past 60 days)',
+    'podcast_by_bandwidth' => 'Daily used bandwidth (in MB)',
 ];
diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php
index 262884081b..712fc8bd42 100644
--- a/app/Language/en/PodcastNavigation.php
+++ b/app/Language/en/PodcastNavigation.php
@@ -26,4 +26,5 @@ return [
     'podcast-analytics-unique-listeners' => 'Unique listeners',
     'podcast-analytics-players' => 'Players',
     'podcast-analytics-listening-time' => 'Listening time',
+    'podcast-analytics-time-periods' => 'Time periods',
 ];
diff --git a/app/Language/fr/Breadcrumb.php b/app/Language/fr/Breadcrumb.php
index 50405e5375..5c55f168f8 100644
--- a/app/Language/fr/Breadcrumb.php
+++ b/app/Language/fr/Breadcrumb.php
@@ -28,4 +28,5 @@ return [
     'unique-listeners' => 'Auditeurs uniques',
     'players' => 'Lecteurs',
     'listening-time' => 'Durée d’écoute',
+    'time-periods' => 'Périodes',
 ];
diff --git a/app/Language/fr/Charts.php b/app/Language/fr/Charts.php
index 14607d2690..1b72c51b44 100644
--- a/app/Language/fr/Charts.php
+++ b/app/Language/fr/Charts.php
@@ -43,4 +43,7 @@ return [
     'podcast_bots' => 'Robots (bots)',
     'daily_listening_time' => 'Durée quotidienne d’écoute cumulée',
     'monthly_listening_time' => 'Durée mensuelle d’écoute cumulée',
+    'by_weekday' => 'Par jour de la semaine (sur les 60 derniers jours)',
+    'by_hour' => 'Par heure de la journée (sur les 60 derniers jours)',
+    'podcast_by_bandwidth' => 'Bande passante quotidienne consommée (en Mo)',
 ];
diff --git a/app/Language/fr/PodcastNavigation.php b/app/Language/fr/PodcastNavigation.php
index 9a2e09e93f..398521866c 100644
--- a/app/Language/fr/PodcastNavigation.php
+++ b/app/Language/fr/PodcastNavigation.php
@@ -26,4 +26,5 @@ return [
     'podcast-analytics-unique-listeners' => 'Auditeurs uniques',
     'podcast-analytics-players' => 'Lecteurs',
     'podcast-analytics-listening-time' => 'Durée d’écoute',
+    'podcast-analytics-time-periods' => 'Périodes',
 ];
diff --git a/app/Models/AnalyticsPodcastByCountryModel.php b/app/Models/AnalyticsPodcastByCountryModel.php
index 162170a5cf..ad9bb39585 100644
--- a/app/Models/AnalyticsPodcastByCountryModel.php
+++ b/app/Models/AnalyticsPodcastByCountryModel.php
@@ -37,15 +37,33 @@ class AnalyticsPodcastByCountryModel extends Model
                 "{$podcastId}_analytics_podcast_by_country_weekly"
             ))
         ) {
+            $oneWeekAgo=date('Y-m-d', strtotime('-1 week'));
             $found = $this->select('`country_code` as `labels`')
                 ->selectSum('`hits`', '`values`')
                 ->where([
                     '`podcast_id`' => $podcastId,
-                    '`date` >' => date('Y-m-d', strtotime('-1 week')),
+                    '`date` >' => $oneWeekAgo,
                 ])
                 ->groupBy('`labels`')
                 ->orderBy('`values`', 'DESC')
-                ->findAll(10);
+                ->findAll(9);
+
+            $found[] = $this->db
+                ->query(
+                    "SELECT
+                        \"Other\" AS `labels`,
+                        SUM(`others`.`values`) AS `values`
+                    FROM
+                        (SELECT SUM(`hits`) AS `values`
+                            FROM {$this->db->prefixTable($this->table)}
+                            WHERE `date` > $oneWeekAgo
+                            GROUP BY `country_code`
+                            ORDER BY `values` DESC
+                            LIMIT 18446744073709551610 OFFSET 9
+                        ) AS `others`
+                    GROUP BY `labels`"
+                )
+                ->getRow(0);
 
             cache()->save(
                 "{$podcastId}_analytics_podcast_by_country_weekly",
@@ -70,16 +88,34 @@ class AnalyticsPodcastByCountryModel extends Model
                 "{$podcastId}_analytics_podcast_by_country_yearly"
             ))
         ) {
+            $oneYearAgo = date('Y-m-d', strtotime('-1 year'));
             $found = $this->select('`country_code` as `labels`')
                 ->selectSum('`hits`', '`values`')
                 ->where([
                     '`podcast_id`' => $podcastId,
-                    '`date` >' => date('Y-m-d', strtotime('-1 year')),
+                    '`date` >' => $oneYearAgo,
                 ])
                 ->groupBy('`labels`')
                 ->orderBy('`values`', 'DESC')
                 ->findAll(10);
 
+            $found[] = $this->db
+                ->query(
+                    "SELECT
+                        \"Other\" AS `labels`,
+                        SUM(`others`.`values`) AS `values`
+                    FROM
+                        (SELECT SUM(`hits`) AS `values`
+                            FROM {$this->db->prefixTable($this->table)}
+                            WHERE `date` > $oneyearago
+                            GROUP BY `country_code`
+                            ORDER BY `values` DESC
+                            LIMIT 18446744073709551610 OFFSET 9
+                        ) AS `others`
+                    GROUP BY `labels`"
+                )
+                ->getRow(0);
+
             cache()->save(
                 "{$podcastId}_analytics_podcast_by_country_yearly",
                 $found,
diff --git a/app/Models/AnalyticsPodcastByHourModel.php b/app/Models/AnalyticsPodcastByHourModel.php
index 9dd82a4d1a..1960e7836e 100644
--- a/app/Models/AnalyticsPodcastByHourModel.php
+++ b/app/Models/AnalyticsPodcastByHourModel.php
@@ -22,4 +22,38 @@ class AnalyticsPodcastByHourModel extends Model
     protected $useSoftDeletes = false;
 
     protected $useTimestamps = false;
+
+    /**
+     * Gets hits data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getData(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_podcasts_by_hour"))) {
+            $found = $this->select('`hour` as `labels`')
+                ->selectSum('`hits`', '`values`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-60 days')),
+                ])
+                ->groupBy('`labels`')
+                ->orderBy('`labels`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_podcasts_by_hour",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
+
+
+
+
+    
 }
diff --git a/app/Models/AnalyticsPodcastModel.php b/app/Models/AnalyticsPodcastModel.php
index e2c60ff583..1d44a55150 100644
--- a/app/Models/AnalyticsPodcastModel.php
+++ b/app/Models/AnalyticsPodcastModel.php
@@ -46,6 +46,66 @@ class AnalyticsPodcastModel extends Model
         return $found;
     }
 
+    /**
+     * Gets hits data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataByWeekday(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_podcasts_by_weekday"))) {
+            $found = $this->select(
+                'LEFT(DAYNAME(`date`),3) as `labels`, WEEKDAY(`date`) as `sort_labels`'
+            )
+                ->selectSum('`hits`', '`values`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-60 days')),
+                ])
+                ->groupBy('`labels`, `sort_labels`')
+                ->orderBy('`sort_labels`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_podcasts_by_weekday",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
+
+    /**
+     * Gets bandwidth data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataBandwidthByDay(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_podcast_by_bandwidth"))) {
+            $found = $this->select(
+                '`date` as `labels`, round(`bandwidth` / 1048576, 1) as `values`'
+            )
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-60 days')),
+                ])
+                ->orderBy('`labels`', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_podcast_by_bandwidth",
+                $found,
+                600
+            );
+        }
+        return $found;
+    }
+
     /**
      * Gets hits data for a podcast
      *
diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts
index 41149edd4b..9e2f754e94 100644
--- a/app/Views/_assets/modules/Charts.ts
+++ b/app/Views/_assets/modules/Charts.ts
@@ -52,7 +52,7 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => {
   const series = chart.series.push(new am4charts.LineSeries());
   series.dataFields.valueY = "values";
   series.dataFields.dateX = "labels";
-  series.tooltipText = "{valueY} hits";
+  series.tooltipText = "{valueY}";
   series.strokeWidth = 2;
   // Make bullets grow on hover
   const bullet = series.bullets.push(new am4charts.CircleBullet());
@@ -68,6 +68,35 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => {
   chart.scrollbarX = new am4core.Scrollbar();
 };
 
+const drawBarChart = (chartDivId: string, dataUrl: string | null): void => {
+  // Create chart instance
+  const chart = am4core.create(chartDivId, am4charts.XYChart);
+  am4core.percent(100);
+  chart.exporting.menu = new am4core.ExportMenu();
+  chart.exporting.menu.align = "right";
+  chart.exporting.menu.verticalAlign = "bottom";
+  // Set theme
+  am4core.useTheme(am4themes_material);
+  chart.dataSource.url = dataUrl || "";
+  chart.dataSource.parser.options.emptyAs = 0;
+  const categoryAxis = chart.xAxes.push(new am4charts.CategoryAxis());
+  categoryAxis.dataFields.category = "labels";
+  categoryAxis.renderer.grid.template.location = 0;
+  categoryAxis.renderer.minGridDistance = 30;
+  chart.yAxes.push(new am4charts.ValueAxis());
+  // Create series
+  const series = chart.series.push(new am4charts.ColumnSeries());
+  series.dataFields.valueY = "values";
+  series.dataFields.categoryX = "labels";
+  series.name = "Hits";
+  series.columns.template.tooltipText = "{valueY} hits";
+  series.columns.template.fillOpacity = .8;
+  const columnTemplate = series.columns.template;
+  columnTemplate.strokeWidth = 2;
+  columnTemplate.strokeOpacity = 1;
+};
+
+
 const drawXYDurationChart = (
   chartDivId: string,
   dataUrl: string | null
@@ -205,6 +234,9 @@ const DrawCharts = (): void => {
       case "xy-chart":
         drawXYChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
         break;
+      case "bar-chart":
+        drawBarChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
+        break;
       case "xy-duration-chart":
         drawXYDurationChart(
           chartDiv.id,
diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php
index cf63746504..704a744405 100644
--- a/app/Views/admin/podcast/_sidebar.php
+++ b/app/Views/admin/podcast/_sidebar.php
@@ -16,6 +16,7 @@ $podcastNavigation = [
             'podcast-analytics-listening-time',
             'podcast-analytics-players',
             'podcast-analytics-locations',
+            'podcast-analytics-time-periods',
             'podcast-analytics-webpages',
         ],
     ],
diff --git a/app/Views/admin/podcast/analytics/index.php b/app/Views/admin/podcast/analytics/index.php
index f604d17536..54b9544389 100644
--- a/app/Views/admin/podcast/analytics/index.php
+++ b/app/Views/admin/podcast/analytics/index.php
@@ -29,6 +29,16 @@
 ) ?>"></div>
 </div>
 
+<div class="mb-12 text-center">
+<h2><?= lang('Charts.podcast_by_bandwidth') ?></h2>
+<div class="chart-xy" id="by-bandwidth-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
+    'analytics-data',
+    $podcast->id,
+    'Podcast',
+    'BandwidthByDay'
+) ?>"></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(
diff --git a/app/Views/admin/podcast/analytics/time_periods.php b/app/Views/admin/podcast/analytics/time_periods.php
new file mode 100644
index 0000000000..8b56873072
--- /dev/null
+++ b/app/Views/admin/podcast/analytics/time_periods.php
@@ -0,0 +1,36 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= $podcast->title ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= $podcast->title ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+
+<div class="lg:divide-x lg:grid lg:grid-cols-2">
+    <div class="mb-12 mr-6 text-center">
+        <h2><?= lang('Charts.by_weekday') ?></h2>
+        <div class="chart-xy" id="by-weekday-barchart" data-chart-type="bar-chart" data-chart-url="<?= route_to(
+            'analytics-data',
+            $podcast->id,
+            'Podcast',
+            'ByWeekday'
+        ) ?>"></div>
+    </div>
+
+    <div class="mb-12 mr-6 text-center">
+        <h2><?= lang('Charts.by_hour') ?></h2>
+        <div class="chart-xy" id="by-hour-barchart" data-chart-type="bar-chart" data-chart-url="<?= route_to(
+            'analytics-full-data',
+            $podcast->id,
+            'PodcastByHour'
+        ) ?>"></div>
+    </div>
+
+</div>
+
+<script src="/assets/charts.js" type="module"></script>
+<?= $this->endSection() ?>
-- 
GitLab