diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4b36487369b4ba838b937a4a8194644299b55c96..9a5a0312c1fc8aa286a2b4e3a324cd33aead7183 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -39,13 +39,11 @@ bundle_app:
   script:
     # build all assets for views
     - npm run build
-    # download GeoLite2-Country and opawg/user-agents archives and extract them to writable/uploads
-    - wget -c "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=$MAXMIND_LICENCE_KEY&suffix=tar.gz" -O - | tar -xz -C ./writable/uploads/
-    - wget -c "https://github.com/opawg/user-agents/archive/master.tar.gz" -O - | tar -xz -C ./writable/uploads/
+    # download GeoLite2-City archive and extract it to writable/uploads
+    - wget -c "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=$MAXMIND_LICENCE_KEY&suffix=tar.gz" -O - | tar -xz -C ./writable/uploads/
 
     # rename extracted archives' folders
-    - mv ./writable/uploads/GeoLite2-Country* ./writable/uploads/GeoLite2-Country
-    - mv ./writable/uploads/user-agents* ./writable/uploads/user-agents
+    - mv ./writable/uploads/GeoLite2-City* ./writable/uploads/GeoLite2-City
 
     # create bundle folder: uses .rsync-filter (-F) file to copy only needed files
     - rsync -avF --progress . ./bundle
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index df0c8a6e8d518b0bbe6961fd4269b6a28d38f05a..1948629736dd154fbd2942e252557ebb382761c9 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -12,16 +12,20 @@ PHP Dependencies:
 - [commonmark](https://commonmark.thephpleague.com/) ([BSD 3-Clause "New" or "Revised" License](https://github.com/thephpleague/commonmark/blob/latest/LICENSE))
 - [phpdotenv](https://github.com/vlucas/phpdotenv) ([ BSD-3-Clause License ](https://github.com/vlucas/phpdotenv/blob/master/LICENSE))
 - [HTML To Markdown for PHP](https://github.com/thephpleague/html-to-markdown) ([MIT License](https://github.com/thephpleague/html-to-markdown/blob/master/LICENSE))
+- [podlibre/user-agents-php](https://github.com/podlibre/user-agents-php) ([MIT License](https://github.com/podlibre/user-agents-php/blob/main/LICENSE))
+- [podlibre/ipcat](https://github.com/podlibre/ipcat) ([GNU General Public License v3.0](https://github.com/podlibre/ipcat/blob/master/LICENSE))
 
 Javascript dependencies:
 
 - [rollup](https://rollupjs.org/) ([MIT License](https://github.com/rollup/rollup/blob/master/LICENSE.md))
 - [tailwindcss](https://tailwindcss.com/) ([MIT License](https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE))
 - [ProseMirror](https://prosemirror.net/) ([MIT License](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE))
-- [D3: Data-Driven Documents](https://d3js.org) ([BSD 3-Clause "New" or "Revised" License](https://github.com/d3/d3/blob/master/LICENSE))
+- [amCharts 4](https://github.com/amcharts/amcharts4) ([Free amCharts license](https://github.com/amcharts/amcharts4/blob/master/dist/script/LICENSE))
 - [Choices.js](https://joshuajohnson.co.uk/Choices/) ([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE))
 
 Other:
 
 - [RemixIcon](https://remixicon.com/) ([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
-- [User agent list](https://github.com/opawg/user-agents) ([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE))
+- [OPAWG/User agent list](https://github.com/opawg/user-agents) ([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE))
+- [client9/ipcat](https://github.com/client9/ipcat) ([GNU General Public License v3.0](https://github.com/client9/ipcat/blob/master/LICENSE))
+- [GeoLite2 City](https://dev.maxmind.com/geoip/geoip2/geolite2/) ([Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://www.maxmind.com/en/geolite2/eula))
diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 3a786da094d2cb57fbda09887c7cb05e0a26d550..5d2c45f8051cdec4245b05133294d46209919b99 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -53,10 +53,14 @@ $routes->group(config('App')->installGateway, function ($routes) {
     ]);
 });
 
-// Route for podcast audio file analytics (/stats/podcast_id/episode_id/podcast_folder/filename.mp3)
-$routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [
-    'as' => 'analytics_hit',
-]);
+// Route for podcast audio file analytics (/audio/podcast_id/episode_id/bytes_threshold/filesize/podcast_folder/filename.mp3)
+$routes->add(
+    'audio/(:num)/(:num)/(:num)/(:num)/(:any)',
+    'Analytics::hit/$1/$2/$3/$4/$5',
+    [
+        'as' => 'analytics_hit',
+    ]
+);
 
 // Show the Unknown UserAgents
 $routes->get('.well-known/unknown-useragents', 'UnknownUserAgents');
@@ -113,6 +117,26 @@ $routes->group(
                     'as' => 'podcast-delete',
                     'filter' => 'permission:podcasts-delete',
                 ]);
+                $routes->get('analytics', 'Podcast::analytics/$1', [
+                    'as' => 'podcast-analytics',
+                    'filter' => 'permission:podcasts-view,podcast-view',
+                ]);
+                $routes->get(
+                    'analytics-data/(:segment)/(:segment)',
+                    'AnalyticsData::getData/$1/$2/$3',
+                    [
+                        'as' => 'analytics-data',
+                        'filter' => 'permission:podcasts-view,podcast-view',
+                    ]
+                );
+                $routes->get(
+                    'analytics-data/(:segment)/(:segment)/(:num)',
+                    'AnalyticsData::getData/$1/$2/$3/$4',
+                    [
+                        'as' => 'analytics-filtered-data',
+                        'filter' => 'permission:podcasts-view,podcast-view',
+                    ]
+                );
 
                 // Podcast episodes
                 $routes->group('episodes', function ($routes) {
diff --git a/app/Controllers/Admin/AnalyticsData.php b/app/Controllers/Admin/AnalyticsData.php
new file mode 100644
index 0000000000000000000000000000000000000000..a57960dfd1561982dbdf33aab4d32bd2ba8657f3
--- /dev/null
+++ b/app/Controllers/Admin/AnalyticsData.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Controllers\Admin;
+
+use App\Models\PodcastModel;
+use App\Models\EpisodeModel;
+
+class AnalyticsData extends BaseController
+{
+    /**
+     * @var \App\Entities\Podcast|null
+     */
+    protected $podcast;
+    protected $className;
+    protected $methodName;
+    protected $episode;
+
+    public function _remap($method, ...$params)
+    {
+        if (count($params) > 2) {
+            if (!($this->podcast = (new PodcastModel())->find($params[0]))) {
+                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(
+                    'Podcast not found: ' . $params[0]
+                );
+            }
+            $this->className = '\App\Models\Analytics' . $params[1] . 'Model';
+            $this->methodName = 'getData' . $params[2];
+            if (count($params) > 3) {
+                if (
+                    !($this->episode = (new EpisodeModel())
+                        ->where([
+                            'podcast_id' => $this->podcast->id,
+                            'id' => $params[3],
+                        ])
+                        ->first())
+                ) {
+                    throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(
+                        'Episode not found: ' . $params[3]
+                    );
+                }
+            }
+        }
+
+        return $this->$method();
+    }
+    public function getData()
+    {
+        $analytics_model = new $this->className();
+        $methodName = $this->methodName;
+        if ($this->episode) {
+            return $this->response->setJSON(
+                $analytics_model->$methodName(
+                    $this->podcast->id,
+                    $this->episode->id
+                )
+            );
+        } else {
+            return $this->response->setJSON(
+                $analytics_model->$methodName($this->podcast->id)
+            );
+        }
+    }
+}
diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php
index 439a86dafe359283d6483b8f7edfb33360cd5d2e..64594e07430fcf56196e43fe652a6bea67f2aaf4 100644
--- a/app/Controllers/Admin/Podcast.php
+++ b/app/Controllers/Admin/Podcast.php
@@ -58,6 +58,14 @@ class Podcast extends BaseController
         return view('admin/podcast/view', $data);
     }
 
+    public function analytics()
+    {
+        $data = ['podcast' => $this->podcast];
+
+        replace_breadcrumb_params([0 => $this->podcast->title]);
+        return view('admin/podcast/analytics', $data);
+    }
+
     public function create()
     {
         helper(['form', 'misc']);
@@ -204,7 +212,9 @@ class Podcast extends BaseController
         $podcast = new \App\Entities\Podcast([
             'name' => $this->request->getPost('name'),
             'imported_feed_url' => $this->request->getPost('imported_feed_url'),
-
+            'new_feed_url' => base_url(
+                route_to('podcast_feed', $this->request->getPost('name'))
+            ),
             'title' => $feed->channel[0]->title,
             'description' => $feed->channel[0]->description,
             'image' => download_file($nsItunes->image->attributes()),
@@ -214,7 +224,9 @@ class Podcast extends BaseController
                 ? null
                 : (in_array($nsItunes->explicit, ['yes', 'true'])
                     ? 'explicit'
-                    : null),
+                    : (in_array($nsItunes->explicit, ['no', 'false'])
+                        ? 'clean'
+                        : null)),
             'owner_name' => $nsItunes->owner->name,
             'owner_email' => $nsItunes->owner->email,
             'publisher' => $nsItunes->author,
@@ -302,11 +314,13 @@ class Podcast extends BaseController
                 'image' => empty($nsItunes->image->attributes())
                     ? null
                     : download_file($nsItunes->image->attributes()),
-                'explicit' => $nsItunes->explicit
-                    ? (in_array($nsItunes->explicit, ['yes', 'true'])
+                'parental_advisory' => empty($nsItunes->explicit)
+                    ? null
+                    : (in_array($nsItunes->explicit, ['yes', 'true'])
                         ? 'explicit'
-                        : null)
-                    : null,
+                        : (in_array($nsItunes->explicit, ['no', 'false'])
+                            ? 'clean'
+                            : null)),
                 'number' =>
                     $this->request->getPost('force_renumber') === 'yes'
                         ? $itemNumber
diff --git a/app/Controllers/Analytics.php b/app/Controllers/Analytics.php
index ec89dc2260fed1fe3e8f901ef319fef9e101982f..3482b1eb6939fe694a05e32a1e81c09e5253c2d3 100644
--- a/app/Controllers/Analytics.php
+++ b/app/Controllers/Analytics.php
@@ -40,16 +40,22 @@ class Analytics extends Controller
         // E.g.:
         // $this->session = \Config\Services::session();
 
-        set_user_session_country();
+        set_user_session_deny_list_ip();
+        set_user_session_location();
         set_user_session_player();
     }
 
     // Add one hit to this episode:
-    public function hit($p_podcastId, $p_episodeId, ...$filename)
-    {
+    public function hit(
+        $podcastId,
+        $episodeId,
+        $bytesThreshold,
+        $fileSize,
+        ...$filename
+    ) {
         helper('media');
 
-        podcast_hit($p_podcastId, $p_episodeId);
+        podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize);
         return redirect()->to(media_url(implode('/', $filename)));
     }
 }
diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php
index 2f5bdcff291b15a430e29ade0fdb7b390c6c3391..3bfbd4b0ffe5094dd49e53a9bee1afbff985845e 100644
--- a/app/Controllers/BaseController.php
+++ b/app/Controllers/BaseController.php
@@ -45,9 +45,10 @@ class BaseController extends Controller
         // E.g.:
         // $this->session = \Config\Services::session();
 
-        set_user_session_country();
+        set_user_session_deny_list_ip();
         set_user_session_browser();
         set_user_session_referer();
+        set_user_session_entry_page();
     }
 
     protected static function triggerWebpageHit($podcastId)
diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
index a95e4db10869489e31b17f9e74b3bbc25a191a76..018315cfd952e78c4d6fcca01119c7b034501ae4 100644
--- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
+++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php
@@ -110,6 +110,13 @@ class AddPodcasts extends Migration
                     'The RSS feed URL if this podcast was imported, NULL otherwise.',
                 'null' => true,
             ],
+            'new_feed_url' => [
+                'type' => 'VARCHAR',
+                'constraint' => 1024,
+                'comment' =>
+                    'The RSS new feed URL if this podcast is moving out, NULL otherwise.',
+                'null' => true,
+            ],
             'created_at' => [
                 'type' => 'TIMESTAMP',
             ],
diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php
index 24b2f02fc5dd7497e9515abd1deb1813f41212a5..313346866b8c377c2004c18ed95f3621387afe6a 100644
--- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php
+++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php
@@ -61,6 +61,12 @@ class AddEpisodes extends Migration
                 'unsigned' => true,
                 'comment' => 'File size in bytes',
             ],
+            'enclosure_headersize' => [
+                'type' => 'INT',
+                'constraint' => 10,
+                'unsigned' => true,
+                'comment' => 'Header size in bytes',
+            ],
             'description' => [
                 'type' => 'TEXT',
                 'null' => true,
diff --git a/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php b/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php
new file mode 100644
index 0000000000000000000000000000000000000000..e31d932d5f758bdd897334ffba807fea667b10d2
--- /dev/null
+++ b/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Class AddAnalyticsPodcastsByCountry
+ * Creates analytics_podcasts_by_country table in database
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Database\Migrations;
+
+use CodeIgniter\Database\Migration;
+
+class AddAnalyticsPodcasts extends Migration
+{
+    public function up()
+    {
+        $this->forge->addField([
+            'podcast_id' => [
+                'type' => 'BIGINT',
+                'constraint' => 20,
+                'unsigned' => true,
+            ],
+            'date' => [
+                'type' => 'date',
+            ],
+            'hits' => [
+                'type' => 'INT',
+                'constraint' => 10,
+                'default' => 1,
+            ],
+        ]);
+        $this->forge->addPrimaryKey(['podcast_id', 'date']);
+        $this->forge->addField(
+            '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
+        );
+        $this->forge->addField(
+            '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
+        );
+        $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
+        $this->forge->createTable('analytics_podcasts');
+    }
+
+    public function down()
+    {
+        $this->forge->dropTable('analytics_podcasts');
+    }
+}
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_country.php b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
similarity index 67%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_country.php
rename to app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
index d0e94c6c9337b05830548bf4bf5a58679b7b178a..b189ef062b0a31325a910a9ef5c14230498b9e12 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_country.php
+++ b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php
@@ -12,34 +12,28 @@ namespace App\Database\Migrations;
 
 use CodeIgniter\Database\Migration;
 
-class AddAnalyticsEpisodesByCountry extends Migration
+class AddAnalyticsPodcastsByEpisode extends Migration
 {
     public function up()
     {
         $this->forge->addField([
-            'id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-                'auto_increment' => true,
-            ],
             'podcast_id' => [
                 'type' => 'BIGINT',
                 'constraint' => 20,
                 'unsigned' => true,
             ],
+            'date' => [
+                'type' => 'date',
+            ],
             'episode_id' => [
                 'type' => 'BIGINT',
                 'constraint' => 20,
                 'unsigned' => true,
             ],
-            'country_code' => [
-                'type' => 'VARCHAR',
-                'constraint' => 3,
-                'comment' => 'ISO 3166-1 code.',
-            ],
-            'date' => [
-                'type' => 'date',
+            'age' => [
+                'type' => 'INT',
+                'constraint' => 10,
+                'unsigned' => true,
             ],
             'hits' => [
                 'type' => 'INT',
@@ -47,13 +41,7 @@ class AddAnalyticsEpisodesByCountry extends Migration
                 'default' => 1,
             ],
         ]);
-        $this->forge->addKey('id', true);
-        $this->forge->addUniqueKey([
-            'podcast_id',
-            'episode_id',
-            'country_code',
-            'date',
-        ]);
+        $this->forge->addPrimaryKey(['podcast_id', 'episode_id', 'date']);
         $this->forge->addField(
             '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
         );
@@ -62,11 +50,11 @@ class AddAnalyticsEpisodesByCountry extends Migration
         );
         $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
         $this->forge->addForeignKey('episode_id', 'episodes', 'id');
-        $this->forge->createTable('analytics_episodes_by_country');
+        $this->forge->createTable('analytics_podcasts_by_episode');
     }
 
     public function down()
     {
-        $this->forge->dropTable('analytics_episodes_by_country');
+        $this->forge->dropTable('analytics_podcasts_by_episode');
     }
 }
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_player.php b/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php
similarity index 70%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_player.php
rename to app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php
index 3a13f65fd59bba1ca950c73cc852e9d3d70b8dc0..c1bc04af881e3b5b57741ab80611bfbdf008fe1e 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_player.php
+++ b/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php
@@ -17,32 +17,45 @@ class AddAnalyticsPodcastsByPlayer extends Migration
     public function up()
     {
         $this->forge->addField([
-            'id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-                'auto_increment' => true,
-            ],
             'podcast_id' => [
                 'type' => 'BIGINT',
                 'constraint' => 20,
                 'unsigned' => true,
             ],
-            'player' => [
-                'type' => 'VARCHAR',
-                'constraint' => 191,
-            ],
             'date' => [
                 'type' => 'date',
             ],
+            'app' => [
+                'type' => 'VARCHAR',
+                'constraint' => 128,
+            ],
+            'device' => [
+                'type' => 'VARCHAR',
+                'constraint' => 32,
+            ],
+            'os' => [
+                'type' => 'VARCHAR',
+                'constraint' => 32,
+            ],
+            'bot' => [
+                'type' => 'TINYINT',
+                'constraint' => 1,
+                'default' => 0,
+            ],
             'hits' => [
                 'type' => 'INT',
                 'constraint' => 10,
                 'default' => 1,
             ],
         ]);
-        $this->forge->addKey('id', true);
-        $this->forge->addUniqueKey(['podcast_id', 'player', 'date']);
+        $this->forge->addPrimaryKey([
+            'podcast_id',
+            'app',
+            'device',
+            'os',
+            'bot',
+            'date',
+        ]);
         $this->forge->addField(
             '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
         );
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_country.php b/app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php
similarity index 83%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_country.php
rename to app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php
index 6545a7a11bb1b81dd2b842323ad90357abdc6119..e5f045e6ef76ddf775c76b999e0ad1d0e1820ebd 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_country.php
+++ b/app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php
@@ -17,33 +17,26 @@ class AddAnalyticsPodcastsByCountry extends Migration
     public function up()
     {
         $this->forge->addField([
-            'id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-                'auto_increment' => true,
-            ],
             'podcast_id' => [
                 'type' => 'BIGINT',
                 'constraint' => 20,
                 'unsigned' => true,
             ],
+            'date' => [
+                'type' => 'date',
+            ],
             'country_code' => [
                 'type' => 'VARCHAR',
                 'constraint' => 3,
                 'comment' => 'ISO 3166-1 code.',
             ],
-            'date' => [
-                'type' => 'date',
-            ],
             'hits' => [
                 'type' => 'INT',
                 'constraint' => 10,
                 'default' => 1,
             ],
         ]);
-        $this->forge->addKey('id', true);
-        $this->forge->addUniqueKey(['podcast_id', 'country_code', 'date']);
+        $this->forge->addPrimaryKey(['podcast_id', 'country_code', 'date']);
         $this->forge->addField(
             '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
         );
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_country.php b/app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php
similarity index 60%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_country.php
rename to app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php
index 7f8b1415c0fd0f0afb1242249ae2b1b50790c074..7b787878deec93505bd454c2eab19270845329a4 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_country.php
+++ b/app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php
@@ -1,8 +1,8 @@
 <?php
 
 /**
- * Class AddAnalyticsWebsiteByCountry
- * Creates analytics_website_by_country table in database
+ * Class AddAnalyticsPodcastsByRegion
+ * Creates analytics_podcasts_by_region table in database
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
@@ -12,29 +12,36 @@ namespace App\Database\Migrations;
 
 use CodeIgniter\Database\Migration;
 
-class AddAnalyticsWebsiteByCountry extends Migration
+class AddAnalyticsPodcastsByRegion extends Migration
 {
     public function up()
     {
         $this->forge->addField([
-            'id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-                'auto_increment' => true,
-            ],
             'podcast_id' => [
                 'type' => 'BIGINT',
                 'constraint' => 20,
                 'unsigned' => true,
             ],
+            'date' => [
+                'type' => 'date',
+            ],
             'country_code' => [
                 'type' => 'VARCHAR',
                 'constraint' => 3,
                 'comment' => 'ISO 3166-1 code.',
             ],
-            'date' => [
-                'type' => 'date',
+            'region_code' => [
+                'type' => 'VARCHAR',
+                'constraint' => 3,
+                'comment' => 'ISO 3166-2 code.',
+            ],
+            'latitude' => [
+                'type' => 'FLOAT',
+                'null' => true,
+            ],
+            'longitude' => [
+                'type' => 'FLOAT',
+                'null' => true,
             ],
             'hits' => [
                 'type' => 'INT',
@@ -42,8 +49,12 @@ class AddAnalyticsWebsiteByCountry extends Migration
                 'default' => 1,
             ],
         ]);
-        $this->forge->addKey('id', true);
-        $this->forge->addUniqueKey(['podcast_id', 'country_code', 'date']);
+        $this->forge->addPrimaryKey([
+            'podcast_id',
+            'country_code',
+            'region_code',
+            'date',
+        ]);
         $this->forge->addField(
             '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
         );
@@ -51,11 +62,11 @@ class AddAnalyticsWebsiteByCountry extends Migration
             '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
         );
         $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
-        $this->forge->createTable('analytics_website_by_country');
+        $this->forge->createTable('analytics_podcasts_by_region');
     }
 
     public function down()
     {
-        $this->forge->dropTable('analytics_website_by_country');
+        $this->forge->dropTable('analytics_podcasts_by_region');
     }
 }
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_browser.php b/app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php
similarity index 82%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_browser.php
rename to app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php
index 6e4942d43317c4f7a70577ae4ab345fb10401cf8..21724af7097c7025a23b9aac381437ca2bf751bd 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_browser.php
+++ b/app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php
@@ -17,32 +17,26 @@ class AddAnalyticsWebsiteByBrowser extends Migration
     public function up()
     {
         $this->forge->addField([
-            'id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-                'auto_increment' => true,
-            ],
             'podcast_id' => [
                 'type' => 'BIGINT',
                 'constraint' => 20,
                 'unsigned' => true,
             ],
+            'date' => [
+                'type' => 'date',
+            ],
             'browser' => [
                 'type' => 'VARCHAR',
                 'constraint' => 191,
             ],
-            'date' => [
-                'type' => 'date',
-            ],
+
             'hits' => [
                 'type' => 'INT',
                 'constraint' => 10,
                 'default' => 1,
             ],
         ]);
-        $this->forge->addKey('id', true);
-        $this->forge->addUniqueKey(['podcast_id', 'browser', 'date']);
+        $this->forge->addPrimaryKey(['podcast_id', 'browser', 'date']);
         $this->forge->addField(
             '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
         );
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_referer.php b/app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php
similarity index 78%
rename from app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_referer.php
rename to app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php
index 28808f273e40adf36d617f2ead748070083df75f..579024b02b845edfbf88ab79eaadb9ca8dd11e3c 100644
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_referer.php
+++ b/app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php
@@ -17,24 +17,28 @@ class AddAnalyticsWebsiteByReferer extends Migration
     public function up()
     {
         $this->forge->addField([
-            'id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-                'auto_increment' => true,
-            ],
             'podcast_id' => [
                 'type' => 'BIGINT',
                 'constraint' => 20,
                 'unsigned' => true,
             ],
+            'date' => [
+                'type' => 'date',
+            ],
             'referer' => [
                 'type' => 'VARCHAR',
-                'constraint' => 191,
+                'constraint' => 512,
                 'comment' => 'Referer URL.',
             ],
-            'date' => [
-                'type' => 'date',
+            'domain' => [
+                'type' => 'VARCHAR',
+                'constraint' => 128,
+                'null' => true,
+            ],
+            'keywords' => [
+                'type' => 'VARCHAR',
+                'constraint' => 384,
+                'null' => true,
             ],
             'hits' => [
                 'type' => 'INT',
@@ -42,8 +46,7 @@ class AddAnalyticsWebsiteByReferer extends Migration
                 'default' => 1,
             ],
         ]);
-        $this->forge->addKey('id', true);
-        $this->forge->addUniqueKey(['podcast_id', 'referer', 'date']);
+        $this->forge->addPrimaryKey(['podcast_id', 'referer', 'date']);
         $this->forge->addField(
             '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
         );
diff --git a/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php b/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php
new file mode 100644
index 0000000000000000000000000000000000000000..19bce6de19fb00b85be29b074b5899cc6518b5a3
--- /dev/null
+++ b/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Class AddAnalyticsWebsiteByReferer
+ * Creates analytics_website_by_referer table in database
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Database\Migrations;
+
+use CodeIgniter\Database\Migration;
+
+class AddAnalyticsWebsiteByEntryPage extends Migration
+{
+    public function up()
+    {
+        $this->forge->addField([
+            'podcast_id' => [
+                'type' => 'BIGINT',
+                'constraint' => 20,
+                'unsigned' => true,
+            ],
+            'date' => [
+                'type' => 'date',
+            ],
+            'entry_page' => [
+                'type' => 'VARCHAR',
+                'constraint' => 512,
+                'comment' => 'Entry page URL.',
+            ],
+            'hits' => [
+                'type' => 'INT',
+                'constraint' => 10,
+                'default' => 1,
+            ],
+        ]);
+        $this->forge->addPrimaryKey(['podcast_id', 'entry_page', 'date']);
+        $this->forge->addField(
+            '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
+        );
+        $this->forge->addField(
+            '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
+        );
+        $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
+        $this->forge->createTable('analytics_website_by_entry_page');
+    }
+
+    public function down()
+    {
+        $this->forge->dropTable('analytics_website_by_entry_page');
+    }
+}
diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_player.php b/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_player.php
deleted file mode 100644
index 3a1e257af60fd0f47cb6e814ca0bd3b272dca3e0..0000000000000000000000000000000000000000
--- a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_player.php
+++ /dev/null
@@ -1,71 +0,0 @@
-<?php
-
-/**
- * Class AddAnalyticsEpisodesByPlayer
- * Creates analytics_episodes_by_player table in database
- * @copyright  2020 Podlibre
- * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
- * @link       https://castopod.org/
- */
-
-namespace App\Database\Migrations;
-
-use CodeIgniter\Database\Migration;
-
-class AddAnalyticsEpisodesByPlayer extends Migration
-{
-    public function up()
-    {
-        $this->forge->addField([
-            'id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-                'auto_increment' => true,
-            ],
-            'podcast_id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-            ],
-            'episode_id' => [
-                'type' => 'BIGINT',
-                'constraint' => 20,
-                'unsigned' => true,
-            ],
-            'player' => [
-                'type' => 'VARCHAR',
-                'constraint' => 191,
-            ],
-            'date' => [
-                'type' => 'date',
-            ],
-            'hits' => [
-                'type' => 'INT',
-                'constraint' => 10,
-                'default' => 1,
-            ],
-        ]);
-        $this->forge->addKey('id', true);
-        $this->forge->addUniqueKey([
-            'podcast_id',
-            'episode_id',
-            'player',
-            'date',
-        ]);
-        $this->forge->addField(
-            '`created_at` timestamp NOT NULL DEFAULT current_timestamp()'
-        );
-        $this->forge->addField(
-            '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()'
-        );
-        $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
-        $this->forge->addForeignKey('episode_id', 'episodes', 'id');
-        $this->forge->createTable('analytics_episodes_by_player');
-    }
-
-    public function down()
-    {
-        $this->forge->dropTable('analytics_episodes_by_player');
-    }
-}
diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php
index 18bd203af422ba127b5270491b3b93e96a99da51..caf355009a0420b2581c9cab9439c9737bb29642 100644
--- a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php
+++ b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php
@@ -18,26 +18,42 @@ class AddAnalyticsPodcastsStoredProcedure extends Migration
     {
         // Creates Stored Procedure for data insertion
         // Example: CALL analytics_podcasts(1,2,'FR','phone/android/Deezer');
-        $procedureName = $this->db->prefixTable('analytics_podcasts');
-        $episodesTableName = $this->db->prefixTable('analytics_episodes');
+        $prefix = $this->db->getPrefix();
+
         $createQuery = <<<EOD
-CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` BIGINT(20) UNSIGNED, IN `p_episode_id` BIGINT(20) UNSIGNED, IN `p_country_code` VARCHAR(3) CHARSET utf8mb4, IN `p_player` VARCHAR(191) CHARSET utf8mb4)  MODIFIES SQL DATA
+CREATE PROCEDURE `{$prefix}analytics_podcasts` (
+    IN `p_podcast_id` BIGINT(20) UNSIGNED,
+    IN `p_episode_id` BIGINT(20) UNSIGNED,
+    IN `p_country_code` VARCHAR(3) CHARSET utf8mb4,
+    IN `p_region_code` VARCHAR(3) CHARSET utf8mb4,
+    IN `p_latitude` FLOAT,
+    IN `p_longitude` FLOAT,
+    IN `p_app` VARCHAR(128) CHARSET utf8mb4,
+    IN `p_device` VARCHAR(32) CHARSET utf8mb4,
+    IN `p_os` VARCHAR(32) CHARSET utf8mb4,
+    IN `p_bot` TINYINT(1) UNSIGNED
+    )  MODIFIES SQL DATA
 DETERMINISTIC
 SQL SECURITY INVOKER
 COMMENT 'Add one hit in podcast logs tables.'
 BEGIN
-INSERT INTO `{$procedureName}_by_country`(`podcast_id`, `country_code`, `date`) 
-VALUES (p_podcast_id, p_country_code, DATE(NOW())) 
-ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
-INSERT INTO `{$procedureName}_by_player`(`podcast_id`, `player`, `date`) 
-VALUES (p_podcast_id, p_player, DATE(NOW())) 
-ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
-INSERT INTO `{$episodesTableName}_by_country`(`podcast_id`, `episode_id`, `country_code`, `date`) 
-VALUES (p_podcast_id, p_episode_id, p_country_code, DATE(NOW())) 
-ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
-INSERT INTO `{$episodesTableName}_by_player`(`podcast_id`, `episode_id`,  `player`, `date`) 
-VALUES (p_podcast_id, p_episode_id, p_player, DATE(NOW())) 
-ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
+IF NOT `p_bot` THEN
+    INSERT INTO `{$prefix}analytics_podcasts`(`podcast_id`, `date`) 
+        VALUES (p_podcast_id, DATE(NOW())) 
+        ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
+    INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`) 
+    SELECT p_podcast_id, p_episode_id, DATE(NOW()), datediff(now(),`published_at`) FROM `{$prefix}episodes` WHERE `id`= p_episode_id
+        ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
+    INSERT INTO `{$prefix}analytics_podcasts_by_country`(`podcast_id`, `country_code`, `date`) 
+        VALUES (p_podcast_id, p_country_code, DATE(NOW())) 
+        ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
+    INSERT INTO `{$prefix}analytics_podcasts_by_region`(`podcast_id`, `country_code`, `region_code`, `latitude`, `longitude`, `date`) 
+        VALUES (p_podcast_id, p_country_code, p_region_code, p_latitude, p_longitude, DATE(NOW())) 
+        ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
+END IF;
+INSERT INTO `{$prefix}analytics_podcasts_by_player`(`podcast_id`, `app`, `device`, `os`, `bot`, `date`) 
+    VALUES (p_podcast_id, p_app, p_device, p_os, p_bot, DATE(NOW())) 
+    ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
 END
 EOD;
         $this->db->query($createQuery);
@@ -45,7 +61,9 @@ EOD;
 
     public function down()
     {
-        $procedureName = $this->db->prefixTable('analytics_podcasts');
-        $this->db->query("DROP PROCEDURE IF EXISTS `$procedureName`");
+        $prefix = $this->db->getPrefix();
+        $this->db->query(
+            "DROP PROCEDURE IF EXISTS `{$prefix}analytics_podcasts`"
+        );
     }
 }
diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_website_stored_procedure.php b/app/Database/Migrations/2020-06-11-210000_add_analytics_website_stored_procedure.php
index c263a8a1b3d79b83f4c4cc45298e60d9c7a61a7f..836b5d7d37612f07d1b773cc25b8f9242c2fa053 100644
--- a/app/Database/Migrations/2020-06-11-210000_add_analytics_website_stored_procedure.php
+++ b/app/Database/Migrations/2020-06-11-210000_add_analytics_website_stored_procedure.php
@@ -20,20 +20,20 @@ class AddAnalyticsWebsiteStoredProcedure extends Migration
         // Example: CALL analytics_website(1,'FR','Firefox');
         $procedureName = $this->db->prefixTable('analytics_website');
         $createQuery = <<<EOD
-CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` BIGINT(20) UNSIGNED, IN `p_country_code` VARCHAR(3) CHARSET utf8mb4, IN `p_browser` VARCHAR(191) CHARSET utf8mb4, IN `p_referer` VARCHAR(191) CHARSET utf8mb4)  MODIFIES SQL DATA
+CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` BIGINT(20) UNSIGNED, IN `p_browser` VARCHAR(191) CHARSET utf8mb4, IN `p_entry_page` VARCHAR(512) CHARSET utf8mb4, IN `p_referer` VARCHAR(512) CHARSET utf8mb4, IN `p_domain` VARCHAR(128) CHARSET utf8mb4, IN `p_keywords` VARCHAR(384) CHARSET utf8mb4)  MODIFIES SQL DATA
 DETERMINISTIC
 SQL SECURITY INVOKER
 COMMENT 'Add one hit in website logs tables.'
 BEGIN
-INSERT INTO {$procedureName}_by_country(`podcast_id`, `country_code`, `date`) 
-VALUES (p_podcast_id, p_country_code, DATE(NOW())) 
-ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
 INSERT INTO {$procedureName}_by_browser(`podcast_id`, `browser`, `date`) 
-VALUES (p_podcast_id, p_browser, DATE(NOW())) 
-ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
-INSERT INTO {$procedureName}_by_referer(`podcast_id`, `referer`, `date`) 
-VALUES (p_podcast_id, p_referer, DATE(NOW())) 
-ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
+    VALUES (p_podcast_id, p_browser, DATE(NOW())) 
+    ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
+INSERT INTO {$procedureName}_by_referer(`podcast_id`, `referer`, `domain`, `keywords`, `date`) 
+    VALUES (p_podcast_id, p_referer, p_domain, p_keywords, DATE(NOW())) 
+    ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
+INSERT INTO {$procedureName}_by_entry_page(`podcast_id`, `entry_page`, `date`) 
+    VALUES (p_podcast_id, p_entry_page, DATE(NOW())) 
+    ON DUPLICATE KEY UPDATE `hits`=`hits`+1;
 END
 EOD;
         $this->db->query($createQuery);
diff --git a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
new file mode 100644
index 0000000000000000000000000000000000000000..e312a3a0160e6c8838afcc2e529d00d99c5984a4
--- /dev/null
+++ b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php
@@ -0,0 +1,176 @@
+<?php
+
+/**
+ * Class FakePodcastsAnalyticsSeeder
+ * Inserts Fake Analytics in the database
+ *
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Database\Seeds;
+use App\Models\PodcastModel;
+use App\Models\EpisodeModel;
+
+use CodeIgniter\Database\Seeder;
+
+class FakePodcastsAnalyticsSeeder extends Seeder
+{
+    public function run()
+    {
+        $podcast = (new PodcastModel())->first();
+
+        $jsonUserAgents = json_decode(
+            file_get_contents(
+                'https://raw.githubusercontent.com/opawg/user-agents/master/src/user-agents.json'
+            ),
+            true
+        );
+
+        if ($podcast) {
+            $firstEpisode = (new EpisodeModel())
+                ->selectMin('published_at')
+                ->first();
+
+            for (
+                $date = strtotime($firstEpisode->published_at);
+                $date < strtotime('now');
+                $date = strtotime(date('Y-m-d', $date) . ' +1 day')
+            ) {
+                $analytics_podcasts = [];
+                $analytics_podcasts_by_country = [];
+                $analytics_podcasts_by_episode = [];
+                $analytics_podcasts_by_player = [];
+                $analytics_podcasts_by_region = [];
+
+                $episodes = (new EpisodeModel())
+                    ->where([
+                        'podcast_id' => $podcast->id,
+                        'DATE(published_at) <=' => date('Y-m-d', $date),
+                    ])
+                    ->findAll();
+                foreach ($episodes as $episode) {
+                    $age = floor(
+                        ($date - strtotime($episode->published_at)) / 86400
+                    );
+                    $proba1 = floor(exp(3 - $age / 40)) + 1;
+
+                    for (
+                        $num_line = 0;
+                        $num_line < rand(1, $proba1);
+                        $num_line++
+                    ) {
+                        $proba2 = floor(exp(6 - $age / 20)) + 10;
+
+                        $player =
+                            $jsonUserAgents[
+                                rand(1, count($jsonUserAgents) - 1)
+                            ];
+                        $app = isset($player['app']) ? $player['app'] : '';
+                        $device = isset($player['device'])
+                            ? $player['device']
+                            : '';
+                        $os = isset($player['os']) ? $player['os'] : '';
+                        $bot = isset($player['bot']) ? $player['bot'] : 0;
+
+                        $fakeIp =
+                            rand(0, 255) .
+                            '.' .
+                            rand(0, 255) .
+                            '.' .
+                            rand(0, 255) .
+                            '.' .
+                            rand(0, 255);
+
+                        $cityReader = new \GeoIp2\Database\Reader(
+                            WRITEPATH .
+                                'uploads/GeoLite2-City/GeoLite2-City.mmdb'
+                        );
+
+                        $countryCode = 'N/A';
+                        $regionCode = 'N/A';
+                        $latitude = null;
+                        $longitude = null;
+                        try {
+                            $city = $cityReader->city($fakeIp);
+
+                            $countryCode = empty($city->country->isoCode)
+                                ? 'N/A'
+                                : $city->country->isoCode;
+
+                            $regionCode = empty($city->subdivisions[0]->isoCode)
+                                ? 'N/A'
+                                : $city->subdivisions[0]->isoCode;
+                            $latitude = round($city->location->latitude, 3);
+                            $longitude = round($city->location->longitude, 3);
+                        } catch (\GeoIp2\Exception\AddressNotFoundException $ex) {
+                            //Bad luck, bad IP, nothing to do.
+                        }
+
+                        $hits = rand(0, $proba2);
+
+                        $analytics_podcasts[] = [
+                            'podcast_id' => $podcast->id,
+                            'date' => date('Y-m-d', $date),
+                            'hits' => $hits,
+                        ];
+                        $analytics_podcasts_by_country[] = [
+                            'podcast_id' => $podcast->id,
+                            'date' => date('Y-m-d', $date),
+                            'country_code' => $countryCode,
+                            'hits' => $hits,
+                        ];
+                        $analytics_podcasts_by_episode[] = [
+                            'podcast_id' => $podcast->id,
+                            'date' => date('Y-m-d', $date),
+                            'episode_id' => $episode->id,
+                            'age' => $age,
+                            'hits' => $hits,
+                        ];
+                        $analytics_podcasts_by_player[] = [
+                            'podcast_id' => $podcast->id,
+                            'date' => date('Y-m-d', $date),
+                            'app' => $app,
+                            'device' => $device,
+                            'os' => $os,
+                            'bot' => $bot,
+                            'hits' => $hits,
+                        ];
+                        $analytics_podcasts_by_region[] = [
+                            'podcast_id' => $podcast->id,
+                            'date' => date('Y-m-d', $date),
+                            'country_code' => $countryCode,
+                            'region_code' => $regionCode,
+                            'latitude' => $latitude,
+                            'longitude' => $longitude,
+                            'hits' => $hits,
+                        ];
+                    }
+                }
+                $this->db
+                    ->table('analytics_podcasts')
+                    ->ignore(true)
+                    ->insertBatch($analytics_podcasts);
+                $this->db
+                    ->table('analytics_podcasts_by_country')
+                    ->ignore(true)
+                    ->insertBatch($analytics_podcasts_by_country);
+                $this->db
+                    ->table('analytics_podcasts_by_episode')
+                    ->ignore(true)
+                    ->insertBatch($analytics_podcasts_by_episode);
+                $this->db
+                    ->table('analytics_podcasts_by_player')
+                    ->ignore(true)
+                    ->insertBatch($analytics_podcasts_by_player);
+                $this->db
+                    ->table('analytics_podcasts_by_region')
+                    ->ignore(true)
+                    ->insertBatch($analytics_podcasts_by_region);
+            }
+        } else {
+            echo "Create one podcast and some episodes first.\n";
+        }
+    }
+}
diff --git a/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php b/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php
new file mode 100644
index 0000000000000000000000000000000000000000..67270d4c5ef5e53cbe5d9953ce0a93461c25af1f
--- /dev/null
+++ b/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php
@@ -0,0 +1,260 @@
+<?php
+
+/**
+ * Class FakeWebsiteAnalyticsSeeder
+ * Inserts Fake Analytics in the database
+ *
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Database\Seeds;
+use App\Models\PodcastModel;
+use App\Models\EpisodeModel;
+
+use CodeIgniter\Database\Seeder;
+
+class FakeWebsiteAnalyticsSeeder extends Seeder
+{
+    protected $keywords = [
+        'all the smoke podcast',
+        'apple podcast',
+        'bad friends podcast',
+        'best podcast',
+        'best podcasts',
+        'best podcasts 2020',
+        'blood ties',
+        'call her daddy',
+        'call her daddy podcast',
+        'call her daddy podcast controversy',
+        'call her daddy podcast drama',
+        'counter clock podcast',
+        'counterclock podcast',
+        'crime junkie podcast',
+        'crime podcast',
+        'down the hill podcast',
+        'gerry callahan podcast',
+        'google podcast',
+        'history podcast',
+        'joe rogan',
+        'joe rogan podcast',
+        'lana rhoades and logan paul podcast',
+        'last podcast on the left',
+        'michael moore podcast',
+        'michelle obama podcast',
+        'missing in alaska podcast',
+        'murder podcast',
+        'nice white parents podcast',
+        'nick cannon podcast',
+        'npr podcast',
+        'office ladies podcast',
+        'podcast app',
+        'podcasts',
+        'rogan podcast',
+        'rudy giuliani podcast',
+        'savage podcast',
+        'serial podcast',
+        'smartless podcast',
+        'ted cruz podcast',
+        'the daily',
+        'the daily podcast',
+        'the last podcast on the left',
+        'the new abnormal podcast',
+        'tiger king podcast',
+        'trey gowdy podcast',
+        'true crime podcast',
+        'what is a podcast',
+        'what is podcast',
+        'wind of change podcast',
+        'your own backyard podcast',
+    ];
+    protected $domains = [
+        '360.cn ',
+        'adobe.com ',
+        'aliexpress.com ',
+        'alipay.com ',
+        'amazon.co.jp ',
+        'amazon.com ',
+        'amazon.in ',
+        'apple.com ',
+        'baidu.com ',
+        'bing.com ',
+        'bongacams.com ',
+        'chaturbate.com ',
+        'china.com.cn ',
+        'csdn.net ',
+        'ebay.com ',
+        'facebook.com ',
+        'google.co.in ',
+        'google.com ',
+        'google.com.hk ',
+        'instagram.com ',
+        'jd.com ',
+        'live.com ',
+        'livejasmin.com ',
+        'microsoft.com ',
+        'microsoftonline.com ',
+        'myshopify.com ',
+        'naver.com ',
+        'netflix.com ',
+        'office.com ',
+        'okezone.com ',
+        'panda.tv ',
+        'qq.com ',
+        'reddit.com ',
+        'sina.com.cn ',
+        'sohu.com ',
+        'taobao.com ',
+        'tianya.cn ',
+        'tmall.com ',
+        'tribunnews.com ',
+        'twitch.tv ',
+        'twitter.com ',
+        'vk.com ',
+        'weibo.com ',
+        'wikipedia.org ',
+        'xinhuanet.com ',
+        'yahoo.co.jp ',
+        'yahoo.com ',
+        'youtube.com ',
+        'zhanqi.tv ',
+        'zoom.us ',
+    ];
+
+    protected $browsers = [
+        'Android Browser',
+        'Avast Secure Browser',
+        'BlackBerry Browser',
+        'Chrome',
+        'Chrome Mobile',
+        'Chrome Mobile iOS',
+        'Chrome Webview',
+        'Chromium',
+        'Ecosia',
+        'Fennec',
+        'Firebird',
+        'Firefox',
+        'Firefox Mobile',
+        'Firefox Mobile iOS',
+        'Galeon',
+        'GNOME Web',
+        'Headless Chrome',
+        'Huawei Browser',
+        'IE Mobile',
+        'Inconnu',
+        'Internet Explorer',
+        'Kindle Browser',
+        'Konqueror',
+        'Maxthon',
+        'Meizu Browser',
+        'Microsoft Edge',
+        'MIUI Browser',
+        'Mobile Safari',
+        'Mobile Silk',
+        'OmniWeb',
+        'Openwave Mobile Browser',
+        'Opera',
+        'Opera Mini',
+        'Opera Mobile',
+        'Opera Next',
+        'Palm Blazer',
+        'Puffin',
+        'QupZilla',
+        'Safari',
+        'Samsung Browser',
+        'UC Browser',
+        'WOSBrowser',
+    ];
+
+    public function run()
+    {
+        $podcast = (new PodcastModel())->first();
+
+        if ($podcast) {
+            $firstEpisode = (new EpisodeModel())
+                ->selectMin('published_at')
+                ->first();
+
+            for (
+                $date = strtotime($firstEpisode->published_at);
+                $date < strtotime('now');
+                $date = strtotime(date('Y-m-d', $date) . ' +1 day')
+            ) {
+                $website_by_browser = [];
+                $website_by_entry_page = [];
+                $website_by_referer = [];
+
+                $episodes = (new EpisodeModel())
+                    ->where([
+                        'podcast_id' => $podcast->id,
+                        'DATE(published_at) <=' => date('Y-m-d', $date),
+                    ])
+                    ->findAll();
+                foreach ($episodes as $episode) {
+                    $age = floor(
+                        ($date - strtotime($episode->published_at)) / 86400
+                    );
+                    $proba1 = floor(exp(3 - $age / 40)) + 1;
+
+                    for (
+                        $num_line = 0;
+                        $num_line < rand(1, $proba1);
+                        $num_line++
+                    ) {
+                        $proba2 = floor(exp(6 - $age / 20)) + 10;
+
+                        $domain =
+                            $this->domains[rand(0, count($this->domains) - 1)];
+                        $keyword =
+                            $this->keywords[
+                                rand(0, count($this->keywords) - 1)
+                            ];
+                        $browser =
+                            $this->browsers[
+                                rand(0, count($this->browsers) - 1)
+                            ];
+
+                        $hits = rand(0, $proba2);
+
+                        $website_by_browser[] = [
+                            'podcast_id' => $podcast->id,
+                            'date' => date('Y-m-d', $date),
+                            'browser' => $browser,
+                            'hits' => $hits,
+                        ];
+                        $website_by_entry_page[] = [
+                            'podcast_id' => $podcast->id,
+                            'date' => date('Y-m-d', $date),
+                            'entry_page' => $episode->link,
+                            'hits' => $hits,
+                        ];
+                        $website_by_referer[] = [
+                            'podcast_id' => $podcast->id,
+                            'date' => date('Y-m-d', $date),
+                            'referer' =>
+                                'http://' . $domain . '/?q=' . $keyword,
+                            'domain' => $domain,
+                            'keywords' => $keyword,
+                            'hits' => $hits,
+                        ];
+                    }
+                }
+                $this->db
+                    ->table('analytics_website_by_browser')
+                    ->ignore(true)
+                    ->insertBatch($website_by_browser);
+                $this->db
+                    ->table('analytics_website_by_entry_page')
+                    ->ignore(true)
+                    ->insertBatch($website_by_entry_page);
+                $this->db
+                    ->table('analytics_website_by_referer')
+                    ->ignore(true)
+                    ->insertBatch($website_by_referer);
+            }
+        } else {
+            echo "Create one podcast and some episodes first.\n";
+        }
+    }
+}
diff --git a/app/Entities/AnalyticsWebsiteByCountry.php b/app/Entities/AnalyticsPodcasts.php
similarity index 67%
rename from app/Entities/AnalyticsWebsiteByCountry.php
rename to app/Entities/AnalyticsPodcasts.php
index 9839e3cb71f0e6db47b33368707a9614ddec5910..7f0f169e17cc6a33da15a39890de8b373ccf764e 100644
--- a/app/Entities/AnalyticsWebsiteByCountry.php
+++ b/app/Entities/AnalyticsPodcasts.php
@@ -1,8 +1,8 @@
 <?php
 
 /**
- * Class AnalyticsWebsiteByCountry
- * Entity for AnalyticsWebsiteByCountry
+ * Class AnalyticsPodcasts
+ * Entity for AnalyticsPodcasts
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
@@ -12,11 +12,10 @@ namespace App\Entities;
 
 use CodeIgniter\Entity;
 
-class AnalyticsWebsiteByCountry extends Entity
+class AnalyticsPodcasts extends Entity
 {
     protected $casts = [
         'podcast_id' => 'integer',
-        'country_code' => 'string',
         'date' => 'datetime',
         'hits' => 'integer',
     ];
diff --git a/app/Entities/AnalyticsEpisodesByPlayer.php b/app/Entities/AnalyticsPodcastsByEpisode.php
similarity index 70%
rename from app/Entities/AnalyticsEpisodesByPlayer.php
rename to app/Entities/AnalyticsPodcastsByEpisode.php
index 3e48c0aa95711b452e13d416e7e9992eade15b29..783bf2d54e1c13f2522d5c8ee99786e9bc570689 100644
--- a/app/Entities/AnalyticsEpisodesByPlayer.php
+++ b/app/Entities/AnalyticsPodcastsByEpisode.php
@@ -1,8 +1,8 @@
 <?php
 
 /**
- * Class AnalyticsEpisodesByPlayer
- * Entity for AnalyticsEpisodesByPlayer
+ * Class AnalyticsPodcastsByEpisode
+ * Entity for AnalyticsPodcastsByEpisode
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
@@ -12,12 +12,11 @@ namespace App\Entities;
 
 use CodeIgniter\Entity;
 
-class AnalyticsEpisodesByPlayer extends Entity
+class AnalyticsPodcastsByEpisode extends Entity
 {
     protected $casts = [
         'podcast_id' => 'integer',
         'episode_id' => 'integer',
-        'player' => 'string',
         'date' => 'datetime',
         'hits' => 'integer',
     ];
diff --git a/app/Entities/AnalyticsPodcastsByPlayer.php b/app/Entities/AnalyticsPodcastsByPlayer.php
index 9e33ba980704e67fd6c72354431284e91cbdf737..b0e19d26948fe5017458651d2eb0efdf6f118456 100644
--- a/app/Entities/AnalyticsPodcastsByPlayer.php
+++ b/app/Entities/AnalyticsPodcastsByPlayer.php
@@ -16,7 +16,10 @@ class AnalyticsPodcastsByPlayer extends Entity
 {
     protected $casts = [
         'podcast_id' => 'integer',
-        'player' => 'string',
+        'app' => '?string',
+        'device' => '?string',
+        'os' => '?string',
+        'bot' => 'boolean',
         'date' => 'datetime',
         'hits' => 'integer',
     ];
diff --git a/app/Entities/AnalyticsPodcastsByRegion.php b/app/Entities/AnalyticsPodcastsByRegion.php
new file mode 100644
index 0000000000000000000000000000000000000000..8f6a9d603056f4c5d697e3569c0b3337de031e98
--- /dev/null
+++ b/app/Entities/AnalyticsPodcastsByRegion.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * Class AnalyticsPodcastsByRegion
+ * Entity for AnalyticsPodcastsByRegion
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Entities;
+
+use CodeIgniter\Entity;
+
+class AnalyticsPodcastsByRegion extends Entity
+{
+    protected $casts = [
+        'podcast_id' => 'integer',
+        'country_code' => 'string',
+        'region_code' => '?string',
+        'latitude' => '?float',
+        'longitude' => '?float',
+        'date' => 'datetime',
+        'hits' => 'integer',
+    ];
+}
diff --git a/app/Entities/AnalyticsEpisodesByCountry.php b/app/Entities/AnalyticsWebsiteByEntryPage.php
similarity index 62%
rename from app/Entities/AnalyticsEpisodesByCountry.php
rename to app/Entities/AnalyticsWebsiteByEntryPage.php
index b1736443194e8967ff958caa4d778f596069a46c..344d60fb2bc4dd136977c656d84b715a015c5a96 100644
--- a/app/Entities/AnalyticsEpisodesByCountry.php
+++ b/app/Entities/AnalyticsWebsiteByEntryPage.php
@@ -1,8 +1,8 @@
 <?php
 
 /**
- * Class AnalyticsEpisodesByCountry
- * Entity for AnalyticsEpisodesByCountry
+ * Class AnalyticsWebsiteByEntryPage
+ * Entity for AnalyticsWebsiteByEntryPage
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
@@ -12,12 +12,11 @@ namespace App\Entities;
 
 use CodeIgniter\Entity;
 
-class AnalyticsEpisodesByCountry extends Entity
+class AnalyticsWebsiteByEntryPage extends Entity
 {
     protected $casts = [
         'podcast_id' => 'integer',
-        'episode_id' => 'integer',
-        'country_code' => 'string',
+        'entry_page' => '?string',
         'date' => 'datetime',
         'hits' => 'integer',
     ];
diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php
index 7bbda5e53f62fb963ae588897f4128a2c8943a52..26f0ee5c27d2186301e064715a65a8cf5c460911 100644
--- a/app/Entities/Episode.php
+++ b/app/Entities/Episode.php
@@ -64,6 +64,7 @@ class Episode extends Entity
         'enclosure_duration' => 'integer',
         'enclosure_mimetype' => 'string',
         'enclosure_filesize' => 'integer',
+        'enclosure_headersize' => 'integer',
         'description' => 'string',
         'image_uri' => '?string',
         'parental_advisory' => '?string',
@@ -143,6 +144,8 @@ class Episode extends Entity
                 $enclosure_metadata['mime_type'];
             $this->attributes['enclosure_filesize'] =
                 $enclosure_metadata['filesize'];
+            $this->attributes['enclosure_headersize'] =
+                $enclosure_metadata['avdataoffset'];
 
             return $this;
         }
@@ -167,6 +170,19 @@ class Episode extends Entity
                 'analytics_hit',
                 $this->attributes['podcast_id'],
                 $this->attributes['id'],
+                // bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics
+                // - if file is shorter than 60sec, then it's enclosure_filesize
+                // - if file is longer than 60 seconds then it's enclosure_headersize + 60 seconds
+                $this->attributes['enclosure_duration'] <= 60
+                    ? $this->attributes['enclosure_filesize']
+                    : $this->attributes['enclosure_headersize'] +
+                        floor(
+                            (($this->attributes['enclosure_filesize'] -
+                                $this->attributes['enclosure_headersize']) /
+                                $this->attributes['enclosure_duration']) *
+                                60
+                        ),
+                $this->attributes['enclosure_filesize'],
                 $this->attributes['enclosure_uri']
             )
         );
diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php
index c0dd2b7defbf5c961a792f1c74edfafec060a582..f60e44dc2b8f20552cff94a23b4fa96926b95e23 100644
--- a/app/Entities/Podcast.php
+++ b/app/Entities/Podcast.php
@@ -82,6 +82,7 @@ class Podcast extends Entity
         'created_by' => 'integer',
         'updated_by' => 'integer',
         'imported_feed_url' => '?string',
+        'new_feed_url' => '?string',
     ];
 
     /**
diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php
index d0ca06cb1c3407d18b95ff6d53fe201eb2a3c994..51561e954a8f152aa10ea5c189d3c3b9e9bd6c26 100644
--- a/app/Helpers/analytics_helper.php
+++ b/app/Helpers/analytics_helper.php
@@ -33,25 +33,56 @@ if (!function_exists('getallheaders')) {
 /**
  * Set user country in session variable, for analytics purpose
  */
-function set_user_session_country()
+function set_user_session_deny_list_ip()
 {
     $session = \Config\Services::session();
     $session->start();
 
-    $country = 'N/A';
+    if (!$session->has('denyListIp')) {
+        $session->set(
+            'denyListIp',
+            \Podlibre\Ipcat\IpDb::find($_SERVER['REMOTE_ADDR']) != null
+        );
+    }
+}
 
-    // Finds country:
-    if (!$session->has('country')) {
+/**
+ * Set user country in session variable, for analytics purpose
+ */
+function set_user_session_location()
+{
+    $session = \Config\Services::session();
+    $session->start();
+
+    $location = [
+        'countryCode' => 'N/A',
+        'regionCode' => 'N/A',
+        'latitude' => null,
+        'longitude' => null,
+    ];
+
+    // Finds location:
+    if (!$session->has('location')) {
         try {
-            $reader = new \GeoIp2\Database\Reader(
-                WRITEPATH . 'uploads/GeoLite2-Country/GeoLite2-Country.mmdb'
+            $cityReader = new \GeoIp2\Database\Reader(
+                WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb'
             );
-            $geoip = $reader->country($_SERVER['REMOTE_ADDR']);
-            $country = $geoip->country->isoCode;
+            $city = $cityReader->city($_SERVER['REMOTE_ADDR']);
+
+            $location = [
+                'countryCode' => empty($city->country->isoCode)
+                    ? 'N/A'
+                    : $city->country->isoCode,
+                'regionCode' => empty($city->subdivisions[0]->isoCode)
+                    ? 'N/A'
+                    : $city->subdivisions[0]->isoCode,
+                'latitude' => round($city->location->latitude, 3),
+                'longitude' => round($city->location->longitude, 3),
+            ];
         } catch (\Exception $e) {
             // If things go wrong the show must go on and the user must be able to download the file
         }
-        $session->set('country', $country);
+        $session->set('location', $location);
     }
 }
 
@@ -67,58 +98,36 @@ function set_user_session_player()
         $session = \Config\Services::session();
         $session->start();
 
-        $playerName = '- Unknown Player -';
-
-        $useragent = $_SERVER['HTTP_USER_AGENT'];
+        $playerFound = null;
+        $userAgent = $_SERVER['HTTP_USER_AGENT'];
 
         try {
-            $jsonUserAgents = json_decode(
-                file_get_contents(
-                    WRITEPATH . 'uploads/user-agents/src/user-agents.json'
-                ),
-                true
-            );
-
-            //Search for current HTTP_USER_AGENT in json file:
-            foreach ($jsonUserAgents as $player) {
-                foreach ($player['user_agents'] as $useragentsRegexp) {
-                    //Does the HTTP_USER_AGENT match this regexp:
-                    if (preg_match("#{$useragentsRegexp}#", $useragent)) {
-                        if (isset($player['bot'])) {
-                            //It’s a bot!
-                            $playerName = '- Bot -';
-                        } else {
-                            //It isn’t a bot, we store device/os/app:
-                            $playerName =
-                                (isset($player['device'])
-                                    ? $player['device'] . '/'
-                                    : '') .
-                                (isset($player['os'])
-                                    ? $player['os'] . '/'
-                                    : '') .
-                                (isset($player['app']) ? $player['app'] : '?');
-                        }
-                        //We found it!
-                        break 2;
-                    }
-                }
-            }
+            $playerFound = \Podlibre\UserAgentsPhp\UserAgents::find($userAgent);
         } catch (\Exception $e) {
             // If things go wrong the show must go on and the user must be able to download the file
         }
-        if ($playerName == '- Unknown Player -') {
+        if ($playerFound) {
+            $session->set('player', $playerFound);
+        } else {
+            $session->set('player', [
+                'app' => '- unknown -',
+                'device' => '',
+                'os' => '',
+                'bot' => 0,
+            ]);
             // Add to unknown list
             try {
                 $db = \Config\Database::connect();
-                $procedureNameAUU = $db->prefixTable(
+                $procedureNameAnalyticsUnknownUseragents = $db->prefixTable(
                     'analytics_unknown_useragents'
                 );
-                $db->query("CALL $procedureNameAUU(?)", [$useragent]);
+                $db->query("CALL $procedureNameAnalyticsUnknownUseragents(?)", [
+                    $userAgent,
+                ]);
             } catch (\Exception $e) {
                 // If things go wrong the show must go on and the user must be able to download the file
             }
         }
-        $session->set('player', $playerName);
     }
 }
 
@@ -165,49 +174,149 @@ function set_user_session_referer()
     }
 }
 
+/**
+ * Set user entry page in session variable, for analytics purpose
+ */
+function set_user_session_entry_page()
+{
+    $session = \Config\Services::session();
+    $session->start();
+
+    $entryPage = $_SERVER['REQUEST_URI'];
+    if (!$session->has('entryPage')) {
+        $session->set('entryPage', $entryPage);
+    }
+}
+
 function webpage_hit($podcast_id)
 {
     $session = \Config\Services::session();
     $session->start();
-    $db = \Config\Database::connect();
-
-    $procedureName = $db->prefixTable('analytics_website');
-    $db->query("call $procedureName(?,?,?,?)", [
-        $podcast_id,
-        $session->get('country'),
-        $session->get('browser'),
-        $session->get('referer'),
-    ]);
+
+    if (!$session->get('denyListIp')) {
+        $db = \Config\Database::connect();
+
+        $referer = $session->get('referer');
+        $domain = empty(parse_url($referer, PHP_URL_HOST))
+            ? null
+            : parse_url($referer, PHP_URL_HOST);
+        parse_str(parse_url($referer, PHP_URL_QUERY), $queries);
+        $keywords = empty($queries['q']) ? null : $queries['q'];
+
+        $procedureName = $db->prefixTable('analytics_website');
+        $db->query("call $procedureName(?,?,?,?,?,?)", [
+            $podcast_id,
+            $session->get('browser'),
+            $session->get('entryPage'),
+            $referer,
+            $domain,
+            $keywords,
+        ]);
+    }
 }
 
-function podcast_hit($p_podcast_id, $p_episode_id)
+/**
+ * Counting podcast episode downloads for analytics purposes
+ * ✅ No IP address is ever stored on the server.
+ * ✅ Only aggregate data is stored in the database.
+ * We follow IAB Podcast Measurement Technical Guidelines Version 2.0:
+ *   https://iabtechlab.com/standards/podcast-measurement-guidelines/
+ *   https://iabtechlab.com/wp-content/uploads/2017/12/Podcast_Measurement_v2-Dec-20-2017.pdf
+ *   ✅ Rolling 24-hour window
+ *   ✅ Castopod does not do pre-load
+ *   ✅ IP deny list https://github.com/client9/ipcat
+ *   ✅ User-agent Filtering https://github.com/opawg/user-agents
+ *   ✅ Ignores 2 bytes range "Range: 0-1"  (performed by official Apple iOS Podcast app)
+ *   ✅ In case of partial content, adds up all requests to check >1mn was downloaded
+ *   ✅ Identifying Uniques is done with a combination of IP Address and User Agent
+ * @param int $podcastId The podcast ID
+ * @param int $episodeId The Episode ID
+ * @param int $bytesThreshold The minimum total number of bytes that must be downloaded so that an episode is counted (>1mn)
+ * @param int $fileSize The podcast complete file size
+ *
+ * @return void
+ */
+function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize)
 {
     $session = \Config\Services::session();
     $session->start();
-    $first_time_for_this_episode = true;
 
-    if ($session->has('episodes')) {
-        if (in_array($p_episode_id, $session->get('episodes'))) {
-            $first_time_for_this_episode = false;
+    // We try to count (but if things went wrong the show should go on and the user should be able to download the file):
+    try {
+        // If the user IP is denied it's probably a bot:
+        if ($session->get('denyListIp')) {
+            $session->get('player')['bot'] = true;
+        }
+        $httpRange = $_SERVER['HTTP_RANGE'];
+        // We create a sha1 hash for this IP_Address+User_Agent+Episode_ID:
+        $hashID =
+            '_IpUaEp_' .
+            sha1(
+                $_SERVER['REMOTE_ADDR'] .
+                    '_' .
+                    $_SERVER['HTTP_USER_AGENT'] .
+                    '_' .
+                    $episodeId
+            );
+        // Was this episode downloaded in the past 24h:
+        $downloadedBytes = cache($hashID);
+        // Rolling window is 24 hours (86400 seconds):
+        $ttl = 86400;
+        if ($downloadedBytes) {
+            // In case it was already downloaded, TTL should be adjusted (rolling window is 24h since 1st download):
+            $ttl = cache()->getMetadata($hashID)['expire'] - time();
         } else {
-            $session->push('episodes', [$p_episode_id]);
+            // If it was never downloaded that means that zero byte were downloaded:
+            $downloadedBytes = 0;
         }
-    } else {
-        $session->set('episodes', [$p_episode_id]);
-    }
+        // If the number of downloaded bytes was previously below the 1mn threshold we go on:
+        // (Otherwise it means that this was already counted, therefore we don't do anything)
+        if ($downloadedBytes < $bytesThreshold) {
+            // If HTTP_RANGE is null we are downloading the complete file:
+            if (!isset($httpRange)) {
+                $downloadedBytes = $fileSize;
+            } else {
+                // [0-1] bytes range requests are used (by Apple) to check that file exists and that 206 partial content is working.
+                // We don't count these requests:
+                if ($httpRange != 'bytes=0-1') {
+                    // We calculate how many bytes are being downloaded based on HTTP_RANGE values:
+                    $ranges = explode(',', substr($httpRange, 6));
+                    foreach ($ranges as $range) {
+                        $parts = explode('-', $range);
+                        $downloadedBytes += empty($parts[1])
+                            ? $fileSize
+                            : $parts[1] - (empty($parts[0]) ? 0 : $parts[0]);
+                    }
+                }
+            }
+            // We save the number of downloaded bytes for this user and this episode:
+            cache()->save($hashID, $downloadedBytes, $ttl);
 
-    if ($first_time_for_this_episode) {
-        $db = \Config\Database::connect();
-        $procedureName = $db->prefixTable('analytics_podcasts');
-        try {
-            $db->query("CALL $procedureName(?,?,?,?);", [
-                $p_podcast_id,
-                $p_episode_id,
-                $session->get('country'),
-                $session->get('player'),
-            ]);
-        } catch (\Exception $e) {
-            // If things go wrong the show must go on and the user must be able to download the file
+            // If more that 1mn was downloaded, we send that to the database:
+            if ($downloadedBytes >= $bytesThreshold) {
+                $db = \Config\Database::connect();
+                $procedureName = $db->prefixTable('analytics_podcasts');
+
+                $app = $session->get('player')['app'];
+                $device = $session->get('player')['device'];
+                $os = $session->get('player')['os'];
+                $bot = $session->get('player')['bot'];
+
+                $db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?);", [
+                    $podcastId,
+                    $episodeId,
+                    $session->get('location')['countryCode'],
+                    $session->get('location')['regionCode'],
+                    $session->get('location')['latitude'],
+                    $session->get('location')['longitude'],
+                    $app == null ? '' : $app,
+                    $device == null ? '' : $device,
+                    $os == null ? '' : $os,
+                    $bot == null ? 0 : $bot,
+                ]);
+            }
         }
+    } catch (\Exception $e) {
+        // If things go wrong the show must go on and the user must be able to download the file
     }
 }
diff --git a/app/Helpers/id3_helper.php b/app/Helpers/id3_helper.php
index a7c1685a54236f07bbc0d24520c19e83c2abc596..61c21e2e55d3f6c29fc83a078a5bc07444e30a94 100644
--- a/app/Helpers/id3_helper.php
+++ b/app/Helpers/id3_helper.php
@@ -24,6 +24,7 @@ function get_file_tags($file)
     return [
         'filesize' => $FileInfo['filesize'],
         'mime_type' => $FileInfo['mime_type'],
+        'avdataoffset' => $FileInfo['avdataoffset'],
         'playtime_seconds' => $FileInfo['playtime_seconds'],
     ];
 }
@@ -68,7 +69,11 @@ function write_enclosure_tags($episode)
         'comment' => [$episode->description],
         'track_number' => [strval($episode->number)],
         'copyright_message' => [$episode->podcast->copyright],
-        'publisher' => ['Podlibre'],
+        'publisher' => [
+            empty($episode->podcast->publisher)
+                ? $episode->podcast->owner_name
+                : $episode->podcast->publisher,
+        ],
         'encoded_by' => ['Castopod'],
 
         // TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php
index 532b9bcb57a57e1ae16001fe109addd3fe1e16d8..9f17a2f73c380065c6413257e02835c365ee598c 100644
--- a/app/Helpers/rss_helper.php
+++ b/app/Helpers/rss_helper.php
@@ -36,6 +36,14 @@ function get_rss_feed($podcast)
     $atom_link->addAttribute('rel', 'self');
     $atom_link->addAttribute('type', 'application/rss+xml');
 
+    if (!empty($podcast->new_feed_url)) {
+        $channel->addChild(
+            'new-feed-url',
+            $podcast->new_feed_url,
+            $itunes_namespace
+        );
+    }
+
     // the last build date corresponds to the creation of the feed.xml cache
     $channel->addChild(
         'lastBuildDate',
@@ -50,7 +58,7 @@ function get_rss_feed($podcast)
     $channel->addChild('title', $podcast->title);
     $channel->addChildWithCDATA('description', $podcast->description_html);
     $itunes_image = $channel->addChild('image', null, $itunes_namespace);
-    $itunes_image->addAttribute('href', $podcast->image->url);
+    $itunes_image->addAttribute('href', $podcast->image->original_url);
     $channel->addChild('language', $podcast->language);
 
     // set main category first, then other categories as apple
diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php
index 70e5beb9ceec00fa9b439075d135b3dd8781b220..5827731b8f23af69c99a1b97620dfdf8a2eec995 100644
--- a/app/Language/en/Breadcrumb.php
+++ b/app/Language/en/Breadcrumb.php
@@ -22,4 +22,5 @@ return [
     'import' => 'feed import',
     'settings' => 'settings',
     'platforms' => 'platforms',
+    'analytics' => 'Analytics',
 ];
diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php
index 8a86df06c9cef6d47e7dce64b0c127ec9e88cb7d..fc54298c41d571240f96300b36d72e986b6fdc8a 100644
--- a/app/Language/en/Podcast.php
+++ b/app/Language/en/Podcast.php
@@ -12,7 +12,7 @@ return [
     'create' => 'Create a podcast',
     'import' => 'Import a podcast',
     'new_episode' => 'New Episode',
-    'feed' => 'RSS feed',
+    'feed' => 'RSS',
     'view' => 'View podcast',
     'edit' => 'Edit podcast',
     'delete' => 'Delete podcast',
diff --git a/app/Language/en/PodcastImport.php b/app/Language/en/PodcastImport.php
index 6b86eb1693dfe2dd8ec7cc2f06ea376a2f7ccbf1..3bb3a912c268e7d04b3edb3e781b3269c046cd7a 100644
--- a/app/Language/en/PodcastImport.php
+++ b/app/Language/en/PodcastImport.php
@@ -7,6 +7,12 @@
  */
 
 return [
+    'legal_dislaimer_title' => 'Legal Disclaimer',
+    'legal_dislaimer_content' =>
+        'Make sure you own the rights for this podcast before importing it.<br/>Copying and broadcasting a podcast without the proper rights is piracy and is liable to prosecution.',
+    'warning_title' => 'Warning',
+    'warning_content' =>
+        'This procedure may take a long time.<br/>The current version does not show any progress while it runs. You will not see anything updated until it is done.<br/>In case of timeout error, increase max_execution_time value.',
     'old_podcast_section_title' => 'The podcast to import',
     'old_podcast_section_subtitle' => '',
     'imported_feed_url' => 'Feed URL',
diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php
index 05fe31f198e986913d8a60437d0efba4c71439b6..3d15320696645e456adab4ab314b9631a4e8102d 100644
--- a/app/Language/en/PodcastNavigation.php
+++ b/app/Language/en/PodcastNavigation.php
@@ -20,4 +20,5 @@ return [
     'contributor-add' => 'Add contributor',
     'settings' => 'Settings',
     'platforms' => 'Podcast platforms',
+    'podcast-analytics' => 'Audiences Overview',
 ];
diff --git a/app/Models/AnalyticsPodcastsByCountryModel.php b/app/Models/AnalyticsPodcastsByCountryModel.php
index 70f5fc3e77ecdc1454447db4565309a31dc74a3a..4f2094532ce8d7d4235a4e202d867899be08f7d9 100644
--- a/app/Models/AnalyticsPodcastsByCountryModel.php
+++ b/app/Models/AnalyticsPodcastsByCountryModel.php
@@ -2,7 +2,7 @@
 
 /**
  * Class AnalyticsPodcastsByCountryModel
- * Model for analytics_episodes_by_country table in database
+ * Model for analytics_podcasts_by_country table in database
  * @copyright  2020 Podlibre
  * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
  * @link       https://castopod.org/
@@ -14,8 +14,7 @@ use CodeIgniter\Model;
 
 class AnalyticsPodcastsByCountryModel extends Model
 {
-    protected $table = 'analytics_episodes_by_country';
-    protected $primaryKey = 'id';
+    protected $table = 'analytics_podcasts_by_country';
 
     protected $allowedFields = [];
 
diff --git a/app/Models/AnalyticsPodcastsByEpisodeModel.php b/app/Models/AnalyticsPodcastsByEpisodeModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..59c82360ddcaa6e0b1d284da93103bbc1d758e48
--- /dev/null
+++ b/app/Models/AnalyticsPodcastsByEpisodeModel.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * Class AnalyticsPodcastsByEpisodeModel
+ * Model for analytics_podcasts_by_episodes table in database
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class AnalyticsPodcastsByEpisodeModel extends Model
+{
+    protected $table = 'analytics_podcasts_by_episode';
+
+    protected $allowedFields = [];
+
+    protected $returnType = \App\Entities\AnalyticsPodcastsByEpisode::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = false;
+
+    /**
+     * @param int $podcastId, $episodeId
+     *
+     * @return array
+     */
+    public function getDataByDay(int $podcastId, int $episodeId = null): array
+    {
+        if (!$episodeId) {
+            if (
+                !($found = cache(
+                    "{$podcastId}_analytics_podcast_by_episode_by_day"
+                ))
+            ) {
+                $lastEpisodes = (new EpisodeModel())
+                    ->select('id, season_number, number, title')
+                    ->orderBy('id', 'DESC')
+                    ->where(['podcast_id' => $podcastId])
+                    ->findAll(5);
+
+                $found = $this->select('age AS X');
+
+                $letter = 97;
+                foreach ($lastEpisodes as $episode) {
+                    $found = $found
+                        ->selectSum(
+                            '(CASE WHEN `episode_id`=' .
+                                $episode->id .
+                                ' THEN `hits` END)',
+                            chr($letter) . 'Y'
+                        )
+                        ->select(
+                            '"' .
+                                (empty($episode->season_number)
+                                    ? ''
+                                    : $episode->season_number) .
+                                (empty($episode->number)
+                                    ? ''
+                                    : '-' . $episode->number . '/ ') .
+                                $episode->title .
+                                '" AS ' .
+                                chr($letter) .
+                                'Value'
+                        );
+                    $letter++;
+                }
+
+                $found = $found
+                    ->where([
+                        'podcast_id' => $podcastId,
+                        'age <' => 60,
+                    ])
+                    ->groupBy('X')
+                    ->orderBy('X', 'ASC')
+                    ->findAll();
+
+                cache()->save(
+                    "{$podcastId}_analytics_podcast_by_episode_by_day",
+                    $found,
+                    14400
+                );
+            }
+            return $found;
+        } else {
+            if (
+                !($found = cache(
+                    "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day"
+                ))
+            ) {
+                $found = $this->select('date 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_day",
+                    $found,
+                    14400
+                );
+            }
+            return $found;
+        }
+    }
+}
diff --git a/app/Models/AnalyticsPodcastsByPlayerModel.php b/app/Models/AnalyticsPodcastsByPlayerModel.php
index 5e0ff822f592e8f5ccbcd13018878b4b4c9eeb0c..900b44fa3929003df4737288e676450853d7ef74 100644
--- a/app/Models/AnalyticsPodcastsByPlayerModel.php
+++ b/app/Models/AnalyticsPodcastsByPlayerModel.php
@@ -15,7 +15,6 @@ use CodeIgniter\Model;
 class AnalyticsPodcastsByPlayerModel extends Model
 {
     protected $table = 'analytics_podcasts_by_player';
-    protected $primaryKey = 'id';
 
     protected $allowedFields = [];
 
@@ -23,4 +22,120 @@ class AnalyticsPodcastsByPlayerModel extends Model
     protected $useSoftDeletes = false;
 
     protected $useTimestamps = false;
+
+    /**
+     * Gets all data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataByApp(int $podcastId): array
+    {
+        if (
+            !($found = cache(
+                "{$podcastId}_analytics_podcasts_by_player_by_app"
+            ))
+        ) {
+            $found = $this->select('`app` as `labels`')
+                ->selectSum('`hits`', '`values`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`app` !=' => null,
+                    '`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_app",
+                $found,
+                14400
+            );
+        }
+
+        return $found;
+    }
+
+    /**
+     * Gets all data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataByDevice(int $podcastId): array
+    {
+        if (
+            !($found = cache(
+                "{$podcastId}_analytics_podcasts_by_player_by_device"
+            ))
+        ) {
+            $foundApp = $this->select(
+                'CONCAT_WS("/", `device`, `os`, `app`) as `ids`, `app` as `labels`, CONCAT_WS("/", `device`, `os`) as `parents`'
+            )
+                ->selectSum('`hits`', '`values`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`app` !=' => null,
+                    '`bot`' => 0,
+                    '`date` >' => date('Y-m-d', strtotime('-1 week')),
+                ])
+                ->groupBy('`ids`')
+                ->orderBy('`values``', 'DESC')
+                ->findAll();
+
+            $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(
+                "{$podcastId}_analytics_podcasts_by_player_by_device",
+                $found,
+                14400
+            );
+        }
+
+        return $found;
+    }
 }
diff --git a/app/Models/AnalyticsPodcastsByRegionModel.php b/app/Models/AnalyticsPodcastsByRegionModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..81ab8537f89a322fda5b0b5973c23b2aea736eed
--- /dev/null
+++ b/app/Models/AnalyticsPodcastsByRegionModel.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * Class AnalyticsPodcastsByRegionModel
+ * Model for analytics_podcasts_by_region table in database
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class AnalyticsPodcastsByRegionModel extends Model
+{
+    protected $table = 'analytics_podcasts_by_region';
+
+    protected $allowedFields = [];
+
+    protected $returnType = \App\Entities\AnalyticsPodcastsByRegion::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = false;
+}
diff --git a/app/Models/AnalyticsPodcastsModel.php b/app/Models/AnalyticsPodcastsModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..8ab115a0ea6a38733344ed1748cbbc99eb101cc7
--- /dev/null
+++ b/app/Models/AnalyticsPodcastsModel.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * Class AnalyticsPodcastsModel
+ * Model for analytics_podcasts table in database
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class AnalyticsPodcastsModel extends Model
+{
+    protected $table = 'analytics_podcasts';
+
+    protected $allowedFields = [];
+
+    protected $returnType = \App\Entities\AnalyticsPodcasts::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = false;
+
+    /**
+     * Gets all data for a podcast
+     *
+     * @param int $podcastId
+     *
+     * @return array
+     */
+    public function getDataByDay(int $podcastId): array
+    {
+        if (!($found = cache("{$podcastId}_analytics_podcast_by_day"))) {
+            $found = $this->select('`date` as `labels`')
+                ->selectSum('`hits`', '`values`')
+                ->where([
+                    '`podcast_id`' => $podcastId,
+                    '`date` >' => date('Y-m-d', strtotime('-1 year')),
+                ])
+                ->groupBy('`labels`')
+                ->orderBy('`labels``', 'ASC')
+                ->findAll();
+
+            cache()->save(
+                "{$podcastId}_analytics_podcast_by_day",
+                $found,
+                14400
+            );
+        }
+
+        return $found;
+    }
+}
diff --git a/app/Models/AnalyticsWebsiteByBrowserModel.php b/app/Models/AnalyticsWebsiteByBrowserModel.php
index ceee6b3e09dacc4af51bf7ca26427b00f0f58bf0..85b4fc92fe65d8fde7e19f11d1b4790b5099f84e 100644
--- a/app/Models/AnalyticsWebsiteByBrowserModel.php
+++ b/app/Models/AnalyticsWebsiteByBrowserModel.php
@@ -15,7 +15,6 @@ use CodeIgniter\Model;
 class AnalyticsWebsiteByBrowserModel extends Model
 {
     protected $table = 'analytics_website_by_browser';
-    protected $primaryKey = 'id';
 
     protected $allowedFields = [];
 
diff --git a/app/Models/AnalyticsWebsiteByEntryPageModel.php b/app/Models/AnalyticsWebsiteByEntryPageModel.php
new file mode 100644
index 0000000000000000000000000000000000000000..6e7cfa0c9a0e2f31189f79ed3eb744b8ff123e1c
--- /dev/null
+++ b/app/Models/AnalyticsWebsiteByEntryPageModel.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * Class AnalyticsWebsiteByEntryPageModel
+ * Model for analytics_website_by_entry_page table in database
+ * @copyright  2020 Podlibre
+ * @license    https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
+ * @link       https://castopod.org/
+ */
+
+namespace App\Models;
+
+use CodeIgniter\Model;
+
+class AnalyticsWebsiteByEntryPageModel extends Model
+{
+    protected $table = 'analytics_website_by_entry_page';
+
+    protected $allowedFields = [];
+
+    protected $returnType = \App\Entities\AnalyticsWebsiteByEntryPage::class;
+    protected $useSoftDeletes = false;
+
+    protected $useTimestamps = false;
+}
diff --git a/app/Models/AnalyticsWebsiteByRefererModel.php b/app/Models/AnalyticsWebsiteByRefererModel.php
index 108a6847ab3c255085a5e1d3dc2846184140652e..5d60298ce1cee9f526a1c0c3cf3bfd0b09d74ab8 100644
--- a/app/Models/AnalyticsWebsiteByRefererModel.php
+++ b/app/Models/AnalyticsWebsiteByRefererModel.php
@@ -15,7 +15,6 @@ use CodeIgniter\Model;
 class AnalyticsWebsiteByRefererModel extends Model
 {
     protected $table = 'analytics_website_by_referer';
-    protected $primaryKey = 'id';
 
     protected $allowedFields = [];
 
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index d72f3f08654f29342656758a134251ed0efeba79..95ca8abb711b51775eb4db8e6f7ef6a161d9d5da 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -24,6 +24,7 @@ class EpisodeModel extends Model
         'enclosure_duration',
         'enclosure_mimetype',
         'enclosure_filesize',
+        'enclosure_headersize',
         'description',
         'image_uri',
         'parental_advisory',
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 2df723492184d0ba5ea6b0fc793ce3e7179bad6e..1fd98403bfe7e0eea9b77f8d317e48a0789edd13 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -35,6 +35,7 @@ class PodcastModel extends Model
         'created_by',
         'updated_by',
         'imported_feed_url',
+        'new_feed_url',
     ];
 
     protected $returnType = \App\Entities\Podcast::class;
diff --git a/app/Views/_assets/charts.ts b/app/Views/_assets/charts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..de52bd13f433cbd3a41ed9ea2e02d6c4cf5ada0c
--- /dev/null
+++ b/app/Views/_assets/charts.ts
@@ -0,0 +1,4 @@
+import "core-js";
+import DrawCharts from "./modules/Charts";
+
+DrawCharts();
diff --git a/app/Views/_assets/images/logo-castopod-circle.svg b/app/Views/_assets/images/logo-castopod-circle.svg
new file mode 100644
index 0000000000000000000000000000000000000000..562d13facf3f588e1b9542bf7e5562ae9a63ed71
--- /dev/null
+++ b/app/Views/_assets/images/logo-castopod-circle.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata>
+<style type="text/css">
+	.st0{fill:#009486;}
+	.st1{fill:#E7F9E4;}
+	.st2{fill:none;}
+	.st3{fill:#E7FFE3;}
+</style>
+<circle cx="32" cy="32" r="32" fill="#e7f9e4" stroke-width="2.0334"/><g transform="matrix(.24971 0 0 .24971 6.7291 14.595)">
+	<path id="dark_greeen_19_" class="st0" d="m181.9 131.7h-32.5s-1.2-2.5-2.5-4.9-4.4-2.3-4.4-2.3h-82.8s-3-0.4-4.5 2.3c-1.6 2.7-2.6 4.9-2.6 4.9h-32c-6.9 0-12.6-5.6-12.6-12.5v-98.9c0-6.9 5.6-12.6 12.5-12.6h161.3c6.9 0 12.6 5.6 12.6 12.5v98.9c0.1 6.9-5.6 12.6-12.5 12.6z"/>
+	<path class="st1" d="m143.7 34.5h-85.1c-14.6 0-26.5 12-26.5 26.6s11.9 26.5 26.5 26.5h85.1c14.6 0 26.5-11.9 26.5-26.5 0.1-14.8-11.8-26.7-26.5-26.6zm-75.4 34.2s-3.9-2.9-9.4-2.9c-4.1 0-8.9 2.5-8.9 2.5-1.3-1.9-2.1-4.1-2.1-6.6 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0 2.7-0.9 5.1-2.4 7zm32.9 6.6c-12.5 0-12-9.6-12-9.6-0.2-1.8 2.1-2.4 2.9-1.3 0.4 0.6 0.4 0.6 0.7 1.7 1.7 5.9 8.4 5.6 8.4 5.6s6.7 0.4 8.4-5.6c0.3-1 0.3-1.1 0.7-1.7 0.8-1 3.1-0.5 2.9 1.3 0 0 0.5 9.6-12 9.6zm51.1-6.9s-4.8-2.5-8.9-2.5c-5.5 0-9.4 2.9-9.4 2.9-1.5-1.9-2.4-4.3-2.4-7 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0.1 2.4-0.7 4.7-2.1 6.6z"/>
+	<path class="st2" d="m110.3 64.3c-0.4 0.6-0.4 0.6-0.7 1.7-1.7 5.9-8.4 5.6-8.4 5.6s-6.7 0.4-8.4-5.6c-0.3-1-0.3-1.1-0.7-1.7-0.8-1-3.1-0.5-2.9 1.3 0 0-0.5 9.6 12 9.6s12-9.6 12-9.6c0.2-1.7-2.1-2.3-2.9-1.3z"/>
+	<path class="st2" d="m143.1 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.6 0.9 5 2.4 7 0 0 3.9-2.9 9.4-2.9 4.1 0 8.9 2.5 8.9 2.5 1.3-1.9 2.1-4.1 2.1-6.6 0-6.3-5.1-11.4-11.4-11.4z"/>
+	<path class="st2" d="m59.3 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.5 0.8 4.7 2.1 6.6 0 0 4.8-2.5 8.9-2.5 5.5 0 9.4 2.9 9.4 2.9 1.5-1.9 2.4-4.3 2.4-7 0-6.3-5.1-11.4-11.4-11.4z"/>
+	
+		
+			<path class="st3" d="m47.1 23.3c-6.3-1.7-11.7 2.1-14.7 7.3-0.7 1.2-0.2 2.2 0.5 2.6 1 0.3 1.7 0.1 2.8-1.5 2.2-3.9 5.9-6.1 10.1-5.3 0 0 2.9 0.9 3.3-1 0.3-1.2-0.8-1.8-2-2.1z"/>
+		
+	
+	
+		
+			<path class="st3" d="m159.9 27.3c-0.1 1.9 2.9 1.9 2.9 1.9 4.2 0.4 6.8 2.3 7.8 6.7 0.6 1.9 1.2 2.2 2.3 2.2 0.8-0.1 1.6-1 1.2-2.4-1.4-5.8-5.1-9.8-11.7-9.9-1.2-0.1-2.4 0.2-2.5 1.5z"/>
+		
+	
+</g>
+</svg>
diff --git a/app/Views/_assets/images/logo-castopod.svg b/app/Views/_assets/images/logo-castopod.svg
index 191b6cc98da66a3e9544ee263650e3306794d02b..0208232af0c03cea02d2bde25bf9c508340e0fbb 100644
--- a/app/Views/_assets/images/logo-castopod.svg
+++ b/app/Views/_assets/images/logo-castopod.svg
@@ -1,86 +1,40 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   inkscape:version="1.0beta1 (ee59332, 2019-11-28)"
-   sodipodi:docname="castopod.svg"
-   id="svg839"
-   version="1.1"
-   viewBox="0 0 64 63.999998"
-   height="64"
-   width="64">
-  <metadata
-     id="metadata845">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <defs
-     id="defs843" />
-  <sodipodi:namedview
-     inkscape:current-layer="svg839"
-     inkscape:window-maximized="0"
-     inkscape:window-y="23"
-     inkscape:window-x="0"
-     inkscape:cy="33.560512"
-     inkscape:cx="32"
-     inkscape:zoom="8.9714173"
-     showgrid="false"
-     id="namedview841"
-     inkscape:window-height="1035"
-     inkscape:window-width="1920"
-     inkscape:pageshadow="2"
-     inkscape:pageopacity="0"
-     guidetolerance="10"
-     gridtolerance="10"
-     objecttolerance="10"
-     borderopacity="1"
-     inkscape:document-rotation="0"
-     bordercolor="#666666"
-     pagecolor="#ffffff" />
-  <circle
-     id="greencircle"
-     fill="#37c837"
-     cx="32"
-     cy="32"
-     r="31.684" />
-  <g
-     id="speak">
-    <path
-       d="M45.21 20.22H18.79c-6.473 0-11.74 5.266-11.74 11.74S12.317 43.7 18.79 43.7h10.756c1.08 0 1.957-.875 1.957-1.956 0-1.08-.877-1.957-1.957-1.957H18.79c-4.315 0-7.826-3.51-7.826-7.827 0-4.316 3.51-7.828 7.827-7.828h26.42c4.315 0 7.826 3.512 7.826 7.828 0 4.316-3.51 7.827-7.827 7.827H43.34v.002c-5.41.096-9.783 4.527-9.783 9.96 0 1.08.875 1.957 1.956 1.957 1.08 0 1.956-.876 1.956-1.957 0-3.336 2.714-6.05 6.05-6.05h1.687c6.473 0 11.74-5.266 11.74-11.74s-5.267-11.74-11.74-11.74"
-       fill="#fff"
-       id="phylactery" />
-    <g
-       id="threedots">
-      <circle
-         r="2"
-         cy="32"
-         cx="24.256159"
-         id="leftdot"
-         style="fill:#ffffff;fill-opacity:1;stroke:none;" />
-      <circle
-         style="fill:#ffffff;fill-opacity:1;stroke:none;"
-         id="middledot"
-         cx="32"
-         cy="32"
-         r="2" />
-      <circle
-         r="2"
-         cy="32"
-         cx="39.743839"
-         id="rightdot"
-         style="fill:#ffffff;fill-opacity:1;stroke:none;" />
-    </g>
-  </g>
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 202.4 137.8" style="enable-background:new 0 0 202.4 137.8;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#009486;}
+	.st1{fill:#E7F9E4;}
+	.st2{fill:none;}
+	.st3{fill:#E7FFE3;}
+</style>
+<g>
+	<path id="dark_greeen_19_" class="st0" d="M181.9,131.7h-32.5c0,0-1.2-2.5-2.5-4.9c-1.3-2.4-4.4-2.3-4.4-2.3H59.7
+		c0,0-3-0.4-4.5,2.3c-1.6,2.7-2.6,4.9-2.6,4.9H20.6c-6.9,0-12.6-5.6-12.6-12.5V20.3c0-6.9,5.6-12.6,12.5-12.6h161.3
+		c6.9,0,12.6,5.6,12.6,12.5v98.9C194.5,126,188.8,131.7,181.9,131.7z"/>
+	<path class="st1" d="M143.7,34.5H58.6c-14.6,0-26.5,12-26.5,26.6c0,14.6,11.9,26.5,26.5,26.5h85.1c14.6,0,26.5-11.9,26.5-26.5
+		C170.3,46.3,158.4,34.4,143.7,34.5z M68.3,68.7c0,0-3.9-2.9-9.4-2.9c-4.1,0-8.9,2.5-8.9,2.5c-1.3-1.9-2.1-4.1-2.1-6.6
+		c0-6.3,5.1-11.4,11.4-11.4s11.4,5.1,11.4,11.4C70.7,64.4,69.8,66.8,68.3,68.7z M101.2,75.3c-12.5,0-12-9.6-12-9.6
+		c-0.2-1.8,2.1-2.4,2.9-1.3c0.4,0.6,0.4,0.6,0.7,1.7c1.7,5.9,8.4,5.6,8.4,5.6s6.7,0.4,8.4-5.6c0.3-1,0.3-1.1,0.7-1.7
+		c0.8-1,3.1-0.5,2.9,1.3C113.2,65.7,113.7,75.3,101.2,75.3z M152.3,68.4c0,0-4.8-2.5-8.9-2.5c-5.5,0-9.4,2.9-9.4,2.9
+		c-1.5-1.9-2.4-4.3-2.4-7c0-6.3,5.1-11.4,11.4-11.4s11.4,5.1,11.4,11.4C154.5,64.2,153.7,66.5,152.3,68.4z"/>
+	<path class="st2" d="M110.3,64.3c-0.4,0.6-0.4,0.6-0.7,1.7c-1.7,5.9-8.4,5.6-8.4,5.6s-6.7,0.4-8.4-5.6c-0.3-1-0.3-1.1-0.7-1.7
+		c-0.8-1-3.1-0.5-2.9,1.3c0,0-0.5,9.6,12,9.6c12.5,0,12-9.6,12-9.6C113.4,63.9,111.1,63.3,110.3,64.3z"/>
+	<path class="st2" d="M143.1,50.4c-6.3,0-11.4,5.1-11.4,11.4c0,2.6,0.9,5,2.4,7c0,0,3.9-2.9,9.4-2.9c4.1,0,8.9,2.5,8.9,2.5
+		c1.3-1.9,2.1-4.1,2.1-6.6C154.5,55.5,149.4,50.4,143.1,50.4z"/>
+	<path class="st2" d="M59.3,50.4c-6.3,0-11.4,5.1-11.4,11.4c0,2.5,0.8,4.7,2.1,6.6c0,0,4.8-2.5,8.9-2.5c5.5,0,9.4,2.9,9.4,2.9
+		c1.5-1.9,2.4-4.3,2.4-7C70.7,55.5,65.6,50.4,59.3,50.4z"/>
+	<g>
+		<g>
+			<path class="st3" d="M47.1,23.3c-6.3-1.7-11.7,2.1-14.7,7.3c-0.7,1.2-0.2,2.2,0.5,2.6c1,0.3,1.7,0.1,2.8-1.5
+				c2.2-3.9,5.9-6.1,10.1-5.3c0,0,2.9,0.9,3.3-1C49.4,24.2,48.3,23.6,47.1,23.3z"/>
+		</g>
+	</g>
+	<g>
+		<g>
+			<path class="st3" d="M159.9,27.3c-0.1,1.9,2.9,1.9,2.9,1.9c4.2,0.4,6.8,2.3,7.8,6.7c0.6,1.9,1.2,2.2,2.3,2.2
+				c0.8-0.1,1.6-1,1.2-2.4c-1.4-5.8-5.1-9.8-11.7-9.9C161.2,25.7,160,26,159.9,27.3z"/>
+		</g>
+	</g>
+</g>
 </svg>
diff --git a/app/Views/_assets/images/platforms/_default.svg b/app/Views/_assets/images/platforms/_default.svg
index b7c7127fa0fd047f5c9152c752a2ec04500f14d8..42640a76d97dc39466729a99b039ee8e81cb707b 100644
--- a/app/Views/_assets/images/platforms/_default.svg
+++ b/app/Views/_assets/images/platforms/_default.svg
@@ -1,7 +1,26 @@
-<svg id="default" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
-  <rect width="300" height="300" rx="67" fill="#ebfaeb"/>
-  <path id="phylactery" d="M195,112.36H105a40,40,0,0,0,0,80h36.65a6.67,6.67,0,0,0,0-13.33H105a26.67,26.67,0,0,1,0-53.34h90A26.67,26.67,0,0,1,195,179h-6.37A34,34,0,0,0,155.31,213a6.67,6.67,0,1,0,13.33,0,20.64,20.64,0,0,1,20.61-20.61H195a40,40,0,0,0,0-80" fill="#37c837"/>
-  <circle id="leftdot" cx="123.62" cy="152.5" r="6.81" fill="#37c837"/>
-  <circle id="middledot" cx="150.01" cy="152.5" r="6.81" fill="#37c837"/>
-  <circle id="rightdot" cx="176.39" cy="152.5" r="6.81" fill="#37c837"/>
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata>
+<style type="text/css">
+	.st0{fill:#AAAAAA;}
+	.st1{fill:#CCCCCC;}
+	.st2{fill:none;}
+	.st3{fill:#EEEEEE;}
+</style>
+<rect width="64" height="64" fill="#fff" stroke-width=".94495"/><g transform="matrix(.32439 0 0 .32439 -.82859 9.3899)">
+	<path id="dark_greeen_19_" class="st0" d="m181.9 131.7h-32.5s-1.2-2.5-2.5-4.9-4.4-2.3-4.4-2.3h-82.8s-3-0.4-4.5 2.3c-1.6 2.7-2.6 4.9-2.6 4.9h-32c-6.9 0-12.6-5.6-12.6-12.5v-98.9c0-6.9 5.6-12.6 12.5-12.6h161.3c6.9 0 12.6 5.6 12.6 12.5v98.9c0.1 6.9-5.6 12.6-12.5 12.6z"/>
+	<path class="st1" d="m143.7 34.5h-85.1c-14.6 0-26.5 12-26.5 26.6s11.9 26.5 26.5 26.5h85.1c14.6 0 26.5-11.9 26.5-26.5 0.1-14.8-11.8-26.7-26.5-26.6zm-75.4 34.2s-3.9-2.9-9.4-2.9c-4.1 0-8.9 2.5-8.9 2.5-1.3-1.9-2.1-4.1-2.1-6.6 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0 2.7-0.9 5.1-2.4 7zm32.9 6.6c-12.5 0-12-9.6-12-9.6-0.2-1.8 2.1-2.4 2.9-1.3 0.4 0.6 0.4 0.6 0.7 1.7 1.7 5.9 8.4 5.6 8.4 5.6s6.7 0.4 8.4-5.6c0.3-1 0.3-1.1 0.7-1.7 0.8-1 3.1-0.5 2.9 1.3 0 0 0.5 9.6-12 9.6zm51.1-6.9s-4.8-2.5-8.9-2.5c-5.5 0-9.4 2.9-9.4 2.9-1.5-1.9-2.4-4.3-2.4-7 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0.1 2.4-0.7 4.7-2.1 6.6z"/>
+	<path class="st2" d="m110.3 64.3c-0.4 0.6-0.4 0.6-0.7 1.7-1.7 5.9-8.4 5.6-8.4 5.6s-6.7 0.4-8.4-5.6c-0.3-1-0.3-1.1-0.7-1.7-0.8-1-3.1-0.5-2.9 1.3 0 0-0.5 9.6 12 9.6s12-9.6 12-9.6c0.2-1.7-2.1-2.3-2.9-1.3z"/>
+	<path class="st2" d="m143.1 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.6 0.9 5 2.4 7 0 0 3.9-2.9 9.4-2.9 4.1 0 8.9 2.5 8.9 2.5 1.3-1.9 2.1-4.1 2.1-6.6 0-6.3-5.1-11.4-11.4-11.4z"/>
+	<path class="st2" d="m59.3 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.5 0.8 4.7 2.1 6.6 0 0 4.8-2.5 8.9-2.5 5.5 0 9.4 2.9 9.4 2.9 1.5-1.9 2.4-4.3 2.4-7 0-6.3-5.1-11.4-11.4-11.4z"/>
+	
+		
+			<path class="st3" d="m47.1 23.3c-6.3-1.7-11.7 2.1-14.7 7.3-0.7 1.2-0.2 2.2 0.5 2.6 1 0.3 1.7 0.1 2.8-1.5 2.2-3.9 5.9-6.1 10.1-5.3 0 0 2.9 0.9 3.3-1 0.3-1.2-0.8-1.8-2-2.1z"/>
+		
+	
+	
+		
+			<path class="st3" d="m159.9 27.3c-0.1 1.9 2.9 1.9 2.9 1.9 4.2 0.4 6.8 2.3 7.8 6.7 0.6 1.9 1.2 2.2 2.3 2.2 0.8-0.1 1.6-1 1.2-2.4-1.4-5.8-5.1-9.8-11.7-9.9-1.2-0.1-2.4 0.2-2.5 1.5z"/>
+		
+	
+</g>
 </svg>
diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts
new file mode 100644
index 0000000000000000000000000000000000000000..61acd36168b1336ddbc24620be86a98a8e7fb03a
--- /dev/null
+++ b/app/Views/_assets/modules/Charts.ts
@@ -0,0 +1,134 @@
+// Import modules
+import * as am4charts from "@amcharts/amcharts4/charts";
+import * as am4core from "@amcharts/amcharts4/core";
+import am4themes_material from "@amcharts/amcharts4/themes/material";
+
+const drawPieChart = (chartDivId: string, dataUrl: string | null): void => {
+  // Create chart instance
+  const chart = am4core.create(chartDivId, am4charts.PieChart);
+  am4core.percent(100);
+
+  // Set theme
+  am4core.useTheme(am4themes_material);
+
+  chart.innerRadius = am4core.percent(10);
+
+  // Add data
+  chart.dataSource.url = dataUrl || "";
+  chart.dataSource.parser.options.emptyAs = 0;
+
+  // Add and configure Series
+  const pieSeries = chart.series.push(new am4charts.PieSeries());
+  pieSeries.dataFields.value = "values";
+  pieSeries.dataFields.category = "labels";
+
+  pieSeries.slices.template.stroke = am4core.color("#ffffff");
+  pieSeries.slices.template.strokeWidth = 1;
+  pieSeries.slices.template.strokeOpacity = 1;
+  pieSeries.labels.template.disabled = true;
+  pieSeries.ticks.template.disabled = true;
+
+  chart.legend = new am4charts.Legend();
+  chart.legend.position = "right";
+  chart.legend.scrollable = true;
+};
+
+const drawXYChart = (chartDivId: string, dataUrl: string | null): void => {
+  // Create chart instance
+  const chart = am4core.create(chartDivId, am4charts.XYChart);
+  am4core.percent(100);
+
+  // Set theme
+  am4core.useTheme(am4themes_material);
+
+  // Create axes
+  const dateAxis = chart.xAxes.push(new am4charts.DateAxis());
+  dateAxis.renderer.minGridDistance = 60;
+
+  chart.yAxes.push(new am4charts.ValueAxis());
+
+  // Add data
+  chart.dataSource.url = dataUrl || "";
+  chart.dataSource.parser.options.emptyAs = 0;
+
+  // Create series
+  const series = chart.series.push(new am4charts.LineSeries());
+  series.dataFields.valueY = "values";
+  series.dataFields.dateX = "labels";
+  series.tooltipText = "{valueY} downloads";
+
+  series.tooltip.pointerOrientation = "vertical";
+
+  chart.cursor = new am4charts.XYCursor();
+  chart.cursor.snapToSeries = series;
+  chart.cursor.xAxis = dateAxis;
+
+  chart.scrollbarX = new am4core.Scrollbar();
+};
+
+const drawXYSeriesChart = (
+  chartDivId: string,
+  dataUrl: string | null
+): void => {
+  // Create chart instance
+  const chart = am4core.create(chartDivId, am4charts.XYChart);
+  am4core.percent(100);
+
+  // Set theme
+  am4core.useTheme(am4themes_material);
+
+  // Create axes
+  chart.xAxes.push(new am4charts.ValueAxis());
+  chart.yAxes.push(new am4charts.ValueAxis());
+
+  // Add data
+  chart.dataSource.url = dataUrl || "";
+  chart.dataSource.parser.options.emptyAs = 0;
+
+  // Create series
+  const series1 = chart.series.push(new am4charts.LineSeries());
+  series1.dataFields.valueX = "X";
+  series1.dataFields.valueY = "aY";
+
+  const series2 = chart.series.push(new am4charts.LineSeries());
+  series2.dataFields.valueX = "X";
+  series2.dataFields.valueY = "bY";
+
+  const series3 = chart.series.push(new am4charts.LineSeries());
+  series3.dataFields.valueX = "X";
+  series3.dataFields.valueY = "cY";
+
+  const series4 = chart.series.push(new am4charts.LineSeries());
+  series4.dataFields.valueX = "X";
+  series4.dataFields.valueY = "dY";
+
+  const series5 = chart.series.push(new am4charts.LineSeries());
+  series5.dataFields.valueX = "X";
+  series5.dataFields.valueY = "eY";
+};
+
+const DrawCharts = (): void => {
+  const chartDivs: NodeListOf<HTMLDivElement> = document.querySelectorAll(
+    "div[data-chart-type]"
+  );
+
+  for (let i = 0; i < chartDivs.length; i++) {
+    const chartDiv: HTMLDivElement = chartDivs[i];
+    const chartType = chartDiv.dataset.chartType;
+    switch (chartType) {
+      case "pie-chart":
+        drawPieChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
+        break;
+      case "xy-chart":
+        drawXYChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
+        break;
+      case "xy-series-chart":
+        drawXYSeriesChart(chartDiv.id, chartDiv.getAttribute("data-chart-url"));
+        break;
+      default:
+        console.error("Unknown chart type:" + chartType);
+    }
+  }
+};
+
+export default DrawCharts;
diff --git a/app/Views/_layout.php b/app/Views/_layout.php
index c2ce491826353a0c4dcc9c9f0ec0acf743e3cca1..dd3ea6e984c8240ab8dd014b91c487eac451cb8e 100644
--- a/app/Views/_layout.php
+++ b/app/Views/_layout.php
@@ -24,7 +24,7 @@
         <?= render_page_links() ?>
         <small><?= lang('Common.powered_by', [
             'castopod' =>
-                '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
+                '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
         ]) ?></small>
     </footer>
 </body>
diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php
index 8cd3c3f166499691d2e82718a4f07a1444b5a029..d18c245949f167592308d8061db669c1911f76b7 100644
--- a/app/Views/admin/_layout.php
+++ b/app/Views/admin/_layout.php
@@ -9,7 +9,7 @@
     <link rel="shortcut icon" type="image/png" href="/favicon.ico" />
     <link rel="stylesheet" href="/assets/admin.css"/>
     <link rel="stylesheet" href="/assets/index.css"/>
-    <script src="/assets/admin.js" defer></script>
+    <script src="/assets/admin.js" type="module" defer></script>
 </head>
 
 <body class="relative bg-gray-100 holy-grail-grid">
@@ -43,12 +43,12 @@
     <footer class="px-2 py-2 mx-auto text-xs text-right holy-grail-footer">
         <small><?= lang('Common.powered_by', [
             'castopod' =>
-                '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>',
+                '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>',
         ]) ?></small>
     </footer>
     <button
         type="button"
         id="sidebar-toggler"
-        class="fixed bottom-0 left-0 z-50 p-3 mb-3 ml-3 text-xl transition duration-300 ease-in-out bg-white border-2 rounded-full shadow-lg  focus:outline-none md:hidden hover:bg-gray-100 focus:shadow-outline"
+        class="fixed bottom-0 left-0 z-50 p-3 mb-3 ml-3 text-xl transition duration-300 ease-in-out bg-white border-2 rounded-full shadow-lg focus:outline-none md:hidden hover:bg-gray-100 focus:shadow-outline"
         style="transform: translateX(0px);"><?= icon('menu') ?></button>
 </body>
diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php
index 23bc1f3ffcc52653f87c27fe7b4a410e5d4941eb..570008cc875bf1167db5c071985d3a7d0a0063a4 100644
--- a/app/Views/admin/podcast/_sidebar.php
+++ b/app/Views/admin/podcast/_sidebar.php
@@ -10,7 +10,7 @@ $podcastNavigation = [
     ],
     'analytics' => [
         'icon' => 'line-chart',
-        'items' => [],
+        'items' => ['podcast-analytics'],
     ],
     'contributors' => [
         'icon' => 'group',
diff --git a/app/Views/admin/podcast/analytics.php b/app/Views/admin/podcast/analytics.php
new file mode 100644
index 0000000000000000000000000000000000000000..c57559b648bbdeaedc52ed29d8622d87841048b4
--- /dev/null
+++ b/app/Views/admin/podcast/analytics.php
@@ -0,0 +1,32 @@
+<?= $this->extend('admin/_layout') ?>
+
+<?= $this->section('title') ?>
+<?= $podcast->title ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= $podcast->title ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+<div class="h-64" id="by-app-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to(
+    'analytics-data',
+    $podcast->id,
+    'PodcastsByPlayer',
+    'ByApp'
+) ?>"></div>
+<div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to(
+    'analytics-data',
+    $podcast->id,
+    'Podcasts',
+    'ByDay'
+) ?>"></div>
+<div class="h-64" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to(
+    'analytics-data',
+    $podcast->id,
+    'PodcastsByEpisode',
+    'ByDay'
+) ?>"></div>
+
+<script src="/assets/charts.js" type="module"></script>
+<?= $this->endSection() ?>
diff --git a/composer.json b/composer.json
index 063237497ac57f8ee0cfdfb4dc3cc6e766afd804..0057dce13b92f4c80e70636c73ce05cc8e497f88 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +13,9 @@
     "codeigniter4/codeigniter4": "dev-develop",
     "league/commonmark": "^1.5",
     "vlucas/phpdotenv": "^5.2",
-    "league/html-to-markdown": "^4.10"
+    "league/html-to-markdown": "^4.10",
+    "podlibre/user-agents-php": "*",
+    "podlibre/ipcat": "*"
   },
   "require-dev": {
     "mikey179/vfsstream": "1.6.*",
@@ -27,8 +29,14 @@
   },
   "scripts": {
     "test": "phpunit",
+    "post-install-cmd": [
+      "@php vendor/podlibre/user-agents-php/src/UserAgentsGenerate.php >  vendor/podlibre/user-agents-php/src/UserAgents.php",
+      "@php vendor/podlibre/ipcat/IpDbGenerate.php >  vendor/podlibre/ipcat/IpDb.php"
+    ],
     "post-update-cmd": [
-      "@composer dump-autoload"
+      "@composer dump-autoload",
+      "@php vendor/podlibre/user-agents-php/src/UserAgentsGenerate.php >  vendor/podlibre/user-agents-php/src/UserAgents.php",
+      "@php vendor/podlibre/ipcat/IpDbGenerate.php >  vendor/podlibre/ipcat/IpDb.php"
     ]
   },
   "support": {
diff --git a/composer.lock b/composer.lock
index 9e637f246bbbd922a93071b304ed94360402f005..74c6baaf647d972d8771698b44297bb356f04ae4 100644
--- a/composer.lock
+++ b/composer.lock
@@ -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": "38eeae7f5d0143863430cda9df10d487",
+    "content-hash": "47b9f628f03f8c494a9339b054359ec8",
     "packages": [
         {
             "name": "codeigniter4/codeigniter4",
@@ -12,12 +12,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/codeigniter4/CodeIgniter4.git",
-                "reference": "9204aef421921f2c07021dda418ebfc200fe4a31"
+                "reference": "ccf68e1d7fc44bfe5abacc39bf16edae45794a83"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/9204aef421921f2c07021dda418ebfc200fe4a31",
-                "reference": "9204aef421921f2c07021dda418ebfc200fe4a31",
+                "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/ccf68e1d7fc44bfe5abacc39bf16edae45794a83",
+                "reference": "ccf68e1d7fc44bfe5abacc39bf16edae45794a83",
                 "shasum": ""
             },
             "require": {
@@ -37,6 +37,7 @@
                 "phpstan/phpstan": "^0.12",
                 "phpunit/phpunit": "^8.5",
                 "predis/predis": "^1.1",
+                "rector/rector": "^0.8",
                 "squizlabs/php_codesniffer": "^3.3"
             },
             "type": "project",
@@ -45,6 +46,11 @@
                     "CodeIgniter\\": "system/"
                 }
             },
+            "autoload-dev": {
+                "psr-4": {
+                    "Utils\\": "utils"
+                }
+            },
             "scripts": {
                 "post-update-cmd": [
                     "@composer dump-autoload",
@@ -69,7 +75,7 @@
                 "slack": "https://codeigniterchat.slack.com",
                 "issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
             },
-            "time": "2020-09-24T17:15:24+00:00"
+            "time": "2020-10-04T20:15:33+00:00"
         },
         {
             "name": "composer/ca-bundle",
@@ -143,27 +149,27 @@
         },
         {
             "name": "geoip2/geoip2",
-            "version": "v2.10.0",
+            "version": "v2.11.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/maxmind/GeoIP2-php.git",
-                "reference": "419557cd21d9fe039721a83490701a58c8ce784a"
+                "reference": "d01be5894a5c1a3381c58c9b1795cd07f96c30f7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/maxmind/GeoIP2-php/zipball/419557cd21d9fe039721a83490701a58c8ce784a",
-                "reference": "419557cd21d9fe039721a83490701a58c8ce784a",
+                "url": "https://api.github.com/repos/maxmind/GeoIP2-php/zipball/d01be5894a5c1a3381c58c9b1795cd07f96c30f7",
+                "reference": "d01be5894a5c1a3381c58c9b1795cd07f96c30f7",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "maxmind-db/reader": "~1.5",
-                "maxmind/web-service-common": "~0.6",
-                "php": ">=5.6"
+                "maxmind-db/reader": "~1.8",
+                "maxmind/web-service-common": "~0.8",
+                "php": ">=7.2"
             },
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "2.*",
-                "phpunit/phpunit": "5.*",
+                "phpunit/phpunit": "^8.0 || ^9.0",
                 "squizlabs/php_codesniffer": "3.*"
             },
             "type": "library",
@@ -192,7 +198,7 @@
                 "geolocation",
                 "maxmind"
             ],
-            "time": "2019-12-12T18:48:39+00:00"
+            "time": "2020-10-01T18:48:34+00:00"
         },
         {
             "name": "graham-campbell/result-type",
@@ -689,23 +695,23 @@
         },
         {
             "name": "maxmind-db/reader",
-            "version": "v1.7.0",
+            "version": "v1.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git",
-                "reference": "942553da239f12051275f9c666538b5dd09e2908"
+                "reference": "b566d429ac9aec10594b0935be8ff38302f8d5c8"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/942553da239f12051275f9c666538b5dd09e2908",
-                "reference": "942553da239f12051275f9c666538b5dd09e2908",
+                "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/b566d429ac9aec10594b0935be8ff38302f8d5c8",
+                "reference": "b566d429ac9aec10594b0935be8ff38302f8d5c8",
                 "shasum": ""
             },
             "require": {
                 "php": ">=7.2"
             },
             "conflict": {
-                "ext-maxminddb": "<1.7.0,>=2.0.0"
+                "ext-maxminddb": "<1.8.0,>=2.0.0"
             },
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "2.*",
@@ -745,31 +751,31 @@
                 "geolocation",
                 "maxmind"
             ],
-            "time": "2020-08-07T22:10:05+00:00"
+            "time": "2020-10-01T17:30:21+00:00"
         },
         {
             "name": "maxmind/web-service-common",
-            "version": "v0.7.0",
+            "version": "v0.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/maxmind/web-service-common-php.git",
-                "reference": "74c996c218ada5c639c8c2f076756e059f5552fc"
+                "reference": "ba67d9532cfaf499bd71774b8170d05df4f75fb7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/74c996c218ada5c639c8c2f076756e059f5552fc",
-                "reference": "74c996c218ada5c639c8c2f076756e059f5552fc",
+                "url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/ba67d9532cfaf499bd71774b8170d05df4f75fb7",
+                "reference": "ba67d9532cfaf499bd71774b8170d05df4f75fb7",
                 "shasum": ""
             },
             "require": {
                 "composer/ca-bundle": "^1.0.3",
                 "ext-curl": "*",
                 "ext-json": "*",
-                "php": ">=5.6"
+                "php": ">=7.2"
             },
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "2.*",
-                "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0",
+                "phpunit/phpunit": "^8.0 || ^9.0",
                 "squizlabs/php_codesniffer": "3.*"
             },
             "type": "library",
@@ -791,7 +797,7 @@
             ],
             "description": "Internal MaxMind Web Service API",
             "homepage": "https://github.com/maxmind/web-service-common-php",
-            "time": "2020-05-06T14:07:26+00:00"
+            "time": "2020-10-01T15:28:36+00:00"
         },
         {
             "name": "myth/auth",
@@ -918,6 +924,76 @@
             ],
             "time": "2020-07-20T17:29:33+00:00"
         },
+        {
+            "name": "podlibre/ipcat",
+            "version": "dev-master",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/podlibre/ipcat.git",
+                "reference": "1adfc821be508ddc8a742f6a5d5e6e42fdf28e86"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/podlibre/ipcat/zipball/1adfc821be508ddc8a742f6a5d5e6e42fdf28e86",
+                "reference": "1adfc821be508ddc8a742f6a5d5e6e42fdf28e86",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Podlibre\\Ipcat\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "GPL-3.0-only"
+            ],
+            "authors": [
+                {
+                    "name": "Benjamin Bellamy",
+                    "email": "ben@podlibre.org",
+                    "homepage": "https://podlibre.org/"
+                }
+            ],
+            "description": "Categorization of IP Addresses forked from https://github.com/client9/ipcat",
+            "homepage": "https://github.com/podlibre/ipcat",
+            "time": "2020-10-05T17:15:07+00:00"
+        },
+        {
+            "name": "podlibre/user-agents-php",
+            "version": "dev-main",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/podlibre/user-agents-php.git",
+                "reference": "891066bae6b4881a8b7a57eb72a67fca1fcf67c0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/podlibre/user-agents-php/zipball/891066bae6b4881a8b7a57eb72a67fca1fcf67c0",
+                "reference": "891066bae6b4881a8b7a57eb72a67fca1fcf67c0",
+                "shasum": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Podlibre\\UserAgentsPhp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Benjamin Bellamy",
+                    "email": "ben@podlibre.org",
+                    "homepage": "https://podlibre.org/"
+                }
+            ],
+            "description": "PHP implementation for opawg/user-agents.",
+            "homepage": "https://github.com/podlibre/user-agents-php",
+            "time": "2020-10-05T16:58:13+00:00"
+        },
         {
             "name": "psr/cache",
             "version": "1.0.1",
@@ -1801,28 +1877,28 @@
         },
         {
             "name": "phpspec/prophecy",
-            "version": "1.11.1",
+            "version": "1.12.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpspec/prophecy.git",
-                "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160"
+                "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160",
-                "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d",
+                "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d",
                 "shasum": ""
             },
             "require": {
                 "doctrine/instantiator": "^1.2",
-                "php": "^7.2",
-                "phpdocumentor/reflection-docblock": "^5.0",
+                "php": "^7.2 || ~8.0, <8.1",
+                "phpdocumentor/reflection-docblock": "^5.2",
                 "sebastian/comparator": "^3.0 || ^4.0",
                 "sebastian/recursion-context": "^3.0 || ^4.0"
             },
             "require-dev": {
                 "phpspec/phpspec": "^6.0",
-                "phpunit/phpunit": "^8.0"
+                "phpunit/phpunit": "^8.0 || ^9.0 <9.3"
             },
             "type": "library",
             "extra": {
@@ -1860,7 +1936,7 @@
                 "spy",
                 "stub"
             ],
-            "time": "2020-07-08T12:44:21+00:00"
+            "time": "2020-09-29T09:10:42+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
diff --git a/docs/setup-development.md b/docs/setup-development.md
index 66e02a3c561ca3c74cb52b183c3def8afbd67289..5949b1b64a88016453637656228428bd9afad480 100644
--- a/docs/setup-development.md
+++ b/docs/setup-development.md
@@ -104,6 +104,13 @@ docker ps -a
 docker-compose run --rm app php spark migrate -all
 ```
 
+In case you need to roll back, use this command:
+
+```
+# rolls back database schema loading (deletes all tables and their content)
+docker-compose run --rm app php spark migrate:rollback
+```
+
 2. Populate the database with the required data:
 
 ```bash
diff --git a/package-lock.json b/package-lock.json
index ae21fc46fb8b46627eab60f2b188a83ab33ef17e..cb81a0b6e4b537a1b4957f0580d15ad93e3e3805 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,39 @@
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
+    "@amcharts/amcharts4": {
+      "version": "4.10.5",
+      "resolved": "https://registry.npmjs.org/@amcharts/amcharts4/-/amcharts4-4.10.5.tgz",
+      "integrity": "sha512-H4PlVd4jSsD0V0loCg1Jd1gXMCqMaWwQabjd8qejP2lew2ZiXEaJ8eoIuXXoAakehCq/fewtgNO9lq95hdklvA==",
+      "requires": {
+        "@babel/runtime": "^7.6.3",
+        "core-js": "^3.0.0",
+        "d3-force": "^2.0.1",
+        "d3-geo": "^2.0.1",
+        "d3-geo-projection": "^3.0.0",
+        "pdfmake": "^0.1.36",
+        "polylabel": "^1.0.2",
+        "raf": "^3.4.1",
+        "regression": "^2.0.1",
+        "rgbcolor": "^1.0.1",
+        "stackblur-canvas": "^2.0.0",
+        "tslib": "^2.0.1",
+        "venn.js": "^0.2.20",
+        "xlsx": "^0.16.4"
+      },
+      "dependencies": {
+        "tslib": {
+          "version": "2.0.1",
+          "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz",
+          "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ=="
+        }
+      }
+    },
+    "@amcharts/amcharts4-geodata": {
+      "version": "4.1.17",
+      "resolved": "https://registry.npmjs.org/@amcharts/amcharts4-geodata/-/amcharts4-geodata-4.1.17.tgz",
+      "integrity": "sha512-ylzshiOq/aMRlVrRq8dOznZP7fp4xg/XkmhTjGm2dN6O1WqvoVBBOOnGyzrmb2gBlqN5zj59PQyOqSDyQUNe/Q=="
+    },
     "@babel/code-frame": {
       "version": "7.10.1",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz",
@@ -1220,7 +1253,6 @@
       "version": "7.10.4",
       "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz",
       "integrity": "sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==",
-      "dev": true,
       "requires": {
         "regenerator-runtime": "^0.13.4"
       },
@@ -1228,8 +1260,7 @@
         "regenerator-runtime": {
           "version": "0.13.5",
           "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
-          "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
-          "dev": true
+          "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
         }
       }
     },
@@ -2465,8 +2496,7 @@
     "acorn": {
       "version": "7.4.0",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz",
-      "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==",
-      "dev": true
+      "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w=="
     },
     "acorn-jsx": {
       "version": "5.3.1",
@@ -2478,7 +2508,6 @@
       "version": "1.8.2",
       "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz",
       "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==",
-      "dev": true,
       "requires": {
         "acorn": "^7.0.0",
         "acorn-walk": "^7.0.0",
@@ -2488,8 +2517,16 @@
     "acorn-walk": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz",
-      "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
-      "dev": true
+      "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA=="
+    },
+    "adler-32": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz",
+      "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=",
+      "requires": {
+        "exit-on-epipe": "~1.0.1",
+        "printj": "~1.1.0"
+      }
     },
     "aggregate-error": {
       "version": "3.1.0",
@@ -2513,12 +2550,42 @@
         "uri-js": "^4.2.2"
       }
     },
+    "align-text": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
+      "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
+      "requires": {
+        "kind-of": "^3.0.2",
+        "longest": "^1.0.1",
+        "repeat-string": "^1.5.2"
+      },
+      "dependencies": {
+        "kind-of": {
+          "version": "3.2.2",
+          "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+          "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+          "requires": {
+            "is-buffer": "^1.1.5"
+          }
+        },
+        "longest": {
+          "version": "1.0.1",
+          "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
+          "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
+        }
+      }
+    },
     "alphanum-sort": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz",
       "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=",
       "dev": true
     },
