feat: add map analytics, add episodes analytics, clean analytics page layout, translate countries

This commit is contained in:
Benjamin Bellamy 2020-10-14 10:38:48 +00:00 committed by Yassine Doghri
parent 196920d62f
commit 07eae83a00
27 changed files with 726 additions and 213 deletions

View File

@ -123,10 +123,46 @@ $routes->group(
'as' => 'podcast-delete', 'as' => 'podcast-delete',
'filter' => 'permission:podcasts-delete', 'filter' => 'permission:podcasts-delete',
]); ]);
$routes->get('analytics', 'Podcast::analytics/$1', [
$routes->group('analytics', function ($routes) {
$routes->get('/', 'Podcast::viewAnalytics/$1', [
'as' => 'podcast-analytics', 'as' => 'podcast-analytics',
'filter' => 'permission:podcasts-view,podcast-view', '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( $routes->get(
'analytics-data/(:segment)', 'analytics-data/(:segment)',
'AnalyticsData::getData/$1/$2', 'AnalyticsData::getData/$1/$2',

View File

@ -58,12 +58,44 @@ class Podcast extends BaseController
return view('admin/podcast/view', $data); return view('admin/podcast/view', $data);
} }
public function analytics() public function viewAnalytics()
{ {
$data = ['podcast' => $this->podcast]; $data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]); 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() public function create()

View File

@ -20,4 +20,9 @@ class AnalyticsPodcastsByCountry extends Entity
'date' => 'datetime', 'date' => 'datetime',
'hits' => 'integer', 'hits' => 'integer',
]; ];
public function getLabels()
{
return lang('Countries.' . $this->attributes['labels']);
}
} }

View File

@ -23,4 +23,9 @@ class AnalyticsPodcastsByRegion extends Entity
'date' => 'datetime', 'date' => 'datetime',
'hits' => 'integer', 'hits' => 'integer',
]; ];
public function getCountryCode()
{
return lang('Countries.' . $this->attributes['country_code']);
}
} }

View File

@ -20,4 +20,10 @@ class AnalyticsWebsiteByEntryPage extends Entity
'date' => 'datetime', 'date' => 'datetime',
'hits' => 'integer', 'hits' => 'integer',
]; ];
public function getLabels()
{
$split = explode('/', $this->attributes['labels']);
return $split[count($split) - 1];
}
} }

View File

@ -23,4 +23,8 @@ return [
'settings' => 'settings', 'settings' => 'settings',
'platforms' => 'platforms', 'platforms' => 'platforms',
'analytics' => 'Analytics', 'analytics' => 'Analytics',
'locations' => 'Locations',
'website' => 'Website',
'unique-listeners' => 'Unique listeners',
'players' => 'Players',
]; ];

View File

@ -7,14 +7,24 @@
*/ */
return [ 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_daily_listeners' => 'Daily unique listeners',
'unique_monthly_listeners' => 'Monthly 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_day' => 'Podcast daily downloads',
'podcast_by_month' => 'Podcast monthly 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' => 'episodes_by_day' =>
'5 latest episodes downloads (during their first 60 days)', '5 latest episodes downloads (during their first 60 days)',
'by_country' => 'Podcast downloads by country (for the past week)', 'by_country_weekly' => 'Podcast downloads by country (for the past week)',
'by_domain' => 'Website visits by origin (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)',
]; ];

View File

@ -20,5 +20,9 @@ return [
'contributor-add' => 'Add contributor', 'contributor-add' => 'Add contributor',
'settings' => 'Settings', 'settings' => 'Settings',
'platforms' => 'Podcast platforms', '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',
]; ];

View File

@ -20,7 +20,7 @@ return [
'unique_daily_listeners' => 'Auditeurs uniques quotidiens', 'unique_daily_listeners' => 'Auditeurs uniques quotidiens',
'unique_monthly_listeners' => 'Auditeurs uniques mensuels', 'unique_monthly_listeners' => 'Auditeurs uniques mensuels',
'by_browser' => '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_day' => 'Téléchargements quotidiens de podcasts',
'podcast_by_month' => 'Téléchargements mensuels de podcasts', 'podcast_by_month' => 'Téléchargements mensuels de podcasts',
'episode_by_day' => 'episode_by_day' =>
@ -33,10 +33,10 @@ return [
'by_country_yearly' => 'by_country_yearly' =>
'Téléchargement de podcasts par pays (sur la dernière année)', 'Téléchargement de podcasts par pays (sur la dernière année)',
'by_domain_weekly' => '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' => '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' => 'by_entry_page' =>
'Fréquentation du site par page dentrée (sur la dernière semaine)', 'Fréquentation des pages web par page dentrée (sur la dernière semaine)',
'podcast_bots' => 'Robots (bots)', 'podcast_bots' => 'Robots (bots)',
]; ];

