refactor rent-duration to component

This commit is contained in:
tim-herbst 2021-01-02 14:48:58 +01:00
parent cdbf36fadb
commit 6d7c40ada6
7 changed files with 228 additions and 375 deletions

View File

@ -77,117 +77,12 @@
</mat-card> </mat-card>
</div> </div>
<div class="container-table" fxFlex fxLayout="row" fxLayoutAlign="center"> <div class="container-table" fxFlex fxLayout="row" fxLayoutAlign="space-evenly center">
<div class="dashboard-table-to" fxFlex> <app-table fxFlex></app-table>
<table [dataSource]="stationToSource" class="mat-elevation-z8" fxFill mat-table>
<ng-container matColumnDef="select">
<th *matHeaderCellDef mat-header-cell></th>
<td *matCellDef="let row" mat-cell>
<mat-checkbox [disabled]="isCheckBoxDisable(row)"
(change)="$event ? selectRow($event, row) : null" (click)="$event.stopPropagation()"
[checked]="selectionModel.isSelected(row)"
matTooltip="toggle to view marker on map"
matTooltipPosition="above">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="endStationName">
<th *matHeaderCellDef mat-header-cell> station of rental destination</th>
<td *matCellDef="let element" mat-cell><a
[routerLink]="['/dashboard/', element.stationId]">{{element.stationName}}</a></td>
</ng-container>
<ng-container matColumnDef="number">
<th *matHeaderCellDef mat-header-cell> number of drives</th>
<td *matCellDef="let element" mat-cell> {{element.number}} </td>
</ng-container>
<ng-container matColumnDef="avgDuration">
<th *matHeaderCellDef mat-header-cell> average rental duration</th>
<td *matCellDef="let element" mat-cell> {{humanizeAvgDuration(element.avgDuration)}} </td>
</ng-container>
<ng-container matColumnDef="marker">
<th *matHeaderCellDef mat-header-cell> icon on map</th>
<td *matCellDef="let element" mat-cell><img [src]="drawIconInTable(element)" alt="marker"></td>
</ng-container>
<tr *matHeaderRowDef="displayedColumnsTo" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumnsTo;" mat-row></tr>
</table>
</div>
<div class="dashboard-table-from" fxFlex>
<table [dataSource]="stationFromSource" class="mat-elevation-z9" fxFill mat-table>
<ng-container matColumnDef="select">
<th *matHeaderCellDef mat-header-cell></th>
<td *matCellDef="let row" mat-cell>
<mat-checkbox [disabled]="isCheckBoxDisable(row)"
(change)="$event ? selectRow($event, row) : null" (click)="$event.stopPropagation()"
[checked]="selectionModel.isSelected(row)"
matTooltip="toggle to view marker on map"
matTooltipPosition="above">
</mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="startStationName">
<th *matHeaderCellDef mat-header-cell> station of rental origin</th>
<td *matCellDef="let element" mat-cell><a
[routerLink]="['/dashboard/', element.stationId]"> {{element.stationName}}</a></td>
</ng-container>
<ng-container matColumnDef="number">
<th *matHeaderCellDef mat-header-cell> number of drives</th>
<td *matCellDef="let element" mat-cell> {{element.number}} </td>
</ng-container>
<ng-container matColumnDef="avgDuration">
<th *matHeaderCellDef mat-header-cell> average rental duration</th>
<td *matCellDef="let element" mat-cell> {{humanizeAvgDuration(element.avgDuration)}} </td>
</ng-container>
<ng-container matColumnDef="marker">
<th *matHeaderCellDef mat-header-cell> icon on map</th>
<td *matCellDef="let element" mat-cell><img [src]="drawIconInTable(element)" alt="marker"></td>
</ng-container>
<tr *matHeaderRowDef="displayedColumnsFrom" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumnsFrom;" mat-row></tr>
</table>
<mat-card *ngIf="isLoading" style="display: flex; justify-content: center; align-items: center">
<mat-progress-spinner
color="primary"
mode="indeterminate">
</mat-progress-spinner>
</mat-card>
</div>
</div> </div>
<div class="container-borrow-duration" fxLayout="row" fxLayoutAlign="center"> <div class="container-borrow-duration" fxLayout="row" fxLayoutAlign="center">
<mat-card fxFlex fxLayout="column"> <app-rent-duration-chart fxFlex></app-rent-duration-chart>
<mat-card-header>
<mat-card-title>Rental Duration</mat-card-title>
<mat-card-subtitle>
This chart shows the rent duration based on the currently selected station.
The time it takes for a rent which has the current station as origin is displayed here.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content fxFlex fxLayout="row" fxLayoutAlign="center">
<div class="station-dashboard-borrow-duration" fxFlex="97%">
<apx-chart
[chart]="durationChartOptions.chart"
[colors]="durationChartOptions.colors"
[dataLabels]="durationChartOptions.dataLabels"
[fill]="durationChartOptions.fill"
[legend]="durationChartOptions.legend"
[plotOptions]="durationChartOptions.plotOptions"
[series]="durationChartOptions.series"
[stroke]="durationChartOptions.stroke"
[xaxis]="durationChartOptions.xaxis"
[yaxis]="durationChartOptions.yaxis"></apx-chart>
</div>
</mat-card-content>
</mat-card>
</div> </div>
<div class="container-borrow-time" fxLayout="row" fxLayoutAlign="center"> <div class="container-borrow-time" fxLayout="row" fxLayoutAlign="center">