+    "amdefine": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+      "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU="
+    },
     "ansi-colors": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
@@ -2582,6 +2649,11 @@
       "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=",
       "dev": true
     },
+    "array-from": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz",
+      "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU="
+    },
     "array-ify": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz",
@@ -2612,6 +2684,58 @@
       "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=",
       "dev": true
     },
+    "ast-transform": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz",
+      "integrity": "sha1-dJRAWIh9goPhidlUYAlHvJj+AGI=",
+      "requires": {
+        "escodegen": "~1.2.0",
+        "esprima": "~1.0.4",
+        "through": "~2.3.4"
+      },
+      "dependencies": {
+        "escodegen": {
+          "version": "1.2.0",
+          "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz",
+          "integrity": "sha1-Cd55Z3kcyVi3+Jot220jRRrzJ+E=",
+          "requires": {
+            "esprima": "~1.0.4",
+            "estraverse": "~1.5.0",
+            "esutils": "~1.0.0",
+            "source-map": "~0.1.30"
+          }
+        },
+        "esprima": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz",
+          "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0="
+        },
+        "estraverse": {
+          "version": "1.5.1",
+          "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz",
+          "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E="
+        },
+        "esutils": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz",
+          "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA="
+        },
+        "source-map": {
+          "version": "0.1.43",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
+          "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=",
+          "optional": true,
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        }
+      }
+    },
+    "ast-types": {
+      "version": "0.7.8",
+      "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz",
+      "integrity": "sha1-kC0uDWDQcb3NRtwRXhgJ7RHBOKk="
+    },
     "astral-regex": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
