Compare commits

..

74 Commits

Author SHA1 Message Date
mmaurostoffel c3ab7d8e2f global for movingAverage implemented 2025-01-15 21:27:53 +01:00
mmaurostoffel 7e3862a578 global Extractions für region capacities monthly and weekdays eingefügt, closes #15 2025-01-15 20:44:16 +01:00
Giò Diani 0733709366 diagramm etl 2025-01-15 17:48:12 +01:00
Giò Diani 3290c1cce3 some polishing 2025-01-15 16:57:18 +01:00
Giò Diani 959b84d1e1 my fault fixes #14 2025-01-15 14:31:07 +01:00
Giò Diani a5a21fb925 Überarbeitung Dashboard 2025-01-14 22:11:31 +01:00
Giò Diani 8bef4b9621 added simple caching for etl 2025-01-14 19:56:15 +01:00
mmaurostoffel d436c5d892 added missing logic to etl_region_movAverage 2025-01-13 23:06:42 +01:00
Giò Diani cd66207bc7 Prediction Charts 2025-01-13 22:50:03 +01:00
Giò Diani 18a672a5de bugfix unnecessary parentheses broke api 2025-01-13 19:22:54 +01:00
mmaurostoffel 3d7d5bbbe3 etl_region_capacities_monthly eingefügt closes #10 2025-01-13 18:02:19 +01:00
mmaurostoffel d8d2d1e757 Werte von 0-1 zu 0-100 angepasst closes #12 2025-01-13 17:17:46 +01:00
mmaurostoffel 4487932f1b Merge branch 'main' of https://gitea.fhgr.ch/stoffelmauro/ConsultancyProject_2_ETL 2025-01-13 17:12:15 +01:00
mmaurostoffel fcd7ca34ad closes #11 2025-01-13 17:08:37 +01:00
Giò Diani ebcd647a2f Einige Anpassungen am Dashboard. Region Ansicht. 2025-01-13 17:06:54 +01:00
Giò Diani a3121bf58e navigation, regions 2025-01-12 20:55:46 +01:00
Giò Diani 50ea3f1bd8 fix module not found error (matplotlib not needed) 2025-01-12 20:44:05 +01:00
mmaurostoffel af1c2301a9 Added Enpoint for movAvg 2025-01-12 20:38:16 +01:00
mmaurostoffel a571c8c40f Merge branch 'main' of https://gitea.fhgr.ch/stoffelmauro/ConsultancyProject_2_ETL 2025-01-12 20:30:03 +01:00
mmaurostoffel b23879b6d3 First Version of etl_region_movAverage.py eingefügt 2025-01-12 20:27:33 +01:00
Giò Diani 0250221d96 implements regions base endpoint #9 2025-01-12 20:16:20 +01:00
mmaurostoffel f31c23ea51 Merge branch 'main' of https://gitea.fhgr.ch/stoffelmauro/ConsultancyProject_2_ETL 2025-01-12 16:53:33 +01:00
mmaurostoffel c059890ba7 singleScrape_of_region eingefügt 2025-01-12 16:53:31 +01:00
Giò Diani 992e299829 Überarbeitung Property 2025-01-12 16:49:29 +01:00
Giò Diani 67c0d85213 Betrifft #7. Möglicher fix, bitte Resultat kontrollieren. Das Problem lag m.E. darin, dass durch das hin und her zwischen Listen und DataFrame die Typisierungen der Werte verloren gehen, weshalb es dann auch entsprechenden Fehler schmeisste. 2025-01-12 11:56:33 +01:00
mmaurostoffel e176d1e73f Bugfix: Testfunktion aus etl_region_comparison gelöscht
sry
2025-01-12 11:28:53 +01:00
mmaurostoffel f114eb7f5a strict=False hinzugefügt wie in der Fehlermeldung vorgeschlagen 2025-01-12 11:20:39 +01:00
Giò Diani 99a112df24 longer timeouts 2025-01-11 21:54:26 +01:00
Giò Diani 2013d2b440 Wiederinstandsetzung Heatmap @stoffelmauro musste dazu etwas in der API anpassen. 2025-01-11 20:52:02 +01:00
mmaurostoffel 67382003ca closes #7: etl_region_capacities erstellt
!! Wie im Issue beschrieben wurde etl_region_capacities zu etl_region_properties_capacities angepasst und die Endpoints ebenfalls.!!

!!Die Abfrage der globalen Daten ist implementiert und funktioniert, braucht aber recht lange!!
2025-01-11 17:33:50 +01:00
Giò Diani 774e30c945 fix daily chart 2025-01-09 18:49:09 +01:00
Giò Diani 3b935a4d20 Nachbaren in Popup 2025-01-09 18:34:20 +01:00
mmaurostoffel 638d835d3b closes #5
Test to try out the closing feature
2025-01-09 18:26:01 +01:00
mmaurostoffel cb6935b60c updated the output of the etl_property_neighbour.py
closes issue #5
2025-01-09 18:22:55 +01:00
mmaurostoffel 60a3d7d9b3 Little fixes for weekdays
stoffelmauro/ConsultancyProject_2_ETL#6
2025-01-09 17:40:58 +01:00
mmaurostoffel 65b63d1326 Sortierung für etl_property_capacities_monthly eingefügt 2025-01-09 15:59:16 +01:00
mmaurostoffel a6cbe3bc29 update des etl_capacities_weekdays.py 2025-01-09 15:07:38 +01:00
mmaurostoffel 2508b34ceb bugfix: falscher name 2025-01-09 14:49:03 +01:00
mmaurostoffel cc71cbba2d Endpoint für etl_property_neighbours hinzugefügt 2025-01-09 14:48:33 +01:00
Giò Diani 258f1e4df6 Anzeige Auslastung p. Monat bei Properties im Dashboard. 2025-01-08 22:02:33 +01:00
mmaurostoffel 7884febe53 issue 5 resolved
stoffelmauro/ConsultancyProject_2_ETL#3
Ausgabeforma:
{ids: [84, 43...44], lat:[...], lon[...]}
2025-01-07 20:20:48 +01:00
mmaurostoffel 42dc14021f etl_Property_capacities_weekdays.py eingefügt
Abfragemöglichkeit für die Wochentage eingefügt
2025-01-06 19:42:49 +01:00
Giò Diani f5a2b16721 Fehlende Anführungszeichen hinzugefügt resolves #3 2025-01-05 21:45:23 +01:00
mmaurostoffel d9cae3d0ab Issue 3 fast fertig
stoffelmauro/ConsultancyProject_2_ETL#3

Der Issue ist soweit bereit, es gibt aber noch das Problem, dass das ScrapeDate nicht als Datum sondern asl Integer interpretiert wird im database.py. Deshalb ist es im Moment als konstante implementiert
2025-01-05 21:23:10 +01:00
mmaurostoffel 8bcc1c57b5 Gitea Issue 1 Beispiel 2
stoffelmauro/ConsultancyProject_2_ETL#1
etl_region_capacities_comparison eingefügt
2025-01-05 17:25:29 +01:00
mmaurostoffel 03e78a4105 Issue 1 Beispiel 1 resolved
stoffelmauro/ConsultancyProject_2_ETL#1

Globale region capacities eingefügt: Vorsicht! lange Ladezeit
2025-01-05 16:12:16 +01:00
mmaurostoffel 2a9ef9d991 Merge branch 'main' of https://gitea.fhgr.ch/stoffelmauro/ConsultancyProject_2_ETL 2025-01-05 15:51:27 +01:00
mmaurostoffel 8fcaf2a6f7 Gitea Issue 2 resolved
stoffelmauro/ConsultancyProject_2_ETL#2

etl_region_capacities.py: neues Output Format = [datum, prop_id, capacity]
2025-01-05 15:51:19 +01:00
Giò Diani 8655255782 info button, dashboard startseite 2025-01-05 13:26:51 +01:00
mmaurostoffel 281d9d3f5a Merge branch 'main' of https://gitea.fhgr.ch/stoffelmauro/ConsultancyProject_2_ETL 2025-01-05 13:19:45 +01:00
mmaurostoffel c68e6f54bd cleanup commit 2025-01-05 13:19:43 +01:00
Giò Diani 32d162c7c5 Überarbeitung Detailansicht. 2025-01-04 18:16:12 +01:00
Giò Diani 466d3168c4 Added caching (adjust setting in .env accordingly; CACHE_STORE=file) 2025-01-03 16:44:17 +01:00
Giò Diani 5a2cc96a95 Achsenbeschrftungen, Farben 2025-01-03 16:25:30 +01:00
Giò Diani 640a5b2f9e Implement region capacity as test 2024-12-20 21:46:54 +01:00
Giò Diani f585a7a2aa fix fehlender import 2024-12-20 21:36:47 +01:00
mmaurostoffel 818d6fb5ec Merge branch 'main' of https://gitea.fhgr.ch/stoffelmauro/ConsultancyProject_2_ETL 2024-12-20 20:57:20 +01:00
mmaurostoffel a8b856b714 etl_region_capacities erstellt + database und api/main Anpassungen dafür 2024-12-20 20:57:10 +01:00
Giò Diani 0aa0f2345c documentation folder, c4 diagram 2024-12-20 17:38:08 +01:00
Giò Diani eb362d78ad Vorbereitung heatmap 2024-12-20 15:25:33 +01:00
Giò Diani 5f61911a69 More sensible default in env, more documentation how to install and run. 2024-12-20 15:03:22 +01:00
Giò Diani 66d048c70e Enhance coordinated view of property. 2024-12-20 12:20:49 +01:00
Giò Diani 63590d69ab fix wrong import path. 2024-12-20 10:56:10 +01:00
Giò Diani 47a5035787 dashboard einzelansicht trend auslastung. 2024-12-19 19:22:09 +01:00
mmaurostoffel 4b7067fb63 Merge branch 'main' of https://gitea.fhgr.ch/stoffelmauro/ConsultancyProject_2_ETL 2024-12-19 18:11:17 +01:00
mmaurostoffel eba2f0a265 Data Quality updated to include Regions and more information 2024-12-19 18:11:15 +01:00
Giò Diani ce46655003 Add some more description. 2024-12-18 20:10:11 +01:00
Giò Diani 233f3c475a Added README 2024-12-18 19:55:42 +01:00
Giò Diani a8543d619f Further dashboard development. 2024-12-18 19:52:06 +01:00
Giò Diani 1574edea88 First steps Dashboard. 2024-12-18 15:14:13 +01:00
mmaurostoffel a03ce3d647 Änderungen von bevor Monorepo übernommen 2024-12-18 15:11:23 +01:00
Giò Diani f4a927e125 refactor to monorepo, install laravel. 2024-12-18 10:14:56 +01:00
mmaurostoffel 125250a665 data_quality.py erstellt zur Visualisierung der Datenqualität 2024-12-11 01:01:52 +01:00
mmaurostoffel 338d3e9cc2 Untersuchung Vorbuchungszeit abgeschlossen 2024-11-28 16:10:53 +01:00
100 changed files with 65175 additions and 1543 deletions

6
.gitignore vendored
View File

@ -23,6 +23,7 @@
*.ipr *.ipr
.idea/ .idea/
# eclipse project file # eclipse project file
.settings/ .settings/
.classpath .classpath
@ -65,3 +66,8 @@ env3.*/
# duckdb # duckdb
*.duckdb *.duckdb
# cache
*.obj
/src/mauro/dok/

6
README.md Normal file
View File

@ -0,0 +1,6 @@
# Consultancy 2
## Projektstruktur
- etl: Enthält den Programmcode, welcher die Daten aufbereitet und via REST-API zur Verfügung stellt.
- dashboard: Webapplikation zur Exploration und Visualisierung der Daten.

View File

@ -21,14 +21,14 @@ LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug LOG_LEVEL=debug
DB_CONNECTION=sqlite # DB_CONNECTION=sqlite
# DB_HOST=127.0.0.1 # DB_HOST=127.0.0.1
# DB_PORT=3306 # DB_PORT=3306
# DB_DATABASE=laravel # DB_DATABASE=laravel
# DB_USERNAME=root # DB_USERNAME=root
# DB_PASSWORD= # DB_PASSWORD=
SESSION_DRIVER=database SESSION_DRIVER=file
SESSION_LIFETIME=120 SESSION_LIFETIME=120
SESSION_ENCRYPT=false SESSION_ENCRYPT=false
SESSION_PATH=/ SESSION_PATH=/
@ -38,7 +38,7 @@ BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local FILESYSTEM_DISK=local
QUEUE_CONNECTION=database QUEUE_CONNECTION=database
CACHE_STORE=database CACHE_STORE=file
CACHE_PREFIX= CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1 MEMCACHED_HOST=127.0.0.1
@ -49,11 +49,11 @@ REDIS_PASSWORD=null
REDIS_PORT=6379 REDIS_PORT=6379
MAIL_MAILER=log MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1 MAIL_HOST=127.0.0.1
MAIL_PORT=2525 MAIL_PORT=2525
MAIL_USERNAME=null MAIL_USERNAME=null
MAIL_PASSWORD=null MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}" MAIL_FROM_NAME="${APP_NAME}"
@ -64,3 +64,5 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}" VITE_APP_NAME="${APP_NAME}"
FASTAPI_URI=http://localhost:8080

16
dashboard/README.md Normal file
View File

@ -0,0 +1,16 @@
# Install
## Prerequisites
- In order to run this project please install all required software according to the laravel documentation: https://laravel.com/docs/11.x#installing-php
## Configuration & installation
- Make a copy of the .env.example to .env
- Run the following commands:
```bash
composer install && php artisan key:generate && npm i
```
# Run server
```bash
composer run dev
```

109
dashboard/app/Api.php Normal file
View File

@ -0,0 +1,109 @@
<?php
namespace App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class Api
{
public static function get(string $path, string $query = ''): ?array
{
$endpoint = env('FASTAPI_URI');
$request = $endpoint.$path;
if (Cache::has($request)) {
// return Cache::get($request);
}
$get = Http::timeout(1600)->get($request);
if($get->successful()){
$result = $get->json();
Cache::put($request, $result);
return $result;
}
return null;
}
public static function propertiesPerRegion()
{
return self::get('/region/properties');
}
public static function propertiesGrowth()
{
return self::get('/properties/growth');
}
public static function propertiesGeo()
{
return self::get('/properties/geo');
}
public static function propertyExtractions(int $id)
{
return self::get("/property/{$id}/extractions");
}
public static function propertyCapacities(int $id)
{
return self::get("/property/{$id}/capacities");
}
public static function propertyBase(int $id): mixed
{
return self::get("/property/{$id}/base");
}
public static function regionBase(int $id): mixed
{
return self::get("/region/{$id}/base");
}
public static function regionPropertiesCapacities(int $id): mixed
{
return self::get("/region/{$id}/properties/capacities");
}
public static function regionCapacitiesMonthly(int $id, string $date): mixed
{
return self::get("/region/{$id}/capacities/monthly/{$date}");
}
public static function propertyCapacitiesMonthly(int $id, string $date): mixed
{
return self::get("/property/{$id}/capacities/monthly/{$date}");
}
public static function regionCapacitiesDaily(int $id, string $date): mixed
{
return self::get("/region/{$id}/capacities/weekdays/{$date}");
}
public static function propertyCapacitiesDaily(int $id, string $date): mixed
{
return self::get("/property/{$id}/capacities/weekdays/{$date}");
}
public static function propertyNeighbours(int $id): mixed
{
return self::get("/property/{$id}/neighbours");
}
public static function regionCapacities(int $id): mixed
{
return self::get("/region/{$id}/capacities");
}
public static function regionMovingAverage(int $id, string $date): mixed
{
return self::get("/region/{$id}/movingAverage/{$date}");
}
}

12
dashboard/app/Chart.php Normal file
View File

@ -0,0 +1,12 @@
<?php
namespace App;
class Chart
{
public static function colors(int $count = 5){
$colors = ['#9ebcda','#8c96c6','#88419d','#810f7c','#4d004b'];
return json_encode($colors);
}
}

View File

@ -15,7 +15,7 @@ class User extends Authenticatable
/** /**
* The attributes that are mass assignable. * The attributes that are mass assignable.
* *
* @var array<int, string> * @var list<string>
*/ */
protected $fillable = [ protected $fillable = [
'name', 'name',
@ -26,7 +26,7 @@ class User extends Authenticatable
/** /**
* The attributes that should be hidden for serialization. * The attributes that should be hidden for serialization.
* *
* @var array<int, string> * @var list<string>
*/ */
protected $hidden = [ protected $hidden = [
'password', 'password',

View File

@ -3,7 +3,10 @@
"name": "laravel/laravel", "name": "laravel/laravel",
"type": "project", "type": "project",
"description": "The skeleton application for the Laravel framework.", "description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"], "keywords": [
"laravel",
"framework"
],
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^8.2", "php": "^8.2",
@ -68,4 +71,4 @@
}, },
"minimum-stability": "stable", "minimum-stability": "stable",
"prefer-stable": true "prefer-stable": true
} }

File diff suppressed because it is too large Load Diff

View File

