From f74f931562432f79ac6fed7fdac8bb587da2a99e Mon Sep 17 00:00:00 2001 From: Alex Rock Ancelet Date: Sun, 2 Feb 2020 00:37:25 +0100 Subject: [PATCH] Wow, I did my first chart and it works! --- README.md | 8 +- assets/js/app.js | 40 +------- config/packages/easy_admin.yaml | 7 +- src/Controller/AnalyticsController.php | 52 ++++++++++ src/Controller/OperationChartController.php | 58 ----------- src/Highcharts/Chart/AbstractChart.php | 26 +++++ src/Highcharts/Chart/ChartInterface.php | 29 ++++++ src/Highcharts/Chart/TagUsageChart.php | 98 +++++++++++++++++++ src/Repository/OperationRepository.php | 16 +++ src/Twig/SlugifyExtension.php | 44 +++++++++ templates/analytics.html.twig | 20 ++++ .../includes/_select2_widget.html.twig | 4 - .../EasyAdminBundle/default/list.html.twig | 6 -- 13 files changed, 296 insertions(+), 112 deletions(-) create mode 100644 src/Controller/AnalyticsController.php delete mode 100644 src/Controller/OperationChartController.php create mode 100644 src/Highcharts/Chart/AbstractChart.php create mode 100644 src/Highcharts/Chart/ChartInterface.php create mode 100644 src/Highcharts/Chart/TagUsageChart.php create mode 100644 src/Twig/SlugifyExtension.php create mode 100644 templates/analytics.html.twig delete mode 100644 templates/bundles/EasyAdminBundle/default/list.html.twig diff --git a/README.md b/README.md index c366e05..dddc779 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ ADMIN_PASSWORD='$argon2id$v=19$m=65536,t=4,p=1$N0R4Zi5hUWQ3QXB0bjVGdg$VsVcHzGRfG Feel free to contribute 😉. * Make many analytics dashboards (that's what this app is for in the first place, probably with Highcharts). +* Support JS closures in Chart objects (by using a placeholder to remove quotes maybe?). * Add a lot of fixtures to play with. * Add translations for tags (maybe using an extension like gedmo or knp?). * Implement more source file types like xls, ods, etc., that could be transformed to CSV before importing them. [PHPSpreadsheet](https://phpspreadsheet.readthedocs.io/) is already installed, though not used yet. @@ -97,9 +98,10 @@ Feel free to contribute 😉. * Operation tags (insurance, internet provider, car loan, etc.). Multiple tags per operation. * Demo app at https://piers.ovh/compotes/ with credentials `admin`/`admin` and database reset every day. -* Added default tags (in French only for now) -* Docker setup with Compose -* Added tons of other commands to the Makefile +* Added default tags (in French only for now). +* Docker setup with Compose. +* Added tons of other commands to the Makefile. +* Made a first PoC for the analytics dashboard. # License diff --git a/assets/js/app.js b/assets/js/app.js index 617cadd..b1df6ec 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,42 +1,6 @@ import '../css/app.css'; import * as Highcharts from "highcharts"; -// import $ from 'jquery'; +require('../css/app.css'); -console.info('App startup'); - -(async function () { - - const chartsContainer = document.getElementById('charts'); - - if (chartsContainer) { - try { - const filtersUrl = chartsContainer.getAttribute('data-filters-url'); - - const url = new URL(filtersUrl); - url.searchParams.append('filters', '...'); - - const result = await fetch(url); - - if (result.status !== 200) { - throw new Error('Filters request returned an error.'); - } - - const json = await result.json(); - - console.info('Filters fetch result: ', json); - - if (!json.highcharts) { - throw new Error('Highcharts field is not defined in response.'); - } - - const myChart = Highcharts.chart('charts', json.highcharts); - - console.info('Chart: ', myChart); - } catch (e) { - console.error('Cannot fetch filters: '+e.message); - } - - } - -})(); +global.Highcharts = Highcharts; diff --git a/config/packages/easy_admin.yaml b/config/packages/easy_admin.yaml index 9a144fc..124118a 100644 --- a/config/packages/easy_admin.yaml +++ b/config/packages/easy_admin.yaml @@ -8,11 +8,12 @@ easy_admin: - { icon: sync, label: "Sync operations", route: apply_rules } - label: '' - - { icon: ruler-vertical, entity: TagRule } - - { icon: tags, entity: Tag } + - { icon: money-check, entity: Operation } + - { icon: chart-bar, label: "Analytics", route: analytics } - label: '' - - { icon: money-check, entity: Operation } + - { icon: ruler-vertical, entity: TagRule } + - { icon: tags, entity: Tag } entities: TagRule: diff --git a/src/Controller/AnalyticsController.php b/src/Controller/AnalyticsController.php new file mode 100644 index 0000000..49bfbb5 --- /dev/null +++ b/src/Controller/AnalyticsController.php @@ -0,0 +1,52 @@ +. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Controller; + +use App\Highcharts\Chart\TagUsageChart; +use App\Repository\OperationRepository; +use App\Repository\TagRepository; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Twig\Environment; + +class AnalyticsController +{ + private Environment $twig; + private TagRepository $tagRepository; + private OperationRepository $operationRepository; + + public function __construct( + Environment $twig, + TagRepository $tagRepository, + OperationRepository $operationRepository + ) { + $this->twig = $twig; + $this->tagRepository = $tagRepository; + $this->operationRepository = $operationRepository; + } + + /** + * @Route("/admin/analytics", name="analytics") + */ + public function analytics(): Response + { + $operations = $this->operationRepository->findWithTags(); + + return new Response($this->twig->render('analytics.html.twig', [ + 'charts' => [ + TagUsageChart::create($operations), + ], + ])); + } +} diff --git a/src/Controller/OperationChartController.php b/src/Controller/OperationChartController.php deleted file mode 100644 index 3562b66..0000000 --- a/src/Controller/OperationChartController.php +++ /dev/null @@ -1,58 +0,0 @@ -. - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace App\Controller; - -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Annotation\Route; - -class OperationChartController -{ - /** - * @Route("/admin/operation-chart", name="operation_chart") - */ - public function chart(Request $request) - { - $filters = $request->query->get('filters', []); - - return new JsonResponse([ - 'filters' => $filters, - 'highcharts' => [ - 'chart' => ['type' => 'bar'], - 'title' => ['text' => 'Fruit Consumption'], - 'xAxis' => [ - 'categories' => [ - 'Apples', - 'Bananas', - 'Oranges', - ], - ], - 'yAxis' => [ - 'title' => ['text' => 'Fruit eaten'], - ], - 'series' => [ - [ - 'name' => 'Jane', - 'data' => [1, 0, 4], - ], - - [ - 'name' => 'John', - 'data' => [5, 7, 3], - ], - ], - ], - ]); - } -} diff --git a/src/Highcharts/Chart/AbstractChart.php b/src/Highcharts/Chart/AbstractChart.php new file mode 100644 index 0000000..a29d00d --- /dev/null +++ b/src/Highcharts/Chart/AbstractChart.php @@ -0,0 +1,26 @@ +. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Highcharts\Chart; + +abstract class AbstractChart implements ChartInterface +{ + public function getConfig(): array + { + return $this->getOptions() + ['series' => $this->getSeries()]; + } + + abstract protected function getSeries(): array; + + abstract protected function getOptions(): array; +} diff --git a/src/Highcharts/Chart/ChartInterface.php b/src/Highcharts/Chart/ChartInterface.php new file mode 100644 index 0000000..0c25a4f --- /dev/null +++ b/src/Highcharts/Chart/ChartInterface.php @@ -0,0 +1,29 @@ +. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Highcharts\Chart; + +interface ChartInterface +{ + public function getName(): string; + + /** + * This corresponds to the options sent to the Highcharts js object. + * + * Config has to be convertible to JSON or JS. + * If your config contains closures, it will be rendered as a string (for now). + * + * @see https://api.highcharts.com/highcharts/ + */ + public function getConfig(): array; +} diff --git a/src/Highcharts/Chart/TagUsageChart.php b/src/Highcharts/Chart/TagUsageChart.php new file mode 100644 index 0000000..fae9cd6 --- /dev/null +++ b/src/Highcharts/Chart/TagUsageChart.php @@ -0,0 +1,98 @@ +. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Highcharts\Chart; + +use App\Entity\Operation; + +class TagUsageChart extends AbstractChart +{ + /** * @var Operation[] */ + private array $operations = []; + + private function __construct() + { + } + + public function getName(): string + { + return 'Tags'; + } + + public static function create($operations): self + { + $self = new self(); + + foreach ($operations as $operation) { + $self->addOperation($operation); + } + + return $self; + } + + protected function getOptions(): array + { + return [ + 'chart' => [ + 'type' => $type = 'bar', + 'height' => 500, + ], + 'legend' => [ + 'align' => 'right', + 'layout' => 'vertical', + ], + 'title' => ['text' => 'Tags usage'], + 'xAxis' => [ + 'categories' => ['Tags'], + ], + 'yAxis' => [ + 'title' => ['text' => 'Number of operations with these tags'], + ], + 'plotOptions' => [ + $type => [ + 'pointWidth' => 10, + 'borderWidth' => 0, + 'groupPadding' => 0.01, + ], + ], + ]; + } + + protected function getSeries(): array + { + $series = []; + + foreach ($this->operations as $operation) { + foreach ($operation->getTags() as $tag) { + $tagName = $tag->getName(); + if (!isset($series[$tagName])) { + $series[$tagName] = [ + 'name' => $tagName, + 'data' => [0], + ]; + } + + $series[$tagName]['data'][0]++; + } + } + + \ksort($series); + + return \array_values($series); + } + + private function addOperation(Operation $operation): void + { + $this->operations[] = $operation; + } +} diff --git a/src/Repository/OperationRepository.php b/src/Repository/OperationRepository.php index 88b3a96..4d21a1f 100644 --- a/src/Repository/OperationRepository.php +++ b/src/Repository/OperationRepository.php @@ -58,4 +58,20 @@ public function monthIsPopulated(DateTimeImmutable $month): bool return $count > 0; } + + /** + * @return Operation[] + */ + public function findWithTags(): array + { + return $this->_em->createQuery( + <<_entityName} as operation + LEFT JOIN operation.tags as tags + DQL + ) + ->getResult() + ; + } } diff --git a/src/Twig/SlugifyExtension.php b/src/Twig/SlugifyExtension.php new file mode 100644 index 0000000..6880869 --- /dev/null +++ b/src/Twig/SlugifyExtension.php @@ -0,0 +1,44 @@ +. + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace App\Twig; + +use Symfony\Component\String\Slugger\SluggerInterface; +use Twig\Extension\AbstractExtension; +use Twig\TwigFilter; + +class SlugifyExtension extends AbstractExtension +{ + private SluggerInterface $slugger; + + public function __construct(SluggerInterface $slugger) + { + $this->slugger = $slugger; + } + + public function getFilters() + { + return [ + new TwigFilter('slug', [$this, 'slugify']), + ]; + } + + public function slugify($string): string + { + if (\is_object($string)) { + $string = (string) $string; + } + + return $this->slugger->slug($string)->toString(); + } +} diff --git a/templates/analytics.html.twig b/templates/analytics.html.twig new file mode 100644 index 0000000..c728b80 --- /dev/null +++ b/templates/analytics.html.twig @@ -0,0 +1,20 @@ +{% extends '@EasyAdmin/default/layout.html.twig' %} + +{% block content_title %} + Analytics +{% endblock %} + +{% block main %} + {% for chart in charts %} +
+ {% endfor %} +{% endblock %} + +{% block body_custom_javascript %} + {{ parent() }} + +{% endblock %} diff --git a/templates/bundles/EasyAdminBundle/default/includes/_select2_widget.html.twig b/templates/bundles/EasyAdminBundle/default/includes/_select2_widget.html.twig index 0563e29..bea6f29 100644 --- a/templates/bundles/EasyAdminBundle/default/includes/_select2_widget.html.twig +++ b/templates/bundles/EasyAdminBundle/default/includes/_select2_widget.html.twig @@ -3,9 +3,6 @@ {% if (_entity_config.name|default('')) is same as('TagRule') %}