@@ -2667,6 +2791,27 @@
         "object.assign": "^4.1.0"
       }
     },
+    "babel-runtime": {
+      "version": "6.26.0",
+      "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+      "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
+      "requires": {
+        "core-js": "^2.4.0",
+        "regenerator-runtime": "^0.11.0"
+      },
+      "dependencies": {
+        "core-js": {
+          "version": "2.6.11",
+          "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
+          "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
+        },
+        "regenerator-runtime": {
+          "version": "0.11.1",
+          "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+          "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
+        }
+      }
+    },
     "bail": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
@@ -2676,8 +2821,7 @@
     "balanced-match": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
-      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
-      "dev": true
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
     },
     "base": {
       "version": "0.11.2",
@@ -2734,6 +2878,11 @@
         }
       }
     },
+    "base64-js": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
+      "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg="
+    },
     "big.js": {
       "version": "5.2.2",
       "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -2756,7 +2905,6 @@
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
       "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
       "requires": {
         "balanced-match": "^1.0.0",
         "concat-map": "0.0.1"
@@ -2791,6 +2939,68 @@
         }
       }
     },
+    "brfs": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brfs/-/brfs-2.0.2.tgz",
+      "integrity": "sha512-IrFjVtwu4eTJZyu8w/V2gxU7iLTtcHih67sgEdzrhjLBMHp2uYefUBfdM4k2UvcuWMgV7PQDZHSLeNWnLFKWVQ==",
+      "requires": {
+        "quote-stream": "^1.0.1",
+        "resolve": "^1.1.5",
+        "static-module": "^3.0.2",
+        "through2": "^2.0.0"
+      },
+      "dependencies": {
+        "through2": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+          "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+          "requires": {
+            "readable-stream": "~2.3.6",
+            "xtend": "~4.0.1"
+          }
+        }
+      }
+    },
+    "brotli": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz",
+      "integrity": "sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y=",
+      "requires": {
+        "base64-js": "^1.1.2"
+      },
+      "dependencies": {
+        "base64-js": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+          "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+        }
+      }
+    },
+    "browser-resolve": {
+      "version": "1.11.3",
+      "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz",
+      "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==",
+      "requires": {
+        "resolve": "1.1.7"
+      },
+      "dependencies": {
+        "resolve": {
+          "version": "1.1.7",
+          "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+          "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
+        }
+      }
+    },
+    "browserify-optional": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz",
+      "integrity": "sha1-HhNyLP3g2F8SFnbCpyztUzoBiGk=",
+      "requires": {
+        "ast-transform": "0.0.0",
+        "ast-types": "^0.7.0",
+        "browser-resolve": "^1.8.1"
+      }
+    },
     "browserslist": {
       "version": "4.12.0",
       "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz",
@@ -2803,11 +3013,15 @@
         "pkg-up": "^2.0.0"
       }
     },
