mmaurostoffel 2024-12-19 18:11:17 +01:00
commit 4b7067fb63
20 changed files with 562 additions and 744 deletions

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# 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

@ -64,3 +64,5 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"
FASTAPI_URI=http://localhost:8080

View File

@ -1,66 +1,8 @@
<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>
# How to run
```bash
composer run dev
```
<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).
```bash
npm run build
```

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

@ -0,0 +1,57 @@
<?php
namespace App;
use Illuminate\Support\Facades\Http;
class Api
{
public function __construct()
{
}
public static function get(string $path, string $query = ''): ?array
{
$endpoint = env('FASTAPI_URI');
$request = $endpoint.$path;
$get = Http::get($request);
if($get->successful()){
return $get->json();
}
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 propertyBase(int $id): mixed
{
return self::get("/property/{$id}/base");
}
}

View File

@ -46,6 +46,14 @@ h1, h2, h3, h4, h5, h6 {
text-wrap: balance;
}
dt{
font-weight: 600;
}
dd + dt{
margin-top: .2em;
}
/*
9. Create a root stacking context
*/
@ -53,6 +61,10 @@ h1, h2, h3, h4, h5, h6 {
isolation: isolate;
}
nav>ul{
list-style: none;
}
body>header{
position: fixed;

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,25 @@
<!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>
<header>
<span>Dashboard</span>
<nav>
<ul>
<li>
<a href="/">Start</a>
</li>
</ul>
</nav>
</header>
<main>
@yield('main')
</main>
</body>
</html>

View File

@ -1,45 +0,0 @@
<!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>
<header>
<span>Dashboard</span>
</header>
<main>
<article class="header">
<header>
<h2>
Headline
</h2>
</header>
<p>Lorem Ipsum...</p>
</article>
<article class="header">
<header>
<h2>
Anzahl Properties p. Extractions
</h2>
</header>
<div id="extractions"></div>
</article>
<article class="header">
<header>
<h2>
Anzahl Properties p. Extractions
</h2>
</header>
<div id="capacity"></div>
</article>
<article>
<div id="leaflet"></div>
</article>
</main>
</body>
</html>

View File

@ -0,0 +1,131 @@
@extends('base')
@section('main')
<article class="header">
<header>
<h2>
Anzahl jemals gefundene Kurzzeitmietobjekte pro Region
</h2>
</header>
<div id="chart-props-per-region"></div>
</article>
<article class="header">
<header>
<h2>
Entwicklung der Anzahl jemals gefunden Kurzzeitmietobjekte
</h2>
</header>
<div id="extractions"></div>
</article>
<article>
<div id="leaflet"></div>
</article>
<script type="module">
const chartPropsPerRegion = document.getElementById('chart-props-per-region');
const cPropsPerRegion = echarts.init(chartPropsPerRegion);
const cPropsPerRegionOptions = {
grid: {
top: 20,
left: 30,
right: 0,
bottom: 20
},
xAxis: {
type: 'category',
data: {!! $propsPerRegion[0] !!}
},
yAxis: {
type: 'value'
},
series: [
{
data: {!! $propsPerRegion[1] !!},
type: 'bar'
}
]
};
cPropsPerRegion.setOption(cPropsPerRegionOptions);
const chartExtractions = document.getElementById('extractions');
const cExtractions = echarts.init(chartExtractions);
const filters = {
regions: ["Alle", "Davos", "Engadin", "Heidiland", "St. Moritz"]
}
const cExtractionsOptions = {
tooltip: {
trigger: 'axis'
},
legend: {
data: filters.regions
},
grid: {
left: '0',
right: 10,
bottom: '0',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: {!! json_encode($growth['dates']) !!}
},
yAxis: {
type: 'value'
},
series: [
{
name: 'Alle',
type: 'line',
stack: 'Total',
data: {!! json_encode($growth['total_all']) !!}
},
{
name: 'Heidiland',
type: 'line',
stack: 'Heidiland',
data: {!! json_encode($growth['total_heidiland']) !!}
},
{
name: 'Davos',
type: 'line',
stack: 'Davos',
data: {!! json_encode($growth['total_davos']) !!}
},
{
name: 'Engadin',
type: 'line',
stack: 'Engadin',
data: {!! json_encode($growth['total_engadin']) !!}
},
{
name: 'St. Moritz',
type: 'line',
stack: 'St. Moritz',
data: {!! json_encode($growth['total_stmoritz']) !!}
},
]
};
cExtractions.setOption(cExtractionsOptions);
const map = L.map('leaflet').setView([46.862962, 9.535296], 9);
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);
const properties = {!! json_encode($geo) !!}
properties.forEach( prop => {
console.log(prop);
let coords = prop.coordinates.split(',');
L.marker(coords).addTo(map).bindPopup('<a href="/prop/'+prop.id+'">'+prop.coordinates+'</a>');
})
</script>
@endsection

View File

@ -0,0 +1,113 @@
@extends('base')
@section('main')
<article class="header">
<header>
<h2>
Angaben zur Property
</h2>
</header>
<dl>
@foreach ($base as $dt => $dd)
<dt>{{ $dt }}</dt>
<dd>
@if ($dt === 'property_platform_id')
<a href="https://www.e-domizil.ch/rental/{{ $dd }}" rel="noopener noreferrer" target="_blank">
{{ $dd }}
</a>
@else
{{ $dd }}
@endif
</dd>
@endforeach
</dl>
</article>
<article class="header">
<header>
<h2 id="belegung-title">
Belegung am {{ json_decode($extractiondates)[0] }}
</h2>
</header>
<div id="chart-calendar"></div>
</article>
<script type="module">
const chartCalendar = document.getElementById('chart-calendar');
const cCalendar = echarts.init(chartCalendar);
const h2Belegung = document.getElementById('belegung-title');
const cCalendarOptions = {
timeline: {
data: {!! $extractiondates !!},
playInterval: 1000,
axisType: 'time',
left: 0,
right: 0,
label: {
show: false
}
},
visualMap: {
categories: [0,1,2],
inRange: {
color: ['red', 'purple', 'green']
},
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: 50,
right: 0,
left: 50,
bottom: "55%"
},
{
orient: 'horizontal',
range: '2025',
right: 100,
left: 50,
bottom: 60,
top: '55%'
},
],
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) !!}
}]
},
@endforeach
]
};
cCalendar.setOption(cCalendarOptions);
cCalendar.on('timelinechanged', (e) => {
h2Belegung.innerText = "Belegung am "+cCalendarOptions.timeline.data[e.currentIndex];
})
</script>
@endsection

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,50 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Api;
Route::get('/', function () {
return view('main');
$propertiesGrowth = Api::propertiesGrowth();
$propsPerRegion = Api::propertiesPerRegion();
$propsPerRegionName = [];
$propsPerRegionCounts = [];
foreach ($propsPerRegion as $el) {
$propsPerRegionName[] = $el['name'];
$propsPerRegionCounts[] = $el['count_properties'];
}
$propertiesGeo = Api::propertiesGeo();
//dump($propertiesGeo);
return view('overview', ["geo" => $propertiesGeo, "growth" => $propertiesGrowth, "propsPerRegion" => [json_encode($propsPerRegionName), json_encode($propsPerRegionCounts)]]);
});
Route::get('/prop/{id}', function (int $id) {
$propertyBase = Api::propertyBase($id);
$extractions = Api::propertyExtractions($id);
$data = [];
$dates = [];
foreach ($extractions as $ext) {
$series = [];
$dates[] = $ext['created_at'];
$extCalendar = json_decode($ext['calendar'], 1);
foreach ($extCalendar as $date => $status) {
$series[] = [$date, $status];
}
$data[] = $series;
}
return view('property', ['base' => $propertyBase[0], "extractiondates" => json_encode($dates), "calendar" => $data]);
});