View File

@ -20,8 +20,8 @@ return [
'contributor-add' => 'Ajouter un contributeur', 'contributor-add' => 'Ajouter un contributeur',
'settings' => 'Paramètres', 'settings' => 'Paramètres',
'platforms' => 'Plateformes du podcast', 'platforms' => 'Plateformes du podcast',
'podcast-analytics' => 'Mesures daudience', 'podcast-analytics' => 'Vue densemble',
'podcast-analytics-website' => 'Visites du site web', 'podcast-analytics-webpages' => 'Visites des pages web',
'podcast-analytics-locations' => 'Localisations', 'podcast-analytics-locations' => 'Localisations',
'podcast-analytics-unique-listeners' => 'Auditeurs uniques', 'podcast-analytics-unique-listeners' => 'Auditeurs uniques',
'podcast-analytics-players' => 'Lecteurs', 'podcast-analytics-players' => 'Lecteurs',

View File

@ -30,9 +30,14 @@ class AnalyticsPodcastByCountryModel extends Model
* *
* @return array * @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`') $found = $this->select('`country_code` as `labels`')
->selectSum('`hits`', '`values`') ->selectSum('`hits`', '`values`')
->where([ ->where([
@ -44,7 +49,41 @@ class AnalyticsPodcastByCountryModel extends Model
->findAll(10); ->findAll(10);
cache()->save( 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, $found,
600 600
); );

View File

@ -37,12 +37,12 @@ class AnalyticsPodcastByEpisodeModel extends Model
)) ))
) { ) {
$lastEpisodes = (new EpisodeModel()) $lastEpisodes = (new EpisodeModel())
->select('id, season_number, number, title') ->select('`id`, `season_number`, `number`, `title`')
->orderBy('id', 'DESC') ->orderBy('`id`', 'DESC')
->where(['podcast_id' => $podcastId]) ->where(['`podcast_id`' => $podcastId])
->findAll(5); ->findAll(5);
$found = $this->select('age AS X'); $found = $this->select('`age` AS `X`');
$letter = 97; $letter = 97;
foreach ($lastEpisodes as $episode) { foreach ($lastEpisodes as $episode) {
@ -51,7 +51,7 @@ class AnalyticsPodcastByEpisodeModel extends Model
'(CASE WHEN `episode_id`=' . '(CASE WHEN `episode_id`=' .
$episode->id . $episode->id .
' THEN `hits` END)', ' THEN `hits` END)',
chr($letter) . 'Y' '`' . chr($letter) . 'Y`'
) )
->select( ->select(
'"' . '"' .
@ -62,20 +62,20 @@ class AnalyticsPodcastByEpisodeModel extends Model
? '' ? ''
: '-' . $episode->number . '/ ') . : '-' . $episode->number . '/ ') .
$episode->title . $episode->title .
'" AS ' . '" AS `' .
chr($letter) . chr($letter) .
'Value' 'Value`'
); );
$letter++; $letter++;
} }
$found = $found $found = $found
->where([ ->where([
'podcast_id' => $podcastId, '`podcast_id`' => $podcastId,
'age <' => 60, '`age` <' => 60,
]) ])
->groupBy('X') ->groupBy('`X`')
->orderBy('X', 'ASC') ->orderBy('`X`', 'ASC')
->findAll(); ->findAll();
cache()->save( cache()->save(
@ -91,14 +91,15 @@ class AnalyticsPodcastByEpisodeModel extends Model
"{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day" "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day"
)) ))
) { ) {
$found = $this->select('date as labels') $found = $this->select('`date as `labels`')
->selectSum('hits', 'values') ->selectSum('`hits`', '`values`')
->where([ ->where([
'episode_id' => $episodeId, '`episode_id`' => $episodeId,
'podcast_id' => $podcastId, '`podcast_id`' => $podcastId,
'`age` <' => 60,
]) ])
->groupBy('labels') ->groupBy('`labels`')
->orderBy('labels', 'ASC') ->orderBy('`labels`', 'ASC')
->findAll(); ->findAll();
cache()->save( cache()->save(
@ -110,4 +111,35 @@ class AnalyticsPodcastByEpisodeModel extends Model
return $found; 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;
}
} }