+    "buffer-equal": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz",
+      "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs="
+    },
     "buffer-from": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
-      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
-      "dev": true
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
     },
     "builtin-modules": {
       "version": "3.1.0",
@@ -2915,6 +3129,25 @@
       "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==",
       "dev": true
     },
+    "center-align": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
+      "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
+      "requires": {
+        "align-text": "^0.1.3",
+        "lazy-cache": "^1.0.3"
+      }
+    },
+    "cfb": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.0.tgz",
+      "integrity": "sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ==",
+      "requires": {
+        "adler-32": "~1.2.0",
+        "crc-32": "~1.2.0",
+        "printj": "~1.1.2"
+      }
+    },
     "chalk": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz",
@@ -3239,6 +3472,11 @@
         }
       }
     },
+    "clone": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+      "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
+    },
     "clone-regexp": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
@@ -3280,6 +3518,22 @@
         }
       }
     },
+    "codepage": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz",
+      "integrity": "sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k=",
+      "requires": {
+        "commander": "~2.14.1",
+        "exit-on-epipe": "~1.0.1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.14.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
+          "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw=="
+        }
+      }
+    },
     "collapse-white-space": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz",
@@ -3444,8 +3698,18 @@
     "concat-map": {
       "version": "0.0.1",
       "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
-      "dev": true
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "concat-stream": {
+      "version": "1.6.2",
+      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+      "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+      "requires": {
+        "buffer-from": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^2.2.2",
+        "typedarray": "^0.0.6"
+      }
     },
     "concat-with-sourcemaps": {
       "version": "1.1.0",
@@ -3464,6 +3728,11 @@
         }
       }
     },