@ -39,10 +39,10 @@ return [
'smtp' => [ 'smtp' => [
'transport' => 'smtp', 'transport' => 'smtp',
'scheme' => env('MAIL_SCHEME'),
'url' => env('MAIL_URL'), 'url' => env('MAIL_URL'),
'host' => env('MAIL_HOST', '127.0.0.1'), 'host' => env('MAIL_HOST', '127.0.0.1'),
'port' => env('MAIL_PORT', 2525), 'port' => env('MAIL_PORT', 2525),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'), 'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'), 'password' => env('MAIL_PASSWORD'),
'timeout' => null, 'timeout' => null,

View File

@ -1,10 +1,11 @@
{ {
"name": "frontend", "name": "dashboard",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@patternfly/patternfly": "^6.0.0",
"@picocss/pico": "^2.0.6", "@picocss/pico": "^2.0.6",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"leaflet": "^1.9.4" "leaflet": "^1.9.4"
@ -442,9 +443,9 @@
} }
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5", "version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -532,6 +533,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@patternfly/patternfly": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-6.1.0.tgz",
"integrity": "sha512-w+QazL8NHKkg5j01eotblsswKxQQSYB0CN3yBXQL9ScpHdp/fK8M6TqWbKZNRpf+NqhMxcH/om8eR0N/fDCJqw==",
"license": "MIT"
},
"node_modules/@picocss/pico": { "node_modules/@picocss/pico": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/@picocss/pico/-/pico-2.0.6.tgz", "resolved": "https://registry.npmjs.org/@picocss/pico/-/pico-2.0.6.tgz",
@ -553,9 +560,9 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
"integrity": "sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==", "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -567,9 +574,9 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
"integrity": "sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==", "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -581,9 +588,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
"integrity": "sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==", "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -595,9 +602,9 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
"integrity": "sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==", "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -609,9 +616,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
"integrity": "sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==", "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -623,9 +630,9 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
"integrity": "sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==", "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -637,9 +644,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
"integrity": "sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==", "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -651,9 +658,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
"integrity": "sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==", "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -665,9 +672,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
"integrity": "sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==", "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -679,9 +686,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
"integrity": "sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==", "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -692,10 +699,24 @@
"linux" "linux"
] ]
}, },
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
"integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
"integrity": "sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==", "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -707,9 +728,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
"integrity": "sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==", "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -721,9 +742,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
"integrity": "sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==", "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -735,9 +756,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
"integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==", "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -749,9 +770,9 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
"integrity": "sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==", "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -763,9 +784,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
"integrity": "sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==", "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -777,9 +798,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
"integrity": "sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==", "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -791,9 +812,9 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.4.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
"integrity": "sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==", "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -914,9 +935,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.7.8", "version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==", "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -969,9 +990,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.2", "version": "4.24.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -989,9 +1010,9 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001669", "caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.41", "electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.18", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.1"
}, },
"bin": { "bin": {
@ -1012,9 +1033,9 @@
} }
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001684", "version": "1.0.30001689",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
"integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==", "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -1323,9 +1344,9 @@
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.66", "version": "1.5.74",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.66.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
"integrity": "sha512-pI2QF6+i+zjPbqRzJwkMvtvkdI7MjVbSh2g8dlMguDJIXEPw+kwasS1Jl+YGPEBfGVxsVgGUratAKymPdPo2vQ==", "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1611,9 +1632,9 @@
} }
}, },
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.15.1", "version": "2.16.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1693,9 +1714,9 @@
} }
}, },
"node_modules/jiti": { "node_modules/jiti": {
"version": "1.21.6", "version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
"integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@ -1703,9 +1724,9 @@
} }
}, },
"node_modules/laravel-vite-plugin": { "node_modules/laravel-vite-plugin": {
"version": "1.0.6", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.6.tgz", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.1.1.tgz",
"integrity": "sha512-B34OqmZc/rV1KvSjst8SsUm/LKHsuDusw8jiZCIhlnTHXbXnK89JUM9pTJuk6E/Vc/1DT2gX7qNfhipak1WS8w==", "integrity": "sha512-HMZXpoSs1OR+7Lw1+g4Iy/s3HF3Ldl8KxxYT2Ot8pEB4XB/QRuZeWgDYJdu552UN03YRSRNK84CLC9NzYRtncA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1716,10 +1737,10 @@
"clean-orphaned-assets": "bin/clean.js" "clean-orphaned-assets": "bin/clean.js"
}, },
"engines": { "engines": {
"node": "^18.0.0 || >=20.0.0" "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"vite": "^5.0.0" "vite": "^5.0.0 || ^6.0.0"
} }
}, },
"node_modules/leaflet": { "node_modules/leaflet": {
@ -1729,13 +1750,16 @@
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/lilconfig": { "node_modules/lilconfig": {
"version": "2.1.0", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
"integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
} }
}, },
"node_modules/lines-and-columns": { "node_modules/lines-and-columns": {
@ -1864,9 +1888,9 @@
} }
}, },
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.18", "version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2094,19 +2118,6 @@
} }
} }
}, },
"node_modules/postcss-load-config/node_modules/lilconfig": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz",
"integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antonk52"
}
},
"node_modules/postcss-nested": { "node_modules/postcss-nested": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
@ -2216,13 +2227,13 @@
} }
}, },
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.9",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-core-module": "^2.13.0", "is-core-module": "^2.16.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0" "supports-preserve-symlinks-flag": "^1.0.0"
}, },
@ -2245,9 +2256,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.27.4", "version": "4.28.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.4.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
"integrity": "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==", "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2261,24 +2272,25 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.27.4", "@rollup/rollup-android-arm-eabi": "4.28.1",
"@rollup/rollup-android-arm64": "4.27.4", "@rollup/rollup-android-arm64": "4.28.1",
"@rollup/rollup-darwin-arm64": "4.27.4", "@rollup/rollup-darwin-arm64": "4.28.1",
"@rollup/rollup-darwin-x64": "4.27.4", "@rollup/rollup-darwin-x64": "4.28.1",
"@rollup/rollup-freebsd-arm64": "4.27.4", "@rollup/rollup-freebsd-arm64": "4.28.1",
"@rollup/rollup-freebsd-x64": "4.27.4", "@rollup/rollup-freebsd-x64": "4.28.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.27.4", "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
"@rollup/rollup-linux-arm-musleabihf": "4.27.4", "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
"@rollup/rollup-linux-arm64-gnu": "4.27.4", "@rollup/rollup-linux-arm64-gnu": "4.28.1",
"@rollup/rollup-linux-arm64-musl": "4.27.4", "@rollup/rollup-linux-arm64-musl": "4.28.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.27.4", "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
"@rollup/rollup-linux-riscv64-gnu": "4.27.4", "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
"@rollup/rollup-linux-s390x-gnu": "4.27.4", "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
"@rollup/rollup-linux-x64-gnu": "4.27.4", "@rollup/rollup-linux-s390x-gnu": "4.28.1",
"@rollup/rollup-linux-x64-musl": "4.27.4", "@rollup/rollup-linux-x64-gnu": "4.28.1",
"@rollup/rollup-win32-arm64-msvc": "4.27.4", "@rollup/rollup-linux-x64-musl": "4.28.1",
"@rollup/rollup-win32-ia32-msvc": "4.27.4", "@rollup/rollup-win32-arm64-msvc": "4.28.1",
"@rollup/rollup-win32-x64-msvc": "4.27.4", "@rollup/rollup-win32-ia32-msvc": "4.28.1",
"@rollup/rollup-win32-x64-msvc": "4.28.1",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@ -2532,9 +2544,9 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.4.15", "version": "3.4.17",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
"integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2547,7 +2559,7 @@
"glob-parent": "^6.0.2", "glob-parent": "^6.0.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"jiti": "^1.21.6", "jiti": "^1.21.6",
"lilconfig": "^2.1.0", "lilconfig": "^3.1.3",
"micromatch": "^4.0.8", "micromatch": "^4.0.8",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",

View File

