Fixing stats page, replacing ob/highcharts-bundle with own implementation based on ghunti/highcharts-php. Some refactoring.
This commit is contained in:
parent
3a69565ecb
commit
4c33ce9d84
|
@ -13,6 +13,7 @@
|
|||
"doctrine/doctrine-fixtures-bundle": "^3.4",
|
||||
"doctrine/doctrine-migrations-bundle": "^3.2",
|
||||
"doctrine/orm": "^2.14",
|
||||
"ghunti/highcharts-php": "^5.0",
|
||||
"jms/serializer-bundle": "^5.2",
|
||||
"knplabs/knp-paginator-bundle": "^6.2",
|
||||
"league/commonmark": "^2.4",
|
||||
|
|
56
composer.lock
generated
56
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6088d0629768085da296aa5598445c4f",
|
||||
"content-hash": "274812d219a3d19c925c0e67b5f86926",
|
||||
"packages": [
|
||||
{
|
||||
"name": "dflydev/dot-access-data",
|
||||
|
@ -1635,6 +1635,60 @@
|
|||
},
|
||||
"time": "2022-05-23T21:33:49+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ghunti/highcharts-php",
|
||||
"version": "v5.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ghunti/HighchartsPHP.git",
|
||||
"reference": "7bccba4278fcc5d3a50cf6aa35975b0943a03cad"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/ghunti/HighchartsPHP/zipball/7bccba4278fcc5d3a50cf6aa35975b0943a03cad",
|
||||
"reference": "7bccba4278fcc5d3a50cf6aa35975b0943a03cad",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^9.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Ghunti\\HighchartsPHP\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"GPL-3.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Gonçalo Queirós",
|
||||
"email": "mail@goncaloqueiros.net",
|
||||
"homepage": "http://goncaloqueiros.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A php wrapper for highcharts and highstock javascript libraries",
|
||||
"homepage": "https://goncaloqueiros.net/highcharts.php",
|
||||
"keywords": [
|
||||
"charts",
|
||||
"highcharts",
|
||||
"highstock",
|
||||
"javascript",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"email": "mail@goncaloqueiros.net",
|
||||
"issues": "https://github.com/ghunti/HighchartsPHP/issues",
|
||||
"source": "https://github.com/ghunti/HighchartsPHP"
|
||||
},
|
||||
"time": "2023-04-26T21:37:31+00:00"
|
||||
},
|
||||
{
|
||||
"name": "jms/metadata",
|
||||
"version": "2.8.0",
|
||||
|
|
|
@ -26,7 +26,7 @@ user_show:
|
|||
|
||||
statistics:
|
||||
path: /statistics
|
||||
defaults: { _controller: App\Controller\UserController::top }
|
||||
defaults: { _controller: App\Controller\StatsController::show }
|
||||
methods: [GET]
|
||||
|
||||
events_last:
|
||||
|
|
|
@ -89,7 +89,7 @@ class UserRepositoryTest extends KernelTestCase
|
|||
|
||||
public function testGetTopUsers()
|
||||
{
|
||||
$topUsers = $this->userRepo->getTopUsers();
|
||||
$topUsers = $this->userRepo->getTopUsersBySubscribersCount();
|
||||
|
||||
$this->assertCount(3, $topUsers, 'Found not exactly 3 top users');
|
||||
|
||||
|
|
78
src/Chart/ChartGenerator.php
Normal file
78
src/Chart/ChartGenerator.php
Normal file
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Chart;
|
||||
|
||||
use App\DTO\DailyEventsDTO;
|
||||
use App\DTO\TopUserDTO;
|
||||
use Ghunti\HighchartsPHP\Highchart;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ChartGenerator
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DailyEventsDTO[] $events
|
||||
*/
|
||||
public function eventsDynamicChart(array $events): Highchart
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($events as $event) {
|
||||
$data[$event->date->format('d.m')] = $event->eventsCount;
|
||||
}
|
||||
|
||||
return $this->createChart('line', $data, 'Events by day', 'amount');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TopUserDTO[] $topUsers
|
||||
*/
|
||||
public function topUsersChart(array $topUsers): Highchart
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($topUsers as $topUser) {
|
||||
$data[$topUser->login] = $topUser->subscribersCount;
|
||||
}
|
||||
|
||||
return $this->createChart('bar', $data, 'Top users', 'amount');
|
||||
}
|
||||
|
||||
/** @see https://github.com/ghunti/HighchartsPHP/blob/master/demos/highcharts/line/basic_line.php */
|
||||
private function createChart(
|
||||
string $type,
|
||||
array $data,
|
||||
string $bottomLabel,
|
||||
string $amountLabel,
|
||||
): Highchart {
|
||||
$c = new Highchart();
|
||||
|
||||
$c->chart->type = $type;
|
||||
|
||||
$c->title->text = $this->translator->trans($bottomLabel);
|
||||
|
||||
// Preparing chart data
|
||||
foreach ($data as $key => $value) {
|
||||
$chartData['keys'][] = $key;
|
||||
$chartData['values'][] = $value;
|
||||
}
|
||||
|
||||
$c->xAxis->title = ['text' => null];
|
||||
$c->xAxis->categories = $chartData['keys'] ?? [];
|
||||
|
||||
$c->yAxis->title->text = $this->translator->trans($amountLabel);
|
||||
$c->yAxis->plotOptions->bar->dataLabels->enabled = true;
|
||||
|
||||
$c->series[] = [
|
||||
'name' => $this->translator->trans($amountLabel),
|
||||
'data' => $chartData['values'] ?? [],
|
||||
];
|
||||
|
||||
return $c;
|
||||
}
|
||||
}
|
26
src/Controller/StatsController.php
Normal file
26
src/Controller/StatsController.php
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Chart\ChartGenerator;
|
||||
use App\Repository\{SubscriptionEventRepository, UserRepository};
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class StatsController extends AbstractController
|
||||
{
|
||||
public function show(
|
||||
UserRepository $userRepository,
|
||||
SubscriptionEventRepository $subscriptionEventRepository,
|
||||
ChartGenerator $chartGenerator,
|
||||
): Response {
|
||||
$topUsers = $userRepository->getTopUsersBySubscribersCount();
|
||||
$eventsByDay = $subscriptionEventRepository->getLastEventsByDay();
|
||||
|
||||
return $this->render('Web/User/top.html.twig', [
|
||||
'events_dynamic_chat' => $chartGenerator->eventsDynamicChart($eventsByDay),
|
||||
'top_chart' => $chartGenerator->topUsersChart($topUsers),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -3,29 +3,21 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\DTO\{TopUserDTO, DailyEventsDTO};
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Ob\HighchartsBundle\Highcharts\Highchart;
|
||||
use App\Entity\User;
|
||||
use App\Repository\{SubscriptionEventRepository, UserRenameEventRepository, UserRepository};
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\{Request, Response};
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class UserController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function show(
|
||||
Request $request,
|
||||
string $login,
|
||||
SubscriptionEventRepository $subscriptionEventRepository,
|
||||
UserRepository $userRepository,
|
||||
UserRenameEventRepository $renameEventRepository,
|
||||
PaginatorInterface $paginator
|
||||
PaginatorInterface $paginator,
|
||||
): Response {
|
||||
/** @var User $user */
|
||||
$user = $userRepository->findUserByLogin($login);
|
||||
|
@ -37,7 +29,7 @@ class UserController extends AbstractController
|
|||
$subscriberEventsPagination = $paginator->paginate(
|
||||
$subscriptionEventRepository->createUserLastSubscribersEventsQuery($user),
|
||||
$request->query->getInt('page', 1),
|
||||
10
|
||||
10,
|
||||
);
|
||||
|
||||
return $this->render('Web/User/show.html.twig', [
|
||||
|
@ -47,84 +39,4 @@ class UserController extends AbstractController
|
|||
'rename_log' => $renameEventRepository->findBy(['user' => $user], ['date' => 'DESC'], 10),
|
||||
]);
|
||||
}
|
||||
|
||||
public function top(UserRepository $userRepository, SubscriptionEventRepository $subscriptionEventRepository): Response
|
||||
{
|
||||
$topUsers = $userRepository->getTopUsers();
|
||||
$eventsByDay = $subscriptionEventRepository->getLastEventsByDay();
|
||||
|
||||
return $this->render('Web/User/top.html.twig', [
|
||||
'events_dynamic_chat' => $this->createEventsDynamicChart($eventsByDay),
|
||||
'top_chart' => $this->createTopUsersGraph($topUsers),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DailyEventsDTO[] $eventsByDay
|
||||
*@todo move to the service
|
||||
*
|
||||
*/
|
||||
private function createEventsDynamicChart(array $eventsByDay = []): Highchart
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($eventsByDay as $dailyEvents) {
|
||||
$data[$dailyEvents->date->format('d.m')] = $dailyEvents->eventsCount;
|
||||
}
|
||||
|
||||
return $this->createChart('eventschart', 'line', $data, 'Events by day', 'amount');
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo move to the service
|
||||
*
|
||||
* @param TopUserDTO[] $topUsers
|
||||
*/
|
||||
private function createTopUsersGraph(array $topUsers = []): Highchart
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($topUsers as $topUser) {
|
||||
$data[$topUser->login] = $topUser->subscribersCount;
|
||||
}
|
||||
|
||||
return $this->createChart('topchart', 'bar', $data, 'Top users', 'amount');
|
||||
}
|
||||
|
||||
private function createChart(string $blockId, string $type, array $data, string $bottomLabel, string $amountLabel): Highchart
|
||||
{
|
||||
$chartData = [
|
||||
'keys' => [],
|
||||
'values' => [],
|
||||
];
|
||||
|
||||
// Preparing chart data
|
||||
foreach ($data as $key => $value) {
|
||||
$chartData['keys'][] = $key;
|
||||
$chartData['values'][] = $value;
|
||||
}
|
||||
|
||||
// Chart
|
||||
$series = [[
|
||||
'name' => $this->translator->trans($amountLabel),
|
||||
'data' => $chartData['values'],
|
||||
]];
|
||||
|
||||
// Initializing chart
|
||||
$ob = new Highchart();
|
||||
$ob->chart->renderTo($blockId);
|
||||
$ob->chart->type($type);
|
||||
$ob->title->text($this->translator->trans($bottomLabel));
|
||||
$ob->xAxis->title(['text' => null]);
|
||||
$ob->xAxis->categories($chartData['keys']);
|
||||
$ob->yAxis->title(['text' => $this->translator->trans($amountLabel)]);
|
||||
$ob->plotOptions->bar([
|
||||
'dataLabels' => [
|
||||
'enabled' => true
|
||||
]
|
||||
]);
|
||||
$ob->series($series);
|
||||
|
||||
return $ob;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,7 @@ declare(strict_types=1);
|
|||
namespace App\Repository;
|
||||
|
||||
use App\DTO\DailyEventsDTO;
|
||||
use App\Entity\SubscriptionEvent;
|
||||
use App\Entity\User;
|
||||
use App\Entity\{SubscriptionEvent, User};
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
@ -63,11 +62,7 @@ class SubscriptionEventRepository extends ServiceEntityRepository
|
|||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last user subscriber events
|
||||
*
|
||||
* @return SubscriptionEvent[]
|
||||
*/
|
||||
/** @return list<SubscriptionEvent> */
|
||||
public function getUserLastSubscribersEvents(User $user, int $limit = 20): array
|
||||
{
|
||||
$qb = $this->createUserLastSubscribersEventsQuery($user);
|
||||
|
@ -89,11 +84,7 @@ class SubscriptionEventRepository extends ServiceEntityRepository
|
|||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last global subscription events
|
||||
*
|
||||
* @return SubscriptionEvent[]
|
||||
*/
|
||||
/** @return SubscriptionEvent[] */
|
||||
public function getLastSubscriptionEvents(int $limit = 20): array
|
||||
{
|
||||
$qb = $this->createLastSubscriptionEventsQuery();
|
||||
|
|
|
@ -4,8 +4,7 @@ declare(strict_types=1);
|
|||
namespace App\Repository;
|
||||
|
||||
use App\DTO\TopUserDTO;
|
||||
use App\Entity\Subscription;
|
||||
use App\Entity\User;
|
||||
use App\Entity\{Subscription, User};
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
|
@ -108,12 +107,8 @@ class UserRepository extends ServiceEntityRepository
|
|||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns top users by subscribers count
|
||||
*
|
||||
* @return TopUserDTO[]
|
||||
*/
|
||||
public function getTopUsers(int $limit = 30): array
|
||||
/** @return list<TopUserDTO> */
|
||||
public function getTopUsersBySubscribersCount(int $limit = 30): array
|
||||
{
|
||||
$qb = $this->getEntityManager()->getRepository(Subscription::class)->createQueryBuilder('s');
|
||||
|
||||
|
|
46
src/Twig/HighchartExtension.php
Normal file
46
src/Twig/HighchartExtension.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use Ghunti\HighchartsPHP\Highchart;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\{TwigFilter, TwigFunction};
|
||||
|
||||
class HighchartExtension extends AbstractExtension
|
||||
{
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
new TwigFilter('hc_scripts', [$this, 'printScripts'], ['is_safe' => ['html']]),
|
||||
new TwigFilter('hc_render', [$this, 'render'], ['is_safe' => ['html']]),
|
||||
new TwigFilter('hc_options', [$this, 'renderOptions'], ['is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
new TwigFunction('hc_scripts', [$this, 'printScripts'], ['is_safe' => ['html']]),
|
||||
new TwigFunction('hc_render', [$this, 'render'], ['is_safe' => ['html']]),
|
||||
new TwigFunction('hc_options', [$this, 'renderOptions'], ['is_safe' => ['html']]),
|
||||
];
|
||||
}
|
||||
|
||||
public function printScripts(Highchart $c): string
|
||||
{
|
||||
return $c->printScripts(true);
|
||||
}
|
||||
|
||||
public function render(Highchart $c, string $blockId, ?string $varName = null, bool $withScriptTag = false): string
|
||||
{
|
||||
$c->chart->renderTo = $blockId;
|
||||
|
||||
return $c->render($varName, withScriptTag: $withScriptTag);
|
||||
}
|
||||
|
||||
public function renderOptions(Highchart $c): string
|
||||
{
|
||||
return $c->renderOptions();
|
||||
}
|
||||
}
|
|
@ -9,14 +9,13 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<script type="text/javascript">
|
||||
// Top chart
|
||||
{{ chart(top_chart) }}
|
||||
// Events by day chart
|
||||
{{ chart(events_dynamic_chat) }}
|
||||
</script>
|
||||
|
||||
<div id="topchart" style="min-width: 400px; height: 600px; margin: 0 auto;"></div>
|
||||
|
||||
<div id="eventschart" style="min-width: 400px; height: 600px; margin: 0 auto;"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
{{ top_chart | hc_render('topchart') }}
|
||||
{{ events_dynamic_chat | hc_render('eventschart') }}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in a new issue