+    "contour_plot": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/contour_plot/-/contour_plot-0.0.1.tgz",
+      "integrity": "sha1-R1hw8DK44zhBKqX8UHiA8L9JXHc="
+    },
     "conventional-changelog-angular": {
       "version": "5.0.11",
       "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.11.tgz",
@@ -3510,7 +3779,6 @@
       "version": "1.7.0",
       "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
       "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==",
-      "dev": true,
       "requires": {
         "safe-buffer": "~5.1.1"
       }
@@ -3524,8 +3792,7 @@
     "core-js": {
       "version": "3.6.5",
       "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
-      "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==",
-      "dev": true
+      "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA=="
     },
     "core-js-compat": {
       "version": "3.6.5",
@@ -3548,8 +3815,7 @@
     "core-util-is": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
-      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
-      "dev": true
+      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
     },
     "cosmiconfig": {
       "version": "5.2.1",
@@ -3563,6 +3829,15 @@
         "parse-json": "^4.0.0"
       }
     },
+    "crc-32": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
+      "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
+      "requires": {
+        "exit-on-epipe": "~1.0.1",
+        "printj": "~1.1.0"
+      }
+    },
     "crelt": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.4.tgz",
@@ -3599,6 +3874,11 @@
         }
       }
     },
+    "crypto-js": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz",
+      "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q=="
+    },
     "css-blank-pseudo": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz",
