feat: Add some diagrams into live report

This commit is contained in:
LWR 2023-01-30 19:23:25 +08:00
parent cbe27f09d0
commit 5f0764dc0b
5 changed files with 429 additions and 64 deletions

View File

@ -294,7 +294,8 @@ class MySQLDataSource(DataSource):
"`enabled`, `logo`, `logo_base64`, `time`, `fans_change`, `fans_medal_change`, `guard_change`, "
"`danmu`, `box`, `gift`, `sc`, `guard`, "
"`danmu_ranking`, `box_ranking`, `box_profit_ranking`, `gift_ranking`, `sc_ranking`, "
"`guard_list`, `danmu_cloud` "
"`guard_list`, `box_profit_diagram`, `danmu_diagram`, `box_diagram`, `gift_diagram`, "
"`sc_diagram`, `guard_diagram`, `danmu_cloud` "
"FROM `groups` AS `g` LEFT JOIN `live_report` AS `l` "
"ON g.`uid` = l.`uid` AND g.`index` = l.`index` "
f"WHERE g.`uid` = {uid} "

View File

@ -136,6 +136,24 @@ class LiveReport(BaseModel):
guard_list = False
"""是否展示本场直播开通大航海观众列表。默认False"""
box_profit_diagram = False
"""是否展示本场直播的盲盒盈亏曲线图。默认False"""
danmu_diagram = False
"""是否展示本场直播的弹幕互动曲线图。默认False"""
box_diagram = False
"""是否展示本场直播的盲盒互动曲线图。默认False"""
gift_diagram = False
"""是否展示本场直播的礼物互动曲线图。默认False"""
sc_diagram = False
"""是否展示本场直播的 SC醒目留言互动曲线图。默认False"""
guard_diagram = False
"""是否展示本场直播的开通大航海互动曲线图。默认False"""
danmu_cloud: Optional[bool] = False
"""是否生成本场直播弹幕词云。默认False。默认False"""
@ -143,25 +161,38 @@ class LiveReport(BaseModel):
def default(cls):
"""
获取功能全部开启的默认 LiveReport 实例
默认配置启用直播报告生成弹幕词云
默认配置启用直播报告无主播立绘展示直播时间段和直播时长展示粉丝变动展示粉丝团粉丝勋章数变动展示大航海变动
展示收到弹幕数发送弹幕人数展示收到盲盒数送出盲盒人数盲盒盈亏展示礼物收益送礼物人数
展示 SC醒目留言收益发送 SC醒目留言人数展示开通大航海数
展示弹幕排行榜前 3 展示盲盒数量排行榜前 3 展示盲盒盈亏排行榜前 3 展示礼物排行榜前 3
展示 SC醒目留言排行榜前 3 展示开通大航海观众列表
展示盲盒盈亏曲线图展示弹幕互动曲线图展示盲盒互动曲线图展示礼物互动曲线图
展示 SC醒目留言互动曲线图展示开通大航海互动曲线图
生成弹幕词云
"""
return LiveReport(enabled=True, logo=None, logo_base64=None,
time=True, fans_change=True, fans_medal_change=True,guard_change=True,
danmu=True, box=True, gift=True, sc=True, guard=True,
danmu_ranking=3, box_ranking=3, box_profit_ranking=3, gift_ranking=3, sc_ranking=3,
guard_list=True, danmu_cloud=True)
guard_list=True, box_profit_diagram=True,
danmu_diagram=True, box_diagram=True, gift_diagram=True, sc_diagram=True, guard_diagram=True,
danmu_cloud=True)
def __str__(self):
return (f"启用: {self.enabled}\n直播时长数据: {self.time}\n粉丝变动数据: {self.fans_change}\n"
f"粉丝团变动数据: {self.fans_medal_change}\n大航海变动数据: {self.guard_change}\n"
f"弹幕相关数据: {self.danmu}\n盲盒相关数据: {self.box}\n礼物相关数据: {self.gift}\n"
f"SC相关数据: {self.sc}\n大航海相关数据: {self.guard}\n"
f"SC 相关数据: {self.sc}\n大航海相关数据: {self.guard}\n"
f"弹幕排行榜: {f'{self.danmu_ranking}' if self.danmu_ranking else False}\n"
f"盲盒数量排行榜: {f'{self.box_ranking}' if self.box_ranking else False}\n"
f"盲盒盈亏排行榜: {f'{self.box_profit_ranking}' if self.box_profit_ranking else False}\n"
f"礼物排行榜: {f'{self.gift_ranking}' if self.gift_ranking else False}\n"
f"SC 排行榜: {f'{self.sc_ranking}' if self.sc_ranking else False}\n"
f"开通大航海观众列表: {self.guard_list}\n生成弹幕词云: {self.danmu_cloud}")
f"开通大航海观众列表: {self.guard_list}\n盲盒盈亏曲线图: {self.box_profit_diagram}\n"
f"弹幕互动曲线图: {self.danmu_diagram}\n盲盒互动曲线图: {self.box_diagram}\n"
f"礼物互动曲线图: {self.gift_diagram}\nSC 互动曲线图: {self.sc_diagram}\n"
f"开通大航海互动曲线图: {self.guard_diagram}\n"
f"生成弹幕词云: {self.danmu_cloud}")
class DynamicUpdate(BaseModel):