View File

@ -30,11 +30,11 @@ class AnalyticsPodcastByPlayerModel extends Model
* *
* @return array * @return array
*/ */
public function getDataByApp(int $podcastId): array public function getDataByAppWeekly(int $podcastId): array
{ {
if ( if (
!($found = cache( !($found = cache(
"{$podcastId}_analytics_podcasts_by_player_by_app" "{$podcastId}_analytics_podcasts_by_player_by_app_weekly"
)) ))
) { ) {
$found = $this->select('`app` as `labels`') $found = $this->select('`app` as `labels`')
@ -50,92 +50,148 @@ class AnalyticsPodcastByPlayerModel extends Model
->findAll(10); ->findAll(10);
cache()->save( cache()->save(
"{$podcastId}_analytics_podcasts_by_player_by_app", "{$podcastId}_analytics_podcasts_by_player_by_app_weekly",
$found, $found,
600 600
); );
} }
return $found; return $found;
} }
/** /**
* Gets device data for a podcast * Gets player data for a podcast
* *
* @param int $podcastId * @param int $podcastId
* *
* @return array * @return array
*/ */
public function getDataByDevice(int $podcastId): array public function getDataByAppYearly(int $podcastId): array
{ {
if ( if (
!($found = cache( !($found = cache(
"{$podcastId}_analytics_podcasts_by_player_by_device" "{$podcastId}_analytics_podcasts_by_player_by_app_yearly"
)) ))
) { ) {
$foundApp = $this->select( $found = $this->select('`app` as `labels`')
'CONCAT_WS("/", `device`, `os`, `app`) as `ids`, `app` as `labels`, CONCAT_WS("/", `device`, `os`) as `parents`'
)
->selectSum('`hits`', '`values`') ->selectSum('`hits`', '`values`')
->where([ ->where([
'`podcast_id`' => $podcastId, '`podcast_id`' => $podcastId,
'`app` !=' => null, '`app` !=' => '',
'`bot`' => 0, '`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') ->orderBy('`values`', 'DESC')
->findAll(); ->findAll(10);
$foundOs = $this->select(
'CONCAT_WS("/", `device`, `os`) as `ids`, `os` as `labels`, `device` as `parents`'
)
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`os` !=' => null,
'`bot`' => 0,
'`date` >' => date('Y-m-d', strtotime('-1 week')),
])
->groupBy('`ids`')
->orderBy('`values`', 'DESC')
->findAll();
$foundDevice = $this->select(
'`device` as `ids`, `device` as `labels`, "" as `parents`'
)
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`device` !=' => null,
'`bot`' => 0,
'`date` >' => date('Y-m-d', strtotime('-1 week')),
])
->groupBy('`ids`')
->orderBy('`values`', 'DESC')
->findAll();
$foundBot = $this->select(
'"bots" as `ids`, "Bots" as `labels`, "" as `parents`'
)
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`bot`' => 1,
'`date` >' => date('Y-m-d', strtotime('-1 week')),
])
->groupBy('`ids`')
->orderBy('`values`', 'DESC')
->findAll();
$found = array_merge($foundApp, $foundOs, $foundDevice, $foundBot);
cache()->save( cache()->save(
"{$podcastId}_analytics_podcasts_by_player_by_device", "{$podcastId}_analytics_podcasts_by_player_by_app_yearly",
$found, $found,
600 600
); );
} }
return $found;
}
/**
* Gets os data for a podcast
*
* @param int $podcastId
*
* @return array
*/
public function getDataByOsWeekly(int $podcastId): array
{
if (
!($found = cache(
"{$podcastId}_analytics_podcasts_by_player_by_os_weekly"
))
) {
$found = $this->select('`os` as `labels`')
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`app` !=' => '',
'`bot`' => 0,
'`date` >' => date('Y-m-d', strtotime('-1 week')),
])
->groupBy('`labels`')
->orderBy('`values`', 'DESC')
->findAll(10);
cache()->save(
"{$podcastId}_analytics_podcasts_by_player_by_os_weekly",
$found,
600
);
}
return $found;
}
/**
* Gets player data for a podcast
*
* @param int $podcastId
*
* @return array
*/
public function getDataByDeviceWeekly(int $podcastId): array
{
if (
!($found = cache(
"{$podcastId}_analytics_podcasts_by_player_by_device_weekly"
))
) {
$found = $this->select('`device` as `labels`')
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`device` !=' => '',
'`bot`' => 0,
'`date` >' => date('Y-m-d', strtotime('-1 week')),
])
->groupBy('`labels`')
->orderBy('`values`', 'DESC')
->findAll(10);
cache()->save(
"{$podcastId}_analytics_podcasts_by_player_by_device_weekly",
$found,
600
);
}
return $found;
}
/**
* Gets bots data for a podcast
*
* @param int $podcastId
*
* @return array
*/
public function getDataBots(int $podcastId): array
{
if (
!($found = cache("{$podcastId}_analytics_podcasts_by_player_bots"))
) {
$found = $this->select('DATE_FORMAT(`date`,"%Y-%m-01") as `labels`')
->selectSum('`hits`', '`values`')
->where([
'`podcast_id`' => $podcastId,
'`bot`' => 1,
'`date` >' => date('Y-m-d', strtotime('-1 year')),
])
->groupBy('`labels`')
->orderBy('`labels`', 'ASC')
->findAll(10);
cache()->save(
"{$podcastId}_analytics_podcasts_by_player_bots",
$found,
600
);
}
return $found; return $found;
} }
} }