@@ -3954,12 +4234,130 @@
         }
       }
     },
+    "d": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+      "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+      "requires": {
+        "es5-ext": "^0.10.50",
+        "type": "^1.0.1"
+      }
+    },
+    "d3-array": {
+      "version": "2.8.0",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz",
+      "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw=="
+    },
+    "d3-color": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
+      "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
+    },
+    "d3-dispatch": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz",
+      "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA=="
+    },
+    "d3-ease": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz",
+      "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ=="
+    },
+    "d3-force": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz",
+      "integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==",
+      "requires": {
+        "d3-dispatch": "1 - 2",
+        "d3-quadtree": "1 - 2",
+        "d3-timer": "1 - 2"
+      }
+    },
+    "d3-geo": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.1.tgz",
+      "integrity": "sha512-M6yzGbFRfxzNrVhxDJXzJqSLQ90q1cCyb3EWFZ1LF4eWOBYxFypw7I/NFVBNXKNqxv1bqLathhYvdJ6DC+th3A==",
+      "requires": {
+        "d3-array": ">=2.5"
+      }
+    },
+    "d3-geo-projection": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-3.0.0.tgz",
+      "integrity": "sha512-1JE+filVbkEX2bT25dJdQ05iA4QHvUwev6o0nIQHOSrNlHCAKfVss/U10vEM3pA4j5v7uQoFdQ4KLbx9BlEbWA==",
+      "requires": {
+        "commander": "2",
+        "d3-array": "1 - 2",
+        "d3-geo": "1.12.0 - 2",
+        "resolve": "^1.1.10"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.20.3",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+          "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+        }
+      }
+    },
+    "d3-interpolate": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
+      "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
+      "requires": {
+        "d3-color": "1"
+      }
+    },
+    "d3-quadtree": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz",
+      "integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw=="
+    },
+    "d3-selection": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz",
+      "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg=="
+    },
+    "d3-timer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz",
+      "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA=="
+    },
+    "d3-transition": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz",
+      "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==",
+      "requires": {
+        "d3-color": "1",
+        "d3-dispatch": "1",
+        "d3-ease": "1",
+        "d3-interpolate": "1",
+        "d3-selection": "^1.1.0",
+        "d3-timer": "1"
+      },
+      "dependencies": {
+        "d3-dispatch": {
+          "version": "1.0.6",
+          "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
+          "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA=="
+        },
+        "d3-timer": {
+          "version": "1.0.10",
+          "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz",
+          "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw=="
+        }
+      }
+    },
     "dargs": {
       "version": "7.0.0",
       "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz",
       "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==",
       "dev": true
     },