View File

@ -1,17 +1,11 @@
import asyncio
import base64
import os
import time
import typing
from asyncio import AbstractEventLoop
from collections import Counter
from io import BytesIO
from typing import Optional, Any, Union, List
import jieba
from loguru import logger
from pydantic import BaseModel, PrivateAttr
from wordcloud import WordCloud
from .live import LiveDanmaku, LiveRoom
from .model import PushTarget
@ -24,8 +18,6 @@ from ..utils.utils import get_credential, timestamp_format, get_unames_and_faces
if typing.TYPE_CHECKING:
from .sender import Bot
jieba.setLogLevel(jieba.logging.INFO)
class Up(BaseModel):
"""
@ -77,6 +69,10 @@ class Up(BaseModel):
def dispatch(self, name, data):
self.__room.dispatch(name, data)
async def accumulate_and_reset_data(self):
await self.__accumulate_data()
await self.__reset_data()
def is_connecting(self):
return (self.__room is not None) and (self.__room.get_status() != 2)
@ -195,8 +191,7 @@ class Up(BaseModel):
await redis.hset(f"FansMedalCount:{self.room_id}", live_start_time, fans_medal_count)
await redis.hset(f"GuardCount:{self.room_id}", live_start_time, guard_count)
await self.__accumulate_data()
await self.__reset_data()
await self.accumulate_and_reset_data()
# 推送开播消息
arg_base = room_info["room_info"]
@ -230,7 +225,7 @@ class Up(BaseModel):
self.__bot.send_live_off(self, live_off_args)
self.__bot.send_live_report(self, live_report_param)
danmu_items = ["danmu", "danmu_cloud"]
danmu_items = ["danmu", "danmu_ranking", "danmu_diagram", "danmu_cloud"]
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled(danmu_items):
@self.__room.on("DANMU_MSG")
async def on_danmu(event):
@ -250,8 +245,12 @@ class Up(BaseModel):
# 弹幕词云所需弹幕记录
if isinstance(base[0][13], str):
await redis.rpush(f"RoomDanmu:{self.room_id}", content)
await redis.hincrby(f"RoomDanmuTime:{self.room_id}", int(time.time()))
gift_items = ["box", "gift"]
gift_items = [
"box", "gift", "box_ranking", "box_profit_ranking", "gift_ranking",
"box_profit_diagram", "box_diagram", "gift_diagram"
]
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled(gift_items):
@self.__room.on("SEND_GIFT")
async def on_gift(event):
@ -270,6 +269,8 @@ class Up(BaseModel):
await redis.hincrbyfloat("RoomGiftProfit", self.room_id, price)
await redis.zincrby(f"UserGiftProfit:{self.room_id}", uid, price)
await redis.hincrbyfloat(f"RoomGiftTime:{self.room_id}", int(time.time()), price)
# 盲盒统计
if base["blind_gift"] is not None:
box_price = base["total_coin"] / 1000
@ -279,10 +280,14 @@ class Up(BaseModel):
await redis.hincrby("RoomBoxCount", self.room_id, gift_num)
await redis.zincrby(f"UserBoxCount:{self.room_id}", uid, gift_num)
await redis.hincrbyfloat("RoomBoxProfit", self.room_id, profit)
box_profit_after = await redis.hincrbyfloat("RoomBoxProfit", self.room_id, profit)
await redis.zincrby(f"UserBoxProfit:{self.room_id}", uid, profit)
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled("sc"):
await redis.rpush(f"RoomBoxProfitRecord:{self.room_id}", box_profit_after)
await redis.hincrby(f"RoomBoxTime:{self.room_id}", int(time.time()))
sc_items = ["sc", "sc_ranking", "sc_diagram"]
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled(sc_items):
@self.__room.on("SUPER_CHAT_MESSAGE")
async def on_sc(event):
"""
@ -298,7 +303,10 @@ class Up(BaseModel):
await redis.hincrby("RoomScProfit", self.room_id, price)
await redis.zincrby(f"UserScProfit:{self.room_id}", uid, price)
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled("guard"):
await redis.hincrby(f"RoomScTime:{self.room_id}", int(time.time()), price)
guard_items = ["guard", "guard_list", "guard_diagram"]
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled(guard_items):
@self.__room.on("GUARD_BUY")
async def on_guard(event):
"""
@ -320,6 +328,8 @@ class Up(BaseModel):
await redis.hincrby(f"Room{type_mapping[guard_type]}Count", self.room_id, month)
await redis.zincrby(f"User{type_mapping[guard_type]}Count:{self.room_id}", uid, month)
await redis.hincrby(f"RoomGuardTime:{self.room_id}", int(time.time()), month)
if self.__any_dynamic_update_enabled():
@self.__room.on("DYNAMIC_UPDATE")
async def dynamic_update(event):
@ -407,6 +417,13 @@ class Up(BaseModel):
# 清空弹幕记录
await redis.delete(f"RoomDanmu:{self.room_id}")
# 清空数据分布
await redis.delete(f"RoomDanmuTime:{self.room_id}")
await redis.delete(f"RoomBoxTime:{self.room_id}")
await redis.delete(f"RoomGiftTime:{self.room_id}")
await redis.delete(f"RoomScTime:{self.room_id}")
await redis.delete(f"RoomGuardTime:{self.room_id}")
# 重置弹幕数
await redis.hset(f"RoomDanmuCount", self.room_id, 0)
await redis.delete(f"UserDanmuCount:{self.room_id}")
@ -419,6 +436,9 @@ class Up(BaseModel):
await redis.hset(f"RoomBoxProfit", self.room_id, 0)
await redis.delete(f"UserBoxProfit:{self.room_id}")
# 清空盲盒盈亏记录
await redis.delete(f"RoomBoxProfitRecord:{self.room_id}")
# 重置礼物收益
await redis.hset(f"RoomGiftProfit", self.room_id, 0)
await redis.delete(f"UserGiftProfit:{self.room_id}")
@ -459,6 +479,8 @@ class Up(BaseModel):
hour, minute = divmod(minute, 60)
live_report_param.update({
"start_timestamp": start_time,
"end_timestamp": end_time,
"start_time": timestamp_format(start_time, "%m/%d %H:%M:%S"),
"end_time": timestamp_format(end_time, "%m/%d %H:%M:%S"),
"hour": hour,
@ -470,17 +492,16 @@ class Up(BaseModel):
if self.__any_live_report_item_enabled(["fans_change", "fans_medal_change", "guard_change"]):
room_info = await self.__live_room.get_room_info()
live_start_time = await redis.hgeti("StartTime", self.room_id)
if await redis.hexists(f"FansCount:{self.room_id}", live_start_time):
fans_count = await redis.hgeti(f"FansCount:{self.room_id}", live_start_time)
if await redis.hexists(f"FansCount:{self.room_id}", start_time):
fans_count = await redis.hgeti(f"FansCount:{self.room_id}", start_time)
else:
fans_count = -1
if await redis.hexists(f"FansMedalCount:{self.room_id}", live_start_time):
fans_medal_count = await redis.hgeti(f"FansMedalCount:{self.room_id}", live_start_time)
if await redis.hexists(f"FansMedalCount:{self.room_id}", start_time):
fans_medal_count = await redis.hgeti(f"FansMedalCount:{self.room_id}", start_time)
else:
fans_medal_count = -1
if await redis.hexists(f"GuardCount:{self.room_id}", live_start_time):
guard_count = await redis.hgeti(f"GuardCount:{self.room_id}", live_start_time)
if await redis.hexists(f"GuardCount:{self.room_id}", start_time):
guard_count = await redis.hgeti(f"GuardCount:{self.room_id}", start_time)
else:
guard_count = -1
@ -512,21 +533,27 @@ class Up(BaseModel):
# 弹幕相关
"danmu_count": await redis.hgeti("RoomDanmuCount", self.room_id),
"danmu_person_count": await redis.zcard(f"UserDanmuCount:{self.room_id}"),
"danmu_diagram": await redis.hgetalltuplei(f"RoomDanmuTime:{self.room_id}"),
# 盲盒相关
"box_count": await redis.hgeti("RoomBoxCount", self.room_id),
"box_person_count": await redis.zcard(f"UserBoxCount:{self.room_id}"),
"box_profit": box_profit,
"box_beat_percent": percent,
"box_profit_diagram": await redis.lrangef1(f"RoomBoxProfitRecord:{self.room_id}", 0, -1),
"box_diagram": await redis.hgetalltuplei(f"RoomBoxTime:{self.room_id}"),
# 礼物相关
"gift_profit": await redis.hgetf1("RoomGiftProfit", self.room_id),
"gift_person_count": await redis.zcard(f"UserGiftProfit:{self.room_id}"),
"gift_diagram": await redis.hgetalltuplef1(f"RoomGiftTime:{self.room_id}"),
# SC醒目留言相关
"sc_profit": await redis.hgeti("RoomScProfit", self.room_id),
"sc_person_count": await redis.zcard(f"UserScProfit:{self.room_id}"),
"sc_diagram": await redis.hgetalltuplei(f"RoomScTime:{self.room_id}"),
# 大航海相关
"captain_count": await redis.hgeti("RoomCaptainCount", self.room_id),
"commander_count": await redis.hgeti("RoomCommanderCount", self.room_id),
"governor_count": await redis.hgeti("RoomGovernorCount", self.room_id)
"governor_count": await redis.hgeti("RoomGovernorCount", self.room_id),
"guard_diagram": await redis.hgetalltuplei(f"RoomGuardTime:{self.room_id}")
})
# 弹幕排行
@ -649,31 +676,11 @@ class Up(BaseModel):
# 弹幕词云
if self.__any_live_report_item_enabled("danmu_cloud"):
all_danmu = " ".join(await redis.lrange(f"RoomDanmu:{self.room_id}", 0, -1))
all_danmu = await redis.lrange(f"RoomDanmu:{self.room_id}", 0, -1)
if len(all_danmu) == 0:
live_report_param.update({
"danmu_cloud": ""
})
else:
words = list(jieba.cut(all_danmu))
counts = dict(Counter(words))
font_base_path = os.path.dirname(os.path.dirname(__file__))
io = BytesIO()
word_cloud = WordCloud(width=900,
height=450,
font_path=f"{font_base_path}/resource/{config.get('DANMU_CLOUD_FONT')}",
background_color=config.get("DANMU_CLOUD_BACKGROUND_COLOR"),
max_font_size=config.get("DANMU_CLOUD_MAX_FONT_SIZE"),
max_words=config.get("DANMU_CLOUD_MAX_WORDS"))
word_cloud.generate_from_frequencies(counts)
word_cloud.to_image().save(io, format="png")
base64_str = base64.b64encode(io.getvalue()).decode()
live_report_param.update({
"danmu_cloud": base64_str
})
live_report_param.update({
"all_danmu": all_danmu
})
return live_report_param

View File

@ -1,13 +1,26 @@
import base64
import bisect
import io
import math
import os
from collections import Counter
from io import BytesIO
from typing import Union, Tuple, List, Dict, Any
import jieba
import numpy as np
from PIL import Image, ImageDraw
from matplotlib import pyplot as plt
from mpl_toolkits import axisartist
from scipy.interpolate import make_interp_spline
from wordcloud import WordCloud
from .PicGenerator import Color, PicGenerator
from ..core.model import LiveReport
from ..utils.utils import split_list, limit_str_length, mask_round
from ..utils import config
from ..utils.utils import split_list, limit_str_length, mask_round, timestamp_format
jieba.setLogLevel(jieba.logging.INFO)
class LiveReportGenerator:
@ -261,14 +274,107 @@ class LiveReportGenerator:
)
pic.draw_img_alpha(pic.auto_size_img_by_limit(guard_list_img, logo_limit))
# 盲盒盈亏曲线图
if model.box_profit_diagram:
profits = param.get("box_profit_diagram", [])
if profits:
profits.insert(0, 0.0)
pic.draw_section("盲盒盈亏曲线图")
diagram = cls.__get_box_profit_diagram(profits, pic.width - (margin * 2))
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
# 弹幕互动曲线图
if model.danmu_diagram:
start = param.get('start_timestamp', 0)
end = param.get('end_timestamp', 0)
times = param.get("danmu_diagram", [])
if end - start >= 100 and times:
pic.draw_section("弹幕互动曲线图")
pic.draw_tip("收获弹幕数量在本场直播中的分布情况")
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
# 盲盒互动曲线图
if model.box_diagram:
start = param.get('start_timestamp', 0)
end = param.get('end_timestamp', 0)
times = param.get("box_diagram", [])
if end - start >= 100 and times:
pic.draw_section("盲盒互动曲线图")
pic.draw_tip("收获盲盒数量在本场直播中的分布情况")
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
# 礼物互动曲线图
if model.gift_diagram:
start = param.get('start_timestamp', 0)
end = param.get('end_timestamp', 0)
times = param.get("gift_diagram", [])
if end - start >= 100 and times:
pic.draw_section("礼物互动曲线图")
pic.draw_tip("收获礼物价值在本场直播中的分布情况")
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
# SC醒目留言互动曲线图
if model.sc_diagram:
start = param.get('start_timestamp', 0)
end = param.get('end_timestamp', 0)
times = param.get("sc_diagram", [])
if end - start >= 100 and times:
pic.draw_section("SC(醒目留言)互动曲线图")
pic.draw_tip("收获SC(醒目留言)价值在本场直播中的分布情况")
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
# 开通大航海互动曲线图
if model.guard_diagram:
start = param.get('start_timestamp', 0)
end = param.get('end_timestamp', 0)
times = param.get("guard_diagram", [])
if end - start >= 100 and times:
pic.draw_section("开通大航海互动曲线图")
pic.draw_tip("收获大航海开通时长在本场直播中的分布情况")
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
# 弹幕词云
if model.danmu_cloud:
base64_str = param.get('danmu_cloud', "")
if base64_str != "":
all_danmu = param.get('all_danmu', [])
if all_danmu:
pic.draw_section("弹幕词云")
img_bytes = BytesIO(base64.b64decode(base64_str))
img = pic.auto_size_img_by_limit(Image.open(img_bytes), logo_limit)
all_danmu_str = " ".join(all_danmu)
words = list(jieba.cut(all_danmu_str))
counts = dict(Counter(words))
font_base_path = os.path.dirname(os.path.dirname(__file__))
word_cloud = WordCloud(width=900,
height=450,
font_path=f"{font_base_path}/resource/{config.get('DANMU_CLOUD_FONT')}",
background_color=config.get("DANMU_CLOUD_BACKGROUND_COLOR"),
max_font_size=config.get("DANMU_CLOUD_MAX_FONT_SIZE"),
max_words=config.get("DANMU_CLOUD_MAX_WORDS"))
word_cloud.generate_from_frequencies(counts)
img = pic.auto_size_img_by_limit(word_cloud.to_image(), logo_limit)
pic.draw_img_with_border(img)
# 底部版权信息,请务必保留此处
@ -577,4 +683,206 @@ class LiveReportGenerator:
cls.__get_guard_line_pic(pic, width, face_size, faces, unames, counts, icon_map[i], color_map[i])
).move_pos(0, -pic.row_space)
return img.img
return img.img
@classmethod
def __insert_zeros(cls, lst: List[float]) -> List[float]:
"""
在列表正负变化处添加 0.0
Args:
lst: 源列表
Returns:
在正负变化处添加 0.0 后的列表
"""
result = []
for i in range(len(lst)):
if i == 0:
result.append(lst[i])
continue
if (lst[i - 1] < 0 < lst[i]) or (lst[i] < 0 < lst[i - 1]):
result.append(0.0)
result.append(lst[i])
return result
@classmethod
def __smooth_xy(cls, lx: List[Any], ly: List[Any]) -> Tuple[Any, Any]:
"""
平滑处理折线图数据
Args:
lx: x 轴数据
ly: y 轴数据
Returns:
平滑处理后的 x y 轴数据组成的元组
"""
x = np.array(lx)
y = np.array(ly)
x_smooth = np.linspace(0, max(x), 300)
y_smooth = make_interp_spline(x, y)(x_smooth)
return x_smooth, y_smooth
@classmethod
def __get_line_diagram(cls,
xs: List[Any],
ys: List[Any],
xticks: List[Any],
yticks: List[Any],
xlabels: List[Any],
ylabels: List[Any],
xlimits: Tuple[Any, Any],
ylimits: Tuple[Any, Any],
width: int) -> Image:
"""
绘制折线图
Args:
xs: x 轴数据
ys: y 轴数据
xticks: x 轴标签点
yticks: y 轴标签点
xlabels: x 轴标签
ylabels: y 轴标签
xlimits: x 轴范围
ylimits: y 轴范围
width: 折线图图片宽度
"""
fig = plt.figure(figsize=(width / 100, width / 100 * 0.75))
ax = axisartist.Subplot(fig, 111)
fig.add_axes(ax)
ax.axis[:].set_visible(False)
ax.axis["x"] = ax.new_floating_axis(0, 0)
ax.axis["y"] = ax.new_floating_axis(1, 0)
ax.axis["x"].set_axis_direction('top')
ax.axis["y"].set_axis_direction('left')
ax.axis["x"].set_axisline_style("->", size=2.0)
ax.axis["y"].set_axisline_style("->", size=2.0)
ax.plot(xs, ys, color='red')
ax.grid(True, linestyle='--', alpha=0.5)
for i in range(len(ys) - 1):
if ys[i] >= 0 and ys[i + 1] >= 0:
plt.fill_between(xs[i:i + 2], 0, ys[i:i + 2], facecolor='red', alpha=0.3)
else:
plt.fill_between(xs[i:i + 2], 0, ys[i:i + 2], facecolor='green', alpha=0.3)
ax.set_xticks(xticks)
ax.set_yticks(yticks)
if xlabels:
ax.set_xticklabels(xlabels)
if ylabels:
ax.set_yticklabels(ylabels)
ax.set_xlim(xlimits[0], xlimits[1])
ax.set_ylim(ylimits[0], ylimits[1])
buf = io.BytesIO()
fig.savefig(buf)
buf.seek(0)
return Image.open(buf)
@classmethod
def __get_box_profit_diagram(cls, profits: List[float], width: int) -> Image:
"""
绘制盲盒盈亏曲线图
Args:
profits: 盲盒盈亏记录
width: 盲盒盈亏曲线图图片宽度
"""
profits = cls.__insert_zeros(profits)
length = len(profits)
indexs = list(range(0, length))
abs_max = int(max(max(profits), abs(min(profits))))
start = -abs_max - (-abs_max % 10)
end = abs_max + (-abs_max % 10)
step = int((end - start) / 10)
yticks = list(range(start, end)[::step])
yticks.append(end)
return cls.__get_line_diagram(
indexs, profits, [], yticks, [], [], (-1, length), (start, end), width
)
@classmethod
def __calc_interaction_diagram_xy(cls,
times: List[Tuple[str, Union[int, float]]],
start: int,
end: int) -> Tuple[List[int], List[int]]:
"""
根据互动时间列表计算互动曲线图中 x 轴和 y 轴数据
Args:
times: 互动时间及权重列表
start: 直播开始时间戳
end: 直播结束时间戳
Returns:
x 轴和 y 轴数据组成的元组
"""
count = 20
step = (end - start) // count
times = [(int(x[0]), x[1]) for x in times]
divisions = [(start + i * step, start + (i + 1) * step - 1) for i in range(count)]
divisions[-1] = (divisions[-1][0], end)
results = [0] * count
times.sort(key=lambda x: x[0])
for i in range(len(times) - 1, -1, -1):
if times[i][0] <= end:
times = times[:i + 1]
break
for data in times:
index = bisect.bisect_left([d[1] for d in divisions], data[0])
results[index] += data[1]
result = [d[0] for d in divisions], results
result[0].append(start + count * step)
result[1].append(0)
return result
@classmethod
def __get_interaction_diagram(cls,
times: List[Tuple[str, Union[int, float]]],
start: int,
end: int,
width: int) -> Image:
"""
绘制互动曲线图
Args:
times: 互动时间及权重列表
start: 直播开始时间戳
end: 直播结束时间戳
width: 互动曲线图图片宽度
"""
xs, ys = cls.__calc_interaction_diagram_xy(times, start, end)
xs = [x - start for x in xs]
max_x = max(xs)
xs, ys = cls.__smooth_xy(xs, ys)
max_y = math.ceil(max(ys))
xticks = [x * max_x // 4 for x in range(1, 4)]
xticks.insert(0, 0)
xticks.append(max_x)
xlabels = [timestamp_format(start + x * (end - start) // 4, "%H:%M:%S") for x in range(1, 4)]
xlabels.insert(0, timestamp_format(start, "%H:%M:%S"))
xlabels.append(timestamp_format(end, "%H:%M:%S"))
ystep = max(max_y // 10, 1)
yticks = list(range(0, max_y)[::ystep])
yticks.append(yticks[-1] + ystep)
return cls.__get_line_diagram(
xs, ys, xticks, yticks, xlabels, [], (0, max_x), (0, yticks[-1]), width
)

View File

@ -33,8 +33,16 @@ async def delete(key: str):
# List
async def lrange(key: str, start: int, end: int) -> List:
return list(map(lambda x: x.decode(), await __redis.lrange(key, start, end)))
async def lrange(key: str, start: int, end: int) -> List[str]:
return [x.decode() for x in await __redis.lrange(key, start, end)]
async def lrangei(key: str, start: int, end: int) -> List[float]:
return [int(x) for x in await __redis.lrange(key, start, end)]
async def lrangef1(key: str, start: int, end: int) -> List[float]:
return [float("{:.1f}".format(float(x))) for x in await __redis.lrange(key, start, end)]
async def rpush(key: str, value: Any):
@ -61,6 +69,16 @@ async def hgetf1(key: str, hkey: Union[str, int]) -> float:
return float("{:.1f}".format(float(result)))
async def hgetalltuplei(key: str) -> List[Tuple[str, int]]:
result = await __redis.hgetall(key)
return [(x.decode(), int(result[x])) for x in result]
async def hgetalltuplef1(key: str) -> List[Tuple[str, float]]:
result = await __redis.hgetall(key)
return [(x.decode(), float("{:.1f}".format(float(result[x])))) for x in result]
async def hset(key: str, hkey: Union[str, int], value: Any):
await __redis.hset(key, hkey, value)
@ -87,13 +105,13 @@ async def zrank(key: str, member: str) -> int:
async def zrevrangewithscoresi(key: str, start: int, end: int) -> List[Tuple[str, int]]:
return list(map(lambda x: (x[0].decode(), int(x[1])), await __redis.zrevrange(key, start, end, True)))
return [(x[0].decode(), int(x[1])) for x in await __redis.zrevrange(key, start, end, True)]
async def zrevrangewithscoresf1(key: str, start: int, end: int) -> List[Tuple[str, float]]:
return list(map(
lambda x: (x[0].decode(), float("{:.1f}".format(float(x[1])))), await __redis.zrevrange(key, start, end, True)
))
return [
(x[0].decode(), float("{:.1f}".format(float(x[1])))) for x in await __redis.zrevrange(key, start, end, True)
]
async def zadd(key: str, member: str, score: Union[int, float]):