4
etl/README.md Normal file
View File

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

4
etl/pixi.lock generated
View File

@ -2200,9 +2200,7 @@ packages:
name: consultancy-2
version: 0.1.0
path: .
sha256: c5e1a7be44a8bc92a9119ccc4b7e7e7b3db765694b975b5cd30b288638254471
requires_dist:
- polars
sha256: 878bb6af1502cc9ac71feab6f184f593077f134626d6f8c552e9bcafb178f6b4
requires_python: '>=3.11'
editable: true
- kind: conda

View File

@ -1,7 +1,6 @@
[project]
authors = [{name = "Giò Diani", email = "mail@gionathandiani.name"}]
dependencies = ["polars"]
description = "Add a short description here"
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"}]
description = "Datenauferbeitung"
name = "consultancy_2"
requires-python = ">= 3.11"
version = "0.1.0"

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

@ -0,0 +1,41 @@
import data
import polars as pl
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}/extractions")
def property_extractions(id: int):
return d.extractions_for(property_id = id).pl().to_dicts()
@app.get("/property/{id}/base")
def property_base_data(id: int):
return d.property_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

@ -46,13 +46,93 @@ class Database:
def properties_growth(self):
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
strftime(created_at, '%Y-%m-%d') AS date,
COUNT(*) as properties_count
p.date,
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
consultancy_d.properties
GROUP BY
date;
PropertiesAll p
LEFT JOIN
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):
@ -69,6 +149,8 @@ class Database:
GROUP BY
properties.seed_id,
regions.name
ORDER BY
count_properties ASC
""")
def propIds_with_region(self):
@ -208,7 +290,7 @@ class Database:
""")
def extractions(self):
return self.connection.sql(f"""
return self.connection.sql("""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendar,
property_id,
@ -243,15 +325,15 @@ class Database:
return self.connection.sql(f"""
SELECT
JSON_EXTRACT(body, '$.content.days') as calendar,
property_id,
created_at
FROM
consultancy_d.extractions
WHERE
type == 'calendar' AND
property_id = {property_id}
property_id = {property_id} AND
calendar NOT NULL
ORDER BY
property_id
created_at
""")
# Anzahl der extrahierten properties pro Exktraktionsvorgang
@ -297,3 +379,29 @@ class Database:
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,
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 properties_geo(self):
return self.connection.sql("""
SELECT
p.id,
p.check_data as coordinates
FROM
consultancy_d.properties p
""")

File diff suppressed because one or more lines are too long

View File

@ -1,16 +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_cal"),
)
out = out.drop(['calendar', 'property_id'])
print(out.to_dict(as_series=True))