+    "dash-ast": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz",
+      "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA=="
+    },
     "debug": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@@ -3972,8 +4370,7 @@
     "decamelize": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
-      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
-      "dev": true
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
     },
     "decamelize-keys": {
       "version": "1.1.0",
@@ -4005,11 +4402,23 @@
       "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=",
       "dev": true
     },
+    "deep-equal": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz",
+      "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==",
+      "requires": {
+        "is-arguments": "^1.0.4",
+        "is-date-object": "^1.0.1",
+        "is-regex": "^1.0.4",
+        "object-is": "^1.0.1",
+        "object-keys": "^1.1.1",
+        "regexp.prototype.flags": "^1.2.0"
+      }
+    },
     "deep-is": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
-      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
-      "dev": true
+      "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
     },
     "deepmerge": {
       "version": "4.2.2",
@@ -4020,7 +4429,6 @@
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
       "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
-      "dev": true,
       "requires": {
         "object-keys": "^1.0.12"
       }
@@ -4069,8 +4477,7 @@
     "defined": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
-      "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=",
-      "dev": true
+      "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM="
     },
     "dependency-graph": {
       "version": "0.9.0",
@@ -4101,6 +4508,11 @@
         "minimist": "^1.1.1"
       }
     },
+    "dfa": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz",
+      "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q=="
+    },
     "dir-glob": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -4177,6 +4589,22 @@
         "is-obj": "^2.0.0"
       }
     },
+    "dotignore": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz",
+      "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==",
+      "requires": {
+        "minimatch": "^3.0.4"
+      }
+    },
+    "duplexer2": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+      "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
+      "requires": {
+        "readable-stream": "^2.0.2"
+      }
+    },
     "electron-to-chromium": {
       "version": "1.3.459",
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.459.tgz",
@@ -4240,7 +4668,6 @@
       "version": "1.17.6",
       "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
       "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
-      "dev": true,
       "requires": {
         "es-to-primitive": "^1.2.1",
         "function-bind": "^1.1.1",
@@ -4258,8 +4685,7 @@
         "object-inspect": {
           "version": "1.8.0",
           "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
-          "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
-          "dev": true
+          "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
         }
       }
     },
@@ -4267,13 +4693,77 @@
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
       "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
-      "dev": true,
       "requires": {
         "is-callable": "^1.1.4",
         "is-date-object": "^1.0.1",
         "is-symbol": "^1.0.2"
       }
     },
+    "es5-ext": {
+      "version": "0.10.53",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
+      "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
+      "requires": {
+        "es6-iterator": "~2.0.3",
+        "es6-symbol": "~3.1.3",
+        "next-tick": "~1.0.0"
+      }
+    },
+    "es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "es6-map": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz",
+      "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14",
+        "es6-iterator": "~2.0.1",
+        "es6-set": "~0.1.5",
+        "es6-symbol": "~3.1.1",
+        "event-emitter": "~0.3.5"
+      }
+    },
+    "es6-set": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz",
+      "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14",
+        "es6-iterator": "~2.0.1",
+        "es6-symbol": "3.1.1",
+        "event-emitter": "~0.3.5"
+      },
+      "dependencies": {
+        "es6-symbol": {
+          "version": "3.1.1",
+          "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz",
+          "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=",
+          "requires": {
+            "d": "1",
+            "es5-ext": "~0.10.14"
+          }
+        }
+      }
+    },
+    "es6-symbol": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+      "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+      "requires": {
+        "d": "^1.0.1",
+        "ext": "^1.1.2"
+      }
+    },
     "escalade": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz",
@@ -4283,8 +4773,62 @@
     "escape-string-regexp": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
-      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
-      "dev": true
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "escodegen": {
+      "version": "1.14.3",
+      "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
+      "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
+      "requires": {
+        "esprima": "^4.0.1",
+        "estraverse": "^4.2.0",
+        "esutils": "^2.0.2",
+        "optionator": "^0.8.1",
+        "source-map": "~0.6.1"
+      },
+      "dependencies": {
+        "levn": {
+          "version": "0.3.0",
+          "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+          "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+          "requires": {
+            "prelude-ls": "~1.1.2",
+            "type-check": "~0.3.2"
+          }
+        },
+        "optionator": {
+          "version": "0.8.3",
+          "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
+          "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
+          "requires": {
+            "deep-is": "~0.1.3",
+            "fast-levenshtein": "~2.0.6",
+            "levn": "~0.3.0",
+            "prelude-ls": "~1.1.2",
+            "type-check": "~0.3.2",
+            "word-wrap": "~1.2.3"
+          }
+        },
+        "prelude-ls": {
+          "version": "1.1.2",
+          "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+          "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
+        },
+        "source-map": {
+          "version": "0.6.1",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+          "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+          "optional": true
+        },
+        "type-check": {
+          "version": "0.3.2",
+          "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+          "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+          "requires": {
+            "prelude-ls": "~1.1.2"
+          }
+        }
+      }
     },
     "eslint": {
       "version": "7.10.0",
@@ -4484,8 +5028,7 @@
     "esprima": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-      "dev": true
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
     },
     "esquery": {
       "version": "1.3.1",
@@ -4524,8 +5067,12 @@
     "estraverse": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
-      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
-      "dev": true
+      "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="
+    },
+    "estree-is-function": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/estree-is-function/-/estree-is-function-1.0.0.tgz",
+      "integrity": "sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA=="
     },
     "estree-walker": {
       "version": "1.0.1",
@@ -4536,8 +5083,16 @@
     "esutils": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
-      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
-      "dev": true
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
+    },
+    "event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
     },
     "eventemitter3": {
       "version": "4.0.7",
@@ -4588,6 +5143,11 @@
         "clone-regexp": "^2.1.0"
       }
     },
+    "exit-on-epipe": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
+      "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw=="
+    },
     "expand-brackets": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -4647,6 +5207,21 @@
         "homedir-polyfill": "^1.0.1"
       }
     },
+    "ext": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
+      "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
+      "requires": {
+        "type": "^2.0.0"
+      },
+      "dependencies": {
+        "type": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz",
+          "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA=="
+        }
+      }
+    },
     "extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -4830,8 +5405,7 @@
     "fast-levenshtein": {
       "version": "2.0.6",
       "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
-      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
-      "dev": true
+      "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
     },
     "fastest-levenshtein": {
       "version": "1.0.12",
@@ -4965,12 +5539,129 @@
       "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==",
       "dev": true
     },
+    "fmin": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/fmin/-/fmin-0.0.2.tgz",
+      "integrity": "sha1-Wbu0DUP/3ByUzQClaMQflfGXMBc=",
+      "requires": {
+        "contour_plot": "^0.0.1",
+        "json2module": "^0.0.3",
+        "rollup": "^0.25.8",
+        "tape": "^4.5.1",
+        "uglify-js": "^2.6.2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "rollup": {
+          "version": "0.25.8",
+          "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.25.8.tgz",
+          "integrity": "sha1-v2zoO4dRDRY0Ru6qV37WpvxYNeA=",
+          "requires": {
+            "chalk": "^1.1.1",
+            "minimist": "^1.2.0",
+            "source-map-support": "^0.3.2"
+          }
+        },
+        "source-map": {
+          "version": "0.1.32",
+          "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.32.tgz",
+          "integrity": "sha1-yLbBZ3l7pHQKjqMyUhYv8IWRsmY=",
+          "requires": {
+            "amdefine": ">=0.0.4"
+          }
+        },
+        "source-map-support": {
+          "version": "0.3.3",
+          "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.3.3.tgz",
+          "integrity": "sha1-NJAJd9W6PwfHdX7nLnO7GptTdU8=",
+          "requires": {
+            "source-map": "0.1.32"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+        }
+      }
+    },
+    "fontkit": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.8.1.tgz",
+      "integrity": "sha512-BsNCjDoYRxmNWFdAuK1y9bQt+igIxGtTC9u/jSFjR9MKhmI00rP1fwSvERt+5ddE82544l0XH5mzXozQVUy2Tw==",
+      "requires": {
+        "babel-runtime": "^6.26.0",
+        "brfs": "^2.0.0",
+        "brotli": "^1.2.0",
+        "browserify-optional": "^1.0.1",
+        "clone": "^1.0.4",
+        "deep-equal": "^1.0.0",
+        "dfa": "^1.2.0",
+        "restructure": "^0.5.3",
+        "tiny-inflate": "^1.0.2",
+        "unicode-properties": "^1.2.2",
+        "unicode-trie": "^0.3.0"
+      },
+      "dependencies": {
+        "unicode-trie": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz",
+          "integrity": "sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU=",
+          "requires": {
+            "pako": "^0.2.5",
+            "tiny-inflate": "^1.0.0"
+          }
+        }
+      }
+    },
+    "for-each": {
+      "version": "0.3.3",
+      "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+      "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+      "requires": {
+        "is-callable": "^1.1.3"
+      }
+    },
     "for-in": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
       "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=",
       "dev": true
     },
+    "frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="
+    },
     "fragment-cache": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@@ -4994,8 +5685,7 @@
     "fs.realpath": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
-      "dev": true
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
     },
     "fsevents": {
       "version": "2.1.3",
@@ -5007,8 +5697,7 @@
     "function-bind": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-      "dev": true
+      "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
     },
     "functional-red-black-tree": {
       "version": "1.0.1",
@@ -5036,6 +5725,11 @@
       "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==",
       "dev": true
     },
+    "get-assigned-identifiers": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
+      "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ=="
+    },
     "get-caller-file": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -5086,7 +5780,6 @@
       "version": "7.1.6",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
       "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
-      "dev": true,
       "requires": {
         "fs.realpath": "^1.0.0",
         "inflight": "^1.0.4",
@@ -5189,7 +5882,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
       "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
-      "dev": true,
       "requires": {
         "function-bind": "^1.1.1"
       }
@@ -5198,7 +5890,6 @@
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
       "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
-      "dev": true,
       "requires": {
         "ansi-regex": "^2.0.0"
       },
@@ -5206,8 +5897,7 @@
         "ansi-regex": {
           "version": "2.1.1",
           "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
-          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
-          "dev": true
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
         }
       }
     },
@@ -5220,8 +5910,7 @@
     "has-symbols": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
-      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
-      "dev": true
+      "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg=="
     },
     "has-value": {
       "version": "1.0.0",
@@ -5471,7 +6160,6 @@
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
       "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "dev": true,
       "requires": {
         "once": "^1.3.0",
         "wrappy": "1"
@@ -5480,8 +6168,7 @@
     "inherits": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
-      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
-      "dev": true
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
     },
     "ini": {
       "version": "1.3.5",
@@ -5580,6 +6267,11 @@
         "is-decimal": "^1.0.0"
       }
     },
+    "is-arguments": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz",
+      "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA=="
+    },
     "is-arrayish": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@@ -5598,14 +6290,12 @@
     "is-buffer": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
-      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
-      "dev": true
+      "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
     },
     "is-callable": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz",
-      "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==",
-      "dev": true
+      "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw=="
     },
     "is-color-stop": {
       "version": "1.1.0",
@@ -5644,8 +6334,7 @@
     "is-date-object": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
-      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
-      "dev": true
+      "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g=="
     },
     "is-decimal": {
       "version": "1.0.4",
@@ -5717,6 +6406,11 @@
       "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
       "dev": true
     },
+    "is-negative-zero": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz",
+      "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE="
+    },
     "is-number": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -5771,7 +6465,6 @@
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz",
       "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==",
-      "dev": true,
       "requires": {
         "has-symbols": "^1.0.1"
       }
@@ -5807,7 +6500,6 @@
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
       "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
-      "dev": true,
       "requires": {
         "has-symbols": "^1.0.1"
       }
@@ -5854,8 +6546,7 @@
     "isarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
-      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
-      "dev": true
+      "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
     },
     "isexe": {
       "version": "2.0.0",
@@ -5942,6 +6633,14 @@
       "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
       "dev": true
     },
+    "json2module": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/json2module/-/json2module-0.0.3.tgz",
+      "integrity": "sha1-APtfSpt638PwZHwpyxe80Zeb6bI=",
+      "requires": {
+        "rw": "^1.3.2"
+      }
+    },
     "json5": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz",
@@ -5978,6 +6677,11 @@
       "integrity": "sha512-eYboRV94Vco725nKMlpkn3nV2+96p9c3gKXRsYqAJSswSENvBhN7n5L+uDhY58xQa0UukWsDMTGELzmD8Q+wTA==",
       "dev": true
     },
+    "lazy-cache": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
+      "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4="
+    },
     "leven": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -6003,6 +6707,16 @@
         "type-check": "~0.4.0"
       }
     },
+    "linebreak": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.0.2.tgz",
+      "integrity": "sha512-bJwSRsJeAmaZYnkcwl5sCQNfSDAhBuXxb6L27tb+qkBRtUQSSTUa5bcgCPD6hFEkRNlpWHfK7nFMmcANU7ZP1w==",
+      "requires": {
+        "base64-js": "0.0.8",
+        "brfs": "^2.0.2",
+        "unicode-trie": "^1.0.0"
+      }
+    },
     "lines-and-columns": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
@@ -6665,6 +7379,14 @@
       "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==",
       "dev": true
     },
+    "merge-source-map": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz",
+      "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=",
+      "requires": {
+        "source-map": "^0.5.6"
+      }
+    },
     "merge-stream": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -6720,7 +7442,6 @@
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
       "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-      "dev": true,
       "requires": {
         "brace-expansion": "^1.1.7"
       }
@@ -6728,8 +7449,7 @@
     "minimist": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
-      "dev": true
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
     },
     "minimist-options": {
       "version": "4.1.0",
@@ -6809,6 +7529,11 @@
       "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
       "dev": true
     },
+    "next-tick": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+      "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
+    },
     "node-emoji": {
       "version": "1.10.0",
       "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz",
@@ -6933,11 +7658,69 @@
       "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==",
       "dev": true
     },