View File

@ -6,14 +6,6 @@ mat-sidenav-content {
flex: 1 1 auto; flex: 1 1 auto;
} }
a {
color: black;
}
img {
width: 60px;
}
.button-back:hover, .button-wiki:hover { .button-back:hover, .button-wiki:hover {
background: #086ed2; background: #086ed2;
} }
@ -53,7 +45,6 @@ img {
margin-left: 39px; margin-left: 39px;
} }
.container-map { .container-map {
height: 40em; height: 40em;
margin: 1em 2em; margin: 1em 2em;
@ -64,14 +55,6 @@ img {
margin: 1em 3em; margin: 1em 3em;
} }
.dashboard-table-to {
margin-right: 1em;
}
.dashboard-table-from {
margin-left: 1em;
}
.container-borrow-duration { .container-borrow-duration {
height: 41em; height: 41em;
margin: 1em 2em; margin: 1em 2em;

View File

@ -2,10 +2,8 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Injectable, OnIni
import {ActivatedRoute, Router} from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import {DashboardService} from '../service/dashboard.service'; import {DashboardService} from '../service/dashboard.service';
import {IDashboardCommonBikePoint} from '../service/domain/dashboard-common-bike-point'; import {IDashboardCommonBikePoint} from '../service/domain/dashboard-common-bike-point';
import {MatTableDataSource} from '@angular/material/table';
import {FormBuilder, FormControl, FormGroup} from '@angular/forms'; import {FormBuilder, FormControl, FormGroup} from '@angular/forms';
import {MapService} from '../service/map.service'; import {MapService} from '../service/map.service';
import stht from 'seconds-to-human-time';
import { import {
@ -20,14 +18,13 @@ import {
ApexTitleSubtitle, ApexTitleSubtitle,
ApexTooltip, ApexTooltip,
ApexXAxis, ApexXAxis,
ApexYAxis, ApexYAxis
ChartComponent
} from 'ng-apexcharts'; } from 'ng-apexcharts';
import {IMapBikePoint} from '../service/domain/map-bike-point'; import {IMapBikePoint} from '../service/domain/map-bike-point';
import {SelectionModel} from '@angular/cdk/collections';
import {MatCheckboxChange} from '@angular/material/checkbox';
import {DateAdapter, MAT_DATE_FORMATS, NativeDateAdapter} from '@angular/material/core'; import {DateAdapter, MAT_DATE_FORMATS, NativeDateAdapter} from '@angular/material/core';
import {formatDate} from '@angular/common'; import {formatDate} from '@angular/common';
import {TableComponent} from './table/table.component';
import {RentDurationChartComponent} from "./rent-duration-chart/rent-duration-chart.component";
export type ChartOptions = { export type ChartOptions = {
title: ApexTitleSubtitle; title: ApexTitleSubtitle;
@ -80,19 +77,12 @@ const chartHeight = 460;
] ]
}) })
export class DashboardComponent implements OnInit { export class DashboardComponent implements OnInit {
@ViewChild('Station-Dashboard-Borrow-Duration') chart: ChartComponent; @ViewChild(TableComponent) table: TableComponent;
@ViewChild(RentDurationChartComponent) durationChart: RentDurationChartComponent;
public durationChartOptions: Partial<ChartOptions>; public durationChartOptions: Partial<ChartOptions>;
public timeChartOptions: Partial<ChartOptions>; public timeChartOptions: Partial<ChartOptions>;
public bikePointChartOptions: Partial<ChartOptions>; public bikePointChartOptions: Partial<ChartOptions>;
displayedColumnsTo: string[] = ['select', 'endStationName', 'number', 'avgDuration', 'marker'];
displayedColumnsFrom: string[] = ['select', 'startStationName', 'number', 'avgDuration', 'marker'];
stationToSource = new MatTableDataSource<IDashboardCommonBikePoint>();
iterableToSource: any[];
stationFromSource = new MatTableDataSource<IDashboardCommonBikePoint>();
iterableFromSource: any[];
selectionModel = new SelectionModel<IDashboardCommonBikePoint>(true, []);
colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
isLoading: boolean;
station: IDashboardCommonBikePoint; station: IDashboardCommonBikePoint;
maxStartDate: Date; maxStartDate: Date;
@ -138,7 +128,6 @@ export class DashboardComponent implements OnInit {
text: 'Loading...' text: 'Loading...'
} }
}; };
this.isLoading = true;
} }
ngOnInit(): void { ngOnInit(): void {
@ -151,8 +140,6 @@ export class DashboardComponent implements OnInit {
this.changeDetectorRefs.detectChanges(); this.changeDetectorRefs.detectChanges();
this.map.removeTableStationMarkerOnReload(); this.map.removeTableStationMarkerOnReload();
this.route.params.subscribe(params => { this.route.params.subscribe(params => {
this.selectionModel.clear();
this.colors = ['black', 'gray', 'green', 'orange', 'purple', 'red'];
this.service.fetchDashboardInit(params.id).then(data => { this.service.fetchDashboardInit(params.id).then(data => {
this.station = data; this.station = data;
this.maxStartDate = new Date(data.maxStartDate); this.maxStartDate = new Date(data.maxStartDate);
@ -254,18 +241,6 @@ export class DashboardComponent implements OnInit {
const initDate = this.maxEndDate.toISOString().substring(0, 10); const initDate = this.maxEndDate.toISOString().substring(0, 10);
this.form.get('daterange').get('start').setValue(initDate); this.form.get('daterange').get('start').setValue(initDate);
this.form.get('daterange').get('end').setValue(initDate); this.form.get('daterange').get('end').setValue(initDate);
await this.service.fetchDashboardStationTo(this.station.id, initDate, initDate).then((source) => {
this.stationToSource = this.setBikePointColorToSource(source);
this.iterableToSource = source;
this.isLoading = false;
this.changeDetectorRefs.detectChanges();
});
await this.service.fetchDashboardStationFrom(this.station.id, initDate, initDate).then((source) => {
this.stationFromSource = this.setBikePointColorFromSource(source);
this.iterableFromSource = source;
this.isLoading = false;
this.changeDetectorRefs.detectChanges();
});
this.service.fetchDashboardStationCharts(this.station.id, initDate, initDate, 'duration').then((source) => { this.service.fetchDashboardStationCharts(this.station.id, initDate, initDate, 'duration').then((source) => {
const numbers = []; const numbers = [];
const minutesGroup = []; const minutesGroup = [];
@ -404,232 +379,17 @@ export class DashboardComponent implements OnInit {
} }
async onSubmit(): Promise<any> { async onSubmit(): Promise<any> {
this.isLoading = false;
this.actualStartDate = this.form.get('daterange').value.start; this.actualStartDate = this.form.get('daterange').value.start;
this.actualEndDate = this.form.get('daterange').value.end; this.actualEndDate = this.form.get('daterange').value.end;
this.map.removeTableStationMarkerOnReload(); this.table.onSubmit(
this.selectionModel.clear();
await this.service.fetchDashboardStationTo(
this.station.id,
this.actualStartDate.toISOString().substring(0, 10), this.actualStartDate.toISOString().substring(0, 10),
this.actualEndDate.toISOString().substring(0, 10) this.actualEndDate.toISOString().substring(0, 10)
).then((source) => { );
this.colors = ['black', 'gray', 'green', 'orange', 'purple', 'red']; this.durationChart.onSubmit(
this.stationToSource = this.setBikePointColorToSource(source);
this.iterableToSource = source;
this.isLoading = false;
this.changeDetectorRefs.detectChanges();
});
await this.service.fetchDashboardStationFrom(
this.station.id,
this.actualStartDate.toISOString().substring(0, 10), this.actualStartDate.toISOString().substring(0, 10),
this.actualEndDate.toISOString().substring(0, 10) this.actualEndDate.toISOString().substring(0, 10)
).then((source) => { );
this.stationFromSource = this.setBikePointColorFromSource(source);
this.iterableFromSource = source;
this.isLoading = false;
this.changeDetectorRefs.detectChanges();
});
this.service.fetchDashboardStationCharts(
this.station.id,
this.actualStartDate.toISOString().substring(0, 10),
this.actualEndDate.toISOString().substring(0, 10),
'duration'
).then((source) => {
const numbers = [];
const minutesGroup = [];
source.forEach(value => {
numbers.push(value.number);
minutesGroup.push(value.minutesGroup);
});
this.durationChartOptions = {
series: [
{
name: 'amount of drives for given borrow duration',
data: numbers
}
],
chart: {
type: 'bar',
height: chartHeight
},
colors: ['#017bfe'],
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
endingShape: 'flat'
}
},
dataLabels: {
enabled: false
},
stroke: {
show: true,
width: 2,
colors: ['transparent']
},
xaxis: {
title: {
text: 'average rental duration'
},
categories: minutesGroup,
labels: {
formatter: value => {
return value + ' min';
}
}
},
yaxis: {
title: {
text: 'amount of drives'
}
},
fill: {
opacity: 1
}
};
});
this.service.fetchDashboardStationCharts(
this.station.id,
this.actualStartDate.toISOString().substring(0, 10),
this.actualEndDate.toISOString().substring(0, 10),
'time'
).then((source) => {
const timeFrame = [];
const numbers = [];
const avgDuration = [];
source.forEach(value => {
timeFrame.push(value.timeFrame);
numbers.push(value.number);
avgDuration.push(Math.round(value.avgDuration / 60));
});
this.timeChartOptions = {
series: [
{
name: 'amount of drives',
type: 'bar',
data: numbers
},
{
name: 'average rental duration',
type: 'line',
data: avgDuration
}
],
chart: {
toolbar: {
show: false
},
type: 'line',
height: chartHeight,
zoom: {
enabled: true,
}
},
colors: ['#017bfe', '#51ca49'],
dataLabels: {
enabled: false
},
stroke: {
curve: 'straight'
},
xaxis: {
title: {
text: 'time of the day'
},
categories: timeFrame,
tickAmount: 24,
tickPlacement: 'between'
},
yaxis: [{
title: {
text: 'amount of drives',
},
}, {
opposite: true,
title: {
text: 'average rental duration'
},
labels: {
formatter: (val: number): string => {
return val + ' min';
}
}
}],
legend: {
horizontalAlign: 'left'
},
fill: {
opacity: 1
}
};
});
} }
humanizeAvgDuration(avgDuration: number): string {
return stht(avgDuration);
}
selectRow(selection: MatCheckboxChange, row): void {
const markerToDisplay = [];
this.iterableToSource.forEach(point => {
if (point.stationId === row.stationId) {
this.selectionModel.toggle(point);
}
});
this.iterableFromSource.forEach(point => {
if (point.stationId === row.stationId) {
this.selectionModel.toggle(point);
}
});
this.selectionModel.selected.forEach(point => {
markerToDisplay.push(point);
});
this.map.drawTableStationMarker(markerToDisplay);
}
public drawIconInTable(bikePoint: any): string {
return `../../assets/bike-point-${bikePoint.color}.png`;
}
setBikePointColorToSource(source): any {
for (const station of source) {
if (station.stationId === this.station.id) {
station.color = 'blue';
continue;
}
station.color = this.getRandomColor();
}
return source;
}
setBikePointColorFromSource(source): any {
for (const station of source) {
if (station.stationId === this.station.id) {
station.color = 'blue';
continue;
}
for (const to of this.iterableToSource) {
if (station.stationId === to.stationId) {
station.color = to.color;
break;
}
}
if (!station.color) {
station.color = this.getRandomColor();
}
}
return source;
}
getRandomColor(): string {
const color = this.colors[Math.floor(Math.random() * this.colors.length)];
this.colors = this.colors.filter(c => c !== color);
return color;
}
isCheckBoxDisable(row): boolean {
return row.stationId === this.station.id;
}
} }

View File

@ -0,0 +1,24 @@
<mat-card fxFlex fxLayout="column">
<mat-card-header>
<mat-card-title>Rental Duration</mat-card-title>
<mat-card-subtitle>
This chart shows the rent duration based on the currently selected station.
The time it takes for a rent which has the current station as origin is displayed here.
</mat-card-subtitle>
</mat-card-header>
<mat-card-content fxFlex fxLayout="row" fxLayoutAlign="center">
<div class="station-dashboard-borrow-duration" fxFlex>
<apx-chart
[chart]="chartOptions.chart"
[colors]="chartOptions.colors"
[dataLabels]="chartOptions.dataLabels"
[fill]="chartOptions.fill"
[legend]="chartOptions.legend"
[plotOptions]="chartOptions.plotOptions"
[series]="chartOptions.series"
[stroke]="chartOptions.stroke"
[xaxis]="chartOptions.xaxis"
[yaxis]="chartOptions.yaxis"></apx-chart>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,17 @@
.station-dashboard-borrow-duration {
margin: 1em;
}
.mat-card {
padding: 1px 1px 1px;
margin: 10px;
}
.mat-card-title {
margin-top: 1em;
margin-left: 2em;
}
.mat-card-subtitle {
margin-left: 39px;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RentDurationChartComponent } from './rent-duration-chart.component';
describe('RentDurationChartComponent', () => {
let component: RentDurationChartComponent;
let fixture: ComponentFixture<RentDurationChartComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ RentDurationChartComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(RentDurationChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,149 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import {
ApexAxisChartSeries,
ApexChart,
ApexDataLabels,
ApexFill,
ApexLegend,
ApexNoData,
ApexPlotOptions,
ApexStroke,
ApexTitleSubtitle,
ApexTooltip,
ApexXAxis,
ApexYAxis,
ChartComponent
} from 'ng-apexcharts';
import {ActivatedRoute} from '@angular/router';
import {DashboardService} from '../../service/dashboard.service';
import {IDashboardCommonBikePoint} from '../../service/domain/dashboard-common-bike-point';
export type ChartOptions = {
title: ApexTitleSubtitle;
subtitle: ApexTitleSubtitle;
series: ApexAxisChartSeries;
chart: ApexChart;
colors: string[];
dataLabels: ApexDataLabels;
plotOptions: ApexPlotOptions;
yaxis: ApexYAxis;
xaxis: ApexXAxis;
fill: ApexFill;
tooltip: ApexTooltip;
stroke: ApexStroke;
legend: ApexLegend;
noData: ApexNoData;
};
const chartType = 'duration';
@Component({
selector: 'app-rent-duration-chart',
templateUrl: './rent-duration-chart.component.html',
styleUrls: ['./rent-duration-chart.component.scss']
})
export class RentDurationChartComponent implements OnInit {
@ViewChild(ChartComponent) chart: ChartComponent;
chartOptions: Partial<ChartOptions>;
bikePoint: IDashboardCommonBikePoint;
maxStartDate: Date;
maxEndDate: Date;
constructor(
private route: ActivatedRoute,
private service: DashboardService,
) {
this.chartOptions = {
series: [],
chart: {
type: 'bar'
},
noData: {
text: 'Loading...'
}
};
}
ngOnInit(): void {
this.route.params.subscribe(params => {
this.service.fetchDashboardInit(params.id).then(data => {
this.bikePoint = data;
this.maxStartDate = new Date(data.maxStartDate);
this.maxEndDate = new Date(data.maxEndDate);
this.initChart();
});
});
}
async initChart(): Promise<void> {
const initDate = this.maxEndDate.toISOString().substring(0, 10);
await this.service.fetchDashboardStationCharts(this.bikePoint.id, initDate, initDate, chartType).then(source => {
this.chartOptions = {
series: [
{
name: 'amount of drives',
data: source.map(value => value.number)
}
],
chart: {
type: 'bar',
height: '460'
},
colors: ['#017bfe'],
plotOptions: {
bar: {
horizontal: false,
columnWidth: '55%',
endingShape: 'flat'
}
},
dataLabels: {
enabled: false
},
stroke: {
show: true,
width: 2,
colors: ['transparent']
},
xaxis: {
title: {
text: 'average rental duration'
},
categories: source.map(value => value.minutesGroup),
labels: {
formatter: value => {
return value + ' min';
}
}
},
yaxis: {
title: {
text: 'amount of drives'
}
},
noData: {
text: 'loading'
},
fill: {
opacity: 1
}
};
this.chart.updateOptions(this.chartOptions);
});
}
async onSubmit(actualStartDate: string, actualEndDate: string): Promise<void> {
await this.service.fetchDashboardStationCharts(
this.bikePoint.id,
actualStartDate,
actualEndDate,
chartType
).then(source => {
this.chart.updateSeries([{
data: source.map(value => value.number)
}]);
});
}
}