diff options
8 files changed, 70 insertions, 45 deletions
diff --git a/client/package.json b/client/package.json index c6a5fa1bb..52647ce1d 100644 --- a/client/package.json +++ b/client/package.json | |||
@@ -56,6 +56,7 @@ | |||
56 | "@ngx-loading-bar/router": "^4.2.0", | 56 | "@ngx-loading-bar/router": "^4.2.0", |
57 | "@ngx-meta/core": "^8.0.2", | 57 | "@ngx-meta/core": "^8.0.2", |
58 | "@ngx-translate/i18n-polyfill": "^1.0.0", | 58 | "@ngx-translate/i18n-polyfill": "^1.0.0", |
59 | "@types/chart.js": "^2.9.16", | ||
59 | "@types/core-js": "^2.5.2", | 60 | "@types/core-js": "^2.5.2", |
60 | "@types/debug": "^4.1.5", | 61 | "@types/debug": "^4.1.5", |
61 | "@types/hls.js": "^0.12.4", | 62 | "@types/hls.js": "^0.12.4", |
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html index 94e74938b..03d45227e 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html | |||
@@ -20,7 +20,7 @@ | |||
20 | <div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> | 20 | <div i18n class="video-channel-followers">{videoChannel.followersCount, plural, =1 {1 subscriber} other {{{ videoChannel.followersCount }} subscribers}}</div> |
21 | 21 | ||
22 | <div *ngIf="!isInSmallView" class="w-100 d-flex justify-content-end"> | 22 | <div *ngIf="!isInSmallView" class="w-100 d-flex justify-content-end"> |
23 | <p-chart *ngIf="videoChannelsData && videoChannelsData[i]" type="line" [data]="videoChannelsData[i]" [options]="chartOptions" width="40vw" height="100px"></p-chart> | 23 | <p-chart *ngIf="videoChannelsChartData && videoChannelsChartData[i]" type="line" [data]="videoChannelsChartData[i]" [options]="chartOptions" width="40vw" height="100px"></p-chart> |
24 | </div> | 24 | </div> |
25 | </div> | 25 | </div> |
26 | 26 | ||
diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts index 27a157621..153fc0127 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts | |||
@@ -8,7 +8,8 @@ import { ScreenService } from '@app/shared/misc/screen.service' | |||
8 | import { User } from '@app/shared' | 8 | import { User } from '@app/shared' |
9 | import { flatMap } from 'rxjs/operators' | 9 | import { flatMap } from 'rxjs/operators' |
10 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
11 | import { minBy, maxBy } from 'lodash-es' | 11 | import { min, minBy, max, maxBy } from 'lodash-es' |
12 | import { ChartData } from 'chart.js' | ||
12 | 13 | ||
13 | @Component({ | 14 | @Component({ |
14 | selector: 'my-account-video-channels', | 15 | selector: 'my-account-video-channels', |
@@ -17,7 +18,7 @@ import { minBy, maxBy } from 'lodash-es' | |||
17 | }) | 18 | }) |
18 | export class MyAccountVideoChannelsComponent implements OnInit { | 19 | export class MyAccountVideoChannelsComponent implements OnInit { |
19 | videoChannels: VideoChannel[] = [] | 20 | videoChannels: VideoChannel[] = [] |
20 | videoChannelsData: any[] | 21 | videoChannelsChartData: ChartData[] |
21 | videoChannelsMinimumDailyViews = 0 | 22 | videoChannelsMinimumDailyViews = 0 |
22 | videoChannelsMaximumDailyViews: number | 23 | videoChannelsMaximumDailyViews: number |
23 | 24 | ||
@@ -125,7 +126,9 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
125 | .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true))) | 126 | .pipe(flatMap(() => this.videoChannelService.listAccountVideoChannels(this.user.account, null, true))) |
126 | .subscribe(res => { | 127 | .subscribe(res => { |
127 | this.videoChannels = res.data | 128 | this.videoChannels = res.data |
128 | this.videoChannelsData = this.videoChannels.map(v => ({ | 129 | |
130 | // chart data | ||
131 | this.videoChannelsChartData = this.videoChannels.map(v => ({ | ||
129 | labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()), | 132 | labels: v.viewsPerDay.map(day => day.date.toLocaleDateString()), |
130 | datasets: [ | 133 | datasets: [ |
131 | { | 134 | { |
@@ -135,9 +138,22 @@ export class MyAccountVideoChannelsComponent implements OnInit { | |||
135 | borderColor: "#c6c6c6" | 138 | borderColor: "#c6c6c6" |
136 | } | 139 | } |
137 | ] | 140 | ] |
138 | })) | 141 | } as ChartData)) |
139 | this.videoChannelsMinimumDailyViews = minBy(this.videoChannels.map(v => minBy(v.viewsPerDay, day => day.views)), day => day.views).views | 142 | |
140 | this.videoChannelsMaximumDailyViews = maxBy(this.videoChannels.map(v => maxBy(v.viewsPerDay, day => day.views)), day => day.views).views | 143 | // chart options that depend on chart data: |
144 | // we don't want to skew values and have min at 0, so we define what the floor/ceiling is here | ||
145 | this.videoChannelsMinimumDailyViews = min( | ||
146 | this.videoChannels.map(v => minBy( // compute local minimum daily views for each channel, by their "views" attribute | ||
147 | v.viewsPerDay, | ||
148 | day => day.views | ||
149 | ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute | ||
150 | ) | ||
151 | this.videoChannelsMaximumDailyViews = max( | ||
152 | this.videoChannels.map(v => maxBy( // compute local maximum daily views for each channel, by their "views" attribute | ||
153 | v.viewsPerDay, | ||
154 | day => day.views | ||
155 | ).views) // the object returned is a ViewPerDate, so we still need to get the views attribute | ||
156 | ) | ||
141 | }) | 157 | }) |
142 | } | 158 | } |
143 | } | 159 | } |
diff --git a/client/src/app/shared/video-channel/video-channel.model.ts b/client/src/app/shared/video-channel/video-channel.model.ts index c93af0ca5..617d6d44d 100644 --- a/client/src/app/shared/video-channel/video-channel.model.ts +++ b/client/src/app/shared/video-channel/video-channel.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | import { VideoChannel as ServerVideoChannel, viewsPerTime } from '../../../../../shared/models/videos' | 1 | import { VideoChannel as ServerVideoChannel, ViewsPerDate } from '../../../../../shared/models/videos' |
2 | import { Actor } from '../actor/actor.model' | 2 | import { Actor } from '../actor/actor.model' |
3 | import { Account } from '../../../../../shared/models/actors' | 3 | import { Account } from '../../../../../shared/models/actors' |
4 | 4 | ||
@@ -12,7 +12,7 @@ export class VideoChannel extends Actor implements ServerVideoChannel { | |||
12 | ownerAccount?: Account | 12 | ownerAccount?: Account |
13 | ownerBy?: string | 13 | ownerBy?: string |
14 | ownerAvatarUrl?: string | 14 | ownerAvatarUrl?: string |
15 | viewsPerDay?: viewsPerTime[] | 15 | viewsPerDay?: ViewsPerDate[] |
16 | 16 | ||
17 | constructor (hash: ServerVideoChannel) { | 17 | constructor (hash: ServerVideoChannel) { |
18 | super(hash) | 18 | super(hash) |
diff --git a/client/yarn.lock b/client/yarn.lock index b3f38a664..e34da3d6e 100644 --- a/client/yarn.lock +++ b/client/yarn.lock | |||
@@ -1149,6 +1149,13 @@ | |||
1149 | dependencies: | 1149 | dependencies: |
1150 | "@types/node" "*" | 1150 | "@types/node" "*" |
1151 | 1151 | ||
1152 | "@types/chart.js@^2.9.16": | ||
1153 | version "2.9.16" | ||
1154 | resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.16.tgz#ac9d268fa192c0ec0efd740f802683e3ed97642c" | ||
1155 | integrity sha512-Mofg7xFIeAWME46YMVKHPCyUz2Z0KsVMNE1f4oF3T74mK3RiPQxOm9qzoeNTyMs6lpl4x0tiHL+Wsz2DHCxQlQ== | ||
1156 | dependencies: | ||
1157 | moment "^2.10.2" | ||
1158 | |||
1152 | "@types/core-js@^2.5.2": | 1159 | "@types/core-js@^2.5.2": |
1153 | version "2.5.2" | 1160 | version "2.5.2" |
1154 | resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.2.tgz#d4c25420044d4a5b65e00a82fc04b7824b62691f" | 1161 | resolved "https://registry.yarnpkg.com/@types/core-js/-/core-js-2.5.2.tgz#d4c25420044d4a5b65e00a82fc04b7824b62691f" |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index 78fc3d7e4..642e129ff 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -166,42 +166,43 @@ export type SummaryOptions = { | |||
166 | VideoModel | 166 | VideoModel |
167 | ] | 167 | ] |
168 | }, | 168 | }, |
169 | [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => ({ | 169 | [ScopeNames.WITH_STATS]: (options: AvailableWithStatsOptions = { daysPrior: 30 }) => { |
170 | attributes: { | 170 | const daysPrior = parseInt(options.daysPrior + '', 10) |
171 | include: [ | 171 | |
172 | [ | 172 | return { |
173 | literal( | 173 | attributes: { |
174 | '(' + | 174 | include: [ |
175 | `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` + | 175 | [ |
176 | 'FROM ( ' + | 176 | literal( |
177 | 'WITH ' + | 177 | '(' + |
178 | 'days AS ( ' + | 178 | `SELECT string_agg(concat_ws('|', t.day, t.views), ',') ` + |
179 | `SELECT generate_series(date_trunc('day', now()) - '${options.daysPrior} day'::interval, ` + | 179 | 'FROM ( ' + |
180 | `date_trunc('day', now()), '1 day'::interval) AS day ` + | 180 | 'WITH ' + |
181 | '), ' + | 181 | 'days AS ( ' + |
182 | 'views AS ( ' + | 182 | `SELECT generate_series(date_trunc('day', now()) - '${daysPrior} day'::interval, ` + |
183 | 'SELECT * ' + | 183 | `date_trunc('day', now()), '1 day'::interval) AS day ` + |
184 | 'FROM "videoView" ' + | 184 | '), ' + |
185 | 'WHERE "videoView"."videoId" IN ( ' + | 185 | 'views AS ( ' + |
186 | 'SELECT "video"."id" ' + | 186 | 'SELECT v.* ' + |
187 | 'FROM "video" ' + | 187 | 'FROM "videoView" AS v ' + |
188 | 'INNER JOIN "video" ON "video"."id" = v."videoId" ' + | ||
188 | 'WHERE "video"."channelId" = "VideoChannelModel"."id" ' + | 189 | 'WHERE "video"."channelId" = "VideoChannelModel"."id" ' + |
189 | ') ' + | 190 | ') ' + |
190 | ') ' + | 191 | 'SELECT days.day AS day, ' + |
191 | 'SELECT days.day AS day, ' + | 192 | 'COALESCE(SUM(views.views), 0) AS views ' + |
192 | 'COALESCE(SUM(views.views), 0) AS views ' + | 193 | 'FROM days ' + |
193 | 'FROM days ' + | 194 | `LEFT JOIN views ON date_trunc('day', "views"."startDate") = date_trunc('day', days.day) ` + |
194 | `LEFT JOIN views ON date_trunc('day', "views"."startDate") = date_trunc('day', days.day) ` + | 195 | 'GROUP BY day ' + |
195 | 'GROUP BY 1 ' + | 196 | 'ORDER BY day ' + |
196 | 'ORDER BY day ' + | 197 | ') t' + |
197 | ') t' + | 198 | ')' |
198 | ')' | 199 | ), |
199 | ), | 200 | 'viewsPerDay' |
200 | 'viewsPerDay' | 201 | ] |
201 | ] | 202 | ] |
202 | ] | 203 | } |
203 | } | 204 | } |
204 | }) | 205 | } |
205 | })) | 206 | })) |
206 | @Table({ | 207 | @Table({ |
207 | tableName: 'videoChannel', | 208 | tableName: 'videoChannel', |
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index bde45584d..876a6ab66 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts | |||
@@ -2,7 +2,7 @@ | |||
2 | 2 | ||
3 | import * as chai from 'chai' | 3 | import * as chai from 'chai' |
4 | import 'mocha' | 4 | import 'mocha' |
5 | import { User, Video, VideoChannel, viewsPerTime, VideoDetails } from '../../../../shared/index' | 5 | import { User, Video, VideoChannel, ViewsPerDate, VideoDetails } from '../../../../shared/index' |
6 | import { | 6 | import { |
7 | cleanupTests, | 7 | cleanupTests, |
8 | createUser, | 8 | createUser, |
@@ -376,7 +376,7 @@ describe('Test video channels', function () { | |||
376 | res.body.data.forEach((channel: VideoChannel) => { | 376 | res.body.data.forEach((channel: VideoChannel) => { |
377 | expect(channel).to.haveOwnProperty('viewsPerDay') | 377 | expect(channel).to.haveOwnProperty('viewsPerDay') |
378 | expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today | 378 | expect(channel.viewsPerDay).to.have.length(30 + 1) // daysPrior + today |
379 | channel.viewsPerDay.forEach((v: viewsPerTime) => { | 379 | channel.viewsPerDay.forEach((v: ViewsPerDate) => { |
380 | expect(v.date).to.be.an('string') | 380 | expect(v.date).to.be.an('string') |
381 | expect(v.views).to.equal(0) | 381 | expect(v.views).to.equal(0) |
382 | }) | 382 | }) |
diff --git a/shared/models/videos/channel/video-channel.model.ts b/shared/models/videos/channel/video-channel.model.ts index 5fe6609d9..421004e68 100644 --- a/shared/models/videos/channel/video-channel.model.ts +++ b/shared/models/videos/channel/video-channel.model.ts | |||
@@ -2,7 +2,7 @@ import { Actor } from '../../actors/actor.model' | |||
2 | import { Account } from '../../actors/index' | 2 | import { Account } from '../../actors/index' |
3 | import { Avatar } from '../../avatars' | 3 | import { Avatar } from '../../avatars' |
4 | 4 | ||
5 | export type viewsPerTime = { | 5 | export type ViewsPerDate = { |
6 | date: Date | 6 | date: Date |
7 | views: number | 7 | views: number |
8 | } | 8 | } |
@@ -13,7 +13,7 @@ export interface VideoChannel extends Actor { | |||
13 | support: string | 13 | support: string |
14 | isLocal: boolean | 14 | isLocal: boolean |
15 | ownerAccount?: Account | 15 | ownerAccount?: Account |
16 | viewsPerDay?: viewsPerTime[] // chronologically ordered | 16 | viewsPerDay?: ViewsPerDate[] // chronologically ordered |
17 | } | 17 | } |
18 | 18 | ||
19 | export interface VideoChannelSummary { | 19 | export interface VideoChannelSummary { |