From ebcd647a2f7f078d273948fd15523180630f70d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gi=C3=B2=20Diani?= Date: Mon, 13 Jan 2025 17:06:54 +0100 Subject: [PATCH] Einige Anpassungen am Dashboard. Region Ansicht. --- dashboard/app/Api.php | 7 +- dashboard/resources/css/app.css | 52 +++++++-- dashboard/resources/views/overview.blade.php | 59 +++++++---- dashboard/resources/views/property.blade.php | 37 ++++++- dashboard/resources/views/region.blade.php | 93 ++++++++++++++-- dashboard/routes/web.php | 9 +- .../diagrams/Systemarchitektur_C4.drawio | 100 ++++++++++++++---- etl/src/api/main.py | 3 +- etl/src/data/database.py | 13 ++- 9 files changed, 306 insertions(+), 67 deletions(-) diff --git a/dashboard/app/Api.php b/dashboard/app/Api.php index 5d0033e..02d714b 100644 --- a/dashboard/app/Api.php +++ b/dashboard/app/Api.php @@ -23,7 +23,7 @@ class Api return Cache::get($request); } - $get = Http::timeout(800)->get($request); + $get = Http::timeout(1600)->get($request); if($get->successful()){ $result = $get->json(); @@ -94,6 +94,11 @@ class Api return self::get("/region/{$id}/capacities"); } + public static function regionMovingAverage(int $id, string $date): mixed + { + return self::get("/region/{$id}/movingAverage/{$date}"); + } + } diff --git a/dashboard/resources/css/app.css b/dashboard/resources/css/app.css index dcec28e..390f76f 100644 --- a/dashboard/resources/css/app.css +++ b/dashboard/resources/css/app.css @@ -178,10 +178,10 @@ body.property main{ grid-template-columns: repeat(4, minmax(10%, 50%)); grid-template-rows: repeat(3, 1fr) 4em; grid-template-areas: - "chart2 chart2 chart5 chart5" - "chart1 chart1 chart3 chart4" - "chart1 chart1 chart3 chart4" - "timeline timeline timeline timeline"; + "chart2 chart2 chart1 chart1" + "chart5 chart5 chart1 chart1" + "chart5 chart5 chart3 chart4" + "chart5 chart5 timeline timeline"; } body.region main{ @@ -190,8 +190,8 @@ body.region main{ grid-template-areas: "chart1 chart1 chart2 chart2" "chart1 chart1 chart3 chart4" - "chart1 chart1 chart3 chart4" - "chart1 chart1 timeline timeline"; + "chart6 chart6 chart3 chart4" + "chart6 chart6 timeline timeline"; } article{ @@ -204,10 +204,18 @@ article{ article.header{ grid-template-columns: 100%; - grid-template-rows: minmax(1%, 10%) 1fr; + grid-template-rows: minmax(1%, 2.5em) 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; @@ -231,3 +239,33 @@ article>header>h2{ grid-template-rows: repeat(4, minmax(20em, 25em)); } } + +.leaflet-marker-icon span{ + background: blue; + 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: #9ecae1; +} + +.leaflet-marker-icon.region2 span{ + background: #6baed6; +} + +.leaflet-marker-icon.region3 span{ + background: #4292c6; +} + +.leaflet-marker-icon.region4 span{ + background: #2171b5; +} diff --git a/dashboard/resources/views/overview.blade.php b/dashboard/resources/views/overview.blade.php index 476288c..292f68b 100644 --- a/dashboard/resources/views/overview.blade.php +++ b/dashboard/resources/views/overview.blade.php @@ -49,7 +49,7 @@ const sharedOptions = { basic: { - color: ['#f1eef6','#bdc9e1','#74a9cf','#2b8cbe','#045a8d'], + color: ['#9ecae1','#6baed6','#4292c6','#2171b5','#08519c','#08306b'], grid: { top: 20, left: 60, @@ -156,6 +156,7 @@ 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', @@ -178,8 +179,13 @@ const cPropsPerRegionOptions = { series: [ { data: {!! $propsPerRegion[1] !!}, - type: 'bar' - } + type: 'bar', + itemStyle: { + color: (e) => { + return sharedOptions.basic.color[e.dataIndex]; + } + } + }, ] }; @@ -193,13 +199,13 @@ const filters = { } const cExtractionsOptions = { + color: sharedOptions.basic.color, tooltip: { trigger: 'axis' }, legend: { data: filters.regions }, - color: sharedOptions.basic.color, grid: sharedOptions.basic.grid, xAxis: { name: 'Zeitpunkt Scraping', @@ -226,30 +232,26 @@ const cExtractionsOptions = { name: 'Alle', type: 'line', stack: 'Total', - data: {!! json_encode($growth['total_all']) !!} - }, - { - name: 'Heidiland', - type: 'line', - stack: 'Heidiland', - data: {!! json_encode($growth['total_heidiland']) !!} + data: {!! json_encode($growth['total_all']) !!}, }, { 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: 'Heidiland', + type: 'line', + data: {!! json_encode($growth['total_heidiland']) !!} + }, { name: 'St. Moritz', type: 'line', - stack: 'St. Moritz', data: {!! json_encode($growth['total_stmoritz']) !!} }, ] @@ -257,19 +259,38 @@ const cExtractionsOptions = { cExtractions.setOption(cExtractionsOptions); -const map = L.map('leaflet').setView([46.862962, 9.535296], 9); +const map = L.map('leaflet'); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map); -const properties = {!! json_encode($geo) !!} -properties.forEach( prop => { - let coords = prop.coordinates.split(','); - L.marker(coords).addTo(map).bindPopup(''+prop.coordinates+''); +function icon(id){ + return L.divIcon({ + className: "region"+id, + html: '' + }) +} + +const markers = L.featureGroup([ + @foreach($geo as $g) + L.marker([{{ $g['latlng'] }}], {icon: icon({{ $g['region_id'] }})}).bindPopup('{{ $g['latlng'] }}'), + @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'); +}) + + @endsection diff --git a/dashboard/resources/views/property.blade.php b/dashboard/resources/views/property.blade.php index a33b7fb..da91f28 100644 --- a/dashboard/resources/views/property.blade.php +++ b/dashboard/resources/views/property.blade.php @@ -32,6 +32,14 @@
+
+
+

+ Kurzzeitmietobjekte in der Nähe +

+
+
+

@@ -80,7 +88,7 @@ const cTimelineOptions = { label: { show: false } - } + }, }; cTimeline.setOption(cTimelineOptions); @@ -168,6 +176,7 @@ const chartCapacity = document.getElementById('chart-capacity'); const cCapacity = echarts.init(chartCapacity); const cCapacityOptions = { + color: ['#9ecae1','#6baed6','#4292c6','#2171b5','#084594'], legend: { data: ['Auslastung Property', 'Auslastung {{ $base['region_name'] }}', 'Auslastung alle Regionen'] }, @@ -176,7 +185,7 @@ const cCapacityOptions = { valueFormatter: (value) => value.toFixed(2)+' %' }, grid: { - top: 20, + top: 40, left: 25, right: 10, bottom: 20, @@ -348,6 +357,29 @@ cTimeline.on('timelinechanged', (e) => { }) +/* Map w/ neighbours*/ +const map = L.map('chart-map'); + +L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 19, + attribution: '© OpenStreetMap' +}).addTo(map); + +function icon(id = 0){ + return L.divIcon({ + className: "region"+id, + html: '' + }) +} + +const markers = L.featureGroup([ + @foreach($neighbours as $n) + L.marker([{{ $n['lat'] }}, {{ $n['lon'] }}], {icon: icon()}).bindPopup('{{ $n['lat'] }}, {{ $n['lon'] }}'), + @endforeach +]).addTo(map); + +map.fitBounds(markers.getBounds(), {padding: [20,20]}) + cCapacity.on('click', 'series', (e) => { // Switch to correct calendar in the timeline @@ -356,7 +388,6 @@ cCapacity.on('click', 'series', (e) => { currentIndex: e.dataIndex }); - }); diff --git a/dashboard/resources/views/region.blade.php b/dashboard/resources/views/region.blade.php index 23cbf66..5a16c6d 100644 --- a/dashboard/resources/views/region.blade.php +++ b/dashboard/resources/views/region.blade.php @@ -17,9 +17,15 @@
+
+
+

Auslastung Vorhersage

+
+
+
-

+

Gesamtauslastung

@@ -58,7 +64,7 @@ const sharedOptions = { basic: { - color: ['#f1eef6','#bdc9e1','#74a9cf','#2b8cbe','#045a8d'], + color: ['#9ecae1','#6baed6','#4292c6','#2171b5','#084594'], grid: { top: 20, left: 60, @@ -134,22 +140,90 @@ const cCapacityOptions = { cCapacity.setOption(cCapacityOptions); +const chartPrediction = document.getElementById('chart-prediction'); +const cPrediction = echarts.init(chartPrediction); + +const cPredictionOptions = { + legend: { + data: ['Moving Average', 'Earlier', 'Later'] + }, + tooltip: { + trigger: 'axis', + valueFormatter: (value) => value.toFixed(2)+' %' + }, + grid: { + top: 20, + left: 25, + right: 10, + bottom: 20, + containLabel: true + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: {!! json_encode($prediction['dates']) !!}, + name: 'Zeitpunkt Scraping', + nameLocation: 'center', + nameGap: 24, + nameTextStyle: { + fontWeight: 'bold', + } + }, + yAxis: { + type: 'value', + min: 0, + max: 1, + name: 'Auslastung in Prozent', + nameLocation: 'center', + nameGap: 38, + nameTextStyle: { + fontWeight: 'bold', + } + }, + series: [{ + name: 'Moving Average', + type: 'line', + symbolSize: 7, + data: {!! json_encode($prediction['movAvg']) !!} + }, + { + name: 'Earlier', + type: 'line', + symbolSize: 7, + data: {!! json_encode($prediction['cap_earlierTimeframe']) !!} + }, + { + name: 'Later', + type: 'line', + symbolSize: 7, + data: {!! json_encode($prediction['cap_laterTimeframe']) !!} + }] +}; + +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: 0, - bottom: 0, - left: 0 + right: 45, + bottom: 50, + left: 5 }, dataZoom: [{ - type: 'inside' - } - ], + type: 'slider' + }, + { + type: 'slider', + show: true, + yAxisIndex: 0, + }], xAxis: { show: false, name: 'Kurzzeitmietobjekt', @@ -196,8 +270,7 @@ const cHeatmapOptions = { }, tooltip: { formatter: (data) => { - let v = data.value - return `Kurzzeitmietobjekte-ID: ${data.name}
Datum Scraping: ${extractionDates[v[1]]}
Auslastung: ${v[2]} %` + return `Kurzzeitmietobjekte-ID: ${data.data[1]}
Datum Scraping: ${data.data[0]}
Auslastung: ${data.data[2].toFixed(2)} %` }, }, emphasis: { diff --git a/dashboard/routes/web.php b/dashboard/routes/web.php index f7edc0a..f266f5e 100644 --- a/dashboard/routes/web.php +++ b/dashboard/routes/web.php @@ -12,18 +12,20 @@ Route::get('/', function () { $propsPerRegion = Api::propertiesPerRegion(); $propsPerRegionName = []; $propsPerRegionCounts = []; + $propsPerRegionId = []; foreach ($propsPerRegion as $el) { $propsPerRegionName[] = $el['name']; + $propsPerRegionId[] = $el['id']; $propsPerRegionCounts[] = $el['count_properties']; } $propertiesGeo = Api::propertiesGeo(); - return view('overview', ["regions" => $regionBase, "regionPropertiesCapacities" => $regionPropertyCapacities, "geo" => $propertiesGeo, "growth" => $propertiesGrowth, "propsPerRegion" => [json_encode($propsPerRegionName), json_encode($propsPerRegionCounts)]]); + return view('overview', ["regions" => $regionBase, "regionPropertiesCapacities" => $regionPropertyCapacities, "geo" => $propertiesGeo, "growth" => $propertiesGrowth, "propsPerRegion" => [json_encode($propsPerRegionId), json_encode($propsPerRegionName), json_encode($propsPerRegionCounts)]]); }); -Route::get('/prop/{id}', function (int $id) { +Route::get('/property/{id}', function (int $id) { $propertyBase = Api::propertyBase($id); $calendars = Api::propertyExtractions($id); @@ -90,13 +92,14 @@ 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']]; + $regionMovingAverage = Api::regionMovingAverage($id, '2024-04-25'); $regionPropertiesCapacities = Api::regionPropertiesCapacities($id); $regionCapacitiesRegion = Api::regionCapacities($id); $regionCapacitiesAll = Api::regionCapacities(-1); $regionCapacities = [$regionCapacitiesAll, $regionCapacitiesRegion]; - return view('region', ['regions' => $regionBaseAll, 'region' => $regionBaseRegion, 'region_id' => $id, 'regionCapacities' => $regionCapacities, 'regionPropertiesCapacities' => $regionPropertiesCapacities]); + return view('region', ['regions' => $regionBaseAll, 'region' => $regionBaseRegion, 'region_id' => $id, 'regionCapacities' => $regionCapacities, 'regionPropertiesCapacities' => $regionPropertiesCapacities, 'prediction' => $regionMovingAverage]); }); diff --git a/documentation/diagrams/Systemarchitektur_C4.drawio b/documentation/diagrams/Systemarchitektur_C4.drawio index 1a22463..e281f70 100644 --- a/documentation/diagrams/Systemarchitektur_C4.drawio +++ b/documentation/diagrams/Systemarchitektur_C4.drawio @@ -1,30 +1,92 @@ - - - + + + - - - - - - - - - - - - - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/etl/src/api/main.py b/etl/src/api/main.py index 21b4307..ac43f05 100644 --- a/etl/src/api/main.py +++ b/etl/src/api/main.py @@ -5,9 +5,8 @@ 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_movAverage as etl_rmA -from data import etl_region_properties_capacities as etl_rpc from data import etl_region_capacities_comparison as etl_rcc +from data import etl_region_movAverage as etl_rmA from data import etl_region_properties_capacities as etl_rpc from fastapi import FastAPI, Response diff --git a/etl/src/data/database.py b/etl/src/data/database.py index 2d937b2..b73d81d 100644 --- a/etl/src/data/database.py +++ b/etl/src/data/database.py @@ -137,6 +137,7 @@ class Database: return self.connection.sql(""" SELECT regions.name, + regions.id, COUNT(*) AS count_properties FROM consultancy_d.properties @@ -146,7 +147,8 @@ class Database: consultancy_d.regions ON regions.id = seeds.region_id GROUP BY properties.seed_id, - regions.name + regions.name, + regions.id ORDER BY count_properties ASC """) @@ -427,10 +429,15 @@ class Database: def properties_geo(self): return self.connection.sql(""" SELECT - p.id, - p.check_data as coordinates + 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):