19 changed files with 629 additions and 214 deletions
@ -0,0 +1,10 @@ |
|||||
|
export const VipGiveMethod = [ |
||||
|
"Бесплатно", |
||||
|
"Steam", |
||||
|
"Qiwi", |
||||
|
"Админ", |
||||
|
"Убрана", |
||||
|
"Итого",//??????
|
||||
|
"Donation Alerts", |
||||
|
"Промокод" |
||||
|
]; |
||||
@ -0,0 +1,203 @@ |
|||||
|
import {Component, OnInit} from "@angular/core"; |
||||
|
import {Chart} from "chart.js/auto"; |
||||
|
import {PerPeriodStatistic} from "../../services/graph.service"; |
||||
|
import {ServerService} from "../../services/server.service"; |
||||
|
import {SearchFilter} from "../../entities/search/SearchFilter"; |
||||
|
import {BaseUtils} from "../../utils/BaseUtils"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-abstract-per-period-graph', |
||||
|
template: '' |
||||
|
}) |
||||
|
export abstract class AbstractPerperiodGraphComponent implements OnInit { |
||||
|
protected constructor(protected serverService: ServerService) {} |
||||
|
ngOnInit(): void { |
||||
|
const fill = (res:any) => { |
||||
|
const keys = res.data ? Object.keys(res.data) : []; |
||||
|
for (const key of keys) { |
||||
|
// @ts-ignore
|
||||
|
this.serverList.push({name: res.data[key].name, server_id: key}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.serverService.getServers().subscribe( |
||||
|
(res) => { |
||||
|
fill(res); |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
settingsOfChart: { |
||||
|
chart: Chart|null |
||||
|
period: 'day' | 'month' | 'year', |
||||
|
loading: boolean |
||||
|
} = { |
||||
|
chart: null, |
||||
|
period: "day", |
||||
|
loading: false |
||||
|
}; |
||||
|
|
||||
|
serverList: {name: string, server_id: string }[] = []; |
||||
|
|
||||
|
public getServerName(server_id: string|null) { |
||||
|
try { |
||||
|
// @ts-ignore
|
||||
|
return this.serverList.filter(s => s.server_id == server_id).pop().name; |
||||
|
} catch (e) { |
||||
|
return "Неизвестно"; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public getSearchFilter(steam64:string|null): SearchFilter { |
||||
|
const filter: SearchFilter = new SearchFilter(); |
||||
|
if (steam64) |
||||
|
filter.addAccountToSearch(steam64); |
||||
|
let endDate = new Date(); |
||||
|
endDate.setUTCHours(23, 59, 59); |
||||
|
filter.addEndTimeToSearch(endDate.getTime()); |
||||
|
switch (this.settingsOfChart.period) { |
||||
|
case "day": { |
||||
|
endDate.setDate(endDate.getDate() - 30); |
||||
|
break; |
||||
|
} |
||||
|
case "month": { |
||||
|
endDate.setDate(endDate.getDate() - 365); |
||||
|
break; |
||||
|
} |
||||
|
case "year": { |
||||
|
endDate.setDate(endDate.getDate() - (365*10)); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
filter.addBeginTimeToSearch(endDate); |
||||
|
return filter; |
||||
|
} |
||||
|
|
||||
|
public updateGraph(objs: PerPeriodStatistic[], |
||||
|
canvasName: string, |
||||
|
tooltopFooter:any = null, |
||||
|
text = "", |
||||
|
timeDelimiter:number = -1) { |
||||
|
let chartConfig = {type: 'bar', data: {}, options: { |
||||
|
plugins: { |
||||
|
title: { |
||||
|
display: true, |
||||
|
text: text |
||||
|
}, |
||||
|
tooltip:{ |
||||
|
callbacks: { |
||||
|
footer: tooltopFooter |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
responsive: true, |
||||
|
scales: { |
||||
|
x: { |
||||
|
stacked: true, |
||||
|
}, |
||||
|
y: { |
||||
|
stacked: true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
let chartData: {labels: string[], datasets: any[]} = { |
||||
|
labels: [],//даты
|
||||
|
datasets: [] |
||||
|
/** |
||||
|
* { |
||||
|
* label: 'Dataset 1', |
||||
|
* data: Utils.numbers(NUMBER_CFG), |
||||
|
* backgroundColor: Utils.CHART_COLORS.red, |
||||
|
* }, |
||||
|
*/ |
||||
|
}; |
||||
|
let valueOnServer: {[srv_name: string]: number[]} = {} |
||||
|
let srvList: string[] = []; |
||||
|
|
||||
|
let maxValue = 0; |
||||
|
//////
|
||||
|
objs.forEach((obj) => {if (obj.value > maxValue) maxValue = obj.value}) |
||||
|
|
||||
|
console.log(timeDelimiter) |
||||
|
if (timeDelimiter != -1) { |
||||
|
if (maxValue >= 60) { |
||||
|
timeDelimiter = 60; |
||||
|
chartConfig.options.plugins.title.text = "Наиграно минут по серверам"; |
||||
|
} //po minutam
|
||||
|
if (maxValue >= 60 * 60) { |
||||
|
timeDelimiter = 60 * 60; |
||||
|
chartConfig.options.plugins.title.text = "Наиграно часов по серверам"; |
||||
|
} //po chasam
|
||||
|
if (maxValue >= 60 * 60 * 24) { |
||||
|
timeDelimiter = 60 * 60 * 24; |
||||
|
chartConfig.options.plugins.title.text = "Наиграно дней по серверам"; |
||||
|
}//po dnyam
|
||||
|
|
||||
|
const tooltipFooterInternal = (items:any) => { //todo remove
|
||||
|
return "Наиграно: " + BaseUtils.formatSeconds(items.pop().parsed.y * timeDelimiter); |
||||
|
}; |
||||
|
chartConfig.options.plugins.tooltip.callbacks.footer = tooltipFooterInternal; |
||||
|
} |
||||
|
console.log(timeDelimiter) |
||||
|
|
||||
|
const groupByDate = objs.reduce((acc: {[date: string]: PerPeriodStatistic[]}, obj) => { |
||||
|
const key = obj.date; |
||||
|
if (!acc[key]) acc[key] = []; |
||||
|
acc[key].push(obj) |
||||
|
return acc; |
||||
|
}, {}); |
||||
|
chartData.labels = Object.keys(groupByDate); |
||||
|
//console.log(groupByDate);
|
||||
|
|
||||
|
chartData.labels.forEach( |
||||
|
(date) => { |
||||
|
groupByDate[date].forEach((stat) => { |
||||
|
if (srvList.indexOf(stat.srv_id) == -1) |
||||
|
srvList.push(stat.srv_id) |
||||
|
}) |
||||
|
} |
||||
|
); |
||||
|
//console.log(srvList)
|
||||
|
srvList.forEach((srv) => { |
||||
|
valueOnServer[srv] = []; |
||||
|
}); |
||||
|
chartData.labels.forEach( |
||||
|
(date) => { |
||||
|
let filled: string[] = []; |
||||
|
groupByDate[date].forEach( |
||||
|
(obj) => { |
||||
|
if (timeDelimiter == -1) { |
||||
|
valueOnServer[obj.srv_id].push(obj.value); |
||||
|
} else { |
||||
|
valueOnServer[obj.srv_id].push(obj.value / timeDelimiter); |
||||
|
} |
||||
|
filled.push(obj.srv_id); |
||||
|
} |
||||
|
) |
||||
|
srvList.forEach( |
||||
|
(srv) => { |
||||
|
if (filled.indexOf(srv) == -1) { |
||||
|
valueOnServer[srv].push(0); |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
}, |
||||
|
) |
||||
|
//console.log(valueOnServer)
|
||||
|
Object.keys(valueOnServer).forEach( |
||||
|
(key) => { |
||||
|
chartData.datasets.push( |
||||
|
{label: this.getServerName(key) + " ("+key+")", data: valueOnServer[key]} |
||||
|
) |
||||
|
} |
||||
|
) |
||||
|
///
|
||||
|
chartConfig.data = chartData; |
||||
|
|
||||
|
if (this.settingsOfChart.chart) |
||||
|
this.settingsOfChart.chart.destroy(); |
||||
|
// @ts-ignore
|
||||
|
this.settingsOfChart.chart = new Chart(canvasName, chartConfig); |
||||
|
this.settingsOfChart.loading = false; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
import {Component, Input} from "@angular/core"; |
||||
|
import {AbstractPerperiodGraphComponent} from "./abstract.perperiod.graph.component"; |
||||
|
import {ServerService} from "../../services/server.service"; |
||||
|
import {GraphService} from "../../services/graph.service"; |
||||
|
import {MatSnackBar} from "@angular/material/snack-bar"; |
||||
|
import {BaseUtils} from "../../utils/BaseUtils"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: "app-connections-graph", |
||||
|
template: ` |
||||
|
<div> |
||||
|
<mat-radio-group |
||||
|
style="display: flex; justify-content: center;flex-direction: row;" |
||||
|
[(ngModel)]="settingsOfChart.period" |
||||
|
[disabled]="settingsOfChart.loading" |
||||
|
(ngModelChange)="updateConnectionsGraph()"> |
||||
|
<mat-radio-button style="padding-right: 1%; padding-left: 1%" value="day">За месяц</mat-radio-button> |
||||
|
<mat-radio-button style="padding-right: 1%; padding-left: 1%" value="month">За год</mat-radio-button> |
||||
|
<mat-radio-button style="padding-right: 1%; padding-left: 1%" value="year">За 10 лет</mat-radio-button> |
||||
|
</mat-radio-group> |
||||
|
</div> |
||||
|
<div class="chart-container"> |
||||
|
<canvas id="connectionsCanvasChart" >{{ settingsOfChart.chart }}</canvas> |
||||
|
</div>` |
||||
|
}) |
||||
|
export class ConnectionsGraphComponent extends AbstractPerperiodGraphComponent { |
||||
|
|
||||
|
@Input("steam64") |
||||
|
steam64: string|null = null; |
||||
|
|
||||
|
tooltipFooter = (items:any) => { |
||||
|
return `Подключений: ${items.pop().parsed.y}` |
||||
|
}; |
||||
|
|
||||
|
constructor(protected override serverService: ServerService, |
||||
|
private graphService: GraphService, |
||||
|
private snack: MatSnackBar) { |
||||
|
super(serverService); |
||||
|
} |
||||
|
|
||||
|
connectionsTabChanged(event: any, tabIndex: number) { |
||||
|
if (event.index == tabIndex) { |
||||
|
this.updateConnectionsGraph(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateConnectionsGraph() { |
||||
|
if (this.steam64 || true) { |
||||
|
if (this.settingsOfChart.loading) return; |
||||
|
this.settingsOfChart.loading = true; |
||||
|
this.graphService.getConnectionsOnPeriod(this.getSearchFilter(this.steam64)).subscribe( |
||||
|
(objs) => { |
||||
|
this.updateGraph(objs, "connectionsCanvasChart", |
||||
|
this.tooltipFooter, |
||||
|
"Количество уникальных игроков по серверам (уникальность, первый заход на любой из серверов, сыграл более 5 минут,Ï последующие не учитываются)", |
||||
|
-1); |
||||
|
}, (err) => { |
||||
|
this.settingsOfChart.loading = false; |
||||
|
this.snack.open("Ошибка загрузка графика"); |
||||
|
} |
||||
|
); |
||||
|
} else { |
||||
|
this.snack.open("Нельзя загрузить график т.к нельзя определить индификатор профиля"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,210 @@ |
|||||
|
import {Component, Input, OnInit} from "@angular/core"; |
||||
|
import {Chart} from "chart.js/auto"; |
||||
|
import {SearchFilter} from "../../entities/search/SearchFilter"; |
||||
|
import {GraphService, PerPeriodStatistic, VipPerPeriodStatistic} from "../../services/graph.service"; |
||||
|
import {MatSnackBar} from "@angular/material/snack-bar"; |
||||
|
import {VipGiveMethod} from "../../entities/VipGiveMethod"; |
||||
|
import {BaseUtils} from "../../utils/BaseUtils"; |
||||
|
|
||||
|
@Component({ |
||||
|
selector: 'app-vip-graph', |
||||
|
template: ` |
||||
|
<div style="display: flex; justify-content: space-evenly;flex-direction: row;"> |
||||
|
<mat-radio-group |
||||
|
style="display: flex; justify-content: center;flex-direction: row;" |
||||
|
[(ngModel)]="chartConfig.period" |
||||
|
[disabled]="chartConfig.loading" |
||||
|
(ngModelChange)="updateGraph()"> |
||||
|
<mat-radio-button style="padding-right: 1%; padding-left: 1%" value="day">За месяц</mat-radio-button> |
||||
|
<mat-radio-button style="padding-right: 1%; padding-left: 1%" value="month">За год</mat-radio-button> |
||||
|
<mat-radio-button style="padding-right: 1%; padding-left: 1%" value="year">За 10 лет</mat-radio-button> |
||||
|
</mat-radio-group> |
||||
|
<mat-checkbox [(ngModel)]="removeNotStandartValue" (ngModelChange)="processGraph()">Не учитывать нестандартное время</mat-checkbox> |
||||
|
</div> |
||||
|
<div class="chart-container"> |
||||
|
<canvas id="vipChart"></canvas> |
||||
|
</div> |
||||
|
` |
||||
|
}) |
||||
|
export class VipGraphComponents implements OnInit { |
||||
|
chartConfig: { |
||||
|
chart: Chart|null, |
||||
|
period: 'day' | 'month' | 'year', |
||||
|
loading: boolean |
||||
|
} = { |
||||
|
chart: null, |
||||
|
period: 'day', |
||||
|
loading: false |
||||
|
} |
||||
|
|
||||
|
@Input("steam64") |
||||
|
steam64: string|null = null; |
||||
|
objs: VipPerPeriodStatistic[] = []; |
||||
|
|
||||
|
removeNotStandartValue: boolean = true; |
||||
|
standartPeriods: number[] = [86400, 86400*7, 86400*30, 86400*31]; |
||||
|
|
||||
|
constructor(private graphService: GraphService, |
||||
|
private snack: MatSnackBar) { |
||||
|
} |
||||
|
|
||||
|
ngOnInit(): void { |
||||
|
//nothing here
|
||||
|
} |
||||
|
|
||||
|
tabChangedTrigger(event: any, tabIndex: number) { |
||||
|
if (event.index == tabIndex) { |
||||
|
this.updateGraph() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
updateGraph() { |
||||
|
if (this.chartConfig.loading) return; |
||||
|
this.chartConfig.loading = true; |
||||
|
|
||||
|
const filter: SearchFilter = new SearchFilter(); |
||||
|
if (this.steam64) |
||||
|
filter.addAccountToSearch(this.steam64) |
||||
|
|
||||
|
let endDate = new Date(); |
||||
|
endDate.setUTCHours(23, 59, 59); |
||||
|
filter.addEndTimeToSearch(endDate.getTime()); |
||||
|
switch (this.chartConfig.period) { |
||||
|
case "day": { |
||||
|
endDate.setDate(endDate.getDate() - 30); |
||||
|
break; |
||||
|
} |
||||
|
case "month": { |
||||
|
endDate.setDate(endDate.getDate() - 365); |
||||
|
break; |
||||
|
} |
||||
|
case "year": { |
||||
|
endDate.setDate(endDate.getDate() - (365*10)); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
filter.addBeginTimeToSearch(endDate); |
||||
|
|
||||
|
this.graphService.getGivedVipOnPeriod(filter).subscribe( |
||||
|
(objs) => { |
||||
|
this.objs = objs; |
||||
|
this.processGraph(); |
||||
|
this.chartConfig.loading = false; |
||||
|
}, (err) => {this.chartConfig.loading = false; this.snack.open("Ошибка загрузка графика")} |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Обертка над рендер графом чтобы брать данные которые есть в переменоой обж |
||||
|
*/ |
||||
|
processGraph() { |
||||
|
this.renderGraph(this.objs); |
||||
|
} |
||||
|
|
||||
|
private renderGraph(objs: VipPerPeriodStatistic[]): void { |
||||
|
let chartConfig = { |
||||
|
type: 'bar', data: {}, options: { |
||||
|
plugins: { |
||||
|
title: { |
||||
|
display: false, |
||||
|
text: "ggggg" |
||||
|
} |
||||
|
}, |
||||
|
responsive: true, |
||||
|
interaction: { |
||||
|
intersect: false |
||||
|
}, |
||||
|
scales: { |
||||
|
x: { |
||||
|
stacked: true, |
||||
|
}, |
||||
|
y: { |
||||
|
stacked: true, |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let chartData: {labels: string[], datasets: any[]} = { |
||||
|
labels: [], |
||||
|
datasets: [] |
||||
|
} |
||||
|
|
||||
|
if (this.removeNotStandartValue) { |
||||
|
objs = objs.filter(value => this.standartPeriods.indexOf(value.amount) != -1) |
||||
|
} |
||||
|
|
||||
|
const groupByDate = objs.reduce((acc: {[date: string]: VipPerPeriodStatistic[]}, obj) => { |
||||
|
const key = obj.date; |
||||
|
if (!acc[key]) acc[key] = []; |
||||
|
acc[key].push(obj) |
||||
|
return acc; |
||||
|
}, {}); |
||||
|
|
||||
|
let existsAmount: number[] = []; |
||||
|
let existsGiveMethod: number[] = [] |
||||
|
objs.forEach((obj) => { |
||||
|
if (existsAmount.indexOf(obj.amount) == -1) { |
||||
|
existsAmount.push(obj.amount); |
||||
|
} |
||||
|
if (existsGiveMethod.indexOf(obj.givemethod) == -1) { |
||||
|
existsGiveMethod.push(obj.givemethod); |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
let datasets:{[str: string]: number[]} = {}; |
||||
|
existsAmount.forEach((ea) => { |
||||
|
existsGiveMethod.forEach((egm) => { |
||||
|
datasets[`${ea}-${egm}`] = []; |
||||
|
}) |
||||
|
}) |
||||
|
|
||||
|
chartData.labels = Object.keys(groupByDate); |
||||
|
//stack это количетсво выданного времени
|
||||
|
chartData.labels.forEach( |
||||
|
(date) => { |
||||
|
let foundAmountsAndGiveMethods: string[] = []; |
||||
|
groupByDate[date].forEach((data) => { |
||||
|
const key = `${data.amount}-${data.givemethod}`; |
||||
|
datasets[key].push(data.value); |
||||
|
foundAmountsAndGiveMethods.push(key); |
||||
|
}); |
||||
|
Object.keys(datasets).forEach(//заполняем 0 пропуски
|
||||
|
(key) => { |
||||
|
if (foundAmountsAndGiveMethods.indexOf(key) == -1) { |
||||
|
datasets[key].push(0) |
||||
|
} |
||||
|
} |
||||
|
) |
||||
|
}); |
||||
|
|
||||
|
const humanGiveMethod = (num: any) => { |
||||
|
try { |
||||
|
return VipGiveMethod[Number.parseInt(num)]; |
||||
|
} catch (e) { |
||||
|
return "Неизвестно" |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
chartData.datasets = Object.keys(datasets) |
||||
|
.map((key) => { |
||||
|
const stack = key.split("-")[0];//amount
|
||||
|
const label = key.split("-")[1];//givemethod
|
||||
|
return { |
||||
|
label: humanGiveMethod(label) + " на " + BaseUtils.formatSeconds(Number.parseInt(stack)), |
||||
|
data: datasets[key], |
||||
|
stack: stack, |
||||
|
skipNull: true, |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
chartConfig.data = chartData; |
||||
|
//console.log(chartData);
|
||||
|
|
||||
|
if (this.chartConfig.chart) |
||||
|
this.chartConfig.chart.destroy(); |
||||
|
|
||||
|
// @ts-ignore
|
||||
|
this.chartConfig.chart = new Chart('vipChart', chartConfig); |
||||
|
} |
||||
|
} |
||||
@ -1,3 +1,7 @@ |
|||||
p { |
p { |
||||
margin: 0 0; |
margin: 0 0; |
||||
} |
} |
||||
|
|
||||
|
::ng-deep .mat-tab-body-content { |
||||
|
overflow: unset !important; |
||||
|
} |
||||
|
|||||
@ -0,0 +1,3 @@ |
|||||
|
::ng-deep .mat-tab-body-content { |
||||
|
overflow: unset !important; |
||||
|
} |
||||
Loading…
Reference in new issue