@ -15,6 +15,7 @@
"vite": "^5.0" "vite": "^5.0"
}, },
"dependencies": { "dependencies": {
"@patternfly/patternfly": "^6.0.0",
"@picocss/pico": "^2.0.6", "@picocss/pico": "^2.0.6",
"echarts": "^5.5.1", "echarts": "^5.5.1",
"leaflet": "^1.9.4" "leaflet": "^1.9.4"

View File

@ -0,0 +1,287 @@
/* 1. Use a more-intuitive box-sizing model */
*, *::before, *::after {
box-sizing: border-box;
}
/* 2. Remove default margin */
* {
margin: 0;
font-family: sans-serif;
}
body {
/* 3. Add accessible line-height */
line-height: 1.5;
/* 4. Improve text rendering */
-webkit-font-smoothing: antialiased;
padding: 0 1em;
height: 100vh;
background-image: radial-gradient(73% 147%, #EADFDF 59%, #ECE2DF 100%), radial-gradient(91% 146%, rgba(255,255,255,0.50) 47%, rgba(0,0,0,0.50) 100%);
background-blend-mode: screen;
}
/* 5. Improve media defaults */
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
/* 6. Inherit fonts for form controls */
input, button, textarea, select {
font: inherit;
}
/* 7. Avoid text overflows */
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
/* 8. Improve line wrapping */
p {
text-wrap: pretty;
}
h1, h2, h3, h4, h5, h6 {
text-wrap: balance;
}
dt{
font-weight: 600;
}
dd + dt{
margin-top: .2em;
}
nav + button,
span + button{
margin-left: .5em;
}
ul{
padding-left: 1em;
}
p + ul{
margin-top: 1em;
}
button[popovertarget]{
background: no-repeat center / .3em #4d004b url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 192 512'%3E%3C!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--%3E%3Cpath fill='%23fff' d='M48 80a48 48 0 1 1 96 0A48 48 0 1 1 48 80zM0 224c0-17.7 14.3-32 32-32l64 0c17.7 0 32 14.3 32 32l0 224 32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 512c-17.7 0-32-14.3-32-32s14.3-32 32-32l32 0 0-192-32 0c-17.7 0-32-14.3-32-32z'/%3E%3C/svg%3E%0A");
cursor: pointer;
display: inline-block;
width: 1.5em;
height: 1.5em;
border-radius: 50%;
border: 1px solid #fff;
}
button[popovertarget]::before{
color: #fff;
font-weight: 700;
}
button[popovertarget]>span{
position: absolute;
left: -999em;
top: -999em;
}
[popover] {
border: none;
border-radius: 1em;
background: #fff;
padding: 1.5em;
border-radius: var(--small-border);
box-shadow: .0625em .0625em .625em rgba(0, 0, 0, 0.1);
max-width: 40em;
top: 4em;
margin: 0 auto;
}
[popover]::backdrop{
background-color: rgba(0,0,0,.5);
}
[popover] h2{
font-size: 1em;
}
/*
9. Create a root stacking context
*/
#root, #__next {
isolation: isolate;
}
body>header{
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3em;
background: #ccc;
z-index: 99;
display: flex;
align-items: center;
padding: 0 1em;
}
body>header>nav{
text-align: center;
min-width: 10em;
background: #fff;
border-radius: .2em;
position: relative;
}
body>header>nav>ul{
position: absolute;
background: #fff;
width: 100%;
list-style: none;
padding: 0;
top: -999em;
left: -999em;
}
body>header>nav:hover ul{
top: initial;
left: 0;
}
body>header>nav>ul>li a,
body>header>nav>strong{
display: inline-block;
padding: .2em .4em;
}
a{
color: #000;
}
a:hover,
a:focus{
color: #aaa;
}
main{
width: 100%;
height: 100vh;
padding: 4em 0 1em;
display: grid;
gap: .5em;
}
body.overview main{
grid-template-columns: repeat(8, minmax(1%, 50%));
grid-template-rows: repeat(4, 1fr);
grid-template-areas:
"chart3 chart3 chart3 chart1 chart1 chart1 chart4 chart4"
"chart3 chart3 chart3 chart1 chart1 chart1 chart4 chart4"
"chart3 chart3 chart3 chart2 chart2 chart2 chart4 chart4"
"chart3 chart3 chart3 chart2 chart2 chart2 chart4 chart4"
}
body.region main{
grid-template-columns: repeat(4, minmax(10%, 50%));
grid-template-rows: repeat(6, 1fr) 4em;
grid-template-areas:
"chart1 chart1 chart2 chart2"
"chart1 chart1 chart2 chart2"
"chart1 chart1 chart3 chart4"
"chart1 chart1 chart3 chart4"
"chart1 chart1 chart6 chart6"
"chart1 chart1 chart6 chart6"
"chart1 chart1 timeline timeline";
}
body.property main{
grid-template-columns: repeat(4, minmax(10%, 50%));
grid-template-rows: repeat(4, 1fr) 4em;
grid-template-areas:
"chart2 chart2 chart1 chart1"
"chart2 chart2 chart1 chart1"
"chart5 chart5 chart3 chart4"
"chart5 chart5 chart3 chart4"
"chart5 chart5 timeline timeline";
}
article{
background: #f9f9f9;
border: .0625em solid #ccc;
box-shadow: 0 5px 10px rgba(154,160,185,.05), 0 15px 40px rgba(166,173,201,.2);
border-radius: .2em;
display: grid;
}
article.header{
grid-template-columns: 100%;
grid-template-rows: minmax(1%, 2em) 1fr;
padding: .5em 1em 1em .5em;
}
article.map{
padding: 0;
}
article.map>header{
padding: .5em 1em 1em .5em;
}
article>header{
display: grid;
grid-template-columns: 1fr 1em;
grid-template-rows: 1fr;
}
article>header>h2{
font-size: .8em;
font-weight: 600;
}
@media(max-width: 960px){
body{
height: auto;
}
main{
height: auto;
grid-template-columns: 100%;
grid-template-rows: repeat(4, minmax(20em, 25em));
}
}
.leaflet-marker-icon span{
background: #4d004b;
width: 2rem;
height: 2rem;
display: block;
left: -1rem;
top: -1rem;
position: relative;
border-radius: 50% 50% 0;
transform: rotate(45deg);
border: 2px solid #fff
}
/*['#9ecae1','#6baed6','#4292c6','#2171b5','#084594'*/
.leaflet-marker-icon.region1 span{
background: #8c96c6;
}
.leaflet-marker-icon.region2 span{
background: #88419d;
}
.leaflet-marker-icon.region3 span{
background: #810f7c;
}
.leaflet-marker-icon.region4 span{
background: #4d004b;
}

4
dashboard/resources/css/pico.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
import * as echarts from 'echarts';
import 'leaflet'
window.echarts = echarts;

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard</title>
@vite(['resources/css/app.css', 'resources/js/app.js', 'node_modules/leaflet/dist/leaflet.css'])
</head>
<body class="@yield('body-class')">
<header>
@yield('header')
</header>
<main>
@yield('main')
</main>
</body>
</html>

View File

@ -0,0 +1,329 @@
@extends('base')
@section('body-class', 'overview')
@section('header')
<nav>
<strong>Start</strong>
<ul>
@foreach($regions as $r)
<li><a href="/region/{{ $r['region_id'] }}">{{ $r['region_name'] }}</a></li>
@endforeach
</ul>
</nav>
@endsection
@section('main')
<article class="header" style="grid-area: chart3;">
<header>
<h2>Auslastung aller Mietobjekte über Gesamte Zeit</h2>
<button popovertarget="pop1">
<span>Erklärungen zum Diagramm</span>
</button>
<div popover id="pop1">
<h2>Auslastung aller Mietobjekte über Gesamte Zeit</h2>
<p>
Das Diagramm gibt eine Übersicht, wie die Auslastung von Mietobjekten am Datum des Scrapings waren. Dazu wird für jedes Mietobjekt die durchschnittliche Verfügbarkeit ermittelt.
</p>
<ul>
<li>X-Achse: Zeitpunkt Scraping.</li>
<li>Y-Achse: Mietobjekte.</li>
<li>Kategorien: 0% = Das Mietobjekt ist komplett verfügbar; 100% = Das Mietobjekt ist komplett ausgebucht.</li>
</ul>
</div>
<div>
</header>
<div id="chart-heatmap"></div>
</article>
<article class="header" style="grid-area: chart1;">
<header>
<h2>
Anzahl jemals gefundene Kurzzeitmietobjekte pro Region
</h2>
<button popovertarget="pop2">
<span>Erklärungen zum Diagramm</span>
</button>
<div popover id="pop2">
<h2>Anzahl jemals gefundene Kurzzeitmietobjekte pro Region</h2>
<p>
Das Balkendiagramm zeigt wieviele Kurzzeitmietobjekte insgesamt pro Region über den gesamten Datenerhebungszeitraum, gefunden wurden.
</p>
<ul>
<li>X-Achse: Bezeichnung der Region.</li>
<li>Y-Achse: Anzahl Mietobjekte.</li>
</ul>
</div>
<div>
</header>
<div id="chart-props-per-region"></div>
</article>
<article class="header" style="grid-area: chart2;">
<header>
<h2>
Entwicklung der Anzahl jemals gefunden Kurzzeitmietobjekte
</h2>
<button popovertarget="pop3">
<span>Erklärungen zum Diagramm</span>
</button>
<div popover id="pop3">
<h2>Entwicklung Anzahl jemals gefundener Kurzzeitmietobjekte pro Region</h2>
<p>
Das Liniendiagramm zeigt die Entwicklung der gefundenen Mietobjekte pro Region.
</p>
<ul>
<li>X-Achse: Zeitpunkt Scraping.</li>
<li>Y-Achse: Anzahl Mietobjekte.</li>
</ul>
</div>
<div>
</header>
<div id="extractions"></div>
</article>
<article style="grid-area: chart4;">
<div id="leaflet"></div>
</article>
<script type="module">
const sharedOptions = {
basic: {
color: {!! $chartOptions['colors'] !!},
grid: {
top: 30,
left: 70,
right: 0,
bottom: 45
},
name: (opt) => {
return {
name: opt.name,
nameLocation: opt.location,
nameGap: 50,
nameTextStyle: {
fontWeight: 'bold',
},
}
}
}
}
const extractionDates = {!! json_encode($regionPropertiesCapacities['scrapeDates']) !!};
const chartHeatmap = document.getElementById('chart-heatmap');
const cHeatmap = echarts.init(chartHeatmap);
const cHeatmapOptions = {
animation: false,
tooltip: {
position: 'top'
},
grid: {
top: 30,
right: 45,
bottom: 50,
left: 5
},
dataZoom: [{
type: 'slider'
},
{
type: 'slider',
show: true,
yAxisIndex: 0,
}],
xAxis: {
show: false,
name: 'Kurzzeitmietobjekt',
type: 'category',
data: extractionDates,
splitArea: {
show: false
},
axisLabel: {
show: true,
}
},
yAxis: {
show: false,
type: 'category',
data: {!! json_encode($regionPropertiesCapacities['property_ids']) !!},
splitArea: {
show: true
}
},
visualMap: {
type: 'piecewise',
min: 0,
max: 100,
calculable: true,
orient: 'horizontal',
left: 'center',
top: 0,
formatter: (v1, v2) => {
return `${v1}${v2}%`;
},
inRange: {
color: sharedOptions.basic.color,
},
},
series: [
{
name: 'Auslastung',
type: 'heatmap',
blurSize: 0,
data: {!! json_encode($regionPropertiesCapacities['values']) !!},
label: {
show: false
},
tooltip: {
formatter: (data) => {
return `Kurzzeitmietobjekte-ID: ${data.data[1]}<br />Datum Scraping: ${data.data[0]}<br/>Auslastung: ${data.data[2].toFixed(2)}%`
},
},
emphasis: {
itemStyle: {
borderColor: '#000',
borderWidth: 2
}
}
}
]
}
cHeatmap.setOption(cHeatmapOptions);
const chartPropsPerRegion = document.getElementById('chart-props-per-region');
const cPropsPerRegion = echarts.init(chartPropsPerRegion);
const cPropsPerRegionOptions = {
grid: sharedOptions.basic.grid,
color: sharedOptions.basic.color,
xAxis: {
name: 'Region',
nameLocation: 'center',
nameGap: 30,
nameTextStyle: {
fontWeight: 'bold',
},
type: 'category',
data: {!! $propsPerRegion[1] !!}
},
yAxis: {
type: 'value',
name: 'Anzahl Mietobjekte',
nameLocation: 'middle',
nameGap: 50,
nameTextStyle: {
fontWeight: 'bold',
},
},
series: [
{
data: {!! $propsPerRegion[2] !!},
type: 'bar',
itemStyle: {
color: (e) => {
return sharedOptions.basic.color[e.dataIndex];
}
}
},
]
};
cPropsPerRegion.setOption(cPropsPerRegionOptions);
const chartExtractions = document.getElementById('extractions');
const cExtractions = echarts.init(chartExtractions);
const filters = {
regions: ["Alle", "Davos", "Engadin", "Heidiland", "St. Moritz"]
}
const cExtractionsOptions = {
color: sharedOptions.basic.color,
tooltip: {
trigger: 'axis'
},
legend: {
data: filters.regions
},
grid: sharedOptions.basic.grid,
xAxis: {
name: 'Zeitpunkt Scraping',
nameLocation: 'center',
nameGap: 24,
nameTextStyle: {
fontWeight: 'bold',
},
type: 'category',
boundaryGap: false,
data: extractionDates
},
yAxis: {
name: 'Anzahl Mietobjekte',
nameLocation: 'center',
nameGap: 50,
nameTextStyle: {
fontWeight: 'bold',
},
type: 'value'
},
series: [
{
name: 'Alle',
type: 'line',
stack: 'Total',
data: {!! json_encode($growth['total_all']) !!},
},
{
name: 'Davos',
type: 'line',
data: {!! json_encode($growth['total_davos']) !!}
},
{
name: 'Engadin',
type: 'line',
data: {!! json_encode($growth['total_engadin']) !!}
},
{
name: 'Heidiland',
type: 'line',
data: {!! json_encode($growth['total_heidiland']) !!}
},
{
name: 'St. Moritz',
type: 'line',
data: {!! json_encode($growth['total_stmoritz']) !!}
},
]
};
cExtractions.setOption(cExtractionsOptions);
const map = L.map('leaflet');
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
function icon(id){
return L.divIcon({
className: "region"+id,
html: '<span></span>'
})
}
const markers = L.featureGroup([
@foreach($geo as $g)
L.marker([{{ $g['latlng'] }}], {icon: icon({{ $g['region_id'] }})}).bindPopup('<a href="/property/{{ $g['property_id'] }}">{{ $g['latlng'] }}</a>'),
@endforeach
]).addTo(map);
map.fitBounds(markers.getBounds(), {padding: [20,20]})
cHeatmap.on('click', 'series', (e) => {
window.open(`/property/${e.value[1]}?date=${e.value[0]}`, '_self');
})
cPropsPerRegion.on('click', 'series', (e) => {
console.log(e.dataIndex);
//window.open(`/property/${e.value[1]}?date=${e.value[0]}`, '_self');
})
</script>
@endsection

View File

@ -0,0 +1,454 @@
@extends('base')
@section('body-class', 'property')
@section('header')
<nav>
<strong>Property: {{ $base['check_data'] }}</strong>
<ul>
<li><a href="/">Start</a></li>
@foreach($regions as $r)
<li><a href="/region/{{ $r['region_id'] }}">{{ $r['region_name'] }}</a></li>
@endforeach
</ul>
</nav>
<button popovertarget="prop-details"></button>
<div popover id="prop-details">
<dl>
<dt>Region</dt>
<dd>{{ $base['region_name'] }}</dd>
<dt>Zum ersten mal gefunden</dt>
<dd>{{ $base['first_found'] }}</dd>
<dt>Zum letzten mal gefunden</dt>
<dd>{{ $base['last_found'] }}</dd>
</dl>
</div>
@endsection
@section('main')
<article style="grid-area: timeline;">
<div id="timeline"></div>
</article>
<article class="header" style="grid-area: chart1;">
<header>
<h2 id="belegung-title">
Kalenderansicht der Belegung am <span class="date">{{ $startDate }}</span>
</h2><button popovertarget="popup-cal"></button>
<div popover id="popup-cal">
<p>
Das Kalenderdiagram zeigt die drei Verfügbarkeitskategorien des Mietobjekts.
</p>
</div>
</header>
<div id="chart-calendar"></div>
</article>
<article class="header map" style="grid-area: chart5;">
<header>
<h2 id="belegung-title">
Kurzzeitmietobjekte in der Nähe
</h2>
</header>
<div id="chart-map"></div>
</article>
<article class="header" style="grid-area: chart3;">
<header>
<h2>
Belegung Mietobjekt Monate am <span class="date">{{ $startDate }}</span>
</h2>
</header>
<div id="chart-capacity-monthly">
</div>
</article>
<article class="header" style="grid-area: chart2;">
<header>
<h2>
Entwicklung der Verfügbarkeit
</h2>
<button popovertarget="chart-capacity-popover"></button>
<div id="chart-capacity-popover" popover>
<h2>Erkläung zum Diagramm</h2>
<p>Das Liniendiagramm zeigt, wie sich die insgesamte Verfügbarkeit des Kurzzeitmietobjekts entwickelt hat.</p>
</div>
</header>
<div id="chart-capacity"></div>
</article>
<article class="header" style="grid-area: chart4;">
<header>
<h2>
Belegung Mietobjekt Tage am <span class="date">{{ $startDate }}</span>
</h2>
</header>
<div id="chart-capacity-daily">
</article>
<script type="module">
const sharedOptions = {
basic: {
color: {!! $chartOptions['colors'] !!},
grid: {
top: 20,
left: 60,
right: 0,
bottom: 50
},
tooltip: {
show: true,
trigger: 'axis',
valueFormatter: (value) => value.toFixed(2)+'%'
},
name: (opt) => {
return {
name: opt.name,
nameLocation: opt.location,
nameGap: 24,
nameTextStyle: {
fontWeight: 'bold',
},
}
}
}
}
const chartTimeline = document.getElementById('timeline');
const cTimeline = echarts.init(chartTimeline);
const cTimelineOptions = {
grid: {
show: false,
},
timeline: {
data: {!! $extractiondates !!},
playInterval: 1000,
axisType: 'time',
left: 8,
right: 8,
bottom: 0,
label: {
show: false
}
},
};
cTimeline.setOption(cTimelineOptions);
const chartCapacityMonthly = document.getElementById('chart-capacity-monthly');
const cCapacityMonthly = echarts.init(chartCapacityMonthly);
const cCapacityMonthlyOptions = {
tooltip: sharedOptions.basic.tooltip,
timeline: {
show: false,
data: {!! $extractiondates !!},
axisType: 'time',
},
grid: {
top: 5,
bottom: 40,
left: 70,
right: 10
},
xAxis: {
type: 'value',
max: 100,
name: 'Auslastung in %',
nameLocation: 'center',
nameGap: 25,
nameTextStyle: {
fontWeight: 'bold',
}
},
yAxis: {
type: 'category',
},
options: [
@foreach ($capacitiesMonthly as $cM)
{
yAxis: {
data: {!! json_encode($cM['months']) !!}
},
series: [{
type: 'bar',
itemStyle: {
color: sharedOptions.basic.color[3]
},
data: {!! json_encode($cM['capacities']) !!}
}]
},
@endforeach
]
};
cCapacityMonthly.setOption(cCapacityMonthlyOptions);
const chartCapacityDaily = document.getElementById('chart-capacity-daily');
const cCapacityDaily = echarts.init(chartCapacityDaily);
const cCapacityDailyOptions = {
tooltip: sharedOptions.basic.tooltip,
timeline: {
show: false,
data: {!! $extractiondates !!},
axisType: 'time',
},
grid: {
top: 5,
bottom: 40,
left: 70,
right: 10
},
xAxis: {
type: 'value',
max: 100,
name: 'Auslastung in %',
nameLocation: 'center',
nameGap: 25,
nameTextStyle: {
fontWeight: 'bold',
}
},
yAxis: {
type: 'category',
},
options: [
@foreach ($capacitiesDaily as $cD)
{
yAxis: {
data: {!! json_encode($cD['weekdays']) !!}
},
series: [{
type: 'bar',
itemStyle: {
color: sharedOptions.basic.color[3]
},
data: {!! json_encode($cD['capacities']) !!}
}]
},
@endforeach
]
};
cCapacityDaily.setOption(cCapacityDailyOptions);
const chartCapacity = document.getElementById('chart-capacity');
const cCapacity = echarts.init(chartCapacity);
const cCapacityOptions = {
color: sharedOptions.basic.color,
legend: {
data: ['Auslastung Property', 'Auslastung {{ $base['region_name'] }}', 'Auslastung alle Regionen']
},
tooltip: {
trigger: 'axis',
valueFormatter: (value) => value.toFixed(2)+'%'
},
grid: {
top: 40,
left: 25,
right: 10,
bottom: 20,
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: {!! json_encode($propertyCapacities['dates']) !!},
name: 'Zeitpunkt Scraping',
nameLocation: 'center',
nameGap: 24,
nameTextStyle: {
fontWeight: 'bold',
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
name: 'Auslastung in Prozent',
nameLocation: 'center',
nameGap: 38,
nameTextStyle: {
fontWeight: 'bold',
}
},
series: [
{
name: 'Auslastung Property',
type: 'line',
symbolSize: 7,
data: {!! json_encode($propertyCapacities['capacities']) !!}
},
{
name: 'Auslastung {{ $base['region_name'] }}',
type: 'line',
symbolSize: 7,
data: {!! json_encode($regionCapacities[0]) !!}
},
{
name: 'Auslastung alle Regionen',
type: 'line',
symbolSize: 7,
data: {!! json_encode($regionCapacities[1]) !!}
}
]
};
cCapacity.setOption(cCapacityOptions);
const chartCalendar = document.getElementById('chart-calendar');
const cCalendar = echarts.init(chartCalendar);
const h2Belegung = document.getElementById('belegung-title');
const cCalendarOptions = {
timeline: {
show: false,
data: {!! json_encode($propertyCapacities['dates']) !!},
axisType: 'time',
},
visualMap: {
categories: [0,1,2],
inRange: {
color: ['#ca0020', '#92c5de', '#0571b0']
},
formatter: (cat) => {
switch (cat) {
case 0:
return 'Ausgebucht';
case 1:
return 'Verfügbar (kein Anreisetag)';
case 2:
return 'Verfügbar';
}
},
type: 'piecewise',
orient: 'horizontal',
left: 'center',
top: 0
},
calendar:[
{
orient: 'horizontal',
range: '2024',
top: '15%',
right: 10,
bottom: '65%',
left: 50,
},
{
orient: 'horizontal',
range: '2025',
top: '47%',
right: 10,
bottom: '33%',
left: 50,
},
{
orient: 'horizontal',
range: '2026',
top: '79%',
right: 10,
bottom: '1%',
left: 50,
}
],
options: [
@foreach ($calendar as $c)
{
series: [{
type: 'heatmap',
coordinateSystem: 'calendar',
calendarIndex: 0,
data: {!! json_encode($c) !!}
},
{
type: 'heatmap',
coordinateSystem: 'calendar',
calendarIndex: 1,
data: {!! json_encode($c) !!}
},
{
type: 'heatmap',
coordinateSystem: 'calendar',
calendarIndex: 2,
data: {!! json_encode($c) !!}
}]
},
@endforeach
]
};
cCalendar.setOption(cCalendarOptions);
cTimeline.on('timelinechanged', (e) => {
let dateTitles = document.querySelectorAll('span.date');
dateTitles.forEach(el => {
el.innerText = cTimelineOptions.timeline.data[e.currentIndex];
});
// Set markpoint on linechart
let x = cCapacityOptions.xAxis.data[e.currentIndex];
let y = cCapacityOptions.series[0].data[e.currentIndex];
cCapacityMonthly.dispatchAction({
type: 'timelineChange',
currentIndex: e.currentIndex
});
cCapacityDaily.dispatchAction({
type: 'timelineChange',
currentIndex: e.currentIndex
});
cCalendar.dispatchAction({
type: 'timelineChange',
currentIndex: e.currentIndex
});
cCapacity.setOption({
series: {
markPoint: {
data: [{
coord: [x, y]
}]
}
}
});
})
/* Map w/ neighbours*/
const map = L.map('chart-map');
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
function icon(id = 0){
return L.divIcon({
className: "region"+id,
html: '<span></span>'
})
}
const markers = L.featureGroup([
L.marker([{{ $base['check_data'] }}], {icon: icon(1)}),
@foreach($neighbours as $n)
L.marker([{{ $n['lat'] }}, {{ $n['lon'] }}], {icon: icon()}).bindPopup('<a href="/property/{{ $n['id'] }}">{{ $n['lat'] }}, {{ $n['lon'] }}</a>'),
@endforeach
]).addTo(map);
map.fitBounds(markers.getBounds(), {padding: [20,20]})
cCapacity.on('click', 'series', (e) => {
// Switch to correct calendar in the timeline
cTimeline.dispatchAction({
type: 'timelineChange',
currentIndex: e.dataIndex
});
});
</script>
@endsection

View File

@ -0,0 +1,510 @@
@extends('base')
@section('body-class', 'region')
@section('header')
<nav>
<strong>{{ $region[0]['region_name'] }}</strong>
<ul>
<li><a href="/">Start</a></li>
@foreach($regions as $r)
@if($r['region_id'] != $region_id)
<li><a href="/region/{{ $r['region_id'] }}">{{ $r['region_name'] }}</a></li>
@endif
@endforeach
</ul>
</nav>
@endsection
@section('main')
<article style="grid-area: timeline;">
<div id="timeline"></div>
</article>
<article class="header" style="grid-area: chart6;">
<header>
<h2 id="prediction-title">Gleitender Mittelwert für die Auslastung der Region</h2>
</header>
<div id="chart-prediction"></div>
</article>
<article class="header" style="grid-area: chart1;">
<header>
<h2 id="belegung-title">Auslastung aller Mietobjekte über Gesamte Zeit der Region</h2>
</header>
<div id="chart-heatmap"></div>
</article>
<article class="header" style="grid-area: chart3;">
<header>
<h2>
Auslastung Region nach Monat am <span class="date">{{ $startDate }}</span>
</h2>
</header>
<div id="chart-capacity-monthly">
</div>
</article>
<article class="header" style="grid-area: chart2;">
<header>
<h2>
Entwicklung der Auslastung
</h2>
<button popovertarget="chart-capacity-popover"></button>
<div id="chart-capacity-popover" popover>
<h2>Erkläung zum Diagramm «Entwicklung der Auslastung»</h2>
<p>Das Liniendiagramm zeigt die Auslastung von Regionen. 100 % = die Region ist kaum ausgelastet; 100 % der Mietobjekte sind verfügbar. 0 % = Die Region ist komplett ausgelastet; Es stehen keine Mietangebote zur Verfügung.</p>
</div>
</header>
<div id="chart-capacity"></div>
</article>
<article class="header" style="grid-area: chart4;">
<header>
<h2>
Auslastung Wochentage am <span class="date">{{ $startDate }}</span>
</h2>
</header>
<div id="chart-capacity-daily">
</article>
<script type="module">
const sharedOptions = {
basic: {
color: {!! $chartOptions['colors'] !!},
grid: {
top: 20,
left: 60,
right: 0,
bottom: 50
},
tooltip: {
show: true,
trigger: 'axis',
valueFormatter: (value) => value.toFixed(2)+'%'
},
name: (opt) => {
return {
name: opt.name,
nameLocation: opt.location,
nameGap: 24,
nameTextStyle: {
fontWeight: 'bold',
},
}
}
}
}
const chartCapacity = document.getElementById('chart-capacity');
const cCapacity = echarts.init(chartCapacity);
const cCapacityOptions = {
legend: {
show: true
},
tooltip: sharedOptions.basic.tooltip,
color: sharedOptions.basic.color,
grid: {
top: 20,
left: 25,
right: 10,
bottom: 20,
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: {!! json_encode($regionCapacities['region']['dates']) !!},
name: 'Zeitpunkt Scraping',
nameLocation: 'center',
nameGap: 24,
nameTextStyle: {
fontWeight: 'bold',
}
},
yAxis: {
type: 'value',
min: 0,
max: 100,
name: 'Auslastung in %',
nameLocation: 'center',
nameGap: 38,
nameTextStyle: {
fontWeight: 'bold',
}
},
series: [{
name: 'Auslastung alle Regionen',
type: 'line',
symbolSize: 7,
data: {!! json_encode($regionCapacities['all']['capacities']) !!}
},
{
name: 'Auslastung Region',
type: 'line',
symbolSize: 7,
data: {!! json_encode($regionCapacities['region']['capacities']) !!}
}]
};
cCapacity.setOption(cCapacityOptions);
const chartCapacityMonthly = document.getElementById('chart-capacity-monthly');
const cCapacityMonthly = echarts.init(chartCapacityMonthly);
const cCapacityMonthlyOptions = {
timeline: {
show: false,
data: {!! json_encode($regionCapacities['region']['dates']) !!},
axisType: 'time',
},
grid: {
top: 5,
bottom: 40,
left: 70,
right: 10
},
xAxis: {
type: 'value',
max: 100,
name: 'Auslastung in %',
nameLocation: 'center',
nameGap: 25,
nameTextStyle: {
fontWeight: 'bold',
}
},
yAxis: {
type: 'category',
},
tooltip: sharedOptions.basic.tooltip,
options: [
@foreach ($regionCapacities['region_monthly'] as $m)
{
yAxis: {
data: {!! json_encode($m['months']) !!}
},
series: [{
type: 'bar',
itemStyle: {
color: sharedOptions.basic.color[3]
},
data: {!! json_encode($m['capacities']) !!}
}]
},
@endforeach
]
};
cCapacityMonthly.setOption(cCapacityMonthlyOptions);
const chartCapacityDaily = document.getElementById('chart-capacity-daily');
const cCapacityDaily = echarts.init(chartCapacityDaily);
const cCapacityDailyOptions = {
timeline: {
show: false,
data: {!! json_encode($regionCapacities['region']['dates']) !!},
axisType: 'time',
},
tooltip: sharedOptions.basic.tooltip,
grid: {
top: 5,
bottom: 40,
left: 70,
right: 10
},
xAxis: {
type: 'value',
max: 100,
name: 'Auslastung in %',
nameLocation: 'center',
nameGap: 25,
nameTextStyle: {
fontWeight: 'bold',
}
},
yAxis: {
type: 'category',
},
options: [
@foreach ($regionCapacities['region_daily'] as $d)
{
yAxis: {
data: {!! json_encode($d['weekdays']) !!}
},
series: [{
type: 'bar',
itemStyle: {
color: sharedOptions.basic.color[3]
},
data: {!! json_encode($d['capacities']) !!}
}]
},
@endforeach
]
};
cCapacityDaily.setOption(cCapacityDailyOptions);
const chartPrediction = document.getElementById('chart-prediction');
const cPrediction = echarts.init(chartPrediction);
const cPredictionOptions = {
color: sharedOptions.basic.color,
timeline: {
show: false,
data: {!! json_encode($regionCapacities['region']['dates']) !!},
axisType: 'time',
replaceMerge: ['graphic', 'series']
},
legend: {
show: true
},
tooltip: sharedOptions.basic.tooltip,
grid: {
top: 20,
left: 25,
right: 10,
bottom: 20,
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
name: 'Zeitpunkt Scraping',
nameLocation: 'center',
nameGap: 24,
nameTextStyle: {
fontWeight: 'bold',
},
},
yAxis: {
type: 'value',
min: 0,
max: 100,
name: 'Auslastung in %',
nameLocation: 'center',
nameGap: 38,
nameTextStyle: {
fontWeight: 'bold',
}
},
options: [
@foreach ($predictions as $p)
@if($p === null)
{
graphic: {
elements: [
{
type: 'text',
left: 'center',
top: 'center',
style: {
text: 'Keine Daten für Zeitspanne',
fontSize: 44,
fontWeight: 'bold',
}
}
]
}
},
@else
{
color: sharedOptions.basic.color,
graphic: {
elements: []
},
xAxis: {
data: {!! json_encode($p['dates']) !!}
},
series: [
{
name: 'Gleitender Mittelwert',
type: 'line',
symbolSize: 7,
data: {!! json_encode($p['movAvg']) !!}
},
{
name: 'Daten vom ...',
type: 'line',
symbolSize: 7,
data: {!! json_encode($p['cap_earlierTimeframe']) !!}
},
{
name: 'Daten vom',
type: 'line',
symbolSize: 7,
data: {!! json_encode($p['cap_laterTimeframe']) !!}
}
]
},
@endif
@endforeach
]
};
cPrediction.setOption(cPredictionOptions);
const chartHeatmap = document.getElementById('chart-heatmap');
const cHeatmap = echarts.init(chartHeatmap);
const cHeatmapOptions = {
animation: false,
tooltip: {
position: 'top'
},
grid: {
top: 30,
right: 45,
bottom: 50,
left: 5
},
dataZoom: [{
type: 'slider'
},
{
type: 'slider',
show: true,
yAxisIndex: 0,
}],
xAxis: {
show: false,
name: 'Kurzzeitmietobjekt',
type: 'category',
data: {!! json_encode($regionPropertiesCapacities['scrapeDates']) !!},
splitArea: {
show: false
},
axisLabel: {
show: true,
}
},
yAxis: {
show: false,
type: 'category',
data: {!! json_encode($regionPropertiesCapacities['property_ids']) !!},
splitArea: {
show: true
}
},
visualMap: {
type: 'piecewise',
min: 0,
max: 100,
calculable: true,
orient: 'horizontal',
left: 'center',
top: 0,
formatter: (v1, v2) => {
return `${v1}${v2}%`;
},
inRange: {
color: sharedOptions.basic.color,
},
},
series: [
{
name: 'Auslastung',
type: 'heatmap',
blurSize: 0,
data: {!! json_encode($regionPropertiesCapacities['values']) !!},
label: {
show: false
},
tooltip: {
formatter: (data) => {
return `Kurzzeitmietobjekte-ID: ${data.data[1]}<br />Datum Scraping: ${data.data[0]}<br/>Auslastung: ${data.data[2].toFixed(2)}%`
},
},
emphasis: {
itemStyle: {
borderColor: '#000',
borderWidth: 2
}
}
}
]
}
cHeatmap.setOption(cHeatmapOptions);
const chartTimeline = document.getElementById('timeline');
const cTimeline = echarts.init(chartTimeline);
const cTimelineOptions = {
grid: {
show: false,
},
timeline: {
data: {!! json_encode($regionCapacities['region']['dates']) !!},
playInterval: 2000,
axisType: 'time',
left: 8,
right: 8,
bottom: 0,
label: {
show: false
}
},
};
cTimeline.setOption(cTimelineOptions);
cTimeline.on('timelinechanged', (e) => {
let dateTitles = document.querySelectorAll('span.date');
dateTitles.forEach(el => {
el.innerText = cTimelineOptions.timeline.data[e.currentIndex];
});
// Set markpoint on linechart
let x = cCapacityOptions.xAxis.data[e.currentIndex];
let y = cCapacityOptions.series[0].data[e.currentIndex];
cCapacityMonthly.dispatchAction({
type: 'timelineChange',
currentIndex: e.currentIndex
});
cCapacityDaily.dispatchAction({
type: 'timelineChange',
currentIndex: e.currentIndex
});
cPrediction.dispatchAction({
type: 'timelineChange',
currentIndex: e.currentIndex
});
cCapacity.setOption({
series: {
markPoint: {
data: [{
coord: [x, y]
}]
}
}
});
})
document.querySelector('header').addEventListener('click', () => {
console.log('test');
cCapacityMonthly.dispatchAction({
type: 'timelineChange',
currentIndex: 10
});
})
cCapacity.on('click', 'series', (e) => {
// Switch to correct calendar in the timeline
cTimeline.dispatchAction({
type: 'timelineChange',
currentIndex: e.dataIndex
});
});
cHeatmap.on('click', 'series', (e) => {
window.open(`/property/${e.value[1]}?date=${e.value[0]}`, '_self');
})
</script>
@endsection

153
dashboard/routes/web.php Normal file
View File

@ -0,0 +1,153 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Api;
use App\Chart;
Route::get('/', function () {
$regionBase = Api::regionBase(-1);
$regionBase[] = ['region_name' => 'Alle Regionen', 'region_id' => -1];
$regionPropertyCapacities = Api::regionPropertiesCapacities(-1);
$propertiesGrowth = Api::propertiesGrowth();
$propsPerRegion = Api::propertiesPerRegion();
$propsPerRegionName = [];
$propsPerRegionCounts = [];
$propsPerRegionId = [];
foreach ($propsPerRegion as $el) {
$propsPerRegionName[] = $el['name'];
$propsPerRegionId[] = $el['id'];
$propsPerRegionCounts[] = $el['count_properties'];
}
$chartOptions = [
'colors' => Chart::colors()
];
$propertiesGeo = Api::propertiesGeo();
return view('overview', [
"regions" => $regionBase,
"regionPropertiesCapacities" => $regionPropertyCapacities,
"geo" => $propertiesGeo,
"growth" => $propertiesGrowth,
"chartOptions" => $chartOptions,
"propsPerRegion" => [json_encode($propsPerRegionId), json_encode($propsPerRegionName), json_encode($propsPerRegionCounts)]]);
});
Route::get('/region/{id}', function (int $id) {
$regionBaseAll = Api::regionBase(-1);
$regionBaseAll[] = ['region_name' => 'Alle Regionen', 'region_id' => -1];
$regionBaseRegion = $id >= 0 ? Api::regionBase($id) : [['region_name' => 'Alle Regionen']];
$regionPropertiesCapacities = Api::regionPropertiesCapacities($id);
$regionCapacitiesRegion = Api::regionCapacities($id);
$regionCapacitiesAll = Api::regionCapacities(-1);
$regionCapacitiesMonthly = [];
$regionCapacitiesDaily = [];
$regionPredictions = [];
foreach ($regionCapacitiesRegion['dates'] as $date) {
$regionCapacitiesMonthly[] = Api::regionCapacitiesMonthly($id, $date);
$regionCapacitiesDaily[] = Api::regionCapacitiesDaily($id, $date);
$regionPredictions[] = Api::regionMovingAverage($id, $date);
}
$chartOptions = [
'colors' => Chart::colors()
];
$regionCapacities = [
'all' => $regionCapacitiesAll,
'region' => $regionCapacitiesRegion,
'region_monthly' => $regionCapacitiesMonthly,
'region_daily' => $regionCapacitiesDaily
];
return view('region', [
'chartOptions' => $chartOptions,
'startDate' => $regionCapacitiesRegion['dates'][0],
'regions' => $regionBaseAll,
'region' => $regionBaseRegion,
'region_id' => $id,
'regionCapacities' => $regionCapacities,
'regionPropertiesCapacities' => $regionPropertiesCapacities,
'predictions' => $regionPredictions]);
});
Route::get('/property/{id}', function (int $id) {
$chartOptions = [
'colors' => Chart::colors()
];
$regionBaseAll = Api::regionBase(-1);
$regionBaseAll[] = ['region_name' => 'Alle Regionen', 'region_id' => -1];
$propertyBase = Api::propertyBase($id);
$calendars = Api::propertyExtractions($id);
$propertyCapacities = Api::propertyCapacities($id);
$propertyNeighbours = Api::propertyNeighbours($id);
$regionCapacitiesRegion = Api::regionCapacities($propertyBase[0]['region_id']);
$regionCapacitiesAll = Api::regionCapacities(-1);
$regionCapacities = [[],[]];
$propertyCapacitiesMonthly = [];
$propertyCapacitiesDaily = [];
foreach ($propertyCapacities['dates'] as $date) {
$propertyCapacitiesMonthly[] = Api::propertyCapacitiesMonthly($id, $date);
$propertyCapacitiesDaily[] = Api::propertyCapacitiesDaily($id, $date);
}
// filter out all date, which were not scraped for the property
foreach ($regionCapacitiesAll['dates'] as $index => $date) {
if(in_array($date, $propertyCapacities['dates'])){
$regionCapacities[0][] = $regionCapacitiesAll['capacities'][$index];
}
}
foreach ($regionCapacitiesRegion['dates'] as $index => $date) {
if(in_array($date, $propertyCapacities['dates'])){
$regionCapacities[1][] = $regionCapacitiesRegion['capacities'][$index];
}
}
// prepare data for calendar chart
$data = [];
$dates = [];
foreach ($calendars as $el) {
$series = [];
$calendar = json_decode($el['calendar'], 1);
foreach ($calendar as $date => $status) {
$series[] = [$date, $status];
}
$data[] = $series;
}
return view('property', [
'chartOptions' => $chartOptions,
'startDate' => $propertyCapacities['dates'][0],
'base' => $propertyBase[0],
'regions' => $regionBaseAll,
'extractiondates' => json_encode($propertyCapacities['dates']),
'calendar' => $data,
'propertyCapacities' => $propertyCapacities,
'capacitiesMonthly' => $propertyCapacitiesMonthly,
'capacitiesDaily' => $propertyCapacitiesDaily,
'regionCapacities' => $regionCapacities,
'neighbours' => $propertyNeighbours
]);
});

View File

@ -0,0 +1,118 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" version="26.0.5">
<diagram name="Seite-1" id="WNMV8rePnVf-2Vz_xhjt">
<mxGraphModel dx="1688" dy="1050" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<object placeholders="1" c4Name="Datenbank Aggregation" c4Type="Container" c4Technology="MySQL" c4Description="Datenbank welche während Aggregation verwendet wurde." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%:&amp;nbsp;%c4Technology%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#E6E6E6&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-1">
<mxCell style="shape=cylinder3;size=15;whiteSpace=wrap;html=1;boundedLbl=1;rounded=0;labelBackgroundColor=none;fillColor=#23A2D9;fontSize=12;fontColor=#ffffff;align=center;strokeColor=#0E7DAD;metaEdit=1;points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];resizable=0;" vertex="1" parent="1">
<mxGeometry x="40" y="100" width="240" height="120" as="geometry" />
</mxCell>
</object>
<object placeholders="1" c4Name="Datenbank Analyse" c4Type="Container" c4Technology="DuckDB" c4Description="Datenbank, welcher für die Analysen verwendet wurden." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%:&amp;nbsp;%c4Technology%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#E6E6E6&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-2">
<mxCell style="shape=cylinder3;size=15;whiteSpace=wrap;html=1;boundedLbl=1;rounded=0;labelBackgroundColor=none;fillColor=#23A2D9;fontSize=12;fontColor=#ffffff;align=center;strokeColor=#0E7DAD;metaEdit=1;points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];resizable=0;" vertex="1" parent="1">
<mxGeometry x="790" y="100" width="240" height="120" as="geometry" />
</mxCell>
</object>
<mxCell id="0Mexl9jQAquWokRCgHYt-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=8 8;" edge="1" parent="1" source="0Mexl9jQAquWokRCgHYt-3" target="0Mexl9jQAquWokRCgHYt-1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="0Mexl9jQAquWokRCgHYt-7" value="liest Datenbank" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="0Mexl9jQAquWokRCgHYt-5">
<mxGeometry x="-0.2497" y="-1" relative="1" as="geometry">
<mxPoint x="-10" y="1" as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="Sling" c4Type="sling-cli" c4Description="Kommandozeilenprogramm zur Migration von Datensätzen." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-3">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="400" y="100" width="240" height="120" as="geometry" />
</mxCell>
</object>
<mxCell id="0Mexl9jQAquWokRCgHYt-6" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;" edge="1" parent="1" source="0Mexl9jQAquWokRCgHYt-3" target="0Mexl9jQAquWokRCgHYt-2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="0Mexl9jQAquWokRCgHYt-8" value="schreibt in Datenbank" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="0Mexl9jQAquWokRCgHYt-6">
<mxGeometry x="-0.1744" relative="1" as="geometry">
<mxPoint x="16" y="-1" as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="Preprocessing" c4Type="ContainerScopeBoundary" c4Application="Component" label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;&lt;div style=&quot;text-align: left&quot;&gt;%c4Name%&lt;/div&gt;&lt;/b&gt;&lt;/font&gt;&lt;div style=&quot;text-align: left&quot;&gt;[%c4Application%]&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-9">
<mxCell style="rounded=1;fontSize=11;whiteSpace=wrap;html=1;dashed=1;arcSize=20;fillColor=none;strokeColor=#666666;fontColor=#333333;labelBackgroundColor=none;align=left;verticalAlign=bottom;labelBorderColor=none;spacingTop=0;spacing=10;dashPattern=8 4;metaEdit=1;rotatable=0;perimeter=rectanglePerimeter;noLabel=0;labelPadding=0;allowArrows=0;connectable=0;expand=0;recursiveResize=0;editable=1;pointerEvents=0;absoluteArcSize=1;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="20" y="40" width="1030" height="270" as="geometry" />
</mxCell>
</object>
<object placeholders="1" c4Name="Datenbank Analyse" c4Type="Container" c4Technology="DuckDB" c4Description="Datenbank, welcher für die Analysen verwendet wurden." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%:&amp;nbsp;%c4Technology%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#E6E6E6&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-10">
<mxCell style="shape=cylinder3;size=15;whiteSpace=wrap;html=1;boundedLbl=1;rounded=0;labelBackgroundColor=none;fillColor=#23A2D9;fontSize=12;fontColor=#ffffff;align=center;strokeColor=#0E7DAD;metaEdit=1;points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];resizable=0;" vertex="1" parent="1">
<mxGeometry x="80" y="480" width="240" height="120" as="geometry" />
</mxCell>
</object>
<object placeholders="1" c4Name="Datenaufbereitung" c4Type="ContainerScopeBoundary" c4Application="ETL" label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;&lt;div style=&quot;text-align: left&quot;&gt;%c4Name%&lt;/div&gt;&lt;/b&gt;&lt;/font&gt;&lt;div style=&quot;text-align: left&quot;&gt;[%c4Application%]&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-11">
<mxCell style="rounded=1;fontSize=11;whiteSpace=wrap;html=1;dashed=1;arcSize=20;fillColor=none;strokeColor=#666666;fontColor=#333333;labelBackgroundColor=none;align=left;verticalAlign=bottom;labelBorderColor=none;spacingTop=0;spacing=10;dashPattern=8 4;metaEdit=1;rotatable=0;perimeter=rectanglePerimeter;noLabel=0;labelPadding=0;allowArrows=0;connectable=0;expand=0;recursiveResize=0;editable=1;pointerEvents=0;absoluteArcSize=1;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="30" y="440" width="1030" height="490" as="geometry" />
</mxCell>
</object>
<mxCell id="0Mexl9jQAquWokRCgHYt-23" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;" edge="1" parent="1" source="0Mexl9jQAquWokRCgHYt-12" target="0Mexl9jQAquWokRCgHYt-14">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="0Mexl9jQAquWokRCgHYt-24" value="verwendet" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="0Mexl9jQAquWokRCgHYt-23">
<mxGeometry x="-0.0114" y="-2" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="etl_*.py" c4Type="Python (Polars)" c4Description="Diverse Python Skripts zur Aufbereitung / Zusammenstellung der Daten." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-12">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="430" y="710" width="240" height="120" as="geometry" />
</mxCell>
</object>
<mxCell id="0Mexl9jQAquWokRCgHYt-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=8 8;" edge="1" parent="1" source="0Mexl9jQAquWokRCgHYt-13" target="0Mexl9jQAquWokRCgHYt-10">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="0Mexl9jQAquWokRCgHYt-17" value="Liest Datenbank" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="0Mexl9jQAquWokRCgHYt-16">
<mxGeometry x="-0.1633" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="database.py" c4Type="Python (DuckDB Interface)" c4Description="Wrapper Skript zum Ausführen von SQL." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-13">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="80" y="710" width="240" height="120" as="geometry" />
</mxCell>
</object>
<mxCell id="0Mexl9jQAquWokRCgHYt-18" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.25;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=8 8;entryX=0.24;entryY=0.981;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="0Mexl9jQAquWokRCgHYt-14" target="0Mexl9jQAquWokRCgHYt-15">
<mxGeometry relative="1" as="geometry">
<mxPoint x="900" y="600" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="0Mexl9jQAquWokRCgHYt-19" value="schreibt pickle objekt" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="0Mexl9jQAquWokRCgHYt-18">
<mxGeometry x="-0.1818" y="2" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="etl_cache.py" c4Type="Python (Pickle)" c4Description="Diverse Python Skripts zur Aufbereitung / Zusammenstellung der Daten." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-14">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="780" y="710" width="240" height="120" as="geometry" />
</mxCell>
</object>
<object placeholders="1" c4Name="Cache" c4Type="Container" c4Technology="Filesystem" c4Description="Das Dateisystem wird als Pufferspeicher verwendet. Die Daten werden als Pickle Objekte gespeichert." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%:&amp;nbsp;%c4Technology%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#E6E6E6&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="0Mexl9jQAquWokRCgHYt-15">
<mxCell style="shape=cylinder3;size=15;whiteSpace=wrap;html=1;boundedLbl=1;rounded=0;labelBackgroundColor=none;fillColor=#23A2D9;fontSize=12;fontColor=#ffffff;align=center;strokeColor=#0E7DAD;metaEdit=1;points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];resizable=0;" vertex="1" parent="1">
<mxGeometry x="780" y="480" width="240" height="120" as="geometry" />
</mxCell>
</object>
<mxCell id="0Mexl9jQAquWokRCgHYt-20" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.746;entryY=1.002;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;exitX=0.75;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;" edge="1" parent="1" source="0Mexl9jQAquWokRCgHYt-14" target="0Mexl9jQAquWokRCgHYt-15">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="0Mexl9jQAquWokRCgHYt-21" value="liest pickle objekt" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="0Mexl9jQAquWokRCgHYt-20">
<mxGeometry x="-0.1076" y="1" relative="1" as="geometry">
<mxPoint x="8" y="-11" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="0Mexl9jQAquWokRCgHYt-25" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;" edge="1" parent="1" source="0Mexl9jQAquWokRCgHYt-12" target="0Mexl9jQAquWokRCgHYt-13">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="0Mexl9jQAquWokRCgHYt-26" value="Verwendet" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="0Mexl9jQAquWokRCgHYt-25">
<mxGeometry x="0.0473" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

View File

@ -0,0 +1,184 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" version="26.0.5" pages="2">
<diagram name="Seite-1" id="chpUGVRRn7alPJZ1I-il">
<mxGraphModel dx="2761" dy="1531" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<object placeholders="1" c4Name="RDBMS" c4Type="Container" c4Technology="DuckDB" c4Description="Aggregierte Daten." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%:&amp;nbsp;%c4Technology%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#E6E6E6&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="_wAeSdXpbb6KPP4DEc36-2">
<mxCell style="shape=cylinder3;size=15;whiteSpace=wrap;html=1;boundedLbl=1;rounded=0;labelBackgroundColor=none;fillColor=#23A2D9;fontSize=12;fontColor=#ffffff;align=center;strokeColor=#0E7DAD;metaEdit=1;points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];resizable=0;" parent="1" vertex="1">
<mxGeometry x="50" y="60" width="240" height="120" as="geometry" />
</mxCell>
</object>
<object placeholders="1" c4Name="ETL" c4Type="SQL, Python (Polars)" c4Description="Bereitet Daten mittels algorithmischer&lt;br&gt; Verfahren auf." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="_wAeSdXpbb6KPP4DEc36-3">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" parent="1" vertex="1">
<mxGeometry x="480" y="60" width="240" height="120" as="geometry" />
</mxCell>
</object>
<mxCell id="_wAeSdXpbb6KPP4DEc36-4" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;" parent="1" source="_wAeSdXpbb6KPP4DEc36-3" target="_wAeSdXpbb6KPP4DEc36-2" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_wAeSdXpbb6KPP4DEc36-5" value="Liest Datenbank" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="_wAeSdXpbb6KPP4DEc36-4" vertex="1" connectable="0">
<mxGeometry x="0.0412" y="1" relative="1" as="geometry">
<mxPoint x="-1" y="-1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="_wAeSdXpbb6KPP4DEc36-15" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=8 8;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" parent="1" source="_wAeSdXpbb6KPP4DEc36-6" target="_wAeSdXpbb6KPP4DEc36-13" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_wAeSdXpbb6KPP4DEc36-21" value="&lt;div&gt;Führt Abfragen aus&lt;/div&gt;&lt;div&gt;[JSON/HTTPS]&lt;br&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="_wAeSdXpbb6KPP4DEc36-15" vertex="1" connectable="0">
<mxGeometry x="-0.0541" y="-1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="Webapplikation" c4Type="PHP (Laravel)" c4Description="Verarbeitet Anfragen von Benutzer:innen" label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="_wAeSdXpbb6KPP4DEc36-6">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" parent="1" vertex="1">
<mxGeometry x="50" y="230" width="240" height="120" as="geometry" />
</mxCell>
</object>
<object placeholders="1" c4Name="Dashboard" c4Type="Container" c4Technology="Apache Echarts" c4Description="Stellt Benutzer:innen Auswertungs-&lt;br&gt;möglichkeiten zur Verfügbarkeit von Kurzzeitmietobjekten." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%:&amp;nbsp;%c4Technology%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#E6E6E6&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="_wAeSdXpbb6KPP4DEc36-8">
<mxCell style="shape=mxgraph.c4.webBrowserContainer2;whiteSpace=wrap;html=1;boundedLbl=1;rounded=0;labelBackgroundColor=none;strokeColor=#118ACD;fillColor=#23A2D9;strokeColor=#118ACD;strokeColor2=#0E7DAD;fontSize=12;fontColor=#ffffff;align=center;metaEdit=1;points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];resizable=0;" parent="1" vertex="1">
<mxGeometry x="480" y="370" width="240" height="160" as="geometry" />
</mxCell>
</object>
<mxCell id="_wAeSdXpbb6KPP4DEc36-10" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=8 8;" parent="1" source="_wAeSdXpbb6KPP4DEc36-9" target="_wAeSdXpbb6KPP4DEc36-6" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_wAeSdXpbb6KPP4DEc36-16" value="&lt;div&gt;Besucht Webapplikation&lt;/div&gt;&lt;div&gt;[HTTPS]&lt;br&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="_wAeSdXpbb6KPP4DEc36-10" vertex="1" connectable="0">
<mxGeometry x="0.1247" y="-2" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="_wAeSdXpbb6KPP4DEc36-11" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=8 8;" parent="1" source="_wAeSdXpbb6KPP4DEc36-9" target="_wAeSdXpbb6KPP4DEc36-8" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_wAeSdXpbb6KPP4DEc36-17" value="Betrachtet Auswertungen" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="_wAeSdXpbb6KPP4DEc36-11" vertex="1" connectable="0">
<mxGeometry x="0.2151" y="-1" relative="1" as="geometry">
<mxPoint x="2" as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="Benutzer:in" c4Type="Person" c4Description="Person welche Auswertungen zur Verfügbarkeit von Kurzzeitmietobjekten in Ferienregionen durchführt." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="_wAeSdXpbb6KPP4DEc36-9">
<mxCell style="html=1;fontSize=11;dashed=0;whiteSpace=wrap;fillColor=#083F75;strokeColor=#06315C;fontColor=#ffffff;shape=mxgraph.c4.person2;align=center;metaEdit=1;points=[[0.5,0,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0]];resizable=0;" parent="1" vertex="1">
<mxGeometry x="314" y="600" width="200" height="180" as="geometry" />
</mxCell>
</object>
<mxCell id="_wAeSdXpbb6KPP4DEc36-14" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;" parent="1" source="_wAeSdXpbb6KPP4DEc36-13" target="_wAeSdXpbb6KPP4DEc36-3" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_wAeSdXpbb6KPP4DEc36-20" value="Ruft ETL Verfahren auf" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="_wAeSdXpbb6KPP4DEc36-14" vertex="1" connectable="0">
<mxGeometry x="-0.0667" y="-1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="FastAPI" c4Type="Python (FastAPI)" c4Description="Stellt aufbereitete Daten via &lt;br&gt;JSON/HTTPS API zur Verfügung." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="_wAeSdXpbb6KPP4DEc36-13">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" parent="1" vertex="1">
<mxGeometry x="480" y="230" width="240" height="120" as="geometry" />
</mxCell>
</object>
<mxCell id="_wAeSdXpbb6KPP4DEc36-18" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;" parent="1" source="_wAeSdXpbb6KPP4DEc36-6" target="_wAeSdXpbb6KPP4DEc36-8" edge="1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="_wAeSdXpbb6KPP4DEc36-19" value="&lt;div&gt;Liefert Inhalte zum Webbrowser&amp;nbsp;&lt;/div&gt;&lt;div&gt;von Benutzer:innen&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="_wAeSdXpbb6KPP4DEc36-18" vertex="1" connectable="0">
<mxGeometry x="-0.0888" y="-2" relative="1" as="geometry">
<mxPoint x="5" y="7" as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="Visual Analytics Tool" c4Type="SystemScopeBoundary" c4Application="Software System" label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;&lt;div style=&quot;text-align: left&quot;&gt;%c4Name%&lt;/div&gt;&lt;/b&gt;&lt;/font&gt;&lt;div style=&quot;text-align: left&quot;&gt;[%c4Application%]&lt;/div&gt;" id="_wAeSdXpbb6KPP4DEc36-23">
<mxCell style="rounded=1;fontSize=11;whiteSpace=wrap;html=1;dashed=1;arcSize=20;fillColor=none;strokeColor=#666666;fontColor=#333333;labelBackgroundColor=none;align=left;verticalAlign=bottom;labelBorderColor=none;spacingTop=0;spacing=10;dashPattern=8 4;metaEdit=1;rotatable=0;perimeter=rectanglePerimeter;noLabel=0;labelPadding=0;allowArrows=0;connectable=0;expand=0;recursiveResize=0;editable=1;pointerEvents=0;absoluteArcSize=1;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" parent="1" vertex="1">
<mxGeometry x="30" y="40" width="710" height="540" as="geometry" />
</mxCell>
</object>
</root>
</mxGraphModel>
</diagram>
<diagram id="2goo0GJ--Dnj9rEJibSb" name="Seite-2">
<mxGraphModel dx="2285" dy="1267" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<object placeholders="1" c4Name="RDBMS" c4Type="Container" c4Technology="DuckDB" c4Description="Aggregierte Daten." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%:&amp;nbsp;%c4Technology%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#E6E6E6&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="Xmw1x83A06H2_JC6hK8s-1">
<mxCell style="shape=cylinder3;size=15;whiteSpace=wrap;html=1;boundedLbl=1;rounded=0;labelBackgroundColor=none;fillColor=#23A2D9;fontSize=12;fontColor=#ffffff;align=center;strokeColor=#0E7DAD;metaEdit=1;points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];resizable=0;" vertex="1" parent="1">
<mxGeometry x="40" y="230" width="240" height="120" as="geometry" />
</mxCell>
</object>
<object placeholders="1" c4Name="ETL" c4Type="SQL, Python (Polars)" c4Description="Bereitet Daten mittels algorithmischer&lt;br&gt; Verfahren auf." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="Xmw1x83A06H2_JC6hK8s-2">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="40" y="464" width="240" height="120" as="geometry" />
</mxCell>
</object>
<mxCell id="Xmw1x83A06H2_JC6hK8s-3" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;" edge="1" parent="1" source="Xmw1x83A06H2_JC6hK8s-2" target="Xmw1x83A06H2_JC6hK8s-1">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Xmw1x83A06H2_JC6hK8s-4" value="Liest Datenbank" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Xmw1x83A06H2_JC6hK8s-3">
<mxGeometry x="0.0412" y="1" relative="1" as="geometry">
<mxPoint x="-1" y="-1" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Xmw1x83A06H2_JC6hK8s-5" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=8 8;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="Xmw1x83A06H2_JC6hK8s-7" target="Xmw1x83A06H2_JC6hK8s-16">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Xmw1x83A06H2_JC6hK8s-6" value="&lt;div&gt;Führt Abfragen aus&lt;/div&gt;&lt;div&gt;[JSON/HTTPS]&lt;br&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Xmw1x83A06H2_JC6hK8s-5">
<mxGeometry x="-0.0541" y="-1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="Webapplikation" c4Type="PHP (Laravel)" c4Description="Verarbeitet Anfragen von Benutzer:innen" label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="Xmw1x83A06H2_JC6hK8s-7">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="710" y="240" width="240" height="120" as="geometry" />
</mxCell>
</object>
<object placeholders="1" c4Name="Dashboard" c4Type="Container" c4Technology="Apache Echarts" c4Description="Stellt Benutzer:innen Auswertungs-&lt;br&gt;möglichkeiten zur Verfügbarkeit von Kurzzeitmietobjekten." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%:&amp;nbsp;%c4Technology%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#E6E6E6&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="Xmw1x83A06H2_JC6hK8s-8">
<mxCell style="shape=mxgraph.c4.webBrowserContainer2;whiteSpace=wrap;html=1;boundedLbl=1;rounded=0;labelBackgroundColor=none;strokeColor=#118ACD;fillColor=#23A2D9;strokeColor=#118ACD;strokeColor2=#0E7DAD;fontSize=12;fontColor=#ffffff;align=center;metaEdit=1;points=[[0.5,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.5,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];resizable=0;" vertex="1" parent="1">
<mxGeometry x="710" y="470" width="240" height="160" as="geometry" />
</mxCell>
</object>
<mxCell id="Xmw1x83A06H2_JC6hK8s-9" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=8 8;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="Xmw1x83A06H2_JC6hK8s-13" target="Xmw1x83A06H2_JC6hK8s-7">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Xmw1x83A06H2_JC6hK8s-10" value="&lt;div&gt;Besucht Webapplikation&lt;/div&gt;&lt;div&gt;[HTTPS]&lt;br&gt;&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Xmw1x83A06H2_JC6hK8s-9">
<mxGeometry x="0.1247" y="-2" relative="1" as="geometry">
<mxPoint x="4" y="4" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="Xmw1x83A06H2_JC6hK8s-11" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;dashed=1;dashPattern=8 8;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="Xmw1x83A06H2_JC6hK8s-13" target="Xmw1x83A06H2_JC6hK8s-8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Xmw1x83A06H2_JC6hK8s-12" value="Betrachtet Auswertungen" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Xmw1x83A06H2_JC6hK8s-11">
<mxGeometry x="0.2151" y="-1" relative="1" as="geometry">
<mxPoint x="13" as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="Benutzer:in" c4Type="Person" c4Description="Person welche Auswertungen zur Verfügbarkeit von Kurzzeitmietobjekten in Ferienregionen durchführt." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="Xmw1x83A06H2_JC6hK8s-13">
<mxCell style="html=1;fontSize=11;dashed=0;whiteSpace=wrap;fillColor=#083F75;strokeColor=#06315C;fontColor=#ffffff;shape=mxgraph.c4.person2;align=center;metaEdit=1;points=[[0.5,0,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0]];resizable=0;" vertex="1" parent="1">
<mxGeometry x="1120" y="320" width="200" height="180" as="geometry" />
</mxCell>
</object>
<mxCell id="Xmw1x83A06H2_JC6hK8s-14" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;exitPerimeter=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;" edge="1" parent="1" source="Xmw1x83A06H2_JC6hK8s-16" target="Xmw1x83A06H2_JC6hK8s-2">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Xmw1x83A06H2_JC6hK8s-15" value="Ruft ETL Verfahren auf" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Xmw1x83A06H2_JC6hK8s-14">
<mxGeometry x="-0.0667" y="-1" relative="1" as="geometry">
<mxPoint as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="FastAPI" c4Type="Python (FastAPI)" c4Description="Stellt aufbereitete Daten via &lt;br&gt;JSON/HTTPS API zur Verfügung." label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;%c4Name%&lt;/b&gt;&lt;/font&gt;&lt;div&gt;[%c4Type%]&lt;/div&gt;&lt;br&gt;&lt;div&gt;&lt;font style=&quot;font-size: 11px&quot;&gt;&lt;font color=&quot;#cccccc&quot;&gt;%c4Description%&lt;/font&gt;&lt;/div&gt;" id="Xmw1x83A06H2_JC6hK8s-16">
<mxCell style="rounded=1;whiteSpace=wrap;html=1;labelBackgroundColor=none;fillColor=#1061B0;fontColor=#ffffff;align=center;arcSize=10;strokeColor=#0D5091;metaEdit=1;resizable=0;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="330" y="240" width="240" height="120" as="geometry" />
</mxCell>
</object>
<mxCell id="Xmw1x83A06H2_JC6hK8s-17" style="rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;exitPerimeter=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;entryPerimeter=0;dashed=1;dashPattern=8 8;" edge="1" parent="1" source="Xmw1x83A06H2_JC6hK8s-7" target="Xmw1x83A06H2_JC6hK8s-8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="Xmw1x83A06H2_JC6hK8s-18" value="&lt;div&gt;Liefert Inhalte zum Webbrowser&amp;nbsp;&lt;/div&gt;&lt;div&gt;von Benutzer:innen&lt;/div&gt;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="Xmw1x83A06H2_JC6hK8s-17">
<mxGeometry x="-0.0888" y="-2" relative="1" as="geometry">
<mxPoint x="5" as="offset" />
</mxGeometry>
</mxCell>
<object placeholders="1" c4Name="Visual Analytics Tool" c4Type="SystemScopeBoundary" c4Application="Software System" label="&lt;font style=&quot;font-size: 16px&quot;&gt;&lt;b&gt;&lt;div style=&quot;text-align: left&quot;&gt;%c4Name%&lt;/div&gt;&lt;/b&gt;&lt;/font&gt;&lt;div style=&quot;text-align: left&quot;&gt;[%c4Application%]&lt;/div&gt;" id="Xmw1x83A06H2_JC6hK8s-19">
<mxCell style="rounded=1;fontSize=11;whiteSpace=wrap;html=1;dashed=1;arcSize=20;fillColor=none;strokeColor=#666666;fontColor=#333333;labelBackgroundColor=none;align=left;verticalAlign=bottom;labelBorderColor=none;spacingTop=0;spacing=10;dashPattern=8 4;metaEdit=1;rotatable=0;perimeter=rectanglePerimeter;noLabel=0;labelPadding=0;allowArrows=0;connectable=0;expand=0;recursiveResize=0;editable=1;pointerEvents=0;absoluteArcSize=1;points=[[0.25,0,0],[0.5,0,0],[0.75,0,0],[1,0.25,0],[1,0.5,0],[1,0.75,0],[0.75,1,0],[0.5,1,0],[0.25,1,0],[0,0.75,0],[0,0.5,0],[0,0.25,0]];" vertex="1" parent="1">
<mxGeometry x="20" y="210" width="1080" height="460" as="geometry" />
</mxCell>
</object>
</root>
</mxGraphModel>
</diagram>
</mxfile>

4
etl/README.md Normal file
View File

@ -0,0 +1,4 @@
# How to run
```bash
fastapi dev api/main.py --port 8080
```

1569
etl/pixi.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
[project] [project]
authors = [{name = "Giò Diani", email = "mail@gionathandiani.name"}] authors = [{name = "Giò Diani", email = "mail@gionathandiani.name"}, {name = "Mauro Stoffel", email = "mauro.stoffel@stud.fhgr.ch"}, {name = "Colin Bolli", email = "colin.bolli@stud.fhgr.ch"}, {name = "Charles Winkler", email = "charles.winkler@stud.fhgr.ch"}]
dependencies = [] description = "Datenauferbeitung"
description = "Add a short description here"
name = "consultancy_2" name = "consultancy_2"
requires-python = ">= 3.11" requires-python = ">= 3.11"
version = "0.1.0" version = "0.1.0"
@ -25,5 +24,6 @@ pandas = ">=2.2.3,<3"
plotly = ">=5.24.1,<6" plotly = ">=5.24.1,<6"
duckdb = ">=1.1.2,<2" duckdb = ">=1.1.2,<2"
python-dotenv = ">=1.0.1,<2" python-dotenv = ">=1.0.1,<2"
fastapi = ">=0.115.4,<0.116"
polars = ">=0.20.26,<2" polars = ">=0.20.26,<2"
pyarrow = ">=18.0.0,<19" pyarrow = ">=18.0.0,<19"

104
etl/src/api/main.py Normal file
View File

@ -0,0 +1,104 @@
import data
import polars as pl
from data import etl_property_capacities as etl_pc
from data import etl_property_capacities_monthly as etl_pcm
from data import etl_property_capacities_weekdays as etl_pcw
from data import etl_property_neighbours as etl_pn
from data import etl_region_capacities as etl_rc
from data import etl_region_capacities_comparison as etl_rcc
from data import etl_region_capacities_monthly as etl_rcm
from data import etl_region_capacities_weekdays as etl_rcw
from data import etl_region_movAverage as etl_rmA
from data import etl_region_properties_capacities as etl_rpc
from fastapi import FastAPI, Response
d = data.load()
app = FastAPI()
@app.get("/")
def read_root():
return {"Hi there!"}
@app.get("/items/{item_id}")
def read_item(item_id: int):
ext = d.extractions_for(item_id).pl()
out = ext.with_columns(pl.col("calendar").str.extract_all(r"([0-9]{4}-[0-9]{2}-[0-9]{2})|[0-2]").alias("calendar_data"))
out = out.drop(['calendar', 'property_id'])
return Response(content=out.write_json(), media_type="application/json")
@app.get("/region/properties")
def properties_region():
return d.properties_per_region().pl().to_dicts()
@app.get("/properties/growth")
def properties_growth():
options = {"dates" : d.properties_growth().pl()['date'].to_list(), "total_all" : d.properties_growth().pl()['total_all'].to_list(), "total_heidiland" : d.properties_growth().pl()['total_heidiland'].to_list(), "total_engadin" : d.properties_growth().pl()['total_engadin'].to_list(), "total_davos" : d.properties_growth().pl()['total_davos'].to_list(), "total_stmoritz" : d.properties_growth().pl()['total_stmoritz'].to_list()}
return options
@app.get("/properties/geo")
def properties_geo():
return d.properties_geo().pl().to_dicts()
@app.get("/property/{id}/neighbours")
def property_neighbours(id: int):
capacities = etl_pn.property_neighbours(id)
return capacities
@app.get("/property/{id}/extractions")
def property_extractions(id: int):
return d.extractions_for(property_id = id).pl().to_dicts()
@app.get("/property/{id}/capacities")
def property_capacities_data(id: int):
capacities = etl_pc.property_capacities(id)
return capacities
@app.get("/property/{id}/capacities/monthly/{scrapeDate}")
def property_capacities_data(id: int, scrapeDate: str):
capacities = etl_pcm.property_capacities_monthly(id, scrapeDate)
return capacities
@app.get("/property/{id}/capacities/weekdays/{scrapeDate}")
def property_capacities_data(id: int, scrapeDate: str):
capacities = etl_pcw.property_capacities_weekdays(id, scrapeDate)
return capacities
@app.get("/property/{id}/base")
def property_base_data(id: int):
return d.property_base_data(id).pl().to_dicts()
@app.get("/region/{id}/properties/capacities")
def region_property_capacities_data(id: int):
capacities = etl_rpc.region_properties_capacities(id)
return capacities
@app.get("/region/{id}/capacities")
def region_capacities_data(id: int):
capacities = etl_rc.region_capacities(id)
return capacities
@app.get("/region/{id}/capacities/monthly/{scrapeDate}")
def region_capacities_data(id: int, scrapeDate: str):
capacities = etl_rcm.region_capacities_monthly(id, scrapeDate)
return capacities
@app.get("/region/{id}/capacities/weekdays/{scrapeDate}")
def region_capacities_data(id: int, scrapeDate: str):
capacities = etl_rcw.region_capacities_weekdays(id, scrapeDate)
return capacities
@app.get("/region/capacities/comparison/{id_1}/{id_2}")
def region_capacities_data(id_1: int, id_2: int):
capacities = etl_rcc.region_capacities_comparison(id_1, id_2)
return capacities
@app.get("/region/{id}/movingAverage/{startDate}")
def region_capacities_data(id: int, startDate: str):
result = etl_rmA.region_movingAverage(id, startDate)
return result
@app.get("/region/{id}/base")
def region_base_data(id: int):
return d.region_base_data(id).pl().to_dicts()

View File

@ -1,22 +0,0 @@
from typing import Union
import polars as pl
from fastapi import FastAPI, Response
import data
d = data.load()
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int):
ext = d.extractions_for(item_id).pl()
out = ext.with_columns(pl.col("calendar").str.extract_all(r"([0-9]{4}-[0-9]{2}-[0-9]{2})|[0-2]").alias("calendar_data"))
out = out.drop(['calendar', 'property_id'])
return Response(content=out.write_json(), media_type="application/json")

View File

@ -28,8 +28,6 @@ class Database:
if(spatial_installed and not spatial_installed[0]): if(spatial_installed and not spatial_installed[0]):
self.connection.sql("INSTALL spatial") self.connection.sql("INSTALL spatial")
def db_overview(self): def db_overview(self):
return self.connection.sql("DESCRIBE;").show() return self.connection.sql("DESCRIBE;").show()
@ -46,19 +44,100 @@ class Database:
def properties_growth(self): def properties_growth(self):
return self.connection.sql(""" return self.connection.sql("""
WITH PropertiesALL AS (
SELECT
strftime(created_at, '%Y-%m-%d') AS date,
COUNT(*) as properties_count,
SUM(properties_count) OVER (ORDER BY date) AS total
FROM
consultancy_d.properties p
GROUP BY
date
ORDER BY
date
),
PropertiesR1 AS (
SELECT
strftime(created_at, '%Y-%m-%d') AS date,
COUNT(*) as properties_count,
SUM(properties_count) OVER (ORDER BY date) AS total
FROM
consultancy_d.properties p
WHERE
p.seed_id = 1
GROUP BY
date
ORDER BY
date
),
PropertiesR2 AS (
SELECT
strftime(created_at, '%Y-%m-%d') AS date,
COUNT(*) as properties_count,
SUM(properties_count) OVER (ORDER BY date) AS total
FROM
consultancy_d.properties p
WHERE
p.seed_id = 2
GROUP BY
date
ORDER BY
date
),
PropertiesR3 AS (
SELECT
strftime(created_at, '%Y-%m-%d') AS date,
COUNT(*) as properties_count,
SUM(properties_count) OVER (ORDER BY date) AS total
FROM
consultancy_d.properties p
WHERE
p.seed_id = 3
GROUP BY
date
ORDER BY
date
),
PropertiesR4 AS (
SELECT
strftime(created_at, '%Y-%m-%d') AS date,
COUNT(*) as properties_count,
SUM(properties_count) OVER (ORDER BY date) AS total
FROM
consultancy_d.properties p
WHERE
p.seed_id = 4
GROUP BY
date
ORDER BY
date
)
SELECT SELECT
strftime(created_at, '%Y-%m-%d') AS date, p.date,
COUNT(*) as properties_count p.total AS total_all,
pR1.total as total_heidiland,
pR2.total AS total_davos,
pR3.total AS total_engadin,
pR4.total AS total_stmoritz
FROM FROM
consultancy_d.properties PropertiesAll p
GROUP BY LEFT JOIN
date; PropertiesR1 pR1 ON p.date = pR1.date
LEFT JOIN
PropertiesR2 pR2 ON p.date = pR2.date
LEFT JOIN
PropertiesR3 pR3 ON p.date = pR3.date
LEFT JOIN
PropertiesR4 pR4 ON p.date = pR4.date
ORDER BY
p.date
""") """)
def properties_per_region(self): def properties_per_region(self):
return self.connection.sql(""" return self.connection.sql("""
SELECT SELECT
regions.name, regions.name,
regions.id,
COUNT(*) AS count_properties COUNT(*) AS count_properties
FROM FROM
consultancy_d.properties consultancy_d.properties
@ -68,7 +147,22 @@ class Database:
consultancy_d.regions ON regions.id = seeds.region_id consultancy_d.regions ON regions.id = seeds.region_id
GROUP BY GROUP BY
properties.seed_id, properties.seed_id,
regions.name regions.name,
regions.id
ORDER BY
count_properties ASC
""")
def propIds_with_region(self):
return self.connection.sql("""
SELECT
properties.id, seed_id, regions.name
FROM
consultancy_d.properties
LEFT JOIN
consultancy_d.seeds ON seeds.id = properties.seed_id
LEFT JOIN
consultancy_d.regions ON regions.id = seeds.region_id
""") """)
def properties_unreachable(self): def properties_unreachable(self):
@ -196,7 +290,7 @@ class Database:
""") """)
def extractions(self): def extractions(self):
return self.connection.sql(f""" return self.connection.sql("""
SELECT SELECT
JSON_EXTRACT(body, '$.content.days') as calendar, JSON_EXTRACT(body, '$.content.days') as calendar,
property_id, property_id,
@ -209,19 +303,54 @@ class Database:
property_id property_id
""") """)
def extractions_with_region(self):
return self.connection.sql("""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendar,
extractions.property_id,
extractions.created_at,
properties.seed_id,
regions.name
FROM
consultancy_d.extractions
LEFT JOIN
consultancy_d.properties ON properties.id = extractions.property_id
LEFT JOIN
consultancy_d.seeds ON seeds.id = properties.seed_id
LEFT JOIN
consultancy_d.regions ON regions.id = seeds.region_id
""")
def extractions_for(self, property_id): def extractions_for(self, property_id):
return self.connection.sql(f""" return self.connection.sql(f"""
SELECT SELECT
JSON_EXTRACT(body, '$.content.days') as calendar, JSON_EXTRACT(body, '$.content.days') as calendar,
property_id,
created_at created_at
FROM FROM
consultancy_d.extractions consultancy_d.extractions
WHERE WHERE
type == 'calendar' AND type == 'calendar' AND
property_id = {property_id} property_id = {property_id} AND
calendar NOT NULL
ORDER BY ORDER BY
property_id created_at
""")
def extractions_propId_scrapeDate(self, property_id: int, scrape_date: str):
return self.connection.sql(f"""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendar,
created_at
FROM
consultancy_d.extractions
WHERE
type == 'calendar' AND
property_id = {property_id} AND
calendar NOT NULL AND
created_at >= '{scrape_date}'
ORDER BY
created_at
LIMIT 1
""") """)
# Anzahl der extrahierten properties pro Exktraktionsvorgang # Anzahl der extrahierten properties pro Exktraktionsvorgang
@ -267,3 +396,172 @@ class Database:
ORDER BY property_id ORDER BY property_id
""") """)
def property_base_data(self, id):
return self.connection.sql(f"""
SELECT
p.property_platform_id,
p.created_at as first_found,
p.last_found,
p.check_data,
r.id as region_id,
r.name as region_name
FROM
consultancy_d.properties p
INNER JOIN consultancy_d.seeds s ON s.id = p.seed_id
INNER JOIN consultancy_d.regions r ON s.region_id = r.id
WHERE
p.id = {id}
""")
def region_base_data(self, id):
if id == -1:
where = ''
else:
where = f"WHERE r.id = {id}"
return self.connection.sql(f"""
SELECT
r.id as region_id,
r.name as region_name
FROM
consultancy_d.regions r
{where}
""")
def properties_geo(self):
return self.connection.sql("""
SELECT
p.id as property_id,
p.check_data as latlng,
r.id as region_id
FROM
consultancy_d.properties p
LEFT JOIN
consultancy_d.seeds s ON s.id = p.seed_id
LEFT JOIN
consultancy_d.regions r ON r.id = s.region_id
""")
def properties_geo_seeds(self):
return self.connection.sql("""
SELECT
p.id,
p.seed_id,
p.check_data as coordinates
FROM
consultancy_d.properties p
""")
def capacity_of_region(self, region_id):
return self.connection.sql(f"""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendarBody,
strftime(extractions.created_at, '%Y-%m-%d') AS ScrapeDate,
extractions.property_id,
FROM
consultancy_d.extractions
LEFT JOIN
consultancy_d.properties ON properties.id = extractions.property_id
WHERE
type == 'calendar' AND
properties.seed_id = {region_id}
""")
def singleScrape_of_region(self, region_id: int, scrape_date_min: str, scrape_date_max: str):
return self.connection.sql(f"""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendarBody,
FROM
consultancy_d.extractions
LEFT JOIN
consultancy_d.properties ON properties.id = extractions.property_id
WHERE
type == 'calendar' AND
properties.seed_id = {region_id} AND
extractions.created_at >= '{scrape_date_min}' AND
extractions.created_at < '{scrape_date_max}'
""")
def singleScrape_of_global(self, scrape_date_min: str, scrape_date_max: str):
return self.connection.sql(f"""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendarBody,
FROM
consultancy_d.extractions
LEFT JOIN
consultancy_d.properties ON properties.id = extractions.property_id
WHERE
type == 'calendar' AND
extractions.created_at >= '{scrape_date_min}' AND
extractions.created_at < '{scrape_date_max}'
""")
def singleScrape_of_region_scrapDate(self, region_id: int, scrape_date_min: str, scrape_date_max: str):
return self.connection.sql(f"""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendarBody,
extractions.created_at
FROM
consultancy_d.extractions
LEFT JOIN
consultancy_d.properties ON properties.id = extractions.property_id
WHERE
type == 'calendar' AND
properties.seed_id = {region_id} AND
extractions.created_at >= '{scrape_date_min}' AND
extractions.created_at < '{scrape_date_max}'
""")
def singleScrape_of_global_scrapDate(self, scrape_date_min: str, scrape_date_max: str):
return self.connection.sql(f"""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendarBody,
extractions.created_at
FROM
consultancy_d.extractions
LEFT JOIN
consultancy_d.properties ON properties.id = extractions.property_id
WHERE
type == 'calendar' AND
extractions.created_at >= '{scrape_date_min}' AND
extractions.created_at < '{scrape_date_max}'
""")
def capacity_global(self):
return self.connection.sql(f"""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendarBody,
strftime(extractions.created_at, '%Y-%m-%d') AS ScrapeDate,
extractions.property_id,
FROM
consultancy_d.extractions
LEFT JOIN
consultancy_d.properties ON properties.id = extractions.property_id
WHERE
type == 'calendar'
""")
def capacity_comparison_of_region(self, region_id_1, region_id_2):
return self.connection.sql(f"""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendarBody,
strftime(extractions.created_at, '%Y-%m-%d') AS ScrapeDate,
extractions.property_id,
properties.seed_id
FROM
consultancy_d.extractions
LEFT JOIN
consultancy_d.properties ON properties.id = extractions.property_id
WHERE
type == 'calendar' AND
(properties.seed_id = {region_id_1} OR
properties.seed_id = {region_id_2})
""")
def unique_scrapeDates(self):
return self.connection.sql(f"""
SELECT DISTINCT
strftime(extractions.created_at, '%Y-%m-%d') AS ScrapeDate,
FROM
consultancy_d.extractions
""")