+    "object-inspect": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
+      "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA=="
+    },
+    "object-is": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz",
+      "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==",
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.18.0-next.1"
+      },
+      "dependencies": {
+        "es-abstract": {
+          "version": "1.18.0-next.1",
+          "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+          "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+          "requires": {
+            "es-to-primitive": "^1.2.1",
+            "function-bind": "^1.1.1",
+            "has": "^1.0.3",
+            "has-symbols": "^1.0.1",
+            "is-callable": "^1.2.2",
+            "is-negative-zero": "^2.0.0",
+            "is-regex": "^1.1.1",
+            "object-inspect": "^1.8.0",
+            "object-keys": "^1.1.1",
+            "object.assign": "^4.1.1",
+            "string.prototype.trimend": "^1.0.1",
+            "string.prototype.trimstart": "^1.0.1"
+          }
+        },
+        "is-callable": {
+          "version": "1.2.2",
+          "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
+          "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
+        },
+        "is-regex": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
+          "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
+          "requires": {
+            "has-symbols": "^1.0.1"
+          }
+        },
+        "object.assign": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
+          "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
+          "requires": {
+            "define-properties": "^1.1.3",
+            "es-abstract": "^1.18.0-next.0",
+            "has-symbols": "^1.0.1",
+            "object-keys": "^1.1.1"
+          }
+        }
+      }
+    },
     "object-keys": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
-      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
-      "dev": true
+      "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
     },
     "object-visit": {
       "version": "1.0.1",
@@ -6952,7 +7735,6 @@
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
       "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.2",
         "function-bind": "^1.1.1",
@@ -6995,7 +7777,6 @@
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
       "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true,
       "requires": {
         "wrappy": "1"
       }
@@ -7112,6 +7893,11 @@
       "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
       "dev": true
     },
+    "pako": {
+      "version": "0.2.9",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+      "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU="
+    },
     "parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -7174,8 +7960,7 @@
     "path-is-absolute": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
-      "dev": true
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
     },
     "path-key": {
       "version": "3.1.1",
@@ -7186,8 +7971,7 @@
     "path-parse": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
-      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
-      "dev": true
+      "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
     },
     "path-type": {
       "version": "4.0.0",
@@ -7195,6 +7979,44 @@
       "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
       "dev": true
     },
+    "pdfkit": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.11.0.tgz",
+      "integrity": "sha512-1s9gaumXkYxcVF1iRtSmLiISF2r4nHtsTgpwXiK8Swe+xwk/1pm8FJjYqN7L3x13NsWnGyUFntWcO8vfqq+wwA==",
+      "requires": {
+        "crypto-js": "^3.1.9-1",
+        "fontkit": "^1.8.0",
+        "linebreak": "^1.0.2",
+        "png-js": "^1.0.0"
+      }
+    },
+    "pdfmake": {
+      "version": "0.1.68",
+      "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.1.68.tgz",
+      "integrity": "sha512-oE1VEjkluro3+QqvLbFgFU/rRgyKdbPy/Fh8SS/nsUxnsiUcm85ChpmD6YD0hQW1E0d3hppAo4Yh+xdXucenIA==",
+      "requires": {
+        "iconv-lite": "^0.6.2",
+        "linebreak": "^1.0.2",
+        "pdfkit": "^0.11.0",
+        "svg-to-pdfkit": "^0.1.8",
+        "xmldoc": "^1.1.2"
+      },
+      "dependencies": {
+        "iconv-lite": {
+          "version": "0.6.2",
+          "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz",
+          "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==",
+          "requires": {
+            "safer-buffer": ">= 2.1.2 < 3.0.0"
+          }
+        }
+      }
+    },
+    "performance-now": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+      "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
+    },
     "php-parser": {
       "version": "github:glayzzle/php-parser#5a0e2e1bf12517bd1c544c0f4e68482d0362a7b5",
       "from": "github:glayzzle/php-parser#5a0e2e1bf12517bd1c544c0f4e68482d0362a7b5",
@@ -7290,6 +8112,19 @@
         "semver-compare": "^1.0.0"
       }
     },
+    "png-js": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz",
+      "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
+    },
+    "polylabel": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz",
+      "integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==",
+      "requires": {
+        "tinyqueue": "^2.0.3"
+      }
+    },
     "posix-character-classes": {
       "version": "0.1.1",
       "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz",
@@ -8798,11 +9633,15 @@
       "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
       "dev": true
     },
+    "printj": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
+      "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ=="
+    },
     "process-nextick-args": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
-      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
-      "dev": true
+      "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
     },
     "progress": {
       "version": "2.0.3",
@@ -9003,6 +9842,35 @@
       "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==",
       "dev": true
     },
+    "quote-stream": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz",
+      "integrity": "sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=",
+      "requires": {
+        "buffer-equal": "0.0.1",
+        "minimist": "^1.1.3",
+        "through2": "^2.0.0"
+      },
+      "dependencies": {
+        "through2": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+          "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+          "requires": {
+            "readable-stream": "~2.3.6",
+            "xtend": "~4.0.1"
+          }
+        }
+      }
+    },
+    "raf": {
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
+      "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
+      "requires": {
+        "performance-now": "^2.1.0"
+      }
+    },
     "randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -9076,7 +9944,6 @@
       "version": "2.3.7",
       "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
       "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
-      "dev": true,
       "requires": {
         "core-util-is": "~1.0.0",
         "inherits": "~2.0.3",
@@ -9173,6 +10040,15 @@
         "safe-regex": "^1.1.0"
       }
     },
+    "regexp.prototype.flags": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz",
+      "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==",
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.17.0-next.1"
+      }
+    },
     "regexpp": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz",
@@ -9216,6 +10092,11 @@
         }
       }
     },
+    "regression": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/regression/-/regression-2.0.1.tgz",
+      "integrity": "sha1-jSnD6CJKEIUMNeM36FqLL6w7DIc="
+    },
     "remark": {
       "version": "12.0.1",
       "resolved": "https://registry.npmjs.org/remark/-/remark-12.0.1.tgz",
@@ -9282,8 +10163,7 @@
     "repeat-string": {
       "version": "1.6.1",
       "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
-      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=",
-      "dev": true
+      "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
     },
     "replace-ext": {
       "version": "1.0.0",
@@ -9307,7 +10187,6 @@
       "version": "1.17.0",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
       "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
-      "dev": true,
       "requires": {
         "path-parse": "^1.0.6"
       }
@@ -9353,6 +10232,22 @@
         "signal-exit": "^3.0.2"
       }
     },
+    "restructure": {
+      "version": "0.5.4",
+      "resolved": "https://registry.npmjs.org/restructure/-/restructure-0.5.4.tgz",
+      "integrity": "sha1-9U591WNZD7NP1r9Vh2EJrsyyjeg=",
+      "requires": {
+        "browserify-optional": "^1.0.0"
+      }
+    },
+    "resumer": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz",
+      "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=",
+      "requires": {
+        "through": "~2.3.4"
+      }
+    },
     "ret": {
       "version": "0.1.15",
       "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
@@ -9377,6 +10272,19 @@
       "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=",
       "dev": true
     },
+    "rgbcolor": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+      "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0="
+    },
+    "right-align": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
+      "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=",
+      "requires": {
+        "align-text": "^0.1.1"
+      }
+    },
     "rimraf": {
       "version": "2.6.3",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
@@ -9582,6 +10490,11 @@
       "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==",
       "dev": true
     },
+    "rw": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+      "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
+    },
     "rxjs": {
       "version": "6.6.2",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz",
@@ -9594,8 +10507,7 @@
     "safe-buffer": {
       "version": "5.1.2",
       "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
-      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
-      "dev": true
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
     },
     "safe-identifier": {
       "version": "0.4.2",
@@ -9615,14 +10527,26 @@
     "safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
-      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
-      "dev": true
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
     "sax": {
       "version": "1.2.4",
       "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
-      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
-      "dev": true
+      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+    },
+    "scope-analyzer": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/scope-analyzer/-/scope-analyzer-2.1.1.tgz",
+      "integrity": "sha512-azEAihtQ9mEyZGhfgTJy3IbOWEzeOrYbg7NcYEshPKnKd+LZmC3TNd5dmDxbLBsTG/JVWmCp+vDJ03vJjeXMHg==",
+      "requires": {
+        "array-from": "^2.1.1",
+        "dash-ast": "^1.0.0",
+        "es6-map": "^0.1.5",
+        "es6-set": "^0.1.5",
+        "es6-symbol": "^3.1.1",
+        "estree-is-function": "^1.0.0",
+        "get-assigned-identifiers": "^1.1.0"
+      }
     },
     "semver": {
       "version": "5.7.1",
@@ -9680,6 +10604,11 @@
         }
       }
     },
+    "shallow-copy": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz",
+      "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA="
+    },
     "shebang-command": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -9852,8 +10781,7 @@
     "source-map": {
       "version": "0.5.7",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
-      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
-      "dev": true
+      "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
     },
     "source-map-resolve": {
       "version": "0.5.3",
@@ -9895,8 +10823,7 @@
     "sourcemap-codec": {
       "version": "1.4.8",
       "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
-      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
-      "dev": true
+      "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
     },
     "spdx-correct": {
       "version": "3.1.1",
@@ -9971,18 +10898,39 @@
       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
       "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
     },
+    "ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "requires": {
+        "frac": "~1.1.2"
+      }
+    },
     "stable": {
       "version": "0.1.8",
       "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz",
       "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==",
       "dev": true
     },
+    "stackblur-canvas": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.4.0.tgz",
+      "integrity": "sha512-Z+HixfgYV0ss3C342DxPwc+UvN1SYWqoz7Wsi3xEDWEnaBkSCL3Ey21gF4io+WlLm8/RIrSnCrDBIEcH4O+q5Q=="
+    },
     "state-toggle": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz",
       "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==",
       "dev": true
     },
+    "static-eval": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.0.tgz",
+      "integrity": "sha512-agtxZ/kWSsCkI5E4QifRwsaPs0P0JmZV6dkLz6ILYfFYQGn+5plctanRN+IC8dJRiFkyXHrwEE3W9Wmx67uDbw==",
+      "requires": {
+        "escodegen": "^1.11.1"
+      }
+    },
     "static-extend": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
@@ -10004,6 +10952,46 @@
         }
       }
     },
+    "static-module": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/static-module/-/static-module-3.0.4.tgz",
+      "integrity": "sha512-gb0v0rrgpBkifXCa3yZXxqVmXDVE+ETXj6YlC/jt5VzOnGXR2C15+++eXuMDUYsePnbhf+lwW0pE1UXyOLtGCw==",
+      "requires": {
+        "acorn-node": "^1.3.0",
+        "concat-stream": "~1.6.0",
+        "convert-source-map": "^1.5.1",
+        "duplexer2": "~0.1.4",
+        "escodegen": "^1.11.1",
+        "has": "^1.0.1",
+        "magic-string": "0.25.1",
+        "merge-source-map": "1.0.4",
+        "object-inspect": "^1.6.0",
+        "readable-stream": "~2.3.3",
+        "scope-analyzer": "^2.0.1",
+        "shallow-copy": "~0.0.1",
+        "static-eval": "^2.0.5",
+        "through2": "~2.0.3"
+      },
+      "dependencies": {
+        "magic-string": {
+          "version": "0.25.1",
+          "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.1.tgz",
+          "integrity": "sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==",
+          "requires": {
+            "sourcemap-codec": "^1.4.1"
+          }
+        },
+        "through2": {
+          "version": "2.0.5",
+          "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+          "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+          "requires": {
+            "readable-stream": "~2.3.6",
+            "xtend": "~4.0.1"
+          }
+        }
+      }
+    },
     "string-argv": {
       "version": "0.3.1",
       "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
@@ -10037,11 +11025,64 @@
         }
       }
     },
+    "string.prototype.trim": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz",
+      "integrity": "sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==",
+      "requires": {
+        "define-properties": "^1.1.3",
+        "es-abstract": "^1.18.0-next.0"
+      },
+      "dependencies": {
+        "es-abstract": {
+          "version": "1.18.0-next.1",
+          "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
+          "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
+          "requires": {
+            "es-to-primitive": "^1.2.1",
+            "function-bind": "^1.1.1",
+            "has": "^1.0.3",
+            "has-symbols": "^1.0.1",
+            "is-callable": "^1.2.2",
+            "is-negative-zero": "^2.0.0",
+            "is-regex": "^1.1.1",
+            "object-inspect": "^1.8.0",
+            "object-keys": "^1.1.1",
+            "object.assign": "^4.1.1",
+            "string.prototype.trimend": "^1.0.1",
+            "string.prototype.trimstart": "^1.0.1"
+          }
+        },
+        "is-callable": {
+          "version": "1.2.2",
+          "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
+          "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA=="
+        },
+        "is-regex": {
+          "version": "1.1.1",
+          "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
+          "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
+          "requires": {
+            "has-symbols": "^1.0.1"
+          }
+        },
+        "object.assign": {
+          "version": "4.1.1",
+          "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz",
+          "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==",
+          "requires": {
+            "define-properties": "^1.1.3",
+            "es-abstract": "^1.18.0-next.0",
+            "has-symbols": "^1.0.1",
+            "object-keys": "^1.1.1"
+          }
+        }
+      }
+    },
     "string.prototype.trimend": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
       "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.5"
@@ -10051,7 +11092,6 @@
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz",
       "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==",
-      "dev": true,
       "requires": {
         "define-properties": "^1.1.3",
         "es-abstract": "^1.17.5"
@@ -10061,7 +11101,6 @@
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
       "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
-      "dev": true,
       "requires": {
         "safe-buffer": "~5.1.0"
       }
@@ -10548,6 +11587,14 @@
       "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=",
       "dev": true
     },
+    "svg-to-pdfkit": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/svg-to-pdfkit/-/svg-to-pdfkit-0.1.8.tgz",
+      "integrity": "sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==",
+      "requires": {
+        "pdfkit": ">=0.8.1"
+      }
+    },
     "svgo": {
       "version": "1.3.2",
       "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
@@ -10648,6 +11695,43 @@
         "resolve": "^1.14.2"
       }
     },
+    "tape": {
+      "version": "4.13.3",
+      "resolved": "https://registry.npmjs.org/tape/-/tape-4.13.3.tgz",
+      "integrity": "sha512-0/Y20PwRIUkQcTCSi4AASs+OANZZwqPKaipGCEwp10dQMipVvSZwUUCi01Y/OklIGyHKFhIcjock+DKnBfLAFw==",
+      "requires": {
+        "deep-equal": "~1.1.1",
+        "defined": "~1.0.0",
+        "dotignore": "~0.1.2",
+        "for-each": "~0.3.3",
+        "function-bind": "~1.1.1",
+        "glob": "~7.1.6",
+        "has": "~1.0.3",
+        "inherits": "~2.0.4",
+        "is-regex": "~1.0.5",
+        "minimist": "~1.2.5",
+        "object-inspect": "~1.7.0",
+        "resolve": "~1.17.0",
+        "resumer": "~0.0.0",
+        "string.prototype.trim": "~1.2.1",
+        "through": "~2.3.8"
+      },
+      "dependencies": {
+        "is-regex": {
+          "version": "1.0.5",
+          "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
+          "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
+          "requires": {
+            "has": "^1.0.3"
+          }
+        },
+        "object-inspect": {
+          "version": "1.7.0",
+          "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
+          "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw=="
+        }
+      }
+    },
     "terser": {
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.2.tgz",
@@ -10688,8 +11772,7 @@
     "through": {
       "version": "2.3.8",
       "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
-      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
-      "dev": true
+      "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
     },
     "through2": {
       "version": "3.0.2",
@@ -10707,6 +11790,16 @@
       "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
       "dev": true
     },
+    "tiny-inflate": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
+      "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="
+    },
+    "tinyqueue": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz",
+      "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
+    },
     "tmp": {
       "version": "0.0.33",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@@ -10815,6 +11908,11 @@
         "tslib": "^1.8.1"
       }
     },
+    "type": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+      "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
+    },
     "type-check": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -10830,6 +11928,11 @@
       "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
       "dev": true
     },
+    "typedarray": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+    },
     "typedarray-to-buffer": {
       "version": "3.1.5",
       "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -10850,6 +11953,50 @@
       "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
       "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
     },
+    "uglify-js": {
+      "version": "2.8.29",
+      "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
+      "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+      "requires": {
+        "source-map": "~0.5.1",
+        "uglify-to-browserify": "~1.0.0",
+        "yargs": "~3.10.0"
+      },
+      "dependencies": {
+        "camelcase": {
+          "version": "1.2.1",
+          "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+          "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk="
+        },
+        "cliui": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+          "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+          "requires": {
+            "center-align": "^0.1.1",
+            "right-align": "^0.1.1",
+            "wordwrap": "0.0.2"
+          }
+        },
+        "yargs": {
+          "version": "3.10.0",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+          "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+          "requires": {
+            "camelcase": "^1.0.2",
+            "cliui": "^2.1.0",
+            "decamelize": "^1.0.0",
+            "window-size": "0.1.0"
+          }
+        }
+      }
+    },
+    "uglify-to-browserify": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
+      "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=",
+      "optional": true
+    },
     "unherit": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
@@ -10882,12 +12029,46 @@
       "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==",
       "dev": true
     },
+    "unicode-properties": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.3.1.tgz",
+      "integrity": "sha512-nIV3Tf3LcUEZttY/2g4ZJtGXhWwSkuLL+rCu0DIAMbjyVPj+8j5gNVz4T/sVbnQybIsd5SFGkPKg/756OY6jlA==",
+      "requires": {
+        "base64-js": "^1.3.0",
+        "unicode-trie": "^2.0.0"
+      },
+      "dependencies": {
+        "base64-js": {
+          "version": "1.3.1",
+          "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+          "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+        },
+        "unicode-trie": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
+          "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
+          "requires": {
+            "pako": "^0.2.5",
+            "tiny-inflate": "^1.0.0"
+          }
+        }
+      }
+    },
     "unicode-property-aliases-ecmascript": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz",
       "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==",
       "dev": true
     },
+    "unicode-trie": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-1.0.0.tgz",
+      "integrity": "sha512-v5raLKsobbFbWLMoX9+bChts/VhPPj3XpkNr/HbqkirXR1DPk8eo9IYKyvk0MQZFkaoRsFj2Rmaqgi2rfAZYtA==",
+      "requires": {
+        "pako": "^0.2.5",
+        "tiny-inflate": "^1.0.0"
+      }
+    },
     "unified": {
       "version": "9.2.0",
       "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz",
@@ -11070,8 +12251,7 @@
     "util-deprecate": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
-      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
-      "dev": true
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
     },
     "util.promisify": {
       "version": "1.0.1",
@@ -11107,6 +12287,16 @@
       "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==",
       "dev": true
     },
+    "venn.js": {
+      "version": "0.2.20",
+      "resolved": "https://registry.npmjs.org/venn.js/-/venn.js-0.2.20.tgz",
+      "integrity": "sha512-bb5SYq/wamY9fvcuErb9a0FJkgIFHJjkLZWonQ+DoKKuDX3WPH2B4ouI1ce4K2iejBklQy6r1ly8nOGIyOCO6w==",
+      "requires": {
+        "d3-selection": "^1.0.2",
+        "d3-transition": "^1.0.1",
+        "fmin": "0.0.2"
+      }
+    },
     "vfile": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.0.tgz",
@@ -11170,11 +12360,30 @@
       "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=",
       "dev": true
     },
+    "window-size": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
+      "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0="
+    },
+    "wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="
+    },
+    "word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="
+    },
     "word-wrap": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
-      "dev": true
+      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ=="
+    },
+    "wordwrap": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
+      "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8="
     },
     "wrap-ansi": {
       "version": "6.2.0",
@@ -11249,8 +12458,7 @@
     "wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-      "dev": true
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
     },
     "write": {
       "version": "1.0.3",
@@ -11273,11 +12481,41 @@
         "typedarray-to-buffer": "^3.1.5"
       }
     },
+    "xlsx": {
+      "version": "0.16.7",
+      "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.16.7.tgz",
+      "integrity": "sha512-Xc4NRjci2Grbh9NDk/XoaWycJurxEug1wwn0aJCmB0NvIMyQuHYq2muWLWGidYNZPf94aUbqm6K8Fbjd7gKTZg==",
+      "requires": {
+        "adler-32": "~1.2.0",
+        "cfb": "^1.1.4",
+        "codepage": "~1.14.0",
+        "commander": "~2.17.1",
+        "crc-32": "~1.2.0",
+        "exit-on-epipe": "~1.0.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.17.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+          "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg=="
+        }
+      }
+    },
+    "xmldoc": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-1.1.2.tgz",
+      "integrity": "sha512-ruPC/fyPNck2BD1dpz0AZZyrEwMOrWTO5lDdIXS91rs3wtm4j+T8Rp2o+zoOYkkAxJTZRPOSnOGei1egoRmKMQ==",
+      "requires": {
+        "sax": "^1.2.1"
+      }
+    },
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
-      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
-      "dev": true
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
     },
     "y18n": {
       "version": "4.0.0",
diff --git a/package.json b/package.json
index a131f985cc87b63e168fa6ae0944c43568649c88..4adcadf9241a2bfe5150eb5a89819a53275c31d0 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,8 @@
     "commit": "git-cz"
   },
   "dependencies": {
+    "@amcharts/amcharts4": "^4.9.37",
+    "@amcharts/amcharts4-geodata": "^4.1.17",
     "@popperjs/core": "^2.5.3",
     "choices.js": "^9.0.1",
     "prosemirror-example-setup": "^1.1.2",
diff --git a/public/favicon.ico b/public/favicon.ico
index 3a7011d31145b36525ab60c8ed7dc0dbf07d7303..a55e7ac72447c43fa31a4a9ac72f1e35545ca29e 100644
Binary files a/public/favicon.ico and b/public/favicon.ico differ