1 import { ChartConfiguration, ChartData } from 'chart.js'
2 import { Observable, of } from 'rxjs'
3 import { Component, OnInit } from '@angular/core'
4 import { ActivatedRoute } from '@angular/router'
5 import { Notifier } from '@app/core'
6 import { NumberFormatterPipe, VideoDetails } from '@app/shared/shared-main'
7 import { secondsToTime } from '@shared/core-utils'
8 import { VideoStatsOverall, VideoStatsRetention, VideoStatsTimeserie, VideoStatsTimeserieMetric } from '@shared/models/videos'
9 import { VideoStatsService } from './video-stats.service'
11 type ActiveGraphId = VideoStatsTimeserieMetric | 'retention' | 'countries'
13 type CountryData = { name: string, viewers: number }[]
15 type ChartIngestData = VideoStatsTimeserie | VideoStatsRetention | CountryData
16 type ChartBuilderResult = {
18 data: ChartData<'line' | 'bar'>
19 displayLegend: boolean
23 templateUrl: './video-stats.component.html',
24 styleUrls: [ './video-stats.component.scss' ],
25 providers: [ NumberFormatterPipe ]
27 export class VideoStatsComponent implements OnInit {
28 overallStatCards: { label: string, value: string | number, moreInfo?: string }[] = []
30 chartOptions: { [ id in ActiveGraphId ]?: ChartConfiguration<'line' | 'bar'> } = {}
32 chartWidth: string = null
37 label: $localize`Viewers`
40 id: 'aggregateWatchTime',
41 label: $localize`Watch time`
45 label: $localize`Retention`
49 label: $localize`Countries`
53 activeGraphId: ActiveGraphId = 'viewers'
57 countries: CountryData = []
60 private route: ActivatedRoute,
61 private notifier: Notifier,
62 private statsService: VideoStatsService,
63 private numberFormatter: NumberFormatterPipe
67 this.video = this.route.snapshot.data.video
69 this.loadOverallStats()
74 return this.countries.length !== 0
77 onChartChange (newActive: ActiveGraphId) {
78 this.activeGraphId = newActive
83 private loadOverallStats () {
84 this.statsService.getOverallStats(this.video.uuid)
87 this.countries = res.countries.slice(0, 10).map(c => ({
88 name: this.countryCodeToName(c.isoCode),
92 this.buildOverallStatCard(res)
95 error: err => this.notifier.error(err.message)
99 private buildOverallStatCard (overallStats: VideoStatsOverall) {
100 this.overallStatCards = [
102 label: $localize`Views`,
103 value: this.numberFormatter.transform(overallStats.views)
106 label: $localize`Comments`,
107 value: this.numberFormatter.transform(overallStats.comments)
110 label: $localize`Likes`,
111 value: this.numberFormatter.transform(overallStats.likes)
114 label: $localize`Average watch time`,
115 value: secondsToTime(overallStats.averageWatchTime)
118 label: $localize`Peak viewers`,
119 value: this.numberFormatter.transform(overallStats.viewersPeak),
120 moreInfo: $localize`at ${new Date(overallStats.viewersPeakDate).toLocaleString()}`
125 private loadChart () {
126 const obsBuilders: { [ id in ActiveGraphId ]: Observable<ChartIngestData> } = {
127 retention: this.statsService.getRetentionStats(this.video.uuid),
128 aggregateWatchTime: this.statsService.getTimeserieStats(this.video.uuid, 'aggregateWatchTime'),
129 viewers: this.statsService.getTimeserieStats(this.video.uuid, 'viewers'),
130 countries: of(this.countries)
133 obsBuilders[this.activeGraphId].subscribe({
135 this.chartOptions[this.activeGraphId] = this.buildChartOptions(this.activeGraphId, res)
138 error: err => this.notifier.error(err.message)
142 private buildChartOptions (
143 graphId: ActiveGraphId,
144 rawData: ChartIngestData
145 ): ChartConfiguration<'line' | 'bar'> {
146 const dataBuilders: {
147 [ id in ActiveGraphId ]: (rawData: ChartIngestData) => ChartBuilderResult
149 retention: (rawData: VideoStatsRetention) => this.buildRetentionChartOptions(rawData),
150 aggregateWatchTime: (rawData: VideoStatsTimeserie) => this.buildTimeserieChartOptions(rawData),
151 viewers: (rawData: VideoStatsTimeserie) => this.buildTimeserieChartOptions(rawData),
152 countries: (rawData: CountryData) => this.buildCountryChartOptions(rawData)
155 const { type, data, displayLegend } = dataBuilders[graphId](rawData)
168 max: this.activeGraphId === 'retention'
173 callback: value => this.formatTick(graphId, value)
180 display: displayLegend
184 label: value => this.formatTick(graphId, value.raw as number | string)
192 private buildRetentionChartOptions (rawData: VideoStatsRetention) {
193 const labels: string[] = []
194 const data: number[] = []
196 for (const d of rawData.data) {
197 labels.push(secondsToTime(d.second))
198 data.push(d.retentionPercent)
202 type: 'line' as 'line',
204 displayLegend: false,
211 borderColor: this.buildChartColor()
218 private buildTimeserieChartOptions (rawData: VideoStatsTimeserie) {
219 const labels: string[] = []
220 const data: number[] = []
222 for (const d of rawData.data) {
223 labels.push(new Date(d.date).toLocaleDateString())
228 type: 'line' as 'line',
230 displayLegend: false,
237 borderColor: this.buildChartColor()
244 private buildCountryChartOptions (rawData: CountryData) {
245 const labels: string[] = []
246 const data: number[] = []
248 for (const d of rawData) {
254 type: 'bar' as 'bar',
266 label: $localize`Viewers`,
267 backgroundColor: this.buildChartColor(),
276 private buildChartColor () {
277 return getComputedStyle(document.body).getPropertyValue('--mainColorLighter')
280 private formatTick (graphId: ActiveGraphId, value: number | string) {
281 if (graphId === 'retention') return value + ' %'
282 if (graphId === 'aggregateWatchTime') return secondsToTime(+value)
284 return value.toLocaleString()
287 private countryCodeToName (code: string) {
288 const intl: any = Intl
289 if (!intl.DisplayNames) return code
291 const regionNames = new intl.DisplayNames([], { type: 'region' })
293 return regionNames.of(code)