18
etl/src/data/etl_cache.py Normal file
View File

@ -0,0 +1,18 @@
from pathlib import Path
from pickle import dump, load
Path('cache').mkdir(parents=True, exist_ok=True)
# load pickle obj
def openObj(file):
filepath = Path(f"cache/{file}")
if filepath.is_file():
with open(filepath, 'rb') as f:
return load(f)
return False
# save pickle obj
def saveObj(file, result):
filepath = Path(f"cache/{file}")
with open(filepath, 'wb') as f:
dump(result, f)

View File

@ -23,7 +23,6 @@ def expansion_Pipeline(df):
df = pl.DataFrame(data, schema=["property_id", "created_at", "calendar_date", "calendar_value"]) df = pl.DataFrame(data, schema=["property_id", "created_at", "calendar_date", "calendar_value"])
return df return df
def liveDates_Pipeline(df): def liveDates_Pipeline(df):
''' '''
Returns the expanded Dataframe with only the live data and no future data Returns the expanded Dataframe with only the live data and no future data

View File

@ -0,0 +1,46 @@
from io import StringIO
import polars as pl
import data
from data import etl_cache
d = data.load()
def property_capacities(id: int):
file = f"etl_property_capacities_{id}.obj"
obj = etl_cache.openObj(file)
if obj:
return obj
extractions = d.extractions_for(id).pl()
df_dates = pl.DataFrame()
for row in extractions.rows(named=True):
df_calendar = pl.read_json(StringIO(row['calendar']))
#df_calendar.insert_column(0, pl.Series("created_at", [row['created_at']]))
df_dates = pl.concat([df_calendar, df_dates], how="diagonal")
# order = sorted(df_dates.columns)
# df_dates = df_dates.select(order)
sum_hor = df_dates.sum_horizontal()
#print(sum_hor)
# Get the available dates per extraction
count_days = []
for dates in df_dates.rows():
# Remove all None values
liste = [x for x in dates if x is not None]
count_days.append(len(liste))
counts = pl.DataFrame({"count_days" : count_days, "sum" : sum_hor})
result = {"capacities": [], "dates": extractions['created_at'].cast(pl.Date).to_list() }
for row in counts.rows(named=True):
max_capacity = row['count_days'] * 2
max_capacity_perc = 100 / max_capacity
result['capacities'].append(round(max_capacity_perc * row['sum'], 2))
result['capacities'].reverse()
etl_cache.saveObj(file, result)
return result