View File

@ -32,11 +32,16 @@ class AnalyticsPodcastByRegionModel extends Model
*/ */
public function getData(int $podcastId): array 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( $found = $this->select(
'`country_code`, `region_code`, `latitude`, `longitude`' '`country_code`, `region_code`, `latitude`, `longitude`'
) )
->selectSum('`hits`', '`values`') ->selectSum('`hits`', '`value`')
->groupBy( ->groupBy(
'`country_code`, `region_code`, `latitude`, `longitude`' '`country_code`, `region_code`, `latitude`, `longitude`'
) )
@ -44,11 +49,11 @@ class AnalyticsPodcastByRegionModel extends Model
'`podcast_id`' => $podcastId, '`podcast_id`' => $podcastId,
'`date` >' => date('Y-m-d', strtotime('-1 week')), '`date` >' => date('Y-m-d', strtotime('-1 week')),
]) ])
->orderBy('`values`', 'DESC') ->orderBy('`value`', 'DESC')
->findAll(); ->findAll();
cache()->save( cache()->save(
"{$podcastId}_analytics_podcast_by_region", "{$podcastId}_analytics_podcast_by_region_{$locale}",
$found, $found,
600 600
); );

View File

@ -36,7 +36,7 @@ class AnalyticsPodcastModel extends Model
$found = $this->select('`date` as `labels`, `hits` as `values`') $found = $this->select('`date` as `labels`, `hits` as `values`')
->where([ ->where([
'`podcast_id`' => $podcastId, '`podcast_id`' => $podcastId,
'`date` >' => date('Y-m-d', strtotime('-1 year')), '`date` >' => date('Y-m-d', strtotime('-60 days')),
]) ])
->orderBy('`labels`', 'ASC') ->orderBy('`labels`', 'ASC')
->findAll(); ->findAll();
@ -60,7 +60,6 @@ class AnalyticsPodcastModel extends Model
->selectSum('`hits`', '`values`') ->selectSum('`hits`', '`values`')
->where([ ->where([
'`podcast_id`' => $podcastId, '`podcast_id`' => $podcastId,
'`date` >' => date('Y-m-d', strtotime('-1 year')),
]) ])
->groupBy('`labels`') ->groupBy('`labels`')
->orderBy('`labels`', 'ASC') ->orderBy('`labels`', 'ASC')
@ -94,7 +93,7 @@ class AnalyticsPodcastModel extends Model
) )
->where([ ->where([
'`podcast_id`' => $podcastId, '`podcast_id`' => $podcastId,
'`date` >' => date('Y-m-d', strtotime('-1 year')), '`date` >' => date('Y-m-d', strtotime('-60 days')),
]) ])
->orderBy('`labels`', 'ASC') ->orderBy('`labels`', 'ASC')
->findAll(); ->findAll();

View File

