mmaurostoffel 2025-01-05 13:19:45 +01:00
commit 281d9d3f5a
11 changed files with 267 additions and 99 deletions

View File

@ -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

View File

@ -1,6 +1,7 @@
<?php <?php
namespace App; namespace App;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
class Api class Api
@ -16,10 +17,17 @@ class Api
$endpoint = env('FASTAPI_URI'); $endpoint = env('FASTAPI_URI');
$request = $endpoint.$path; $request = $endpoint.$path;
if (Cache::has($request)) {
return Cache::get($request);
}
$get = Http::get($request); $get = Http::get($request);
if($get->successful()){ if($get->successful()){
return $get->json(); $result = $get->json();
Cache::put($request, $result);
return $result;
} }
return null; return null;
@ -55,6 +63,11 @@ class Api
return self::get("/property/{$id}/base"); return self::get("/property/{$id}/base");
} }
public static function regionCapacities(int $id): mixed
{
return self::get("/region/{$id}/capacities");
}

View File

@ -54,6 +54,45 @@ dd + dt{
margin-top: .2em; margin-top: .2em;
} }
button[popovertarget]{
cursor: pointer;
display: inline-block;
width: 1.5em;
height: 1.5em;
border-radius: 50%;
border: 1px solid #fff;
background: #5470c6;
}
button[popovertarget]::before{
content: '?';
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);
}
/* /*
9. Create a root stacking context 9. Create a root stacking context
*/ */
@ -65,7 +104,6 @@ nav>ul{
list-style: none; list-style: none;
} }
body>header{ body>header{
position: fixed; position: fixed;
top: 0; top: 0;
@ -81,30 +119,53 @@ body>header{
main{ main{
width: 100%; width: 100%;
height: calc(100vh - 4em); height: 100vh;
padding-top: 4em; padding: 4em 0 1em;
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(25%, 50%)); grid-template-columns: repeat(4, minmax(10%, 50%));
grid-template-rows: repeat(2, 1fr); grid-template-rows: repeat(3, 1fr) 4em;
gap: 1em; grid-template-areas:
"chart2 chart2 chart5 chart5"
"chart1 chart1 chart3 chart4"
"chart1 chart1 chart3 chart4"
"timeline timeline timeline timeline";
gap: .5em;
} }
article{ article{
background: #f9f9f9; background: #f9f9f9;
border: .0625em solid #ccc; border: .0625em solid #ccc;
box-shadow: 0 0 .3em #ccc; box-shadow: 0 5px 10px rgba(154,160,185,.05), 0 15px 40px rgba(166,173,201,.2);
border-radius: 3px; border-radius: .2em;
display: grid; display: grid;
} }
article.header{ article.header{
grid-template-columns: 100%; grid-template-columns: 100%;
grid-template-rows: 2em 1fr; grid-template-rows: minmax(1%, 10%) 1fr;
padding: 1em; padding: .5em 1em 1em .5em;
}
article>header{
display: grid;
grid-template-columns: 1fr 1em;
grid-template-rows: 1fr;
} }
article>header>h2{ article>header>h2{
font-size: 1em; font-size: .8em;
font-weight: 600; 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));
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -28,24 +28,29 @@
<div id="chart-heatmap"></div> <div id="chart-heatmap"></div>
</article> </article>
<script type="module"> <script type="module">
const hours = [
'12a', '1a', '2a', '3a', '4a', '5a', '6a',
'7a', '8a', '9a', '10a', '11a',
'12p', '1p', '2p', '3p', '4p', '5p',
'6p', '7p', '8p', '9p', '10p', '11p'
];
const extractionDates = {!! json_encode($growth['dates']) !!}; const sharedOptions = {
basic: {
color: ['#f1eef6','#bdc9e1','#74a9cf','#2b8cbe','#045a8d'],
// prettier-ignore grid: {
let data = []; top: 20,
left: 60,
for(let i = 0; i <= extractionDates.length; i++){ right: 0,
for(let j = 0; j <= hours.length; j++){ bottom: 50
data.push([j,i,Math.random() * 3]); },
name: (opt) => {
return {
name: opt.name,
nameLocation: opt.location,
nameGap: 24,
nameTextStyle: {
fontWeight: 'bold',
},
}
}
} }
} }
const extractionDates = {!! json_encode($growth['dates']) !!};
const chartHeatmap = document.getElementById('chart-heatmap'); const chartHeatmap = document.getElementById('chart-heatmap');
const cHeatmap = echarts.init(chartHeatmap); const cHeatmap = echarts.init(chartHeatmap);
@ -54,16 +59,20 @@ const cHeatmapOptions = {
position: 'top' position: 'top'
}, },
grid: { grid: {
top: 0, top: 25,
right: 0, right: 0,
bottom: 0, bottom: 0,
left: 0 left: 0
}, },
xAxis: { xAxis: {
name: 'Kurzzeitmietobjekt',
type: 'category', type: 'category',
data: hours, data: {!! json_encode($heat['property_ids']) !!},
splitArea: { splitArea: {
show: false show: false
},
axisLabel: {
show: true,
} }
}, },
yAxis: { yAxis: {
@ -74,25 +83,32 @@ const cHeatmapOptions = {
} }
}, },
visualMap: { visualMap: {
type: 'piecewise',
min: 0, min: 0,
max: 10, max: 100,
calculable: true, calculable: true,
orient: 'horizontal', orient: 'horizontal',
left: 'center', left: 'center',
bottom: '15%' top: 0,
inRange: {
color: ['#f1eef6','#bdc9e1','#74a9cf','#2b8cbe','#045a8d']
},
}, },
series: [ series: [
{ {
name: 'Punch Card', name: 'Auslastung',
type: 'heatmap', type: 'heatmap',
data: data, data: {!! json_encode($heat['values']) !!},
label: { label: {
show: false show: false
}, },
tooltip: {
formatter: '{b0}: {c0}<br />{b1}: {c1}',
},
emphasis: { emphasis: {
itemStyle: { itemStyle: {
shadowBlur: 10, borderColor: '#000',
shadowColor: 'rgba(0, 0, 0, 0.5)' borderWidth: 2
} }
} }
} }
@ -104,19 +120,25 @@ cHeatmap.setOption(cHeatmapOptions);
const chartPropsPerRegion = document.getElementById('chart-props-per-region'); const chartPropsPerRegion = document.getElementById('chart-props-per-region');
const cPropsPerRegion = echarts.init(chartPropsPerRegion); const cPropsPerRegion = echarts.init(chartPropsPerRegion);
const cPropsPerRegionOptions = { const cPropsPerRegionOptions = {
grid: { grid: sharedOptions.basic.grid,
top: 20,
left: 30,
right: 0,
bottom: 20
},
xAxis: { xAxis: {
name: 'Region',
nameLocation: 'center',
nameGap: 24,
nameTextStyle: {
fontWeight: 'bold',
},
type: 'category', type: 'category',
data: {!! $propsPerRegion[0] !!} data: {!! $propsPerRegion[0] !!}
}, },
yAxis: { yAxis: {
type: 'value' type: 'value',
name: 'Anzahl Kurzzeitmietobjekte',
nameLocation: 'middle',
nameGap: 38,
nameTextStyle: {
fontWeight: 'bold',
},
}, },
series: [ series: [
{ {
@ -142,18 +164,26 @@ const cExtractionsOptions = {
legend: { legend: {
data: filters.regions data: filters.regions
}, },
grid: { color: sharedOptions.basic.color,
left: '0', grid: sharedOptions.basic.grid,
right: 10,
bottom: '0',
containLabel: true
},
xAxis: { xAxis: {
name: 'Zeitpunkt Scraping',
nameLocation: 'center',
nameGap: 24,
nameTextStyle: {
fontWeight: 'bold',
},
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: extractionDates data: extractionDates
}, },
yAxis: { yAxis: {
name: 'Anzahl Kurzzeitmietobjekte',
nameLocation: 'center',
nameGap: 38,
nameTextStyle: {
fontWeight: 'bold',
},
type: 'value' type: 'value'
}, },
series: [ series: [

View File

@ -1,27 +1,9 @@
@extends('base') @extends('base')
@section('main') @section('main')
<article class="header"> <article style="grid-area: timeline;">
<header> <div id="timeline"></div>
<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>
<article class="header" style="grid-row-start: 1; grid-row-end: 3;"> <article class="header" style="grid-area: chart1;">
<header> <header>
<h2 id="belegung-title"> <h2 id="belegung-title">
Belegung am {{ json_decode($extractiondates)[0] }} Belegung am {{ json_decode($extractiondates)[0] }}
@ -29,7 +11,16 @@
</header> </header>
<div id="chart-calendar"></div> <div id="chart-calendar"></div>
</article> </article>
<article class="header"> <article class="header" style="grid-area: chart3;">
<header>
<h2>
Auslastung nach Monat am 2024-04-15T07:06:22
</h2>
</header>
<div id="chart-capacity-monthly">
</div>
</article>
<article class="header" style="grid-area: chart2;">
<header> <header>
<h2> <h2>
Entwicklung der Verfügbarkeit Entwicklung der Verfügbarkeit
@ -37,7 +28,72 @@
</header> </header>
<div id="chart-capacity"></div> <div id="chart-capacity"></div>
</article> </article>
<article class="header" style="grid-area: chart4;">
<header>
<h2>
Auslastung Tage für Monat
</h2>
</header>
</article>
<script type="module"> <script type="module">
const chartTimeline = document.getElementById('timeline');
const cTimeline = echarts.init(chartTimeline);
const cTimelineOptions = {
grid: {
show: false,
},
timeline: {
data: {!! $extractiondates !!},
playInterval: 2000,
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 = {
timeline: {
show: false,
data: {!! $extractiondates !!},
axisType: 'time',
},
grid: {
top: 0,
bottom: 25,
left: 50,
right: 10
},
xAxis: {
type: 'value',
max: 100
},
yAxis: {
type: 'category',
},
options: [{
yAxis: {
data: ['Apr 24', 'May 24', 'Jun 24', 'Aug 24', 'Sep 24', 'Oct 24', 'Nov 24', 'Dec 24', 'Jan 25', 'Feb 25', 'Mar 25', 'Apr 25', 'May 25', 'Jun 25', 'Jul 25', 'Aug 25', 'Sep 25', 'Oct 25']
},
series: [{
type: 'bar',
data: [100, 80, 20, 20, 3, 23, 34, 23, 45, 12, 34,23,45,12,34,45,12,34]
}]
}]
};
cCapacityMonthly.setOption(cCapacityMonthlyOptions);
const chartCapacity = document.getElementById('chart-capacity'); const chartCapacity = document.getElementById('chart-capacity');
const cCapacity = echarts.init(chartCapacity); const cCapacity = echarts.init(chartCapacity);
@ -46,9 +102,10 @@ const cCapacityOptions = {
trigger: 'axis' trigger: 'axis'
}, },
grid: { grid: {
left: '0', top: 20,
left: 0,
right: 10, right: 10,
bottom: '0', bottom: 0,
containLabel: true containLabel: true
}, },
xAxis: { xAxis: {
@ -72,26 +129,20 @@ const cCapacityOptions = {
cCapacity.setOption(cCapacityOptions); cCapacity.setOption(cCapacityOptions);
const chartCalendar = document.getElementById('chart-calendar'); const chartCalendar = document.getElementById('chart-calendar');
const cCalendar = echarts.init(chartCalendar); const cCalendar = echarts.init(chartCalendar);
const h2Belegung = document.getElementById('belegung-title'); const h2Belegung = document.getElementById('belegung-title');
const cCalendarOptions = { const cCalendarOptions = {
timeline: { timeline: {
show: false,
data: {!! $extractiondates !!}, data: {!! $extractiondates !!},
playInterval: 2000,
axisType: 'time', axisType: 'time',
left: 0,
right: 0,
label: {
show: false
}
}, },
visualMap: { visualMap: {
categories: [0,1,2], categories: [0,1,2],
inRange: { inRange: {
color: ['red', 'purple', 'green'] color: ['#d95f02', '#7570b3', '#1b9e77']
}, },
formatter: (cat) => { formatter: (cat) => {
switch (cat) { switch (cat) {
@ -112,25 +163,25 @@ const cCalendarOptions = {
{ {
orient: 'horizontal', orient: 'horizontal',
range: '2024', range: '2024',
top: '6.25%', top: '15%',
right: 10, right: 10,
bottom: '68.75%', bottom: '65%',
left: 50, left: 50,
}, },
{ {
orient: 'horizontal', orient: 'horizontal',
range: '2025', range: '2025',
top: '37.5%', top: '47%',
right: 10, right: 10,
bottom: '37.25%', bottom: '33%',
left: 50, left: 50,
}, },
{ {
orient: 'horizontal', orient: 'horizontal',
range: '2026', range: '2026',
top: '68.75%', top: '79%',
right: 10, right: 10,
bottom: '6.25%', bottom: '1%',
left: 50, left: 50,
} }
], ],
@ -161,13 +212,26 @@ const cCalendarOptions = {
}; };
cCalendar.setOption(cCalendarOptions); cCalendar.setOption(cCalendarOptions);
cCalendar.on('timelinechanged', (e) => {
// Change header text according to datapoint
cTimeline.on('timelinechanged', (e) => {
h2Belegung.innerText = "Belegung am "+cCalendarOptions.timeline.data[e.currentIndex]; h2Belegung.innerText = "Belegung am "+cCalendarOptions.timeline.data[e.currentIndex];
// Set markpoint on linechart // Set markpoint on linechart
let x = cCapacityOptions.xAxis.data[e.currentIndex]; let x = cCapacityOptions.xAxis.data[e.currentIndex];
let y = cCapacityOptions.series.data[e.currentIndex]; let y = cCapacityOptions.series.data[e.currentIndex];
cCapacityMonthly.dispatchAction({
type: 'timelineChange',
currentIndex: e.currentIndex
});
cCalendar.dispatchAction({
type: 'timelineChange',
currentIndex: e.currentIndex
});
cCapacity.setOption({ cCapacity.setOption({
series: { series: {
markPoint: { markPoint: {
@ -177,16 +241,18 @@ cCalendar.on('timelinechanged', (e) => {
} }
} }
}); });
}) })
cCapacity.on('click', 'series', (e) => { cCapacity.on('click', 'series', (e) => {
// Switch to correct calendar in the timeline // Switch to correct calendar in the timeline
cCalendar.dispatchAction({ cTimeline.dispatchAction({
type: 'timelineChange', type: 'timelineChange',
currentIndex: e.dataIndex currentIndex: e.dataIndex
}); });
}); });

View File

@ -5,12 +5,14 @@ use App\Api;
Route::get('/', function () { Route::get('/', function () {
$regionHeat = Api::regionCapacities(1);
$propertiesGrowth = Api::propertiesGrowth(); $propertiesGrowth = Api::propertiesGrowth();
$propsPerRegion = Api::propertiesPerRegion(); $propsPerRegion = Api::propertiesPerRegion();
$propsPerRegionName = []; $propsPerRegionName = [];
$propsPerRegionCounts = []; $propsPerRegionCounts = [];
// dump($regionHeat);
foreach ($propsPerRegion as $el) { foreach ($propsPerRegion as $el) {
$propsPerRegionName[] = $el['name']; $propsPerRegionName[] = $el['name'];
$propsPerRegionCounts[] = $el['count_properties']; $propsPerRegionCounts[] = $el['count_properties'];
@ -18,7 +20,7 @@ Route::get('/', function () {
$propertiesGeo = Api::propertiesGeo(); $propertiesGeo = Api::propertiesGeo();
return view('overview', ["geo" => $propertiesGeo, "growth" => $propertiesGrowth, "propsPerRegion" => [json_encode($propsPerRegionName), json_encode($propsPerRegionCounts)]]); return view('overview', ["heat" => $regionHeat, "geo" => $propertiesGeo, "growth" => $propertiesGrowth, "propsPerRegion" => [json_encode($propsPerRegionName), json_encode($propsPerRegionCounts)]]);
}); });
Route::get('/prop/{id}', function (int $id) { Route::get('/prop/{id}', function (int $id) {

View File

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