View File

@ -0,0 +1,35 @@
from io import StringIO
import polars as pl
import data
from data import etl_cache
d = data.load()
def property_capacities_monthly(id: int, scrapeDate: str):
file = f"etl_property_capacities_monthly_{id}_{scrapeDate}.obj"
obj = etl_cache.openObj(file)
if obj:
return obj
extractions = d.extractions_propId_scrapeDate(id, scrapeDate).pl()
df_calendar = pl.DataFrame()
for row in extractions.rows(named=True):
scrapeDate = row['created_at']
df_calendar = pl.read_json(StringIO(row['calendar']))
columnTitles = df_calendar.columns
df_calendar = df_calendar.transpose()
df_calendar = df_calendar.with_columns(pl.Series(name="dates", values=columnTitles))
df_calendar = df_calendar.with_columns((pl.col("dates").str.to_date()))
df_calendar = df_calendar.with_columns((pl.col("dates").dt.strftime("%b") + " " + (pl.col("dates").dt.strftime("%Y"))).alias('date_short'))
df_calendar = df_calendar.with_columns((pl.col("dates").dt.strftime("%Y") + " " + (pl.col("dates").dt.strftime("%m"))).alias('dates'))
df_calendar = df_calendar.group_by(['dates', 'date_short']).agg(pl.col("column_0").sum())
df_calendar = df_calendar.sort('dates')
df_calendar = df_calendar.drop('dates')
result = {"scraping-date": scrapeDate, "months": df_calendar['date_short'].to_list(), 'capacities': df_calendar['column_0'].to_list()}
etl_cache.saveObj(file, result)
return result

