Added run post run statistics view in the non-overlay window. Provides some stats for a run in aggregation on the zone id and then also a toggle to see an unaggregated run
parent
5db8663819
commit
3272c3bda2
@ -0,0 +1,47 @@
|
||||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended",
|
||||
"plugin:@angular-eslint/template/accessibility"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,61 @@
|
||||
<button mat-stroked-button color="warn" (click)="reset()">Reset view</button>
|
||||
|
||||
<ng-container *ngIf="!shouldShowTable && cache">
|
||||
<div class="cache-viewport">
|
||||
<div *ngFor="let item of this.cache | keyvalue" class="cache-item d-flex flex-row p-4">
|
||||
<div class="d-flex flex-column">
|
||||
<span>Plan: {{item.value.associatedName}}</span>
|
||||
<span>Run time: {{hms(item.value.currentElapsedMillis)}}</span>
|
||||
<span>Last updated: {{dateFormat(item.value.last_updated)}}</span>
|
||||
</div>
|
||||
<div class="d-flex flex-column justify-content-center align-items-center">
|
||||
<button mat-stroked-button color="accent" (click)="loadMain(item.key)">Load</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="shouldShowTable">
|
||||
<mat-slide-toggle [(ngModel)]="this.aggregate">Show aggregated results</mat-slide-toggle>
|
||||
<table mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="zoneName">
|
||||
<th mat-header-cell *matHeaderCellDef> Zone </th>
|
||||
<td mat-cell *matCellDef="let entry"> {{entry.zoneName}} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="entryTime">
|
||||
<th mat-header-cell *matHeaderCellDef> Time of entry </th>
|
||||
<td mat-cell *matCellDef="let entry"> {{entry.entryTime}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="estimatedExit">
|
||||
<th mat-header-cell *matHeaderCellDef> Time of estimated exit </th>
|
||||
<td mat-cell *matCellDef="let entry"> {{entry.estimatedExit}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="estimatedTimeSpent">
|
||||
<th mat-header-cell *matHeaderCellDef> Estimated time spent </th>
|
||||
<td mat-cell *matCellDef="let entry"> {{entry.estimatedTimeSpent}} </td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
||||
<ng-container matColumnDef="aggregateFirstEntry">
|
||||
<th mat-header-cell *matHeaderCellDef> Time of first entry </th>
|
||||
<td mat-cell *matCellDef="let entry"> {{entry.aggregateFirstEntry}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="aggregateLastExit">
|
||||
<th mat-header-cell *matHeaderCellDef> Time of estimated last exit </th>
|
||||
<td mat-cell *matCellDef="let entry"> {{entry.aggregateLastExit}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="aggregateTimeSpent">
|
||||
<th mat-header-cell *matHeaderCellDef> Estimated total time spent </th>
|
||||
<td mat-cell *matCellDef="let entry"> {{entry.aggregateTimeSpent}} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="aggregateNumEntries">
|
||||
<th mat-header-cell *matHeaderCellDef> Number of entries </th>
|
||||
<td mat-cell *matCellDef="let entry"> {{entry.aggregateNumEntries}} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</ng-container>
|
@ -0,0 +1,3 @@
|
||||
.cache-item {
|
||||
gap: 12px;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RunStatsComponent } from './run-stats.component';
|
||||
|
||||
describe('RunStatsComponent', () => {
|
||||
let component: RunStatsComponent;
|
||||
let fixture: ComponentFixture<RunStatsComponent>;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RunStatsComponent]
|
||||
});
|
||||
fixture = TestBed.createComponent(RunStatsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,193 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RunHistory, RunHistoryMetadata, TimeTrackerService } from '../_services/time-tracker.service';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { WorldAreaService } from '../_services/world-area.service';
|
||||
import { WorldArea } from '../_models/world-area';
|
||||
|
||||
export interface RunStat {
|
||||
zoneName: string;
|
||||
|
||||
entryTime: string;
|
||||
estimatedExit: string;
|
||||
estimatedTimeSpent: string;
|
||||
}
|
||||
|
||||
export interface AggregateRunStat {
|
||||
zoneName: string;
|
||||
|
||||
aggregateFirstEntry: string;
|
||||
aggregateLastExit: string;
|
||||
aggregateTimeSpent: string;
|
||||
aggregateNumEntries: string;
|
||||
}
|
||||
|
||||
interface UnformattedAggregateRunStat {
|
||||
zoneId: string;
|
||||
|
||||
aggregateFirstEntry: number;
|
||||
aggregateLastExit: number;
|
||||
aggregateTimeSpent: number;
|
||||
aggregateNumEntries: number;
|
||||
}
|
||||
|
||||
type RunStatType = RunStat | AggregateRunStat;
|
||||
|
||||
@Component({
|
||||
selector: 'app-run-stats',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatTableModule, MatSlideToggleModule, FormsModule, ScrollingModule, MatButtonModule],
|
||||
templateUrl: './run-stats.component.html',
|
||||
styleUrls: ['./run-stats.component.scss']
|
||||
})
|
||||
export class RunStatsComponent implements OnInit {
|
||||
aggregated?: AggregateRunStat[];
|
||||
/// practically which zone can't have a last exit time as last exit is not determinable for the last entry
|
||||
aggregateNAId?: string;
|
||||
|
||||
direct?: RunStat[];
|
||||
|
||||
aggregate: boolean = true;
|
||||
cache?: Map<string, RunHistoryMetadata>;
|
||||
worldAreaMap?: Map<string, WorldArea>;
|
||||
|
||||
constructor(private timeTrackerService: TimeTrackerService, private worldAreaService: WorldAreaService) {
|
||||
this.worldAreaService.getFullWorldAreas().subscribe((data) => {
|
||||
this.worldAreaMap = data;
|
||||
})
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.timeTrackerService.storedHistories.subscribe((data) => {
|
||||
this.cache = data;
|
||||
})
|
||||
}
|
||||
|
||||
get dataSource(): RunStatType[] {
|
||||
return (this.aggregate ? this.aggregated : this.direct) ?? [];
|
||||
}
|
||||
|
||||
get hasInitializedData() {
|
||||
return this.aggregated || this.direct;
|
||||
}
|
||||
|
||||
get shouldShowTable() {
|
||||
return this.hasInitializedData;
|
||||
}
|
||||
|
||||
loadMain(uuid: string) {
|
||||
this.timeTrackerService.loadHistory(uuid).subscribe((data => {
|
||||
if (data) {
|
||||
this.onLoad(data);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dateFormat(value: number) {
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
|
||||
onLoad(data: RunHistory) {
|
||||
this.direct = this.calcDirect(data);
|
||||
this.aggregated = this.calcAggregated(data);
|
||||
}
|
||||
|
||||
calcAggregated(data: RunHistory): AggregateRunStat[] {
|
||||
const aggregation = new Map<string, UnformattedAggregateRunStat>();
|
||||
|
||||
this.aggregateNAId = data.entries[data.entries.length - 1].zone;
|
||||
|
||||
data.entries.forEach((entry, index) => {
|
||||
const hasExit = !(data.entries.length - 1 === index);
|
||||
|
||||
let aggregate: UnformattedAggregateRunStat = {
|
||||
zoneId: entry.zone,
|
||||
aggregateFirstEntry: entry.current_elapsed_millis,
|
||||
aggregateLastExit: hasExit ? data.entries[index + 1].current_elapsed_millis : 0,
|
||||
aggregateTimeSpent: hasExit ? (data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : 0,
|
||||
aggregateNumEntries: 1,
|
||||
}
|
||||
|
||||
const existing = aggregation.get(entry.zone);
|
||||
if (existing) {
|
||||
existing.aggregateLastExit = aggregate.aggregateLastExit;
|
||||
existing.aggregateTimeSpent += aggregate.aggregateTimeSpent;
|
||||
existing.aggregateNumEntries++;
|
||||
}
|
||||
|
||||
aggregation.set(entry.zone, existing ?? aggregate);
|
||||
});
|
||||
|
||||
return Array.from(aggregation.values()).map((entry) => {
|
||||
let aggregateTimeSpent;
|
||||
if (this.aggregateNAId === entry.zoneId) {
|
||||
aggregateTimeSpent = this.timeTrackerService.hmsTimestamp(entry.aggregateTimeSpent) + " + N/A"
|
||||
} else {
|
||||
aggregateTimeSpent = this.timeTrackerService.hmsTimestamp(entry.aggregateTimeSpent);
|
||||
}
|
||||
|
||||
return {
|
||||
zoneName: this.resolveZone(entry.zoneId),
|
||||
aggregateFirstEntry: this.timeTrackerService.hmsTimestamp(entry.aggregateFirstEntry),
|
||||
aggregateLastExit: this.aggregateNAId === entry.zoneId ? "N/A" : this.timeTrackerService.hmsTimestamp(entry.aggregateLastExit),
|
||||
aggregateTimeSpent: aggregateTimeSpent,
|
||||
aggregateNumEntries: entry.aggregateNumEntries.toString(),
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
calcDirect(data: RunHistory): RunStat[] {
|
||||
return data.entries.map((entry, index) => {
|
||||
const hasExit = !(data.entries.length - 1 === index);
|
||||
return {
|
||||
zoneName: this.resolveZone(entry.zone),
|
||||
entryTime: this.timeTrackerService.hmsTimestamp(entry.current_elapsed_millis),
|
||||
estimatedExit: hasExit ? this.timeTrackerService.hmsTimestamp(data.entries[index + 1].current_elapsed_millis) : "N/A",
|
||||
estimatedTimeSpent: hasExit ? this.timeTrackerService.hmsTimestamp(data.entries[index + 1].current_elapsed_millis - data.entries[index].current_elapsed_millis) : "N/A",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
hms(time: number) {
|
||||
return this.timeTrackerService.hmsTimestamp(time);
|
||||
}
|
||||
|
||||
private resolveZone(zoneId: string) {
|
||||
const area = this.worldAreaMap?.get(zoneId);
|
||||
if (!area) {
|
||||
return "Unknown zone: " + zoneId;
|
||||
}
|
||||
|
||||
// Act might not be very reasonable but it's the best we have it..
|
||||
return area.name + " (A" + area.act + ")"
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.aggregated = undefined;
|
||||
this.direct = undefined;
|
||||
}
|
||||
|
||||
get displayedColumns() {
|
||||
if (this.aggregate) {
|
||||
return [
|
||||
"zoneName",
|
||||
"aggregateFirstEntry",
|
||||
"aggregateLastExit",
|
||||
"aggregateTimeSpent",
|
||||
"aggregateNumEntries",
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
"zoneName",
|
||||
"entryTime",
|
||||
"estimatedExit",
|
||||
"estimatedTimeSpent",
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue