Skip to content

Commit

Permalink
Add historical duration selection (#9)
Browse files Browse the repository at this point in the history
Closes #10
  • Loading branch information
felixbrucker authored Oct 28, 2023
1 parent 08a9123 commit 8c47a16
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 98 deletions.
9 changes: 6 additions & 3 deletions src/app/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {LoginTokenResult} from './api/types/auth/login-token-result'
import {Account, AccountNotificationSettings, AccountSettings, getAccountIdentifier} from './api/types/account/account'
import {AccountWonBlock} from './api/types/account/account-won-block'
import {makeAccountIdentifierName} from './util'
import {HistoricalStatsDuration} from './api/types/historical-stats-duration'
import {HistoricalStatsDurationProvider} from './historical-stats-duration-provider'

@Injectable({
providedIn: 'root'
Expand Down Expand Up @@ -100,6 +102,7 @@ export class AccountService {
private readonly localStorageService: LocalStorageService,
private readonly toastService: ToastService,
private readonly snippetService: SnippetService,
private readonly historicalStatsDurationProvider: HistoricalStatsDurationProvider,
) {
this.accountIdentifier$ = this.accountIdentifierSubject.pipe(distinctUntilChanged(), shareReplay())
this.haveAccountIdentifier$ = this.accountIdentifier$.pipe(map(identifier => identifier !== null), distinctUntilChanged(), shareReplay())
Expand Down Expand Up @@ -235,7 +238,7 @@ export class AccountService {
}

async updateAccountHistoricalStats() {
this.accountHistoricalStats.next(await this.getAccountHistoricalStats({ accountIdentifier: this.accountIdentifier }))
this.accountHistoricalStats.next(await this.getAccountHistoricalStats({ accountIdentifier: this.accountIdentifier, duration: this.historicalStatsDurationProvider.selectedDuration }))
}

async updateAccountWonBlocks() {
Expand Down Expand Up @@ -276,11 +279,11 @@ export class AccountService {
return accountHarvesters
}

async getAccountHistoricalStats({ accountIdentifier }): Promise<AccountHistoricalStat[]> {
async getAccountHistoricalStats({ accountIdentifier, duration }: { accountIdentifier: string, duration: HistoricalStatsDuration }): Promise<AccountHistoricalStat[]> {
this.isLoading = true
let accountHistoricalStats = []
try {
accountHistoricalStats = await this.statsService.getAccountHistoricalStats(accountIdentifier)
accountHistoricalStats = await this.statsService.getAccountHistoricalStats({ accountIdentifier, duration })
} finally {
this.isLoading = false
}
Expand Down
13 changes: 7 additions & 6 deletions src/app/api/abstract-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {AuthenticationResult} from './types/auth/authentication-result'
import {LoginTokenResult} from './types/auth/login-token-result'
import {BaseTopAccount} from './types/account/top-account'
import {ApiResponse} from './types/api-response'
import {HistoricalStatsDuration} from './types/historical-stats-duration'

export abstract class AbstractApi<
AccountType extends Account,
Expand Down Expand Up @@ -100,14 +101,14 @@ export abstract class AbstractApi<
return data
}

public async getHarvesterStats(harvesterId: string): Promise<HarvesterStats> {
const { data } = await this.client.get<HarvesterStats>(`harvester/${harvesterId}/stats`)
public async getHarvesterStats({ harvesterId, duration }: { harvesterId: string, duration: HistoricalStatsDuration }): Promise<HarvesterStats> {
const { data } = await this.client.get<HarvesterStats>(`harvester/${harvesterId}/stats`, { params: { duration } })

return data
}

public async getHarvesterProofTimes(harvesterId: string): Promise<ProofTime[]> {
const { data } = await this.client.get<ProofTime[]>(`harvester/${harvesterId}/proof-times`)
public async getHarvesterProofTimes({ harvesterId, duration }: { harvesterId: string, duration: HistoricalStatsDuration }): Promise<ProofTime[]> {
const { data } = await this.client.get<ProofTime[]>(`harvester/${harvesterId}/proof-times`, { params: { duration } })

return data
}
Expand All @@ -118,8 +119,8 @@ export abstract class AbstractApi<
return data
}

public async getAccountHistoricalStats(accountIdentifier: string): Promise<AccountHistoricalStat[]> {
const { data } = await this.client.get<AccountHistoricalStat[]>(`account/${accountIdentifier}/historical`)
public async getAccountHistoricalStats({ accountIdentifier, duration }: { accountIdentifier: string, duration: HistoricalStatsDuration }): Promise<AccountHistoricalStat[]> {
const { data } = await this.client.get<AccountHistoricalStat[]>(`account/${accountIdentifier}/historical`, { params: { duration }})

return data
}
Expand Down
18 changes: 18 additions & 0 deletions src/app/api/types/historical-stats-duration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export type HistoricalStatsDuration = '1d' | '7d' | '30d'
export function durationInDays(duration: HistoricalStatsDuration): number {
switch (duration) {
case '1d': return 1
case '7d': return 7
case '30d': return 30
}
}
export function durationInHours(duration: HistoricalStatsDuration): number {
return durationInDays(duration) * 24
}
export function getResolutionInMinutes(duration: HistoricalStatsDuration): number {
switch (duration) {
case '1d': return 15
case '7d': return 60
case '30d': return 60 * 4
}
}
65 changes: 56 additions & 9 deletions src/app/harvester-card/harvester-card.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as moment from 'moment'
import {Moment} from 'moment'
import {BehaviorSubject, Observable, Subscription, take} from 'rxjs'
import {StatsService} from '../stats.service'
import {distinctUntilChanged, filter, map, shareReplay} from 'rxjs/operators'
import {distinctUntilChanged, filter, map, shareReplay, skip} from 'rxjs/operators'
import {BigNumber} from 'bignumber.js'
import Capacity from '../capacity'
import {EChartsOption} from 'echarts'
Expand All @@ -22,6 +22,10 @@ import {ChiaDashboardService} from '../chia-dashboard.service'
import {HarvesterStatus} from '../status/harvester-status'
import {LastUpdatedState} from '../status/last-updated-state'
import {colors, Theme, ThemeProvider} from '../theme-provider'
import {
durationInDays, getResolutionInMinutes,
} from '../api/types/historical-stats-duration'
import {HistoricalStatsDurationProvider} from '../historical-stats-duration-provider'

const sharesPerDayPerK32 = 10
const k32SizeInGb = 108.837
Expand Down Expand Up @@ -73,14 +77,26 @@ export class HarvesterCardComponent implements OnInit, OnDestroy {
return this.accountService.account?.integrations?.chiaDashboardShareKey !== undefined
}

private get historicalIntervalInMinutes(): number {
return getResolutionInMinutes(this.historicalStatsDurationProvider.selectedDuration)
}

private readonly stats: Observable<HarvesterStats>
private readonly statsSubject: BehaviorSubject<HarvesterStats|undefined> = new BehaviorSubject<HarvesterStats>(undefined)
private statsUpdateInterval?: ReturnType<typeof setInterval>
private proofTimesUpdateInterval?: ReturnType<typeof setInterval>
private readonly chartModeSubject: BehaviorSubject<ChartMode> = new BehaviorSubject<ChartMode>(ChartMode.shares)
private readonly proofTimes: BehaviorSubject<ProofTime[]> = new BehaviorSubject<ProofTime[]>([])
private readonly isLoadingProofTimesSubject: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false)
private readonly subscriptions: Subscription[] = []
private readonly subscriptions: Subscription[] = [
this.historicalStatsDurationProvider.selectedDuration$.pipe(skip(1)).subscribe(async _ => {
if (this.harvester === undefined) {
return
}
await this.updateStats()
await this.updateProofTimes()
}),
]

constructor(
public readonly accountService: AccountService,
Expand All @@ -89,6 +105,7 @@ export class HarvesterCardComponent implements OnInit, OnDestroy {
private readonly toastService: ToastService,
private readonly poolsProvider: PoolsProvider,
private readonly themeProvider: ThemeProvider,
private readonly historicalStatsDurationProvider: HistoricalStatsDurationProvider,
) {
this.isLoadingProofTimes = this.isLoadingProofTimesSubject.pipe(shareReplay())
this.showSharesChart = this.chartModeSubject.pipe(map(mode => mode === ChartMode.shares), shareReplay())
Expand All @@ -104,7 +121,7 @@ export class HarvesterCardComponent implements OnInit, OnDestroy {
map(stats => {
const totalShares = stats.submissionStats.reduce((acc, submissionStat) => acc.plus(submissionStat.shares), new BigNumber(0))
const ecInGib = totalShares
.dividedBy(sharesPerDayPerK32)
.dividedBy(sharesPerDayPerK32 * durationInDays(this.historicalStatsDurationProvider.selectedDuration))
.multipliedBy(k32SizeInGib)

return new Capacity(ecInGib.toNumber()).toString()
Expand Down Expand Up @@ -176,7 +193,7 @@ export class HarvesterCardComponent implements OnInit, OnDestroy {
},
xAxis: {
type: 'time',
minInterval: 15 * 60 * 1000,
minInterval: this.historicalIntervalInMinutes * 60 * 1000,
},
yAxis: [{
type: 'value',
Expand Down Expand Up @@ -374,7 +391,7 @@ export class HarvesterCardComponent implements OnInit, OnDestroy {
.filter(satellite => !satellite.hidden)
.map(satellite => satellite.services?.harvester)
.filter(harvester => harvester?.stats !== undefined && harvester.stats.plotCount !== undefined)
.find(harvester => harvester.stats.nodeId === this.harvester.peerId.ensureHexPrefix())
.find(harvester => harvester.stats.nodeId === this.harvester?.peerId.ensureHexPrefix())
),
filter(harvester => harvester !== undefined),
map(harvester => ({
Expand Down Expand Up @@ -750,11 +767,17 @@ export class HarvesterCardComponent implements OnInit, OnDestroy {
}

private async updateStats(): Promise<void> {
this.statsSubject.next(await this.statsService.getHarvesterStats(this.harvester._id))
this.statsSubject.next(await this.statsService.getHarvesterStats({
harvesterId: this.harvester._id,
duration: this.historicalStatsDurationProvider.selectedDuration,
}))
}

private async updateProofTimes(): Promise<void> {
this.proofTimes.next(await this.statsService.getHarvesterProofTimes(this.harvester._id))
this.proofTimes.next(await this.statsService.getHarvesterProofTimes({
harvesterId: this.harvester._id,
duration: this.historicalStatsDurationProvider.selectedDuration,
}))
}

private makeSharesChartUpdateOptions(stats: HarvesterStats): EChartsOption {
Expand Down Expand Up @@ -793,6 +816,25 @@ export class HarvesterCardComponent implements OnInit, OnDestroy {
return date.clone().set({ minutes: minutesRoundedDown, seconds: 0, milliseconds: 0 })
}

const roundToNextLowerHour = (date: Moment): Moment => {
return date.clone().set({ minutes: 0, seconds: 0, milliseconds: 0 })
}

const roundToNextLower4Hour = (date: Moment): Moment => {
const hoursRoundedDown = Math.floor(date.hours() / 4) * 4

return date.clone().set({ hours: hoursRoundedDown, minutes: 0, seconds: 0, milliseconds: 0 })
}

const resolutionInMinutes = this.historicalIntervalInMinutes
const applyRounding = (date: Moment): Moment => {
switch (this.historicalStatsDurationProvider.selectedDuration) {
case '1d': return roundToNextLower15Min(date)
case '7d': return roundToNextLowerHour(date)
case '30d': return roundToNextLower4Hour(date)
}
}

const insertEmptyPositionIfNotExists = (position: number, date: Moment, series: (string | number)[][]) => {
const dateAsIsoString = date.toISOString()
if (position >= series.length) {
Expand All @@ -807,17 +849,22 @@ export class HarvesterCardComponent implements OnInit, OnDestroy {
series.splice(position, 0, [dateAsIsoString, 0])
}

let startDate = roundToNextLower15Min(moment()).subtract(1, 'day')
const historicalDurationInDays = durationInDays(this.historicalStatsDurationProvider.selectedDuration)
let startDate = applyRounding(moment().utc()).subtract(historicalDurationInDays, 'day')
let currentPosition = 0
while (startDate.isBefore(moment())) {
insertEmptyPositionIfNotExists(currentPosition, startDate, invalidSharesSeries)
insertEmptyPositionIfNotExists(currentPosition, startDate, staleSharesSeries)
insertEmptyPositionIfNotExists(currentPosition, startDate, validSharesSeries)
currentPosition += 1
startDate = startDate.add(15, 'minutes')
startDate = startDate.add(resolutionInMinutes, 'minutes')
}

return {
xAxis: {
type: 'time',
minInterval: resolutionInMinutes * 60 * 1000,
},
series: [{
data: invalidSharesSeries,
}, {
Expand Down
28 changes: 28 additions & 0 deletions src/app/historical-stats-duration-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core'
import {BehaviorSubject, Observable} from 'rxjs'
import {HistoricalStatsDuration} from './api/types/historical-stats-duration'
import {distinctUntilChanged, shareReplay} from 'rxjs/operators'

@Injectable({
providedIn: 'root'
})
export class HistoricalStatsDurationProvider {
public get possibleDurations(): HistoricalStatsDuration[] {
return ['1d', '7d', '30d']
}

public get selectedDuration(): HistoricalStatsDuration {
return this.selectedDurationRelay.getValue()
}

public set selectedDuration(duration: HistoricalStatsDuration) {
this.selectedDurationRelay.next(duration)
}

public readonly selectedDuration$: Observable<HistoricalStatsDuration>
private readonly selectedDurationRelay: BehaviorSubject<HistoricalStatsDuration> = new BehaviorSubject<HistoricalStatsDuration>('1d')

public constructor() {
this.selectedDuration$ = this.selectedDurationRelay.pipe(distinctUntilChanged(), shareReplay())
}
}
Loading

0 comments on commit 8c47a16

Please sign in to comment.