View File

@ -0,0 +1,41 @@
from io import StringIO
import polars as pl
import data
from data import etl_cache
d = data.load()
def property_capacities_weekdays(id: int, scrapeDate: str):
file = f"etl_property_capacities_weekdays_{id}_{scrapeDate}.obj"
obj = etl_cache.openObj(file)
if obj:
return obj
extractions = d.extractions_propId_scrapeDate(id, scrapeDate).pl()
weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
df_calendar = pl.DataFrame()
numWeeks = 0
for row in extractions.rows(named=True):
scrapeDate = row['created_at']
df_calendar = pl.read_json(StringIO(row['calendar']))
columnTitles = df_calendar.columns
df_calendar = df_calendar.transpose()
df_calendar = df_calendar.with_columns(pl.Series(name="dates", values=columnTitles))
df_calendar = df_calendar.with_columns((pl.col("dates").str.to_date()))
numWeeks = round((df_calendar.get_column("dates").max() - df_calendar.get_column("dates").min()).days / 7, 0)
df_calendar = df_calendar.with_columns(pl.col("dates").dt.weekday().alias("weekday_num"))
df_calendar = df_calendar.with_columns(pl.col("dates").dt.strftime("%A").alias("weekday"))
df_calendar = df_calendar.drop("dates")
df_calendar = df_calendar.group_by(["weekday", "weekday_num"]).agg(pl.col("column_0").sum())
df_calendar = df_calendar.with_columns((pl.col("column_0") / numWeeks / 2 * 100).alias("column_0"))
df_calendar = df_calendar.sort('weekday_num')
df_calendar = df_calendar.drop('weekday_num')
result = {"scraping-date": scrapeDate, "weekdays": df_calendar['weekday'].to_list(), 'capacities': df_calendar['column_0'].to_list()}
etl_cache.saveObj(file, result)
return result

