2022-04-08 08:22:56 +00:00
|
|
|
import { ChartConfiguration, ChartData, PluginOptionsByType, Scale, TooltipItem } from 'chart.js'
|
|
|
|
import zoomPlugin from 'chartjs-plugin-zoom'
|
2022-04-05 12:03:52 +00:00
|
|
|
import { Observable, of } from 'rxjs'
|
2022-05-06 12:23:02 +00:00
|
|
|
import { SelectOptionsItem } from 'src/types'
|
2022-04-05 12:03:52 +00:00
|
|
|
import { Component, OnInit } from '@angular/core'
|
|
|
|
import { ActivatedRoute } from '@angular/router'
|
2022-04-08 08:22:56 +00:00
|
|
|
import { Notifier, PeerTubeRouterService } from '@app/core'
|
2022-04-05 12:03:52 +00:00
|
|
|
import { NumberFormatterPipe, VideoDetails } from '@app/shared/shared-main'
|
2022-05-06 12:23:02 +00:00
|
|
|
import { LiveVideoService } from '@app/shared/shared-video-live'
|
2022-04-05 12:03:52 +00:00
|
|
|
import { secondsToTime } from '@shared/core-utils'
|
2022-05-06 12:23:02 +00:00
|
|
|
import { HttpStatusCode } from '@shared/models/http'
|
|
|
|
import {
|
|
|
|
LiveVideoSession,
|
|
|
|
VideoStatsOverall,
|
|
|
|
VideoStatsRetention,
|
|
|
|
VideoStatsTimeserie,
|
|
|
|
VideoStatsTimeserieMetric
|
|
|
|
} from '@shared/models/videos'
|
2022-04-05 12:03:52 +00:00
|
|
|
import { VideoStatsService } from './video-stats.service'
|
|
|
|
|
|
|
|
type ActiveGraphId = VideoStatsTimeserieMetric | 'retention' | 'countries'
|
|
|
|
|
|
|
|
type CountryData = { name: string, viewers: number }[]
|
|
|
|
|
|
|
|
type ChartIngestData = VideoStatsTimeserie | VideoStatsRetention | CountryData
|
|
|
|
type ChartBuilderResult = {
|
|
|
|
type: 'line' | 'bar'
|
2022-04-08 08:22:56 +00:00
|
|
|
plugins: Partial<PluginOptionsByType<'line' | 'bar'>>
|
2022-04-05 12:03:52 +00:00
|
|
|
data: ChartData<'line' | 'bar'>
|
|
|
|
displayLegend: boolean
|
|
|
|
}
|
|
|
|
|
2022-05-06 12:56:34 +00:00
|
|
|
type Card = { label: string, value: string | number, moreInfo?: string, help?: string }
|
2022-05-06 12:23:02 +00:00
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
@Component({
|
|
|
|
templateUrl: './video-stats.component.html',
|
|
|
|
styleUrls: [ './video-stats.component.scss' ],
|
|
|
|
providers: [ NumberFormatterPipe ]
|
|
|
|
})
|
|
|
|
export class VideoStatsComponent implements OnInit {
|
2022-05-06 12:23:02 +00:00
|
|
|
// Cannot handle date filters
|
|
|
|
globalStatsCards: Card[] = []
|
|
|
|
// Can handle date filters
|
|
|
|
overallStatCards: Card[] = []
|
2022-04-05 12:03:52 +00:00
|
|
|
|
|
|
|
chartOptions: { [ id in ActiveGraphId ]?: ChartConfiguration<'line' | 'bar'> } = {}
|
|
|
|
chartHeight = '300px'
|
|
|
|
chartWidth: string = null
|
|
|
|
|
2022-05-06 12:23:02 +00:00
|
|
|
availableCharts: { id: string, label: string, zoomEnabled: boolean }[] = []
|
2022-04-05 12:03:52 +00:00
|
|
|
activeGraphId: ActiveGraphId = 'viewers'
|
|
|
|
|
|
|
|
video: VideoDetails
|
|
|
|
|
|
|
|
countries: CountryData = []
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
chartPlugins = [ zoomPlugin ]
|
|
|
|
|
2022-05-06 12:23:02 +00:00
|
|
|
currentDateFilter = 'all'
|
|
|
|
dateFilters: SelectOptionsItem[] = [
|
|
|
|
{
|
|
|
|
id: 'all',
|
|
|
|
label: $localize`Since the video publication`
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
private statsStartDate: Date
|
|
|
|
private statsEndDate: Date
|
2022-04-08 08:22:56 +00:00
|
|
|
|
|
|
|
private chartIngestData: { [ id in ActiveGraphId ]?: ChartIngestData } = {}
|
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
constructor (
|
|
|
|
private route: ActivatedRoute,
|
|
|
|
private notifier: Notifier,
|
|
|
|
private statsService: VideoStatsService,
|
2022-04-08 08:22:56 +00:00
|
|
|
private peertubeRouter: PeerTubeRouterService,
|
2022-05-06 12:23:02 +00:00
|
|
|
private numberFormatter: NumberFormatterPipe,
|
|
|
|
private liveService: LiveVideoService
|
2022-04-05 12:03:52 +00:00
|
|
|
) {}
|
|
|
|
|
|
|
|
ngOnInit () {
|
|
|
|
this.video = this.route.snapshot.data.video
|
|
|
|
|
2022-05-06 12:23:02 +00:00
|
|
|
this.availableCharts = [
|
|
|
|
{
|
|
|
|
id: 'viewers',
|
|
|
|
label: $localize`Viewers`,
|
|
|
|
zoomEnabled: true
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 'aggregateWatchTime',
|
|
|
|
label: $localize`Watch time`,
|
|
|
|
zoomEnabled: true
|
|
|
|
},
|
|
|
|
{
|
|
|
|
id: 'countries',
|
|
|
|
label: $localize`Countries`,
|
|
|
|
zoomEnabled: false
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
if (!this.video.isLive) {
|
|
|
|
this.availableCharts.push({
|
|
|
|
id: 'retention',
|
|
|
|
label: $localize`Retention`,
|
|
|
|
zoomEnabled: false
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
const snapshotQuery = this.route.snapshot.queryParams
|
|
|
|
if (snapshotQuery.startDate || snapshotQuery.endDate) {
|
|
|
|
this.addAndSelectCustomDateFilter()
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
this.route.queryParams.subscribe(params => {
|
2022-05-06 12:23:02 +00:00
|
|
|
this.statsStartDate = params.startDate
|
2022-04-08 08:22:56 +00:00
|
|
|
? new Date(params.startDate)
|
|
|
|
: undefined
|
|
|
|
|
2022-05-06 12:23:02 +00:00
|
|
|
this.statsEndDate = params.endDate
|
2022-04-08 08:22:56 +00:00
|
|
|
? new Date(params.endDate)
|
|
|
|
: undefined
|
|
|
|
|
|
|
|
this.loadChart()
|
2022-05-06 12:23:02 +00:00
|
|
|
this.loadOverallStats()
|
2022-04-08 08:22:56 +00:00
|
|
|
})
|
|
|
|
|
2022-05-06 12:23:02 +00:00
|
|
|
this.loadDateFilters()
|
2022-04-05 12:03:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
hasCountries () {
|
|
|
|
return this.countries.length !== 0
|
|
|
|
}
|
|
|
|
|
|
|
|
onChartChange (newActive: ActiveGraphId) {
|
|
|
|
this.activeGraphId = newActive
|
|
|
|
|
|
|
|
this.loadChart()
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
resetZoom () {
|
|
|
|
this.peertubeRouter.silentNavigate([], {})
|
2022-05-06 12:23:02 +00:00
|
|
|
this.removeAndResetCustomDateFilter()
|
2022-04-08 08:22:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
hasZoom () {
|
2022-05-06 12:23:02 +00:00
|
|
|
return !!this.statsStartDate && this.isTimeserieGraph(this.activeGraphId)
|
|
|
|
}
|
|
|
|
|
|
|
|
getViewersStatsTitle () {
|
|
|
|
if (this.statsStartDate && this.statsEndDate) {
|
|
|
|
return $localize`Viewers stats between ${this.statsStartDate.toLocaleString()} and ${this.statsEndDate.toLocaleString()}`
|
|
|
|
}
|
|
|
|
|
|
|
|
return $localize`Viewers stats`
|
|
|
|
}
|
|
|
|
|
|
|
|
onDateFilterChange () {
|
|
|
|
if (this.currentDateFilter === 'all') {
|
|
|
|
return this.resetZoom()
|
|
|
|
}
|
|
|
|
|
|
|
|
const idParts = this.currentDateFilter.split('|')
|
|
|
|
if (idParts.length === 2) {
|
|
|
|
return this.peertubeRouter.silentNavigate([], { startDate: idParts[0], endDate: idParts[1] })
|
|
|
|
}
|
2022-04-08 08:22:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private isTimeserieGraph (graphId: ActiveGraphId) {
|
|
|
|
return graphId === 'aggregateWatchTime' || graphId === 'viewers'
|
|
|
|
}
|
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
private loadOverallStats () {
|
2022-05-06 12:23:02 +00:00
|
|
|
this.statsService.getOverallStats({ videoId: this.video.uuid, startDate: this.statsStartDate, endDate: this.statsEndDate })
|
2022-04-05 12:03:52 +00:00
|
|
|
.subscribe({
|
|
|
|
next: res => {
|
|
|
|
this.countries = res.countries.slice(0, 10).map(c => ({
|
|
|
|
name: this.countryCodeToName(c.isoCode),
|
|
|
|
viewers: c.viewers
|
|
|
|
}))
|
|
|
|
|
|
|
|
this.buildOverallStatCard(res)
|
|
|
|
},
|
|
|
|
|
|
|
|
error: err => this.notifier.error(err.message)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-05-06 12:23:02 +00:00
|
|
|
private loadDateFilters () {
|
|
|
|
if (this.video.isLive) return this.loadLiveDateFilters()
|
|
|
|
|
|
|
|
return this.loadVODDateFilters()
|
|
|
|
}
|
|
|
|
|
|
|
|
private loadLiveDateFilters () {
|
|
|
|
this.liveService.listSessions(this.video.id)
|
|
|
|
.subscribe({
|
|
|
|
next: ({ data }) => {
|
|
|
|
const newFilters = data.map(session => this.buildLiveFilter(session))
|
|
|
|
|
|
|
|
this.dateFilters = this.dateFilters.concat(newFilters)
|
|
|
|
},
|
|
|
|
|
|
|
|
error: err => this.notifier.error(err.message)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private loadVODDateFilters () {
|
|
|
|
this.liveService.findLiveSessionFromVOD(this.video.id)
|
|
|
|
.subscribe({
|
|
|
|
next: session => {
|
|
|
|
this.dateFilters = this.dateFilters.concat([ this.buildLiveFilter(session) ])
|
|
|
|
},
|
|
|
|
|
|
|
|
error: err => {
|
|
|
|
if (err.status === HttpStatusCode.NOT_FOUND_404) return
|
|
|
|
|
|
|
|
this.notifier.error(err.message)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
private buildLiveFilter (session: LiveVideoSession) {
|
|
|
|
return {
|
|
|
|
id: session.startDate + '|' + session.endDate,
|
|
|
|
label: $localize`Of live of ${new Date(session.startDate).toLocaleString()}`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private addAndSelectCustomDateFilter () {
|
|
|
|
const exists = this.dateFilters.some(d => d.id === 'custom')
|
|
|
|
|
|
|
|
if (!exists) {
|
|
|
|
this.dateFilters = this.dateFilters.concat([
|
|
|
|
{
|
|
|
|
id: 'custom',
|
|
|
|
label: $localize`Custom dates`
|
|
|
|
}
|
|
|
|
])
|
|
|
|
}
|
|
|
|
|
|
|
|
this.currentDateFilter = 'custom'
|
|
|
|
}
|
|
|
|
|
|
|
|
private removeAndResetCustomDateFilter () {
|
|
|
|
this.dateFilters = this.dateFilters.filter(d => d.id !== 'custom')
|
|
|
|
|
|
|
|
this.currentDateFilter = 'all'
|
|
|
|
}
|
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
private buildOverallStatCard (overallStats: VideoStatsOverall) {
|
2022-05-06 12:23:02 +00:00
|
|
|
this.globalStatsCards = [
|
2022-04-05 12:03:52 +00:00
|
|
|
{
|
|
|
|
label: $localize`Views`,
|
2022-05-06 12:56:34 +00:00
|
|
|
value: this.numberFormatter.transform(this.video.views),
|
|
|
|
help: $localize`A view means that someone watched the video for at least 30 seconds`
|
2022-04-05 12:03:52 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
label: $localize`Likes`,
|
2022-05-05 11:47:51 +00:00
|
|
|
value: this.numberFormatter.transform(this.video.likes)
|
2022-05-06 12:23:02 +00:00
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
this.overallStatCards = [
|
2022-04-05 12:03:52 +00:00
|
|
|
{
|
|
|
|
label: $localize`Average watch time`,
|
|
|
|
value: secondsToTime(overallStats.averageWatchTime)
|
|
|
|
},
|
2022-05-06 12:23:02 +00:00
|
|
|
{
|
|
|
|
label: $localize`Total watch time`,
|
|
|
|
value: secondsToTime(overallStats.totalWatchTime)
|
|
|
|
},
|
2022-04-05 12:03:52 +00:00
|
|
|
{
|
|
|
|
label: $localize`Peak viewers`,
|
|
|
|
value: this.numberFormatter.transform(overallStats.viewersPeak),
|
2022-04-20 07:17:23 +00:00
|
|
|
moreInfo: overallStats.viewersPeak !== 0
|
|
|
|
? $localize`at ${new Date(overallStats.viewersPeakDate).toLocaleString()}`
|
|
|
|
: undefined
|
2022-04-05 12:03:52 +00:00
|
|
|
}
|
|
|
|
]
|
2022-05-06 12:23:02 +00:00
|
|
|
|
|
|
|
if (overallStats.countries.length !== 0) {
|
|
|
|
this.overallStatCards.push({
|
|
|
|
label: $localize`Countries`,
|
|
|
|
value: this.numberFormatter.transform(overallStats.countries.length)
|
|
|
|
})
|
|
|
|
}
|
2022-04-05 12:03:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private loadChart () {
|
|
|
|
const obsBuilders: { [ id in ActiveGraphId ]: Observable<ChartIngestData> } = {
|
|
|
|
retention: this.statsService.getRetentionStats(this.video.uuid),
|
2022-04-08 08:22:56 +00:00
|
|
|
|
|
|
|
aggregateWatchTime: this.statsService.getTimeserieStats({
|
|
|
|
videoId: this.video.uuid,
|
2022-05-06 12:23:02 +00:00
|
|
|
startDate: this.statsStartDate,
|
|
|
|
endDate: this.statsEndDate,
|
2022-04-08 08:22:56 +00:00
|
|
|
metric: 'aggregateWatchTime'
|
|
|
|
}),
|
|
|
|
viewers: this.statsService.getTimeserieStats({
|
|
|
|
videoId: this.video.uuid,
|
2022-05-06 12:23:02 +00:00
|
|
|
startDate: this.statsStartDate,
|
|
|
|
endDate: this.statsEndDate,
|
2022-04-08 08:22:56 +00:00
|
|
|
metric: 'viewers'
|
|
|
|
}),
|
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
countries: of(this.countries)
|
|
|
|
}
|
|
|
|
|
|
|
|
obsBuilders[this.activeGraphId].subscribe({
|
|
|
|
next: res => {
|
2022-04-08 08:22:56 +00:00
|
|
|
this.chartIngestData[this.activeGraphId] = res
|
|
|
|
|
|
|
|
this.chartOptions[this.activeGraphId] = this.buildChartOptions(this.activeGraphId)
|
2022-04-05 12:03:52 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
error: err => this.notifier.error(err.message)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
private buildChartOptions (graphId: ActiveGraphId): ChartConfiguration<'line' | 'bar'> {
|
2022-04-05 12:03:52 +00:00
|
|
|
const dataBuilders: {
|
|
|
|
[ id in ActiveGraphId ]: (rawData: ChartIngestData) => ChartBuilderResult
|
|
|
|
} = {
|
|
|
|
retention: (rawData: VideoStatsRetention) => this.buildRetentionChartOptions(rawData),
|
|
|
|
aggregateWatchTime: (rawData: VideoStatsTimeserie) => this.buildTimeserieChartOptions(rawData),
|
|
|
|
viewers: (rawData: VideoStatsTimeserie) => this.buildTimeserieChartOptions(rawData),
|
|
|
|
countries: (rawData: CountryData) => this.buildCountryChartOptions(rawData)
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
const { type, data, displayLegend, plugins } = dataBuilders[graphId](this.chartIngestData[graphId])
|
|
|
|
|
|
|
|
const self = this
|
2022-04-05 12:03:52 +00:00
|
|
|
|
|
|
|
return {
|
|
|
|
type,
|
|
|
|
data,
|
|
|
|
|
|
|
|
options: {
|
|
|
|
responsive: true,
|
|
|
|
|
|
|
|
scales: {
|
2022-04-08 08:22:56 +00:00
|
|
|
x: {
|
|
|
|
ticks: {
|
|
|
|
callback: function (value) {
|
|
|
|
return self.formatXTick({
|
|
|
|
graphId,
|
|
|
|
value,
|
|
|
|
data: self.chartIngestData[graphId] as VideoStatsTimeserie,
|
|
|
|
scale: this
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
y: {
|
|
|
|
beginAtZero: true,
|
|
|
|
|
|
|
|
max: this.activeGraphId === 'retention'
|
|
|
|
? 100
|
|
|
|
: undefined,
|
|
|
|
|
|
|
|
ticks: {
|
2022-04-08 08:22:56 +00:00
|
|
|
callback: value => this.formatYTick({ graphId, value })
|
2022-04-05 12:03:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
plugins: {
|
|
|
|
legend: {
|
|
|
|
display: displayLegend
|
|
|
|
},
|
|
|
|
tooltip: {
|
|
|
|
callbacks: {
|
2022-04-08 08:22:56 +00:00
|
|
|
title: items => this.formatTooltipTitle({ graphId, items }),
|
|
|
|
label: value => this.formatYTick({ graphId, value: value.raw as number | string })
|
2022-04-05 12:03:52 +00:00
|
|
|
}
|
2022-04-08 08:22:56 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
...plugins
|
2022-04-05 12:03:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
private buildRetentionChartOptions (rawData: VideoStatsRetention): ChartBuilderResult {
|
2022-04-05 12:03:52 +00:00
|
|
|
const labels: string[] = []
|
|
|
|
const data: number[] = []
|
|
|
|
|
|
|
|
for (const d of rawData.data) {
|
|
|
|
labels.push(secondsToTime(d.second))
|
|
|
|
data.push(d.retentionPercent)
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: 'line' as 'line',
|
|
|
|
|
|
|
|
displayLegend: false,
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
plugins: {
|
|
|
|
...this.buildDisabledZoomPlugin()
|
|
|
|
},
|
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
data: {
|
|
|
|
labels,
|
|
|
|
datasets: [
|
|
|
|
{
|
|
|
|
data,
|
|
|
|
borderColor: this.buildChartColor()
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
private buildTimeserieChartOptions (rawData: VideoStatsTimeserie): ChartBuilderResult {
|
2022-04-05 12:03:52 +00:00
|
|
|
const labels: string[] = []
|
|
|
|
const data: number[] = []
|
|
|
|
|
|
|
|
for (const d of rawData.data) {
|
2022-04-08 08:22:56 +00:00
|
|
|
labels.push(d.date)
|
2022-04-05 12:03:52 +00:00
|
|
|
data.push(d.value)
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: 'line' as 'line',
|
|
|
|
|
|
|
|
displayLegend: false,
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
plugins: {
|
|
|
|
zoom: {
|
|
|
|
zoom: {
|
|
|
|
wheel: {
|
|
|
|
enabled: false
|
|
|
|
},
|
|
|
|
drag: {
|
|
|
|
enabled: true
|
|
|
|
},
|
|
|
|
pinch: {
|
|
|
|
enabled: true
|
|
|
|
},
|
|
|
|
mode: 'x',
|
|
|
|
onZoomComplete: ({ chart }) => {
|
|
|
|
const { min, max } = chart.scales.x
|
|
|
|
|
|
|
|
const startDate = rawData.data[min].date
|
2022-04-15 08:54:13 +00:00
|
|
|
const endDate = this.buildZoomEndDate(rawData.groupInterval, rawData.data[max].date)
|
2022-04-08 08:22:56 +00:00
|
|
|
|
|
|
|
this.peertubeRouter.silentNavigate([], { startDate, endDate })
|
2022-05-06 12:23:02 +00:00
|
|
|
this.addAndSelectCustomDateFilter()
|
2022-04-08 08:22:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
data: {
|
|
|
|
labels,
|
|
|
|
datasets: [
|
|
|
|
{
|
|
|
|
data,
|
|
|
|
borderColor: this.buildChartColor()
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
private buildCountryChartOptions (rawData: CountryData): ChartBuilderResult {
|
2022-04-05 12:03:52 +00:00
|
|
|
const labels: string[] = []
|
|
|
|
const data: number[] = []
|
|
|
|
|
|
|
|
for (const d of rawData) {
|
|
|
|
labels.push(d.name)
|
|
|
|
data.push(d.viewers)
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
type: 'bar' as 'bar',
|
|
|
|
|
|
|
|
displayLegend: true,
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
plugins: {
|
|
|
|
...this.buildDisabledZoomPlugin()
|
2022-04-05 12:03:52 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
data: {
|
|
|
|
labels,
|
|
|
|
datasets: [
|
|
|
|
{
|
|
|
|
label: $localize`Viewers`,
|
|
|
|
backgroundColor: this.buildChartColor(),
|
|
|
|
maxBarThickness: 20,
|
|
|
|
data
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private buildChartColor () {
|
|
|
|
return getComputedStyle(document.body).getPropertyValue('--mainColorLighter')
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
private formatXTick (options: {
|
|
|
|
graphId: ActiveGraphId
|
|
|
|
value: number | string
|
|
|
|
data: VideoStatsTimeserie
|
|
|
|
scale: Scale
|
|
|
|
}) {
|
|
|
|
const { graphId, value, data, scale } = options
|
|
|
|
|
|
|
|
const label = scale.getLabelForValue(value as number)
|
|
|
|
|
|
|
|
if (!this.isTimeserieGraph(graphId)) {
|
|
|
|
return label
|
|
|
|
}
|
|
|
|
|
|
|
|
const date = new Date(label)
|
|
|
|
|
2022-05-06 12:23:02 +00:00
|
|
|
if (data.groupInterval.match(/ month?$/)) {
|
|
|
|
return date.toLocaleDateString([], { month: 'numeric' })
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
if (data.groupInterval.match(/ days?$/)) {
|
|
|
|
return date.toLocaleDateString([], { month: 'numeric', day: 'numeric' })
|
|
|
|
}
|
|
|
|
|
|
|
|
if (data.groupInterval.match(/ hours?$/)) {
|
|
|
|
return date.toLocaleTimeString([], { month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric' })
|
|
|
|
}
|
|
|
|
|
|
|
|
return date.toLocaleTimeString([], { hour: 'numeric', minute: 'numeric' })
|
|
|
|
}
|
|
|
|
|
|
|
|
private formatYTick (options: {
|
|
|
|
graphId: ActiveGraphId
|
|
|
|
value: number | string
|
|
|
|
}) {
|
|
|
|
const { graphId, value } = options
|
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
if (graphId === 'retention') return value + ' %'
|
|
|
|
if (graphId === 'aggregateWatchTime') return secondsToTime(+value)
|
|
|
|
|
|
|
|
return value.toLocaleString()
|
|
|
|
}
|
|
|
|
|
2022-04-08 08:22:56 +00:00
|
|
|
private formatTooltipTitle (options: {
|
|
|
|
graphId: ActiveGraphId
|
|
|
|
items: TooltipItem<any>[]
|
|
|
|
}) {
|
|
|
|
const { graphId, items } = options
|
|
|
|
const item = items[0]
|
|
|
|
|
|
|
|
if (this.isTimeserieGraph(graphId)) return new Date(item.label).toLocaleString()
|
|
|
|
|
|
|
|
return item.label
|
|
|
|
}
|
|
|
|
|
2022-04-05 12:03:52 +00:00
|
|
|
private countryCodeToName (code: string) {
|
|
|
|
const intl: any = Intl
|
|
|
|
if (!intl.DisplayNames) return code
|
|
|
|
|
|
|
|
const regionNames = new intl.DisplayNames([], { type: 'region' })
|
|
|
|
|
|
|
|
return regionNames.of(code)
|
|
|
|
}
|
2022-04-08 08:22:56 +00:00
|
|
|
|
|
|
|
private buildDisabledZoomPlugin () {
|
|
|
|
return {
|
|
|
|
zoom: {
|
|
|
|
zoom: {
|
|
|
|
wheel: {
|
|
|
|
enabled: false
|
|
|
|
},
|
|
|
|
drag: {
|
|
|
|
enabled: false
|
|
|
|
},
|
|
|
|
pinch: {
|
|
|
|
enabled: false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-15 08:54:13 +00:00
|
|
|
|
|
|
|
private buildZoomEndDate (groupInterval: string, last: string) {
|
|
|
|
const date = new Date(last)
|
|
|
|
|
|
|
|
// Remove parts of the date we don't need
|
|
|
|
if (groupInterval.endsWith(' day') || groupInterval.endsWith(' days')) {
|
|
|
|
date.setHours(23, 59, 59)
|
|
|
|
} else if (groupInterval.endsWith(' hour') || groupInterval.endsWith(' hours')) {
|
|
|
|
date.setMinutes(59, 59)
|
|
|
|
} else {
|
|
|
|
date.setSeconds(59)
|
|
|
|
}
|
|
|
|
|
|
|
|
return date.toISOString()
|
|
|
|
}
|
2022-04-05 12:03:52 +00:00
|
|
|
}
|