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 { |
|||
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