View File

@ -0,0 +1,74 @@
from math import asin, atan2, cos, degrees, radians, sin, sqrt
import polars as pl
import data
from data import etl_cache
d = data.load()
def calcHaversinDistance(latMain, lonMain, lat, lon):
R = 6371
# convert decimal degrees to radians
latMain, lonMain, lat, lon = map(radians, [latMain, lonMain, lat, lon])
# haversine formula
dlon = lonMain - lon
dlat = latMain - lat
a = sin(dlat / 2) ** 2 + cos(lat) * cos(latMain) * sin(dlon / 2) ** 2
c = 2 * asin(sqrt(a)) # 2 * atan2(sqrt(a), sqrt(1-a))
d = R * c
return d
def property_neighbours(id: int):
file = f"etl_property_neighbours_{id}.obj"
obj = etl_cache.openObj(file)
if obj:
return obj
extractions = d.properties_geo_seeds().pl()
# Get lat, long and region from main property
latMain, lonMain = extractions.filter(pl.col('id') == str(id))['coordinates'][0].split(',')
latMain, lonMain = map(float, [latMain, lonMain])
region = extractions.filter(pl.col('id') == str(id))['seed_id'][0]
# Prefilter the dataframe to only the correct region
extractions = extractions.filter(pl.col('seed_id') == str(region))
extractions = extractions.drop('seed_id')
# Remove main property from DF
extractions = extractions.filter(pl.col('id') != str(id))
# Split coordinate into lat and lon
#extractions = extractions.with_columns((pl.col('coordinates').str.split(','))[0].alias("coordinates")).unnest("fields")
extractions = extractions.with_columns(pl.col("coordinates").str.split_exact(",", 1).struct.rename_fields(["lat", "lon"]).alias("lat/lon")).unnest("lat/lon")
extractions = extractions.drop('coordinates')
extractions = extractions.with_columns(pl.col("lat").cast(pl.Float32))
extractions = extractions.with_columns(pl.col("lon").cast(pl.Float32))
# Calculate distances
distances = []
for row in extractions.rows(named=True):
lat = row['lat']
lon = row['lon']
dist = calcHaversinDistance(latMain, lonMain, lat, lon)
distances.append(dist)
# Add distance to DF
extractions = extractions.with_columns(pl.Series(name="distances", values=distances))
# Sort for distance and give only first 10
extractions = extractions.sort("distances").head(10)
extractions = extractions.drop('distances')
#result = {"ids": extractions['id'].to_list(), "lat": extractions['lat'].to_list(), "lon": extractions['lon'].to_list()}
result = extractions.to_dicts()
etl_cache.saveObj(file, result)
return result

View File

@ -0,0 +1,58 @@
from datetime import date
from io import StringIO
import polars as pl
import data
from data import etl_cache
d = data.load()
def region_capacities(id: int):
file = f"etl_region_capacities_{id}.obj"
obj = etl_cache.openObj(file)
if obj:
return obj
# Get Data
if id == -1:
extractions = d.capacity_global().pl()
else:
extractions = d.capacity_of_region(id).pl()
# turn PropertyIDs to ints for sorting
extractions = extractions.cast({"property_id": int})
extractions.drop('property_id')
df_dates = pl.DataFrame()
# Get Data from JSON
gridData = pl.DataFrame(schema=[("scrape_date", pl.String), ("sum_hor", pl.Int64), ("calendar_width", pl.Int64)])
dayCounts = []
for row in extractions.rows(named=True):
# Return 0 for sum if calendar is null
if row['calendarBody']:
calDF = pl.read_json(StringIO(row['calendarBody']))
sum_hor = calDF.sum_horizontal()[0]
else:
sum_hor = 0
gridData = gridData.vstack(pl.DataFrame({"scrape_date" : row['ScrapeDate'], "sum_hor": sum_hor, "calendar_width": calDF.width}))
# Create Aggregates of values
df_count = gridData.group_by("scrape_date").agg(pl.col("sum_hor").count())
df_sum = gridData.group_by("scrape_date").agg(pl.col("sum_hor").sum())
df_numDays = gridData.group_by("scrape_date").agg(pl.col("calendar_width").max())
# Join and rename DF's
df = df_sum.join(df_count, on= 'scrape_date').join(df_numDays, on= 'scrape_date')
# Calculate normed capacities for each scrapeDate
df = df.with_columns((pl.col("sum_hor") / pl.col("sum_hor_right") / (pl.col("calendar_width")*2) * 100).alias("capacity"))
# Sort the date column
df = df.cast({"scrape_date": date}).sort('scrape_date')
result = {"capacities": df['capacity'].to_list(), "dates": df['scrape_date'].to_list()}
etl_cache.saveObj(file, result)
return result

View File

@ -0,0 +1,65 @@
import data
import polars as pl
from io import StringIO
import numpy as np
d = data.load()
def region_capacities_comparison(id_1: int, id_2: int):
fulldf = d.capacity_comparison_of_region(id_1, id_2).pl()
# turn PropertyIDs and seedIDs to ints for sorting and filtering
fulldf = fulldf.cast({"property_id": int})
fulldf = fulldf.cast({"seed_id": int})
df_region1 = fulldf.filter(pl.col("seed_id") == id_1)
df_region2 = fulldf.filter(pl.col("seed_id") == id_2)
df_list = [df_region1, df_region2]
outDictList = []
for df in df_list:
# Get uniques for dates and propIDs and sort them
listOfDates = df.get_column("ScrapeDate").unique().sort()
listOfPropertyIDs = df.get_column("property_id").unique().sort()
# Create DFs from lists to merge later
datesDF = pl.DataFrame(listOfDates).with_row_index("date_index")
propIdDF = pl.DataFrame(listOfPropertyIDs).with_row_index("prop_index")
# Merge Dataframe to generate indices
df = df.join(datesDF, on='ScrapeDate')
df = df.join(propIdDF, on='property_id')
# Drop now useless columns ScrapeDate and property_id
df = df[['ScrapeDate', 'calendarBody', 'date_index', 'prop_index']]
# Calculate grid values
gridData = []
for row in df.rows(named=True):
# Return 0 for sum if calendar is null
if row['calendarBody']:
calDF = pl.read_json(StringIO(row['calendarBody']))
sum_hor = calDF.sum_horizontal()[0]
else:
sum_hor = 0
# With Index
# gridData.append([row['prop_index'], row['date_index'], sum_hor])
# With ScrapeDate
gridData.append([row['ScrapeDate'], row['date_index'], sum_hor])
gridData = np.array(gridData)
# get all values to calculate Max
allValues = gridData[:, 2].astype(int)
maxValue = np.max(allValues)
gridData[:, 2] = (allValues*100)/maxValue
# Return back to list
gridData = gridData.tolist()
# Cast listOfDates to datetime
listOfDates = listOfDates.cast(pl.Date).to_list()
listOfPropertyIDs = listOfPropertyIDs.to_list()
# Create JSON
tempDict = {'scrapeDates': listOfDates, 'property_ids': listOfPropertyIDs, 'values': gridData}
outDictList.append(tempDict)
outDict = {'region1': outDictList[0], 'region2': outDictList[1],}
return outDict

View File

@ -0,0 +1,65 @@
from datetime import datetime, timedelta
from io import StringIO
import polars as pl
import data
from data import etl_cache
d = data.load()
def region_capacities_monthly(id: int, scrapeDate_start: str):
file = f"etl_region_capacities_monthly_{id}_{scrapeDate_start}.obj"
obj = etl_cache.openObj(file)
if obj:
return obj
# String to Date
scrapeDate_start = datetime.strptime(scrapeDate_start, '%Y-%m-%d')
# Get end date of start search-window
scrapeDate_end = scrapeDate_start + timedelta(days=1)
# Get Data
if id == -1:
extractions = d.singleScrape_of_global_scrapDate(scrapeDate_start, scrapeDate_end).pl()
else:
extractions = d.singleScrape_of_region_scrapDate(id, scrapeDate_start, scrapeDate_end).pl()
df_calendar = pl.DataFrame()
numWeeks = 0
firstExe = True
counter = 0
for row in extractions.rows(named=True):
scrapeDate = row['created_at']
if row['calendarBody']:
counter += 1
df_calendar = pl.read_json(StringIO(row['calendarBody']))
columnTitles = df_calendar.columns
df_calendar = df_calendar.transpose()
df_calendar = df_calendar.with_columns(pl.Series(name="dates", values=columnTitles))
df_calendar = df_calendar.with_columns((pl.col("dates").str.to_date()))
df_calendar = df_calendar.with_columns((pl.col("dates").dt.strftime("%b") + " " + (pl.col("dates").dt.strftime("%Y"))).alias('date_short'))
df_calendar = df_calendar.with_columns((pl.col("dates").dt.strftime("%Y") + " " + (pl.col("dates").dt.strftime("%m"))).alias('dates'))
df_calendar = df_calendar.group_by(['dates', 'date_short']).agg(pl.col("column_0").sum())
df_calendar = df_calendar.sort('dates')
df_calendar = df_calendar.drop('dates')
df_calendar = df_calendar.rename({'column_0': str(counter)})
if firstExe:
outDf = df_calendar
firstExe = False
else:
outDf = outDf.join(df_calendar, on='date_short')
# Calculate horizontal Mean
means = outDf.mean_horizontal()
outDf = outDf.insert_column(1, means)
outDf = outDf[['date_short', 'mean']]
result = {"scraping-date": scrapeDate, "months": outDf['date_short'].to_list(),'capacities': outDf['mean'].to_list()}
etl_cache.saveObj(file, result)
return result

View File

@ -0,0 +1,67 @@
from datetime import datetime, timedelta
from io import StringIO
import polars as pl
import data
from data import etl_cache
d = data.load()
def region_capacities_weekdays(id: int, scrapeDate_start: str):
file = f"etl_region_capacities_weekdays_{id}_{scrapeDate_start}.obj"
obj = etl_cache.openObj(file)
if obj:
return obj
# String to Date
scrapeDate_start = datetime.strptime(scrapeDate_start, '%Y-%m-%d')
# Get end date of start search-window
scrapeDate_end = scrapeDate_start + timedelta(days=1)
# Get Data
if id == -1:
extractions = d.singleScrape_of_global_scrapDate(scrapeDate_start, scrapeDate_end).pl()
else:
extractions = d.singleScrape_of_region_scrapDate(id, scrapeDate_start, scrapeDate_end).pl()
df_calendar = pl.DataFrame()
numWeeks = 0
firstExe = True
counter = 0
for row in extractions.rows(named=True):
scrapeDate = row['created_at']
if row['calendarBody']:
counter += 1
df_calendar = pl.read_json(StringIO(row['calendarBody']))
columnTitles = df_calendar.columns
df_calendar = df_calendar.transpose()
df_calendar = df_calendar.with_columns(pl.Series(name="dates", values=columnTitles))
df_calendar = df_calendar.with_columns((pl.col("dates").str.to_date()))
numWeeks = round((df_calendar.get_column("dates").max() - df_calendar.get_column("dates").min()).days / 7, 0)
df_calendar = df_calendar.with_columns(pl.col("dates").dt.weekday().alias("weekday_num"))
df_calendar = df_calendar.with_columns(pl.col("dates").dt.strftime("%A").alias("weekday"))
df_calendar = df_calendar.drop("dates")
df_calendar = df_calendar.group_by(["weekday", "weekday_num"]).agg(pl.col("column_0").sum())
df_calendar = df_calendar.with_columns((pl.col("column_0") / numWeeks / 2 * 100).alias("column_0"))
df_calendar = df_calendar.sort('weekday_num')
df_calendar = df_calendar.drop('weekday_num')
df_calendar = df_calendar.rename({'column_0': str(counter)})
if firstExe:
outDf = df_calendar
firstExe = False
else:
outDf = outDf.join(df_calendar, on='weekday')
# Calculate horizontal Mean
means = outDf.mean_horizontal()
outDf = outDf.insert_column(1, means)
outDf = outDf[['weekday', 'mean']]
result = {"scraping-date": scrapeDate, "weekdays": outDf['weekday'].to_list(),'capacities': outDf['mean'].to_list()}
etl_cache.saveObj(file, result)
return result

