From: Stephen Cameron Date: Mon, 26 Sep 2022 17:53:59 +0000 (+0200) Subject: Added unique visitor stats for newer Fluidbooks, refactored code and improved loading... X-Git-Url: http://git.cubedesigners.com/?a=commitdiff_plain;h=0c8852d41fb1e26d192ff4122ecd3732e9928e4a;p=fluidbook-toolbox.git Added unique visitor stats for newer Fluidbooks, refactored code and improved loading. Wait #5474 @8 --- diff --git a/app/Http/Controllers/Admin/Operations/FluidbookPublication/StatsOperation.php b/app/Http/Controllers/Admin/Operations/FluidbookPublication/StatsOperation.php index 54133d81a..930a005e5 100644 --- a/app/Http/Controllers/Admin/Operations/FluidbookPublication/StatsOperation.php +++ b/app/Http/Controllers/Admin/Operations/FluidbookPublication/StatsOperation.php @@ -15,10 +15,16 @@ trait StatsOperation protected function setupStatsRoutes($segment, $routeName, $controller) { // Main route is only secured by hash (security by obscurity) - Route::get($segment . '/stats/{fluidbook_id}_{hash}/{date?}/{period_override?}', $controller . '@statsSummary') + Route::get($segment . '/stats/{fluidbook_id}_{hash}/{date?}/{period_override?}', $controller . '@statsLoader') ->withoutMiddleware([CheckIfAdmin::class]) ->name('stats'); // Named route is used to generate URLs more consistently using route helper + // Since the stats can sometimes be slow to generate, the initial URL displays + // a loader while the actual report is fetched via AJAX and injected into the page. + Route::get($segment . '/stats-data/{fluidbook_id}_{hash}/{date?}/{period_override?}', $controller . '@statsSummary') + ->withoutMiddleware([CheckIfAdmin::class]) + ->name('stats-report'); + // API testing tool (intended for superadmins only) Route::get($segment . '/stats/API', $controller . '@statsAPI'); @@ -129,7 +135,7 @@ trait StatsOperation // Count 2+ years: period = year $day_count = Carbon::parse($start_date)->diffInDays($end_date); - if ($day_count <= 50) { + if ($day_count <= 60) { return 'day'; } elseif ($day_count <= 180) { return 'week'; @@ -140,6 +146,11 @@ trait StatsOperation return 'year'; } + protected function statsLoader($fluidbook_id, $hash, $date = null, $period_override = null) { + $report_URL = route('stats-report', compact('fluidbook_id', 'hash', 'date', 'period_override')); + return view('fluidbook_stats.loader', compact('report_URL')); + } + protected function statsSummary($fluidbook_id, $hash, $date = null, $period_override = null) { $locale = app()->getLocale(); @@ -264,9 +275,15 @@ trait StatsOperation // types of reports but for now, they have to be filtered. // * Note about unique visitors: - // We added unique visitor stats but found that they were only different to the 'visits' count in our historical - // data from the old stats system. With Matomo, the uniques always seem to be the same as the visits, so they - // don't serve any useful purpose here, only highlighting the inconsistencies with the historical stats. + // During the transition from the old stats system to Matomo, there has been a proxy in place that transfers + // stats from older Fluidbooks to Matomo (necessary because these didn't have the Matomo tracker embedded). + // However, the old system is poor at determining unique visitors, meaning that any "uniques" we received from + // there should be considered normal visits. When displaying the stats, we can only display unique visitors if + // they came from the native Matomo tracker. We'll determine this based on the Fluidbook ID >= 20687, which + // was the first one for 2022, when we're sure that the new tracker was in place. More details: + // - https://redmine.cubedesigners.com/issues/5474#note-5 + // - https://redmine.cubedesigners.com/issues/5473 + $new_stats_cutoff = 20687; // Starting Fluidbook ID for new stats system //=== CUSTOM EVENT STATISTICS (zooms, bookmarks, shares, links, downloads, prints, etc.) // This gathers the events based on their categories and segregated by periods (days, months etc). @@ -315,21 +332,21 @@ trait StatsOperation $page_group = $page_number; $pages[$page_group] = [ - 'page_group' => $page_group, - 'page_number' => $page_number, // Used by table column sorter - 'nb_visits' => $pageUrls["/page/$page_number"]['nb_visits'] ?? 0, - //'nb_unique_visits' => $pageUrls["/page/$page_number"]['sum_daily_nb_uniq_visitors'] ?? 0, - 'nb_pageviews' => $pageUrls["/page/$page_number"]['nb_hits'] ?? 0, - 'nb_zooms' => data_get($eventsByPage, "zoom.subtable.$page_number.nb_events", 0), - 'nb_bookmarks' => data_get($eventsByPage, "bookmark.subtable.$page_number.nb_events", 0), - 'nb_shares' => data_get($eventsByPage, "share.subtable.$page_number.nb_events", 0), + 'page_group' => $page_group, + 'page_number' => $page_number, // Used by table column sorter + 'nb_visits' => $pageUrls["/page/$page_number"]['nb_visits'] ?? 0, + 'nb_uniq_visitors' => $pageUrls["/page/$page_number"]['sum_daily_nb_uniq_visitors'] ?? 0, + 'nb_pageviews' => $pageUrls["/page/$page_number"]['nb_hits'] ?? 0, + 'nb_zooms' => data_get($eventsByPage, "zoom.subtable.$page_number.nb_events", 0), + 'nb_bookmarks' => data_get($eventsByPage, "bookmark.subtable.$page_number.nb_events", 0), + 'nb_shares' => data_get($eventsByPage, "share.subtable.$page_number.nb_events", 0), ]; // Special case: the first page of double-page Fluidbooks is tracked as "page 0", meaning that // for our stats labelled as "page 1", we must get certain stats from the "page 0" data... if (!$pages_are_independent && $page_number === 1) { $pages[$page_group]['nb_visits'] = $pageUrls["/page/0"]['nb_visits'] ?? 0; - $pages[$page_group]['nb_unique_visits'] = $pageUrls["/page/0"]['sum_daily_nb_uniq_visitors'] ?? 0; + $pages[$page_group]['nb_uniq_visitors'] = $pageUrls["/page/0"]['sum_daily_nb_uniq_visitors'] ?? 0; $pages[$page_group]['nb_pageviews'] = $pageUrls["/page/0"]['nb_hits'] ?? 0; $pages[$page_group]['nb_zooms'] = data_get($eventsByPage, "zoom.subtable.0.nb_events", 0); @@ -343,17 +360,17 @@ trait StatsOperation $page_group = $page_number .'—'. $following_page; $pages[$page_group] = [ - 'page_group' => $page_group, - 'page_number' => $page_number, // Used by table column sorter - 'nb_visits' => $pageUrls["/page/$page_number"]['nb_visits'] ?? 0, - // 'nb_unique_visits' => $pageUrls["/page/$page_number"]['sum_daily_nb_uniq_visitors'] ?? 0, - 'nb_pageviews' => $pageUrls["/page/$page_number"]['nb_hits'] ?? 0, - 'nb_zooms' => data_get($eventsByPage, "zoom.subtable.$page_number.nb_events", 0), + 'page_group' => $page_group, + 'page_number' => $page_number, // Used by table column sorter + 'nb_visits' => $pageUrls["/page/$page_number"]['nb_visits'] ?? 0, + 'nb_uniq_visitors' => $pageUrls["/page/$page_number"]['sum_daily_nb_uniq_visitors'] ?? 0, + 'nb_pageviews' => $pageUrls["/page/$page_number"]['nb_hits'] ?? 0, + 'nb_zooms' => data_get($eventsByPage, "zoom.subtable.$page_number.nb_events", 0), // Bookmarks and shares are counted and summed for both pages in the spread - 'nb_bookmarks' => data_get($eventsByPage, "bookmark.subtable.$page_number.nb_events", 0) - + data_get($eventsByPage, "bookmark.subtable.$following_page.nb_events", 0), - 'nb_shares' => data_get($eventsByPage, "share.subtable.$page_number.nb_events", 0) - + data_get($eventsByPage, "share.subtable.$following_page.nb_events", 0), + 'nb_bookmarks' => data_get($eventsByPage, "bookmark.subtable.$page_number.nb_events", 0) + + data_get($eventsByPage, "bookmark.subtable.$following_page.nb_events", 0), + 'nb_shares' => data_get($eventsByPage, "share.subtable.$page_number.nb_events", 0) + + data_get($eventsByPage, "share.subtable.$following_page.nb_events", 0), ]; } } @@ -403,11 +420,12 @@ trait StatsOperation //=== MAIN PERIOD STATISTICS // These are the main statistics used to build the chart and the table below it. // Statistics are segregated by a certain period (eg. years, months or days) - // Note: in order to get the total "nb_uniq_visitors" for the pages, we would need to fetch + // Note: in order to get the total "nb_uniq_visitors" for the pages, we need to fetch // the expanded dataset that includes the subtables of pages. For some reason, Matomo // doesn't aggregrate this value when there are sub pages, so we have to do it ourselves - $pagesByPeriod = collect($report->getPageUrls(['expanded' => 0])) - ->map(function ($item, $date) use ($period, $fluidbook_id, $hash, $eventsByPeriod) { + $expanded_stats = $fluidbook_id >= $new_stats_cutoff ? 1 : 0; // Only fetch extra data when it will be used + $pagesByPeriod = collect($report->getPageUrls(['expanded' => $expanded_stats])) + ->map(function ($item, $date) use ($period, $fluidbook_id, $hash, $eventsByPeriod, $new_stats_cutoff) { if (empty($item)) { return $item; // Some periods might have no data @@ -427,13 +445,15 @@ trait StatsOperation // the returned data flatter and easier to process later for sums etc. $data = $labelled['page']; - // Unique visitors stats disabled - see notes above. - // // Matomo doesn't provide an aggregate of unique visitors, so we must do the sum ourselves. - // // If the period is "day", the number of unique visitors will be in the key 'nb_uniq_visitors' - // // but if it is a longer period (week, month, year) then the key becomes 'sum_daily_nb_uniq_visitors' - // $unique_visitors_key = $period === 'day' ? 'nb_uniq_visitors' : 'sum_daily_nb_uniq_visitors'; - // $subpages = collect($data['subtable'] ?? []); - // $data['nb_uniq_visitors'] = $subpages->sum($unique_visitors_key); + //== Unique Visitors + // Matomo doesn't provide an aggregate of unique visitors, so we must do the sum ourselves. + // If the period is "day", the number of unique visitors will be in the key 'nb_uniq_visitors' + // but if it is a longer period (week, month, year) then the key becomes 'sum_daily_nb_uniq_visitors' + if($fluidbook_id >= $new_stats_cutoff) { + $unique_visitors_key = $period === 'day' ? 'nb_uniq_visitors' : 'sum_daily_nb_uniq_visitors'; + $subpages = collect($data['subtable'] ?? []); + $data['nb_uniq_visitors'] = $subpages->sum($unique_visitors_key); + } $data['raw_date'] = $date; // We still need the raw date for sorting and other formatting purposes @@ -465,6 +485,13 @@ trait StatsOperation return $data; }); + //=== SUMMARY BY PERIOD + // Generate a list of available periods, based on the visits API data. + // If there are no visits for a certain period, we can assume that there won't be any data for other metrics. + // The goal is to find the non-empty results and create a list of Carbon date classes from those + // This list is used to generate the links / formatted dates list + detailed data table + $period_details = $pagesByPeriod->filter(fn ($value, $key) => !empty($value)); // Remove any empty periods + //=== CHART PREPARATION // Format dates for display as labels on the x-axis $labels = $pagesByPeriod->keys()->map(function ($label, $index) use ($period, $start_date, $end_date) { @@ -478,19 +505,53 @@ trait StatsOperation $tooltip_labels = array_combine($labels, $formatted_dates); - //=== SUMMARY BY PERIOD - // Generate a list of available periods, based on the visits API data. - // If there are no visits for a certain period, we can assume that there won't be any data for other metrics. - // The goal is to find the non-empty results and create a list of Carbon date classes from those - // This list is used to generate the links / formatted dates list + detailed data table - $period_details = $pagesByPeriod->filter(fn ($value, $key) => !empty($value)); // Remove any empty periods + $chart_datasets = [ + [ + 'label' => __('Visits'), + 'backgroundColor' => 'hsl(72 100% 38% / 100%)', // Could make bars semi-transparent if desired + // borderColor: 'hsl(72 100% 38%)', + // borderWidth: 1, + 'data' => $pagesByPeriod->pluck('nb_visits')->toArray(), + 'order' => 1, + 'bar_offset' => -5, // Negative offset shifts bar to left + ], + [ + 'label' => __('Page Views'), + 'backgroundColor' => 'hsl(0 0% 53%)', + 'data' => $pagesByPeriod->pluck('nb_hits')->toArray(), + 'order' => 2, + 'bar_offset' => 5, // Positive offset shifts bar to right + ], + ]; + + if ($fluidbook_id >= $new_stats_cutoff) { + // Insert the unique visitors dataset at the beginning of the array + $chart_datasets = array_merge([ + [ + 'label' => __('Unique Visitors'), + 'backgroundColor' => 'hsl(19 100% 48% / 100%)', + 'data' => $pagesByPeriod->pluck('nb_uniq_visitors')->toArray(), + 'order' => 1, + 'bar_offset' => -8, // Negative offset shifts bar to left + ] + ], $chart_datasets); + + // Now that we have 3 bars, we need to update the order and bar offsets for the visits and page views + $chart_datasets[1]['order'] = 2; + $chart_datasets[1]['bar_offset'] = 0; // This will be the middle bar, so no offset + + $chart_datasets[2]['order'] = 3; + $chart_datasets[2]['bar_offset'] = 8; + } + + $chart_datasets = json_encode($chart_datasets, JSON_UNESCAPED_SLASHES); // Map of API data to table headings (used to display summaries under the chart) $table_map = [ // Main summary table 'summary' => [ 'formatted_date' => $period_map[$period], - // 'nb_uniq_visitors' => __('Unique Visitors'), + 'nb_uniq_visitors' => __('Unique Visitors'), 'nb_visits' => __('Visits'), 'nb_hits' => __('Pages Viewed'), 'nb_links' => __('Outgoing Links'), @@ -501,7 +562,7 @@ trait StatsOperation // Per-page detail table 'per-page' => [ 'page_group' => __('Pages'), - // 'nb_unique_visits' => __('Unique Visits'), + 'nb_uniq_visitors' => __('Unique Visits'), 'nb_visits' => __('Visits'), 'nb_pageviews' => __('Views'), 'nb_zooms' => __('Zooms'), @@ -510,6 +571,12 @@ trait StatsOperation ], ]; + // Older Fluidbooks can't show unique visitors (see notes above) + if ($fluidbook_id < $new_stats_cutoff) { + unset($table_map['summary']['nb_uniq_visitors']); + unset($table_map['per-page']['nb_uniq_visitors']); + } + $formatter = NumberFormatter::create($locale, NumberFormatter::DEFAULT_STYLE); return view('fluidbook_stats.summary', @@ -526,6 +593,7 @@ trait StatsOperation 'tooltip_labels', 'formatted_date_range', 'chart_heading', + 'chart_datasets', 'searches', 'outlinks', 'countries', @@ -536,7 +604,6 @@ trait StatsOperation 'period_map', 'table_map', 'pages', - 'pagesByPeriod', 'formatter', 'locale', 'base_URL', @@ -587,9 +654,9 @@ trait StatsOperation if ($crosses_year_boundary) { $week_formatted = $week_start->isoFormat('Do MMMM, YYYY') .' — '. $week_end->isoFormat('Do MMMM, YYYY'); } elseif ($crosses_month_boundary) { - $week_formatted = $week_start->isoFormat('Do MMMM') .' — '. $week_end->isoFormat('Do MMMM'); + $week_formatted = $week_start->isoFormat('Do MMMM') .' — '. $week_end->isoFormat('Do MMMM, YYYY'); } else { - $week_formatted = $week_start->isoFormat('Do') .'—'. $week_end->isoFormat('Do MMMM'); + $week_formatted = $week_start->isoFormat('Do') .'—'. $week_end->isoFormat('Do MMMM, YYYY'); } } diff --git a/resources/views/fluidbook_stats/loader.blade.php b/resources/views/fluidbook_stats/loader.blade.php new file mode 100644 index 000000000..f90ee6386 --- /dev/null +++ b/resources/views/fluidbook_stats/loader.blade.php @@ -0,0 +1,244 @@ +@extends(backpack_view('blank')) + +@section('after_styles') + + +@endsection + +@section('content') +
Loading...
+
+ +@endsection + +@push('after_scripts') + {{-- Fetch the report and inject it into the page when ready... This is to improve the UX --}} + + + {{-- Include the base libraries for the chart and the date picker here so that they're ready + when the report HTML and extra scripts are injected --}} + + {{-- Charting library --}} + + + {{-- Date Range picker: https://sensortower.github.io/daterangepicker/docs --}} + + + +@endpush diff --git a/resources/views/fluidbook_stats/summary.blade.php b/resources/views/fluidbook_stats/summary.blade.php index d0176578f..0deba1d52 100644 --- a/resources/views/fluidbook_stats/summary.blade.php +++ b/resources/views/fluidbook_stats/summary.blade.php @@ -1,519 +1,346 @@ -@extends(backpack_view('blank')) - -@section('after_styles') - - -@endsection - -@section('content') -

- {{ __('Statistics') }} - {{ $fluidbook->name }} -

- - - - - {!! $formatted_date_range !!} - @if($report_timespan) - ({{ $report_timespan }}) - @endif - +{{-- Statistics Report --}} +{{-- This doesn't extend any templates because it is fetched from loader.blade.php and injected via JS --}} + +

+ {{ __('Statistics') }} + {{ $fluidbook->name }} +

+ + + + + {!! $formatted_date_range !!} + @if($report_timespan) + ({{ $report_timespan }}) + @endif + + +
+ @foreach($period_map as $period_key => $period_title) + @if($period_key === $period) + {{ $period_title }} + @else + + {{ $period_title }} + + @endif + @endforeach +
+ + +
+
{{ __('Fluidbook Name') }}
+
{{ $fluidbook->name }}
+ +
{{ __('Creation Date') }}
+
+ {{ $fluidbook->created_at->isoFormat('dddd, Do MMMM YYYY') }} + + ({{ $fluidbook->created_at->diffForHumans([ + 'parts' => 2, // How many levels of detail to go into (ie. years, months, days) + 'join' => true, // Join string with natural language separators for the locale + ]) + }}) + +
-
- @foreach($period_map as $period_key => $period_title) - @if($period_key === $period) - {{ $period_title }} - @else - - {{ $period_title }} - - @endif - @endforeach -
- +
{{ __('Page Count') }}
+
{{ $page_count }}
-
-
{{ __('Fluidbook Name') }}
-
{{ $fluidbook->name }}
- -
{{ __('Creation Date') }}
-
- {{ $fluidbook->created_at->isoFormat('dddd, Do MMMM YYYY') }} - - ({{ $fluidbook->created_at->diffForHumans([ - 'parts' => 2, // How many levels of detail to go into (ie. years, months, days) - 'join' => true, // Join string with natural language separators for the locale - ]) - }}) - -
- -
{{ __('Page Count') }}
-
{{ $page_count }}
- - @if($period_details->isNotEmpty()) - @foreach ($table_map['summary'] as $summary_key => $summary_heading) - @php - if ($summary_key === 'formatted_date') continue; - @endphp -
{{ sprintf(__('Total %s'), $summary_heading) }}
-
{{ $formatter->format($period_details->sum($summary_key)) }}
- @endforeach + @if($period_details->isNotEmpty()) + @foreach ($table_map['summary'] as $summary_key => $summary_heading) + @php + if ($summary_key === 'formatted_date') continue; + @endphp +
{{ sprintf(__('Total %s'), $summary_heading) }}
+
{{ $formatter->format($period_details->sum($summary_key)) }}
+ @endforeach -
{{ __('Total Searches') }}
-
{{ $formatter->format($searches->sum()) }}
- @endif -
+
{{ __('Total Searches') }}
+
{{ $formatter->format($searches->sum()) }}
+ @endif +
- @if($period_details->isNotEmpty()) +@if($period_details->isNotEmpty()) -

{!! $chart_heading !!}

+

{!! $chart_heading !!}

- {{-- Chart --}} - + {{-- Chart --}} + - {{-- Stats for each period entry (year, month or day) --}} - - + {{-- Stats for each period entry (year, month or day) --}} +
+ + + @foreach ($table_map['summary'] as $summary_heading_key => $summary_heading) + + @endforeach + + + + @foreach($period_details as $date_key => $period_data) - @foreach ($table_map['summary'] as $summary_heading_key => $summary_heading) - + @foreach (array_keys($table_map['summary']) as $summary_key) + @endforeach - - - @foreach($period_details as $date_key => $period_data) - - @foreach (array_keys($table_map['summary']) as $summary_key) - - @endforeach - + @endforeach + +
+ {{ $summary_heading }} +
- {{ $summary_heading }} - + {!! is_int($period_data[$summary_key]) ? $formatter->format($period_data[$summary_key]) : $period_data[$summary_key] !!} +
- {!! is_int($period_data[$summary_key]) ? $formatter->format($period_data[$summary_key]) : $period_data[$summary_key] !!} -
+ + {{-- Stats segregated by page number --}} +

{{ __('Details by page') }} ({!! $formatted_date_range !!})

+ + + + + @foreach ($table_map['per-page'] as $page_data_heading_key => $page_data_heading) + {{-- In the case of the "page_group" data, we want it to be sorted in ascending order by default, even though it's a numeric column --}} + @endforeach - -
+ {{ $page_data_heading }} +
- - {{-- Stats segregated by page number --}} -

{{ __('Details by page') }} ({!! $formatted_date_range !!})

- - - + + + + @foreach($pages as $page_group => $page_data) - @foreach ($table_map['per-page'] as $page_data_heading_key => $page_data_heading) - {{-- In the case of the "page_group" data, we want it to be sorted in ascending order by default, even though it's a numeric column --}} - + @foreach (array_keys($table_map['per-page']) as $summary_key) + @endforeach - - - @foreach($pages as $page_group => $page_data) - - @foreach (array_keys($table_map['per-page']) as $summary_key) - - @endforeach - - @endforeach - -
- {{ $page_data_heading }} - + {!! is_int($page_data[$summary_key]) ? $formatter->format($page_data[$summary_key]) : $page_data[$summary_key] !!} +
- {!! is_int($page_data[$summary_key]) ? $formatter->format($page_data[$summary_key]) : $page_data[$summary_key] !!} -
+ @endforeach + + - {{-- Additional stats tables organised into columns (outgoing links, search keywords and countries) --}} - {{-- Sometimes there are no stats for certain categories, so the number of columns adapts accordingly --}} -
+ {{-- Additional stats tables organised into columns (outgoing links, search keywords and countries) --}} + {{-- Sometimes there are no stats for certain categories, so the number of columns adapts accordingly --}} +
- {{-- Outgoing Links --}} - @if($outlinks->isNotEmpty()) -
-

{{ __('Outgoing Links') }}

+ {{-- Outgoing Links --}} + @if($outlinks->isNotEmpty()) +
+

{{ __('Outgoing Links') }}

- - +
+ + + + + + + + @foreach($outlinks as $link) - - + + - - - @foreach($outlinks as $link) - - - - - @endforeach - -
{{ __('URL') }}{{ __('Clicks') }}
{{ __('URL') }}{{ __('Clicks') }} + {{ $link['label'] }} + {{ $formatter->format($link['nb_events']) }}
- {{ $link['label'] }} - {{ $formatter->format($link['nb_events']) }}
-
- @endif - - {{-- Search Keywords --}} - @if($searches->isNotEmpty()) -
-

{{ __('Search Keywords') }}

- - - + @endforeach + +
+
+ @endif + + {{-- Search Keywords --}} + @if($searches->isNotEmpty()) +
+

{{ __('Search Keywords') }}

+ + + + + + + + + + @foreach($searches as $search_query => $search_count) - - + + - - - @foreach($searches as $search_query => $search_count) - - - - - @endforeach - -
{{ __('Query') }}{{ __('Searches') }}
{{ __('Query') }}{{ __('Searches') }}{{ $search_query }}{{ $formatter->format($search_count) }}
{{ $search_query }}{{ $formatter->format($search_count) }}
-
- @endif - - {{-- Visitor Countries --}} - @if($countries->isNotEmpty()) -
-

{{ __('Origin of Visitors') }}

- - - + @endforeach + +
+
+ @endif + + {{-- Visitor Countries --}} + @if($countries->isNotEmpty()) +
+

{{ __('Origin of Visitors') }}

+ + + + + + + + + + @foreach($countries as $country) - - + + - - - @foreach($countries as $country) - - - - - @endforeach - -
{{ __('Country') }}{{ __('Visits') }}
{{ __('Country') }}{{ __('Visits') }} + {{ $country['label'] }} + {{ $country['label'] }} + {{ $formatter->format($country['nb_visits']) }}
- {{ $country['label'] }} - {{ $country['label'] }} - {{ $formatter->format($country['nb_visits']) }}
-
- @endif - -
- - - {{-- It's possible for there to be no statistics returned by the API --}} - @else -
- ⚠️ - {{ __('Sorry, no statistics were found for this period.') }} -
+ @endforeach + + +
+ @endif + +
+ + +{{-- It's possible for there to be no statistics returned by the API --}} +@else +
+ ⚠️ + {{ __('Sorry, no statistics were found for this period.') }} +
+@endif + + +{{--================== SCRIPTS ==================--}} + +{{-- Date Range picker setup: https://sensortower.github.io/daterangepicker/docs --}} + - - - - - {{--============================================================================================================--}} - - {{-- Charting library --}} - - - - dataPoint.x = chart.scales.x.getPixelForValue(index) + offset; - }); - }); - } - }; - - //=== Chart Configuration - const config = { - type: 'bar', - data: data, - options: { - // The best way to make the chart responsive is to set the aspect ratio without setting any sizes on - // the canvas element itself. This way, Chart JS will manage the size, and it will behave much like a - // responsive image - it will take the available width, scaling the height proportionally. - // The aspectRatio value is a measure of the width compared to the height. In other words: - // aspectRatio: 3 = 3x the width compared to height - // aspectRatio: 1 = square - // and an aspectRatio less than 1 will result in a canvas that is taller than it is wide. - aspectRatio: 3, - responsive: true, // Default for Chart JS but defining it explicitly here for clarity - maintainAspectRatio: true, // As above, also a library default - maxBarThickness: 100, // Prevent bars being ridiculously wide when there isn't much data - scales: { - x: { stacked: true }, - y: { stacked: false }, // Don't stack y-axis: prevents datasets from adding - }, - plugins: { - tooltip: { - mode: 'index', - position: 'nearest', - callbacks: { - title: function(context) { - return tooltip_labels[context[0].label]; - } - }, +{{--============================================================================================================--}} + +{{-- Charting library --}} + + //=== Render Chart + const statsChart = new Chart( + document.getElementById('stats_chart'), + config + ); - {{--============================================================================================================--}} + - {{-- Simple Table Sorter --}} - - {{-- This script works on any tables with the "sortable" class. There's no extra setup needed here. --}} +{{--============================================================================================================--}} -@endpush +{{-- Simple Table Sorter --}} + +{{-- This script works on any tables with the "sortable" class. There's no extra setup needed here. --}}