mirror of
https://github.com/A-Minos/nonebot-plugin-tetris-stats.git
synced 2026-03-05 05:36:54 +08:00
✨ 渲染 历史tr 曲线图 (#312)
This commit is contained in:
@@ -23,3 +23,5 @@ RANK_PERCENTILE: dict[Rank, float] = {
|
|||||||
'd+': 97.5,
|
'd+': 97.5,
|
||||||
'd': 100,
|
'd': 100,
|
||||||
}
|
}
|
||||||
|
TR_MIN = 0
|
||||||
|
TR_MAX = 25000
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from collections import defaultdict
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from hashlib import md5, sha512
|
from hashlib import md5, sha512
|
||||||
from math import floor
|
from math import ceil, floor
|
||||||
from re import match
|
from re import match
|
||||||
from statistics import mean
|
from statistics import mean
|
||||||
from typing import Literal
|
from typing import Literal, NamedTuple
|
||||||
from urllib.parse import urlunparse
|
from urllib.parse import urlunparse
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from aiofiles import open
|
from aiofiles import open
|
||||||
from nonebot import get_driver
|
from nonebot import get_driver
|
||||||
@@ -23,6 +24,7 @@ from typing_extensions import override
|
|||||||
from zstandard import ZstdCompressor
|
from zstandard import ZstdCompressor
|
||||||
|
|
||||||
from ...db import BindStatus, create_or_update_bind
|
from ...db import BindStatus, create_or_update_bind
|
||||||
|
from ...db.models import HistoricalData
|
||||||
from ...utils.avatar import get_avatar
|
from ...utils.avatar import get_avatar
|
||||||
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
|
from ...utils.exception import MessageFormatError, RequestError, WhatTheFuckError
|
||||||
from ...utils.host import HostPage, get_self_netloc
|
from ...utils.host import HostPage, get_self_netloc
|
||||||
@@ -32,7 +34,7 @@ from ...utils.retry import retry
|
|||||||
from ...utils.screenshot import screenshot
|
from ...utils.screenshot import screenshot
|
||||||
from .. import Processor as ProcessorMeta
|
from .. import Processor as ProcessorMeta
|
||||||
from .cache import Cache
|
from .cache import Cache
|
||||||
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE
|
from .constant import BASE_URL, GAME_TYPE, RANK_PERCENTILE, TR_MAX, TR_MIN
|
||||||
from .model import IORank
|
from .model import IORank
|
||||||
from .schemas.base import FailedModel
|
from .schemas.base import FailedModel
|
||||||
from .schemas.league_all import LeagueAll
|
from .schemas.league_all import LeagueAll
|
||||||
@@ -124,6 +126,122 @@ class Processor(ProcessorMeta):
|
|||||||
sprint = user_records.data.records.sprint
|
sprint = user_records.data.records.sprint
|
||||||
blitz = user_records.data.records.blitz
|
blitz = user_records.data.records.blitz
|
||||||
if isinstance(league, RatedLeague) and league.vs is not None:
|
if isinstance(league, RatedLeague) and league.vs is not None:
|
||||||
|
today = datetime.now(ZoneInfo('Asia/Shanghai')).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
forward = timedelta(days=9)
|
||||||
|
start_time = (today - forward).astimezone(UTC)
|
||||||
|
async with get_session() as session:
|
||||||
|
historical_data = (
|
||||||
|
await session.scalars(
|
||||||
|
select(HistoricalData)
|
||||||
|
.where(HistoricalData.trigger_time >= start_time)
|
||||||
|
.where(HistoricalData.game_platform == GAME_TYPE)
|
||||||
|
.where(HistoricalData.user_unique_identifier == self.user.unique_identifier)
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
extra = (
|
||||||
|
await session.scalars(
|
||||||
|
select(HistoricalData)
|
||||||
|
.where(HistoricalData.game_platform == GAME_TYPE)
|
||||||
|
.where(HistoricalData.user_unique_identifier == self.user.unique_identifier)
|
||||||
|
.order_by(HistoricalData.id.desc())
|
||||||
|
.where(HistoricalData.id < min([i.id for i in historical_data]))
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
).one_or_none()
|
||||||
|
if extra is not None:
|
||||||
|
historical_data = list(historical_data)
|
||||||
|
historical_data.append(extra)
|
||||||
|
|
||||||
|
class HistoricalTr(NamedTuple):
|
||||||
|
time: datetime
|
||||||
|
tr: float
|
||||||
|
|
||||||
|
histories = [
|
||||||
|
HistoricalTr(
|
||||||
|
time=i.processed_data.user_info.cache.cached_at.astimezone(ZoneInfo('Asia/Shanghai')),
|
||||||
|
tr=i.processed_data.user_info.data.user.league.rating,
|
||||||
|
)
|
||||||
|
for i in historical_data
|
||||||
|
if isinstance(i.processed_data, ProcessedData)
|
||||||
|
and i.processed_data.user_info is not None
|
||||||
|
and isinstance(i.processed_data.user_info.data.user.league, RatedLeague)
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_specified_point(
|
||||||
|
previous_point: HistoricalTr, behind_point: HistoricalTr, point_time: datetime
|
||||||
|
) -> HistoricalTr:
|
||||||
|
"""根据给出的 previous_point 和 behind_point, 推算 point_time 点处的数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
previous_point (HistoricalTr): 前面的数据点
|
||||||
|
behind_point (HistoricalTr): 后面的数据点
|
||||||
|
point_time (datetime): 要推算的点的位置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HistoricalTr: 要推算的点的数据
|
||||||
|
"""
|
||||||
|
# 求两个点的斜率
|
||||||
|
slope = (behind_point.tr - previous_point.tr) / (
|
||||||
|
datetime.timestamp(behind_point.time) - datetime.timestamp(previous_point.time)
|
||||||
|
)
|
||||||
|
return HistoricalTr(
|
||||||
|
time=point_time,
|
||||||
|
tr=previous_point.tr
|
||||||
|
+ slope * (datetime.timestamp(point_time) - datetime.timestamp(previous_point.time)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 按照时间排序
|
||||||
|
histories = sorted(histories, key=lambda x: x.time)
|
||||||
|
for index, value in enumerate(histories):
|
||||||
|
# 在历史记录里找有没有今天0点后的数据
|
||||||
|
if value.time > today:
|
||||||
|
histories = histories[:index] + [
|
||||||
|
get_specified_point(histories[index - 1], histories[index], today.replace(microsecond=1000))
|
||||||
|
]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
histories.append(
|
||||||
|
get_specified_point(
|
||||||
|
histories[-1],
|
||||||
|
HistoricalTr(user_info.cache.cached_at, league.rating),
|
||||||
|
today.replace(microsecond=1000),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if histories[0].time < (today - forward):
|
||||||
|
histories[0] = get_specified_point(
|
||||||
|
histories[0],
|
||||||
|
histories[1],
|
||||||
|
today - forward,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
histories.insert(0, HistoricalTr((today - forward), histories[0].tr))
|
||||||
|
|
||||||
|
def get_value_bounds(values: list[int | float]) -> tuple[int, int]:
|
||||||
|
value_max = 10 * ceil(max(values) / 10)
|
||||||
|
value_min = 10 * floor(min(values) / 10)
|
||||||
|
return value_max, value_min
|
||||||
|
|
||||||
|
def get_split(value_max: int, value_min: int) -> tuple[int, int]:
|
||||||
|
offset = 0
|
||||||
|
overflow = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if (new_max_value := value_max + offset + overflow) > TR_MAX:
|
||||||
|
overflow -= 1
|
||||||
|
continue
|
||||||
|
if (new_min_value := value_min - offset + overflow) < TR_MIN:
|
||||||
|
overflow += 1
|
||||||
|
continue
|
||||||
|
if ((new_max_value - new_min_value) / 40).is_integer():
|
||||||
|
split_value = int((value_max + offset - (value_min - offset)) / 4)
|
||||||
|
break
|
||||||
|
offset += 1
|
||||||
|
return split_value, offset + overflow
|
||||||
|
|
||||||
|
value_max, value_min = get_value_bounds([i.tr for i in histories])
|
||||||
|
split_value, offset = get_split(value_max, value_min)
|
||||||
|
|
||||||
if sprint.record is None:
|
if sprint.record is None:
|
||||||
sprint_value = 'N/A'
|
sprint_value = 'N/A'
|
||||||
else:
|
else:
|
||||||
@@ -160,11 +278,11 @@ class Processor(ProcessorMeta):
|
|||||||
vs=league.vs,
|
vs=league.vs,
|
||||||
sprint=sprint_value,
|
sprint=sprint_value,
|
||||||
blitz=blitz_value,
|
blitz=blitz_value,
|
||||||
data=[[0, 0]],
|
data=[[int(datetime.timestamp(time) * 1000), tr] for time, tr in histories],
|
||||||
split_value=0,
|
split_value=split_value,
|
||||||
value_max=0,
|
offset=offset,
|
||||||
value_min=0,
|
value_max=value_max,
|
||||||
offset=0,
|
value_min=value_min,
|
||||||
app=(app := (league.apm / (60 * league.pps))),
|
app=(app := (league.apm / (60 * league.pps))),
|
||||||
dsps=(dsps := ((league.vs / 100) - (league.apm / 60))),
|
dsps=(dsps := ((league.vs / 100) - (league.apm / 60))),
|
||||||
dspp=(dspp := (dsps / league.pps)),
|
dspp=(dspp := (dsps / league.pps)),
|
||||||
@@ -177,7 +295,7 @@ class Processor(ProcessorMeta):
|
|||||||
urlunparse(('http', get_self_netloc(), f'/host/page/{page_hash}.html', '', '', ''))
|
urlunparse(('http', get_self_netloc(), f'/host/page/{page_hash}.html', '', '', ''))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# call back
|
# fallback
|
||||||
ret_message = ''
|
ret_message = ''
|
||||||
if isinstance(league, NeverPlayedLeague):
|
if isinstance(league, NeverPlayedLeague):
|
||||||
ret_message += f'用户 {user_name} 没有排位统计数据'
|
ret_message += f'用户 {user_name} 没有排位统计数据'
|
||||||
|
|||||||
@@ -38,14 +38,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-gap"></div>
|
<div class="flex-gap"></div>
|
||||||
</div>
|
</div>
|
||||||
{# <div class="chart-shadow box-rounded-corners" id="TR-curve-chart">
|
<div class="chart-shadow box-rounded-corners" id="TR-curve-chart">
|
||||||
<span class="TR-title">Tetra Rating (TR)</span>
|
<span class="TR-title">Tetra Rating (TR)</span>
|
||||||
<img class="rank-icon" src="../../static/static/rank/{{rank}}.svg" />
|
<img class="rank-icon" src="../../static/static/rank/{{rank}}.svg" />
|
||||||
<span class="TR" style="display: flex; align-items: flex-end"
|
<span class="TR" style="display: flex; align-items: flex-end"
|
||||||
>{{TR}}
|
>{{TR}}
|
||||||
<p style="font-size: 30px; font-weight: 400; line-height: 47px">(#{{global_rank}})</p>
|
<p style="font-size: 30px; font-weight: 400; line-height: 47px">(#{{global_rank}})</p>
|
||||||
</span>
|
</span>
|
||||||
</div> #}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="big-title">Multiplayer Stats</span>
|
<span class="big-title">Multiplayer Stats</span>
|
||||||
<div class="multiplayer-box">
|
<div class="multiplayer-box">
|
||||||
@@ -111,7 +111,8 @@
|
|||||||
|
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'time',
|
type: 'time',
|
||||||
minInterval: 3600 * 48 * 1000,
|
minInterval: 3600 * 24 * 1000,
|
||||||
|
maxInterval: 3600 * 24 * 1000,
|
||||||
axisTick: {
|
axisTick: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
@@ -133,12 +134,17 @@
|
|||||||
}
|
}
|
||||||
switch (index) {
|
switch (index) {
|
||||||
case 0:
|
case 0:
|
||||||
case 6:
|
case 1:
|
||||||
|
case 3:
|
||||||
|
case 5:
|
||||||
|
case 7:
|
||||||
|
case 9:
|
||||||
|
case 11:
|
||||||
ret = '';
|
ret = '';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
lst = format_date();
|
lst = format_date();
|
||||||
if (index === 5) {
|
if (index === 10) {
|
||||||
ret = '{last_month|' + lst[0] + '}\n{last_day|' + lst[1] + '}';
|
ret = '{last_month|' + lst[0] + '}\n{last_day|' + lst[1] + '}';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ async def render(
|
|||||||
vs: str | float,
|
vs: str | float,
|
||||||
sprint: str,
|
sprint: str,
|
||||||
blitz: str,
|
blitz: str,
|
||||||
data: list[list[int]],
|
data: list[list[int | float]],
|
||||||
split_value: int,
|
split_value: int,
|
||||||
|
offset: int,
|
||||||
value_max: int,
|
value_max: int,
|
||||||
value_min: int,
|
value_min: int,
|
||||||
offset: int,
|
|
||||||
app: str | float,
|
app: str | float,
|
||||||
dsps: str | float,
|
dsps: str | float,
|
||||||
dspp: str | float,
|
dspp: str | float,
|
||||||
|
|||||||
Reference in New Issue
Block a user