View File

@ -0,0 +1,138 @@
from datetime import date, datetime, timedelta
from io import StringIO
import polars as pl
import data
from data import etl_cache
d = data.load()
def region_movingAverage(id: int, scrape_date_start_min: str):
file = f"etl_region_movingAverage_{id}_{scrape_date_start_min}.obj"
obj = etl_cache.openObj(file)
if obj:
return obj
# Settings
# Offset between actual and predict ScrapeDate
timeOffset = 30
# Calculation Frame
calcFrame = 180
# Filter Setting
windowSize = 7
# Get unique ScrapeDates
uniqueScrapeDates = d.unique_scrapeDates().pl()
uniqueScrapeDates = uniqueScrapeDates.get_column('ScrapeDate').str.to_date()
uniqueScrapeDates = uniqueScrapeDates.sort().to_list()
# String to Date
scrape_date_start_min = datetime.strptime(scrape_date_start_min, '%Y-%m-%d')
# Get end date of start search-window
scrape_date_start_max = scrape_date_start_min + timedelta(days=1)
# Get start and end date of End search-window
scrape_date_end_min = scrape_date_start_min + timedelta(days=timeOffset)
# Get closest ScrapeDate
scrape_date_end_min = min(uniqueScrapeDates, key=lambda x: abs(x - scrape_date_end_min.date()))
scrape_date_end_max = scrape_date_end_min + timedelta(days=1)
final_end_date = scrape_date_end_min + timedelta(days=calcFrame)
# Get Data
if id == -1:
ex_start = d.singleScrape_of_global(scrape_date_start_min, scrape_date_start_max)
ex_start_count = ex_start.shape[0]
ex_end = d.singleScrape_of_global(scrape_date_end_min, scrape_date_end_max)
ex_end_count = ex_end.shape[0]
else:
ex_start = d.singleScrape_of_region(id, scrape_date_start_min, scrape_date_start_max)
ex_start_count = ex_start.shape[0]
ex_end = d.singleScrape_of_region(id, scrape_date_end_min, scrape_date_end_max)
ex_end_count = ex_end.shape[0]
num_properties = [ex_start_count, ex_end_count]
start_end = [ex_start, ex_end]
outDFList = []
for df in start_end:
df = df.pl()
firstExe = True
counter = 1
outDF = pl.DataFrame(schema={"0": int, "dates": date})
for row in df.rows(named=True):
if row['calendarBody']:
calDF = pl.read_json(StringIO(row['calendarBody']))
columnTitles = calDF.columns
calDF = calDF.transpose()
calDF = calDF.with_columns(pl.Series(name="dates", values=columnTitles))
calDF = calDF.with_columns((pl.col("dates").str.to_date()))
# Filter out all Data that's in the calculation frame
calDF = calDF.filter((pl.col("dates") >= scrape_date_end_min))
calDF = calDF.filter((pl.col("dates") < final_end_date))
# Join all information into one Dataframe
if firstExe:
outDF = calDF
firstExe = False
else:
outDF = outDF.join(calDF, on='dates')
outDF = outDF.rename({'column_0': str(counter)})
counter += 1
outDF = outDF.sort('dates')
outDFList.append(outDF)
# Calculate the horizontal Sum for all Dates
arrayCunter = 0
tempDFList = []
for df in outDFList:
dates = df.select(pl.col("dates"))
values = df.select(pl.exclude("dates"))
sum_hor = values.sum_horizontal()
sum_hor = sum_hor / num_properties[arrayCunter] / 2 * 100
arrayCunter += 1
newDF = dates.with_columns(sum_hor=pl.Series(sum_hor))
tempDFList.append(newDF)
# Join actual and predict Values
outDF = tempDFList[1].join(tempDFList[0], on='dates', how='outer')
# Rename Columns for clarity
outDF = outDF.drop_nulls()
outDF = outDF.drop('dates_right')
# sum_hor_predict is the data from the earlier ScrapeDate
outDF = outDF.rename({'sum_hor': 'sum_hor_actual', 'sum_hor_right': 'sum_hor_predict'})
# Calculate Moving average from Start
baseValues = outDF.get_column('sum_hor_predict').to_list()
i = 0
moving_averages = []
while i < len(baseValues) - windowSize + 1:
window = baseValues[i: i + windowSize]
window_average = sum(window) / windowSize
moving_averages.append(window_average)
i += 1
# Add empty values back to the front and end of moving_averages
num_empty = int(windowSize / 2)
moving_averages = [None] *num_empty + moving_averages + [None] * num_empty
# Add moving_averages to df
outDF = outDF.with_columns(moving_averages=pl.Series(moving_averages))
result = {'dates':outDF.get_column('dates').to_list(), 'cap_earlierTimeframe':outDF.get_column('sum_hor_predict').to_list(), 'cap_laterTimeframe':outDF.get_column('sum_hor_actual').to_list(), 'movAvg':outDF.get_column('moving_averages').to_list(),}
etl_cache.saveObj(file, result)
return result

View File

@ -0,0 +1,64 @@
from io import StringIO
import polars as pl
import data
from data import etl_cache
d = data.load()
def region_properties_capacities(id: int):
file = f"etl_region_properties_capacities_{id}.obj"
obj = etl_cache.openObj(file)
if obj:
return obj
# Get Data
if id == -1:
df = d.capacity_global().pl()
else:
df = d.capacity_of_region(id).pl()
# turn PropertyIDs to ints for sorting
df = df.cast({"property_id": int})
# Get uniques for dates and propIDs and sort them
listOfDates = df.get_column("ScrapeDate").unique().sort()
listOfPropertyIDs = df.get_column("property_id").unique().sort()
# Create DFs from lists to merge later
datesDF = pl.DataFrame(listOfDates).with_row_index("date_index")
propIdDF = pl.DataFrame(listOfPropertyIDs).with_row_index("prop_index")
# Merge Dataframe to generate indices
df = df.join(datesDF, on='ScrapeDate')
df = df.join(propIdDF, on='property_id')
# Calculate grid values
gridData = pl.DataFrame(schema=[("scrape_date", pl.String), ("property_id", pl.String), ("sum_hor", pl.Int64)])
for row in df.rows(named=True):
# Return 0 for sum if calendar is null
if row['calendarBody']:
calDF = pl.read_json(StringIO(row['calendarBody']))
sum_hor = calDF.sum_horizontal()[0]
else:
sum_hor = 0
gridData = gridData.vstack(pl.DataFrame({"scrape_date" : row['ScrapeDate'], "property_id": str(row['property_id']), "sum_hor": sum_hor}))
# get the overall maximum sum
maxValue = gridData['sum_hor'].max()
values = []
for row in gridData.rows(named=True):
capacity = (row['sum_hor']*100)/maxValue
values.append((row['scrape_date'], row['property_id'], capacity))
# Cast listOfDates to datetime
listOfDates = listOfDates.cast(pl.Date).to_list()
listOfPropertyIDs = listOfPropertyIDs.cast(pl.String).to_list()
# Create JSON
outDict = {'scrapeDates': listOfDates, 'property_ids': listOfPropertyIDs, 'values': values}
etl_cache.saveObj(file, outDict)
return outDict

File diff suppressed because one or more lines are too long

View File

@ -1,22 +0,0 @@
import polars as pl
import data
inst = data.load()
test = inst.extractions_for(1).pl()
out = test.with_columns(
pl.col("calendar").str.extract_all(r"([0-9]{4}-[0-9]{2}-[0-9]{2})|[0-2]").alias("extracted_nrs"),
)
out.drop(['calendar', 'property_id'])
ll = out.get_column("extracted_nrs").explode().gather_every(2)
llo = out.get_column("extracted_nrs").explode().gather_every(2, offset=1)
lli = ll.list.concat(llo)
print(ll)
print(lli)

View File

@ -0,0 +1,121 @@
from etl.src import data
import json
import polars as pl
from datetime import datetime
import matplotlib.pyplot as plt
import numpy as np
'''
# Get Data from DB
inst = data.load()
df = inst.extractions_with_region().pl()
print(df)
counter = 0
data = []
for row in df.iter_rows():
property_id = row[1]
created_at = row[2].date()
dict = {'property_id': property_id, 'created_at': created_at, 'name': row[3]}
jsonStr = row[0]
if jsonStr:
calendarDict = json.loads(jsonStr)
for key in calendarDict:
dict[key] = calendarDict[key]
data.append(dict)
dfNew = pl.from_dicts(data)
dfNew.write_csv('results/data_quality.csv')
print(dfNew)
'''
dfNew = pl.read_csv('results/data_quality.csv')
dfNew = dfNew.with_columns(pl.col("created_at").map_elements(lambda x: datetime.strptime(x, "%Y-%m-%d").date()))
# Create Row Means
dfTemp = dfNew
# Temporary Remove leading columns but save for later
prop = dfTemp.get_column('property_id')
dfTemp = dfTemp.drop('property_id')
crea = dfTemp.get_column('created_at')
dfTemp = dfTemp.drop('created_at')
name = dfTemp.get_column('name')
dfTemp = dfTemp.drop('name')
dfTemp = dfTemp.with_columns(sum=pl.sum_horizontal(dfTemp.columns))
sumCol = dfTemp.get_column('sum')
# Create new DF with only property_id, created_at ,Location name and sum
df = pl.DataFrame([prop, crea, name, sumCol])
df = df.sort('created_at')
# Create Full Copy
# 0 = Alles
# 1 = Heidiland
# 2 = Davos
# 3 = Engadin
# 4 = St. Moritz
filterList = ['Alle Regionen', 'Heidiland', 'Davos', 'Engadin', 'St. Moritz']
filter = 4
if filter != 0:
df = df.filter(pl.col("name") == filter)
# Remove Location name
df = df.drop('name')
# Get unique property_ids
propsIDs = df.unique(subset=["property_id"])
propsIDs = propsIDs.get_column("property_id").to_list()
propsIDs.sort()
# create Matrix
matrix = []
for id in propsIDs:
dict = {}
temp = df.filter(pl.col("property_id") == id)
for row in temp.iter_rows():
dict[row[1].strftime('%Y-%m-%d')] = row[2]
matrix.append(dict)
matrix = pl.DataFrame(matrix)
dates = matrix.columns
matrix = matrix.to_numpy()
# normalized
matrix = matrix/1111
yRange = range(len(dates))
xRange = range(len(propsIDs))
matrix = matrix.T
plt.imshow(matrix)
plt.yticks(yRange[::5], dates[::5])
plt.xticks(xRange[::10], propsIDs[::10])
plt.title(filterList[filter])
plt.xlabel("Property ID")
plt.ylabel("Scrape Date")
plt.colorbar()
plt.tight_layout()
# Create DiffMatrix
diffMatrix = np.zeros((len(matrix)-1, len(matrix[0])))
for y in range(len(matrix[0])):
for x in range(len(matrix)-1):
diffMatrix[x][y] = abs(matrix[x][y] - matrix[x+1][y])
plt.figure()
plt.imshow(diffMatrix, cmap="Reds")
plt.yticks(yRange[::5], dates[::5])
plt.xticks(xRange[::10], propsIDs[::10])
plt.title(filterList[filter])
plt.xlabel("Property ID")
plt.ylabel("Scrape Date")
plt.colorbar()
plt.tight_layout()
plt.show()

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,34 +1,115 @@
import data from etl.src import data
from data import etl_pipelines as ep from etl.src.data import etl_pipelines as ep
import polars as pl import polars as pl
from datetime import datetime, timedelta
import pandas as pd
''' '''
#Create Data # Get Data from DB
inst = data.load() inst = data.load()
df = inst.extractions().pl() df = inst.extractions().pl()
df = ep.liveDates_Pipeline(df) df = ep.expansion_Pipeline(df)
df.write_csv('dok/liveDates.csv') df.write_csv('dok/flatDates.csv')
print(df) print(df)
''' '''
'''
#Load Data from DF
dfLive = pl.read_csv('dok/liveDates.csv')
dfFlat = pl.read_csv('dok/flatDates.csv')
#Load Data
df = pl.read_csv('dok/liveDates.csv')
propIds = df.get_column('property_id').unique() # Step 1 Get all occupied dates in live data
dfLive = dfLive.filter(pl.col("calendar_value") == 0)
dfLive = dfLive.with_columns(pl.col("created_at").str.to_date("%Y-%m-%d"))
dfLive = dfLive.with_columns(pl.col("calendar_date").str.to_date("%Y-%m-%d"))
#print(dfLive)
createdAt = df.get_column('created_at').unique() dfFlat = dfFlat.with_columns(pl.col("created_at").str.to_date("%Y-%m-%d"))
dfFlat = dfFlat.with_columns(pl.col("calendar_date").str.to_date("%Y-%m-%d"))
propIds = dfLive.get_column('property_id').unique()
createdAt = dfLive.get_column('created_at').unique()
#print(createdAt)
fullPreorderMatrix = []
for propId in propIds: for propId in propIds:
for createdAt in createdAt: curPreorderList = []
temp = df.filter(pl.col("created_at") == createdAt) print("Property ID = " + str(propId))
temp = temp.filter(pl.col("property_id") == propId) tempPropFlatDf = dfFlat.filter(pl.col("property_id") == propId)
if temp.shape[0] > 0: tempPropLiveDf = dfLive.filter(pl.col("property_id") == propId)
print(temp.get_column('calendar_value')[0]) allLiveOccupiedDates = tempPropLiveDf.filter(pl.col("calendar_value") == 0).get_column('created_at')
else: #print("allLiveOccupiedDates = ",allLiveOccupiedDates)
print(0) for date in allLiveOccupiedDates:
calLiveDate = tempPropLiveDf.filter(pl.col("created_at") == date).get_column('calendar_date')[0]
#print("Occupied Date = " + str(date), "with Calendar Date =", str(calLiveDate))
numOfScrapedPreordered = 0
foundLastDate = False
for createDate in createdAt:
if date > createDate:
#print("Finding Flat Date with CreateDate =",createDate, "and Calendar Date =", calLiveDate)
tempFlatDf = tempPropFlatDf.filter(pl.col("created_at") == createDate)
tempFlatDf = tempFlatDf.filter(pl.col("calendar_date") == calLiveDate)
#print("tempLiveDf = ", tempFlatDf)
calVal = tempFlatDf.get_column('calendar_value')
if len(calVal) > 0:
if calVal[0] == 0:
# Still Occupied
if not foundLastDate:
numOfScrapedPreordered += 1
else:
# Found last Date where not occupied
foundLastDate = True
#print("number of Scrapes already occupied =", numOfScrapedPreordered)
#break
#else:
#print("Skipped: Live Date = ",date, "Flat Date =",createDate)
#print(propId, date, numOfScrapedPreordered)
curPreorderList.append(numOfScrapedPreordered)
if len(curPreorderList) > 0:
mean = sum(curPreorderList) / len(curPreorderList)
else: mean = 0
#fullPreorderMatrix.append([propId, mean, curPreorderList])
fullPreorderMatrix.append([propId, mean])
print(fullPreorderMatrix)
fullPreoDF = pl.DataFrame(fullPreorderMatrix,orient="row")
fullPreoDF.write_csv('dok/fullPreoDF.csv')
print(fullPreoDF)
'''
# Filter Props to locations and calculate Means per location
inst = data.load()
propDf = inst.propIds_with_region().pl()
print(propDf)
propDf = propDf.select(
pl.col("id").cast(pl.Int64),
pl.col("seed_id").cast(pl.Int64),
)
preoDF = pl.read_csv('dok/fullPreoDF.csv')
preoDF = preoDF.rename({"column_0": "id", "column_1": "meanPreorderScrapeNum"})
#Hier weiter merge = preoDF.join(propDf, how='inner', on='id')
print(merge)
print("Global meanPreorderTime = ",round(merge.get_column("meanPreorderScrapeNum").mean()*3,2))
# 1 = Heidiland
heidi = merge.filter(pl.col("seed_id") == 1)
print("Heidiland meanPreorderTime = ", round(heidi.get_column("meanPreorderScrapeNum").mean()*3,2))
# 2 = Davos
Davos = merge.filter(pl.col("seed_id") == 2)
print("Davos meanPreorderTime = ", (Davos.get_column("meanPreorderScrapeNum").mean()*3,2))
# 3 = Engadin
Engadin = merge.filter(pl.col("seed_id") == 3)
print("Engadin meanPreorderTime = ", round(Engadin.get_column("meanPreorderScrapeNum").mean()*3,2))
# 4 = St. Moritz
Moritz = merge.filter(pl.col("seed_id") == 4)
print("St. Moritz meanPreorderTime = ", round(Moritz.get_column("meanPreorderScrapeNum").mean()*3,2))

View File

@ -1,66 +0,0 @@
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[WebReinvent](https://webreinvent.com/)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
- **[Jump24](https://jump24.co.uk)**
- **[Redberry](https://redberry.international/laravel/)**
- **[Active Logic](https://activelogic.com)**
- **[byte5](https://byte5.de)**
- **[OP.GG](https://op.gg)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

View File

@ -1,17 +0,0 @@
article{
background: #fff;
}
#extractions{
height: 500px;
background: #fff;
}
#test{
height: 500px;
}
#capacity,
#leaflet{
height: 600px;
}

File diff suppressed because one or more lines are too long

View File

@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css">
@vite(['resources/css/app.css', 'resources/js/app.js', '/home/gio/Code/ConsultancyProject_2_ETL/frontend/node_modules/leaflet/dist/leaflet.css'])
</head>
<body>
<header>Dashboard</header>
<main class="container-fluid">
<div class="grid">
<div class="grid">
<article id="leaflet"></article>
</div>
</div>
<div class="grid">
<div>
<article id="capacity"></article>
</div>
</div>
<div class="grid">
<div>
<article id="extractions"></article>
</div>
<div>
<article id="test"></article>
</div>
</div>
</main>
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('main');
});