@ -59,9 +59,11 @@ class AnalyticsWebsiteByRefererModel extends Model
* *
* @return array * @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`') $found = $this->select('`domain` as `labels`')
->selectSum('`hits`', '`values`') ->selectSum('`hits`', '`values`')
->where([ ->where([
@ -73,7 +75,38 @@ class AnalyticsWebsiteByRefererModel extends Model
->findAll(10); ->findAll(10);
cache()->save( 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, $found,
600 600
); );

View File

@ -1,33 +1,29 @@
// Import modules // Import modules
import am4geodata_worldLow from "@amcharts/amcharts4-geodata/worldLow";
import * as am4charts from "@amcharts/amcharts4/charts"; import * as am4charts from "@amcharts/amcharts4/charts";
import * as am4core from "@amcharts/amcharts4/core"; import * as am4core from "@amcharts/amcharts4/core";
import * as am4maps from "@amcharts/amcharts4/maps";
import am4themes_material from "@amcharts/amcharts4/themes/material"; import am4themes_material from "@amcharts/amcharts4/themes/material";
const drawPieChart = (chartDivId: string, dataUrl: string | null): void => { const drawPieChart = (chartDivId: string, dataUrl: string | null): void => {
// Create chart instance // Create chart instance
const chart = am4core.create(chartDivId, am4charts.PieChart); const chart = am4core.create(chartDivId, am4charts.PieChart);
am4core.percent(100); am4core.percent(100);
// Set theme // Set theme
am4core.useTheme(am4themes_material); am4core.useTheme(am4themes_material);
chart.innerRadius = am4core.percent(10); chart.innerRadius = am4core.percent(10);
// Add data // Add data
chart.dataSource.url = dataUrl || ""; chart.dataSource.url = dataUrl || "";
chart.dataSource.parser.options.emptyAs = 0; chart.dataSource.parser.options.emptyAs = 0;
// Add and configure Series // Add and configure Series
const pieSeries = chart.series.push(new am4charts.PieSeries()); const pieSeries = chart.series.push(new am4charts.PieSeries());
pieSeries.dataFields.value = "values"; pieSeries.dataFields.value = "values";
pieSeries.dataFields.category = "labels"; pieSeries.dataFields.category = "labels";
pieSeries.slices.template.stroke = am4core.color("#ffffff"); pieSeries.slices.template.stroke = am4core.color("#ffffff");
pieSeries.slices.template.strokeWidth = 1; pieSeries.slices.template.strokeWidth = 1;
pieSeries.slices.template.strokeOpacity = 1; pieSeries.slices.template.strokeOpacity = 1;
pieSeries.labels.template.disabled = true; pieSeries.labels.template.disabled = true;
pieSeries.ticks.template.disabled = true; pieSeries.ticks.template.disabled = true;
chart.legend = new am4charts.Legend(); chart.legend = new am4charts.Legend();
chart.legend.position = "right"; chart.legend.position = "right";
chart.legend.scrollable = true; chart.legend.scrollable = true;
@ -37,32 +33,32 @@ const drawXYChart = (chartDivId: string, dataUrl: string | null): void => {
// Create chart instance // Create chart instance
const chart = am4core.create(chartDivId, am4charts.XYChart); const chart = am4core.create(chartDivId, am4charts.XYChart);
am4core.percent(100); am4core.percent(100);
// Set theme // Set theme
am4core.useTheme(am4themes_material); am4core.useTheme(am4themes_material);
// Create axes // Create axes
const dateAxis = chart.xAxes.push(new am4charts.DateAxis()); const dateAxis = chart.xAxes.push(new am4charts.DateAxis());
dateAxis.renderer.minGridDistance = 60; dateAxis.renderer.minGridDistance = 60;
chart.yAxes.push(new am4charts.ValueAxis()); chart.yAxes.push(new am4charts.ValueAxis());
// Add data // Add data
chart.dataSource.url = dataUrl || ""; chart.dataSource.url = dataUrl || "";
chart.dataSource.parser.options.emptyAs = 0; chart.dataSource.parser.options.emptyAs = 0;
// Create series // Create series
const series = chart.series.push(new am4charts.LineSeries()); const series = chart.series.push(new am4charts.LineSeries());
series.dataFields.valueY = "values"; series.dataFields.valueY = "values";
series.dataFields.dateX = "labels"; 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"; series.tooltip.pointerOrientation = "vertical";
chart.cursor = new am4charts.XYCursor(); chart.cursor = new am4charts.XYCursor();
chart.cursor.snapToSeries = series; chart.cursor.snapToSeries = series;
chart.cursor.xAxis = dateAxis; chart.cursor.xAxis = dateAxis;
chart.scrollbarX = new am4core.Scrollbar(); chart.scrollbarX = new am4core.Scrollbar();
}; };
@ -73,40 +69,74 @@ const drawXYSeriesChart = (
// Create chart instance // Create chart instance
const chart = am4core.create(chartDivId, am4charts.XYChart); const chart = am4core.create(chartDivId, am4charts.XYChart);
am4core.percent(100); am4core.percent(100);
// Set theme // Set theme
am4core.useTheme(am4themes_material); am4core.useTheme(am4themes_material);
// Create axes // Create axes
chart.xAxes.push(new am4charts.ValueAxis()); chart.xAxes.push(new am4charts.ValueAxis());
chart.yAxes.push(new am4charts.ValueAxis()); chart.yAxes.push(new am4charts.ValueAxis());
// Add data // Add data
chart.dataSource.url = dataUrl || ""; chart.dataSource.url = dataUrl || "";
chart.dataSource.parser.options.emptyAs = 0; chart.dataSource.parser.options.emptyAs = 0;
// Create series // Create series
const series1 = chart.series.push(new am4charts.LineSeries()); const series1 = chart.series.push(new am4charts.LineSeries());
series1.dataFields.valueX = "X"; series1.dataFields.valueX = "X";
series1.dataFields.valueY = "aY"; series1.dataFields.valueY = "aY";
const series2 = chart.series.push(new am4charts.LineSeries()); const series2 = chart.series.push(new am4charts.LineSeries());
series2.dataFields.valueX = "X"; series2.dataFields.valueX = "X";
series2.dataFields.valueY = "bY"; series2.dataFields.valueY = "bY";
const series3 = chart.series.push(new am4charts.LineSeries()); const series3 = chart.series.push(new am4charts.LineSeries());
series3.dataFields.valueX = "X"; series3.dataFields.valueX = "X";
series3.dataFields.valueY = "cY"; series3.dataFields.valueY = "cY";
const series4 = chart.series.push(new am4charts.LineSeries()); const series4 = chart.series.push(new am4charts.LineSeries());
series4.dataFields.valueX = "X"; series4.dataFields.valueX = "X";
series4.dataFields.valueY = "dY"; series4.dataFields.valueY = "dY";
const series5 = chart.series.push(new am4charts.LineSeries()); const series5 = chart.series.push(new am4charts.LineSeries());
series5.dataFields.valueX = "X"; series5.dataFields.valueX = "X";
series5.dataFields.valueY = "eY"; 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 DrawCharts = (): void => {
const chartDivs: NodeListOf<HTMLDivElement> = document.querySelectorAll( const chartDivs: NodeListOf<HTMLDivElement> = document.querySelectorAll(
"div[data-chart-type]" "div[data-chart-type]"
@ -125,6 +155,9 @@ const DrawCharts = (): void => {
case "xy-series-chart": case "xy-series-chart":
drawXYSeriesChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); drawXYSeriesChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
break; break;
case "map-chart":
drawMapChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
break;
default: default:
console.error("Unknown chart type:" + chartType); console.error("Unknown chart type:" + chartType);
} }

View File

@ -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;
}

View File

@ -5,3 +5,4 @@
@import "./radioBtn.css"; @import "./radioBtn.css";
@import "./switch.css"; @import "./switch.css";
@import "./enclosureInput.css"; @import "./enclosureInput.css";
@import "./charts.css";

View File

@ -46,4 +46,29 @@
</section> </section>
</div> </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() ?> <?= $this->endSection() ?>

View File

@ -10,7 +10,13 @@ $podcastNavigation = [
], ],
'analytics' => [ 'analytics' => [
'icon' => 'line-chart', 'icon' => 'line-chart',
'items' => ['podcast-analytics'], 'items' => [
'podcast-analytics',
'podcast-analytics-unique-listeners',
'podcast-analytics-players',
'podcast-analytics-locations',
'podcast-analytics-webpages',
],
], ],
'contributors' => [ 'contributors' => [
'icon' => 'group', 'icon' => 'group',

View File

@ -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() ?>

View File

@ -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() ?>

View File

@ -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() ?>

View File

@ -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() ?>

View File

@ -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() ?>

View File

@ -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() ?>