From 9e33a1cda0bfbe468e623945d95f666c82b1beaf Mon Sep 17 00:00:00 2001 From: zimfv Date: Tue, 5 Mar 2024 22:22:33 +0300 Subject: [PATCH 01/14] Insert visualisation.py to src --- .gitignore | 1 + src/visualisation.py | 250 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 src/visualisation.py diff --git a/.gitignore b/.gitignore index f1e6744..c1d1806 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__/ +*.ipynb *.py[cod] *$py.class *.so diff --git a/src/visualisation.py b/src/visualisation.py new file mode 100644 index 0000000..73689a4 --- /dev/null +++ b/src/visualisation.py @@ -0,0 +1,250 @@ +import numpy as np +import pandas as pd +import sqlite3 as sql +import matplotlib.pyplot as plt + + +def get_player_answers(cur, username): + """ + Returns table of player with given username results grouped by his answers + + Parameters: + ----------- + cur : sqlite3 cursor + Cursor to the given database. + + username : string from table Players.username + Username of given player. + + Returns: + -------- + res : dict + Keys are answers, values are count numbers + """ + query = f"""SELECT Records.role AS answer, COUNT(*) AS count + FROM Records + INNER JOIN Players ON Players.id = Records.player_id + WHERE Players.username = '{username}' + GROUP BY Records.role;""" + res = cur.execute(query).fetchall() + res = {i[0] : i[1] for i in res} + for key in ['HC', 'HD', 'HL', 'HW', 'FL', 'LL', 'LW', 'FW']: + try: + res[key] + except KeyError: + res.update({key : 0}) + return res + + +def drow_username_winrate(cur, username, ax=None): + """ + Plots a winrate statistics for given player. + + Parameters: + ----------- + cur : sqlite3 cursor + Cursor to the given database. + + username : string from table Players.username + Username of given player. + + ax : matplotlib axes object, default None + An axes of the current figure + """ + info = get_player_answers(cur, username) + bins = pd.DataFrame([[info['LW'], info['LL']], + [info['FW'], info['FL']], + [info['HC'] + info['HW'], info['HD'] + info['HL']]], + columns=['Wins', 'Loses'], + index=['Liberal', 'Fascist', 'Hitler']).transpose() + + bins.plot(kind='bar', stacked=True, color=['deepskyblue', 'orangered', 'darkred'], rot='horizontal', ax=ax) + + +def get_players_stats(cur, order='DESC', top=None): + """ + Returns table containing number of wins and loses for each role and winrate + + Parameters: + ----------- + cur : sqlite3 cursor + Cursor to the given database. + + + order : str values 'DESC' or 'ASC' + + top : uint or None + function returns top number of results, if that's not None + + Retuens: + -------- + res : DataFrame + Columns: username, + LW (liberal wins), + FW (fascist wins), + HW (Hitler wins), + LL (liberal loses), + FL (fascist loses), + HL (Hitler loses), + winrate + """ + query = f"""SELECT username, + SUM(CASE WHEN role = 'LW' THEN 1 ELSE 0 END) AS LW, + SUM(CASE WHEN role = 'FW' THEN 1 ELSE 0 END) AS FW, + SUM(CASE WHEN role IN ('HW', 'HC') THEN 1 ELSE 0 END) AS HW, + SUM(CASE WHEN role = 'LL' THEN 1 ELSE 0 END) AS LL, + SUM(CASE WHEN role = 'FL' THEN 1 ELSE 0 END) AS FL, + SUM(CASE WHEN role IN ('HL', 'HD') THEN 1 ELSE 0 END) AS HL, + AVG(CASE WHEN role IN ('LW', 'FW', 'HW', 'HC') THEN 1 ELSE 0 END) AS winrate + FROM records + INNER JOIN players ON players.id = records.player_id + GROUP BY player_id ORDER BY winrate {order};""" + if top is not None: + query = query[:-1] + f'\nLIMIT {top};' + res = cur.execute(query).fetchall() + res = pd.DataFrame(res, columns=['username', 'LW', 'FW', 'HW', 'LL', 'FL', 'HL', 'winrate']) + return res + + +def draw_topest_players(cur, n=4, best=True, normolize=True, ax=None): + """ + Draw hists for top best or worst players by winrate + + Parameters: + ----------- + cur : sqlite3 cursor + Cursor to the given database. + + n : int + Number of players + + best : bool + If True, draws best players + If False, draws worst players + + normolize : bool + Normolize values to sum be 1 for each player + + ax : matplotlib axes object, default None + An axes of the current figure + """ + order = {True: "DESC", False: "ASC"}[best] + df = get_players_stats(cur, order=order, top=n).iloc[::-1] + df.index = df['username'] + df = df[['LW', 'FW', 'HW', 'LL', 'FL', 'HL']] + df.columns = ['Liberal wins', 'Fascist wins', 'Hitler wins', + 'Liberal loses', 'Fascist loses', 'Hitler loses'] + if normolize: + df = df.transpose() + df = df / df.sum() + df = df.transpose() + df.plot(kind='barh', stacked=True, color=['deepskyblue', 'orangered', 'darkred', + 'lightblue', 'lightpink', 'rosybrown'], ax=ax, ylabel='') + + +def get_connection_stats(cur, username, order='DESC', top=None, which='teammate'): + """ + Returns table containing number of wins and loses for each role and winrate + + Parameters: + ----------- + cur : sqlite3 cursor + Cursor to the given database. + + username : string from table Players.username + Username of given player. + + order : str values 'DESC' or 'ASC' + + top : uint or None + Function will return top n number of results, if that's not None + + which: str or None + Define which stats this function will return: + 'teammate' : teammates stats, + 'opponent' : opponents stats, + None : full stats + + Returns: + -------- + res : DataFrame + Columns: username, + LW - Wins playing in liberal team + LL - Loses playing in liberal team + FW - Wins playing in fascist team + FL - Loses playing in fascist team + winrate + """ + query_which = {None: '', + 'teammate': "\nAND ((records.role in ('LW', 'LL') AND w.team = 'Liberal') OR (records.role IN ('FW', 'FL', 'HC', 'HL') AND w.team = 'Fascist'))", + 'opponent': "\nAND ((records.role in ('LW', 'LL') AND w.team = 'Fascist') OR (records.role IN ('FW', 'FL', 'HC', 'HL') AND w.team = 'Liberal'))" + }[which] + if top is None: + query_limit = '' + else: + query_limit = f'\nLIMIT {top}' + query = f"""WITH w(game_id, team, result) AS (SELECT records.game_id, + CASE WHEN records.role IN ('LL', 'LW') THEN 'Liberal' ELSE 'Fascist' END AS team, + CASE WHEN records.role IN ('FW', 'LW', 'HC', 'HW') THEN 'Win' ELSE 'Lose' END AS result + FROM records INNER JOIN players ON players.id = records.player_id + WHERE players.username = '{username}') + SElECT players.username, + SUM(CASE WHEN team = 'Liberal' AND result = 'Win' THEN 1 ELSE 0 END) AS LW, + SUM(CASE WHEN team = 'Fascist' AND result = 'Win' THEN 1 ELSE 0 END) AS FW, + SUM(CASE WHEN team = 'Liberal' AND result = 'Lose' THEN 1 ELSE 0 END) AS LL, + SUM(CASE WHEN team = 'Fascist' AND result = 'Lose' THEN 1 ELSE 0 END) AS FL, + AVG(CASE WHEN result = 'Win' THEN 1 ELSE 0 END) AS Winrate + FROM records + INNER JOIN w ON w.game_id = records.game_id + INNER JOIN players ON players.id = records.player_id + WHERE players.username != '{username}'{query_which} + GROUP BY records.player_id + ORDER BY Winrate {order}{query_limit};""" + res = cur.execute(query).fetchall() + res = pd.DataFrame(res, columns=['username', 'LW', 'LL', 'FW', 'FL', 'winrate']) + return res + + +def draw_connection_stats(cur, username, n=4, best=True, which='teammate', normolize=True, ax=None): + """ + Draw hists for top best or worst players by winrate + + Parameters: + ----------- + cur : sqlite3 cursor + Cursor to the given database. + + username : string from table Players.username + Username of given player. + + n : int + Number of players + + best : bool + If True, draws best players + If False, draws worst players + + which: str or None + Define which stats this function will return: + 'teammate' : teammates stats, + 'opponent' : opponents stats, + None : full stats + + normolize : bool + Normolize values to sum be 1 for each player + + ax : matplotlib axes object, default None + An axes of the current figure + """ + order = {True: "DESC", False: "ASC"}[best] + df = get_connection_stats(cur, username, order=order, top=n, which=which)[::-1] + df.index = df['username'] + df = df[['LW', 'LL', 'FW', 'FL']] + df.columns = ['Liberal wins', 'Fascist wins', + 'Liberal loses', 'Fascist loses'] + if normolize: + df = df.transpose() + df = df / df.sum() + df = df.transpose() + df.plot(kind='barh', stacked=True, color=['deepskyblue', 'orangered', + 'lightblue', 'lightpink'], ax=ax, ylabel='') \ No newline at end of file From 48622607ef6604553fde35a679eb4043fe536217 Mon Sep 17 00:00:00 2001 From: zimfv Date: Wed, 6 Mar 2024 03:37:56 +0300 Subject: [PATCH 02/14] wrote fetch_player_answers, not tested --- .gitignore | 1 + src/services/db_service.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/.gitignore b/.gitignore index c1d1806..510e734 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ __pycache__/ *.ipynb +example.db *.py[cod] *$py.class *.so diff --git a/src/services/db_service.py b/src/services/db_service.py index f70dc0e..9834f99 100644 --- a/src/services/db_service.py +++ b/src/services/db_service.py @@ -141,3 +141,36 @@ async def fetch_poll_results(poll_id: int) -> tuple[PollResult]: WHERE poll_id = ?""" results = await fetch_all(sql, [poll_id]) return tuple(PollResult(**result) for result in results) + + +async def fetch_player_answers(username): + """ + Returns table of player with given username results grouped by his answers + + Parameters: + ----------- + cur : sqlite3 cursor + Cursor to the given database. + + username : string from table Players.username + Username of given player. + + Returns: + -------- + res : dict + Keys are answers, values are count numbers + """ + query = f"""SELECT SUM(CASE WHEN records.role = 'HC' THEN 1 ELSE 0 END) AS HC, + SUM(CASE WHEN records.role = 'HD' THEN 1 ELSE 0 END) AS HD, + SUM(CASE WHEN records.role = 'HL' THEN 1 ELSE 0 END) AS HL, + SUM(CASE WHEN records.role = 'HW' THEN 1 ELSE 0 END) AS HW, + SUM(CASE WHEN records.role = 'FL' THEN 1 ELSE 0 END) AS FL, + SUM(CASE WHEN records.role = 'LL' THEN 1 ELSE 0 END) AS LL, + SUM(CASE WHEN records.role = 'LW' THEN 1 ELSE 0 END) AS LW, + SUM(CASE WHEN records.role = 'FW' THEN 1 ELSE 0 END) AS FW + FROM Records + INNER JOIN Players ON Players.id = Records.player_id + WHERE Players.username = ? + GROUP BY Players.id;""" + res = await fetch_one(query, [username]) + return res \ No newline at end of file From d6343f05a811cc17a630c61df65a3200b13bc4e7 Mon Sep 17 00:00:00 2001 From: zimfv Date: Thu, 7 Mar 2024 03:39:52 +0300 Subject: [PATCH 03/14] Change table name register in query: Records -> records, Players -> players --- src/services/db_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/db_service.py b/src/services/db_service.py index 9834f99..8f5b53a 100644 --- a/src/services/db_service.py +++ b/src/services/db_service.py @@ -168,9 +168,9 @@ async def fetch_player_answers(username): SUM(CASE WHEN records.role = 'LL' THEN 1 ELSE 0 END) AS LL, SUM(CASE WHEN records.role = 'LW' THEN 1 ELSE 0 END) AS LW, SUM(CASE WHEN records.role = 'FW' THEN 1 ELSE 0 END) AS FW - FROM Records - INNER JOIN Players ON Players.id = Records.player_id - WHERE Players.username = ? - GROUP BY Players.id;""" + FROM records + INNER JOIN players ON players.id = records.player_id + WHERE players.username = ? + GROUP BY players.id;""" res = await fetch_one(query, [username]) return res \ No newline at end of file From ed788c45c54f325231be2d6db2bfd5569567ceb9 Mon Sep 17 00:00:00 2001 From: zimfv Date: Fri, 8 Mar 2024 15:19:11 +0300 Subject: [PATCH 04/14] Rewrote getting stats functions from visualisation.py to fetching stats functions in db_service.py --- .gitignore | 1 + src/services/db_service.py | 100 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/.gitignore b/.gitignore index 510e734..8efcb0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __pycache__/ *.ipynb example.db +db.sqlite *.py[cod] *$py.class *.so diff --git a/src/services/db_service.py b/src/services/db_service.py index 8f5b53a..73208bb 100644 --- a/src/services/db_service.py +++ b/src/services/db_service.py @@ -173,4 +173,104 @@ async def fetch_player_answers(username): WHERE players.username = ? GROUP BY players.id;""" res = await fetch_one(query, [username]) + return res + + +async def fetch_players_stats(order='DESC', top=None): + """ + Returns table containing number of wins and loses for each role and winrate + + Parameters: + ----------- + order : str values 'DESC' or 'ASC' + + top : uint or None + function returns top number of results, if that's not None + + Retuens: + -------- + res : DataFrame + Columns: username, + LW (liberal wins), + FW (fascist wins), + HW (Hitler wins), + LL (liberal loses), + FL (fascist loses), + HL (Hitler loses), + winrate + """ + query = f"""SELECT username, + SUM(CASE WHEN role = 'LW' THEN 1 ELSE 0 END) AS LW, + SUM(CASE WHEN role = 'FW' THEN 1 ELSE 0 END) AS FW, + SUM(CASE WHEN role IN ('HW', 'HC') THEN 1 ELSE 0 END) AS HW, + SUM(CASE WHEN role = 'LL' THEN 1 ELSE 0 END) AS LL, + SUM(CASE WHEN role = 'FL' THEN 1 ELSE 0 END) AS FL, + SUM(CASE WHEN role IN ('HL', 'HD') THEN 1 ELSE 0 END) AS HL, + AVG(CASE WHEN role IN ('LW', 'FW', 'HW', 'HC') THEN 1 ELSE 0 END) AS winrate + FROM records + INNER JOIN players ON players.id = records.player_id + GROUP BY player_id ORDER BY winrate {order};""" + if top is not None: + query = query[:-1] + f'\nLIMIT {top};' + res = await fetch_all(query, []) + return res + + +async def fetch_connection_stats(username, order='DESC', top=None, which='teammate'): + """ + Returns table containing number of wins and loses for each role and winrate + + Parameters: + ----------- + username : string from table Players.username + Username of given player. + + order : str values 'DESC' or 'ASC' + + top : uint or None + Function will return top n number of results, if that's not None + + which: str or None + Define which stats this function will return: + 'teammate' : teammates stats, + 'opponent' : opponents stats, + None : full stats + + Returns: + -------- + res : DataFrame + Columns: username, + LW - Wins playing in liberal team + LL - Loses playing in liberal team + FW - Wins playing in fascist team + FL - Loses playing in fascist team + winrate + """ + query_which = {None: '', + 'teammate': "\nAND ((records.role in ('LW', 'LL') AND w.team = 'Liberal') OR (records.role IN ('FW', 'FL', 'HC', 'HL') AND w.team = 'Fascist'))", + 'opponent': "\nAND ((records.role in ('LW', 'LL') AND w.team = 'Fascist') OR (records.role IN ('FW', 'FL', 'HC', 'HL') AND w.team = 'Liberal'))" + }[which] + if top is None: + query_limit = '' + else: + query_limit = f'\nLIMIT {top}' + query = f"""WITH w(game_id, team, result) AS (SELECT records.game_id, + CASE WHEN records.role IN ('LL', 'LW') THEN 'Liberal' ELSE 'Fascist' END AS team, + CASE WHEN records.role IN ('FW', 'LW', 'HC', 'HW') THEN 'Win' ELSE 'Lose' END AS result + FROM records INNER JOIN players ON players.id = records.player_id + WHERE players.username = ?) + SElECT players.username, + SUM(CASE WHEN team = 'Liberal' AND result = 'Win' THEN 1 ELSE 0 END) AS LW, + SUM(CASE WHEN team = 'Fascist' AND result = 'Win' THEN 1 ELSE 0 END) AS FW, + SUM(CASE WHEN team = 'Liberal' AND result = 'Lose' THEN 1 ELSE 0 END) AS LL, + SUM(CASE WHEN team = 'Fascist' AND result = 'Lose' THEN 1 ELSE 0 END) AS FL, + AVG(CASE WHEN result = 'Win' THEN 1 ELSE 0 END) AS Winrate + FROM records + INNER JOIN w ON w.game_id = records.game_id + INNER JOIN players ON players.id = records.player_id + WHERE players.username != ? {query_which} + GROUP BY records.player_id + ORDER BY Winrate {order}{query_limit};""" + + res = await fetch_all(query, [username, username]) return res \ No newline at end of file From 438c7c5fef91b92428057c444a71fc007e698c31 Mon Sep 17 00:00:00 2001 From: zimfv Date: Sun, 10 Mar 2024 23:46:34 +0300 Subject: [PATCH 05/14] Make async draw_username_winrate function --- src/services/db_service.py | 4 ++-- src/services/draw_graphs.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/services/draw_graphs.py diff --git a/src/services/db_service.py b/src/services/db_service.py index 73208bb..1649d49 100644 --- a/src/services/db_service.py +++ b/src/services/db_service.py @@ -199,7 +199,7 @@ async def fetch_players_stats(order='DESC', top=None): HL (Hitler loses), winrate """ - query = f"""SELECT username, + query = f"""SELECT username, full_name, SUM(CASE WHEN role = 'LW' THEN 1 ELSE 0 END) AS LW, SUM(CASE WHEN role = 'FW' THEN 1 ELSE 0 END) AS FW, SUM(CASE WHEN role IN ('HW', 'HC') THEN 1 ELSE 0 END) AS HW, @@ -259,7 +259,7 @@ async def fetch_connection_stats(username, order='DESC', top=None, which='teamma CASE WHEN records.role IN ('FW', 'LW', 'HC', 'HW') THEN 'Win' ELSE 'Lose' END AS result FROM records INNER JOIN players ON players.id = records.player_id WHERE players.username = ?) - SElECT players.username, + SElECT players.username, players.full_name, SUM(CASE WHEN team = 'Liberal' AND result = 'Win' THEN 1 ELSE 0 END) AS LW, SUM(CASE WHEN team = 'Fascist' AND result = 'Win' THEN 1 ELSE 0 END) AS FW, SUM(CASE WHEN team = 'Liberal' AND result = 'Lose' THEN 1 ELSE 0 END) AS LL, diff --git a/src/services/draw_graphs.py b/src/services/draw_graphs.py new file mode 100644 index 0000000..9c26178 --- /dev/null +++ b/src/services/draw_graphs.py @@ -0,0 +1,28 @@ +import asyncio + +import pandas as pd +import matplotlib.pyplot as plt + +from src.services.db_service import fetch_player_answers, fetch_players_stats, fetch_connection_stats + + +async def draw_username_winrate(username, ax=None): + """ + Plots a winrate statistics for given player. + + Parameters: + ----------- + username : string from table Players.username + Username of given player. + + ax : matplotlib axes object, default None + An axes of the current figure + """ + info = await fetch_player_answers(username) + bins = pd.DataFrame([[info['LW'], info['LL']], + [info['FW'], info['FL']], + [info['HC'] + info['HW'], info['HD'] + info['HL']]], + columns=['Wins', 'Loses'], + index=['Liberal', 'Fascist', 'Hitler']).transpose() + + bins.plot(kind='bar', stacked=True, color=['deepskyblue', 'orangered', 'darkred'], rot='horizontal', ax=ax) \ No newline at end of file From 29f0e538aec09a10f908b5a09d52992aa34b2e08 Mon Sep 17 00:00:00 2001 From: zimfv Date: Tue, 12 Mar 2024 04:48:38 +0300 Subject: [PATCH 06/14] Drawing svg for a player statistics. --- src/services/db_service.py | 45 +++++++++++++++++++++------ src/services/draw_graphs.py | 62 ++++++++++++++++++++++++++++++++++--- 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/src/services/db_service.py b/src/services/db_service.py index 1649d49..103e151 100644 --- a/src/services/db_service.py +++ b/src/services/db_service.py @@ -125,6 +125,25 @@ async def delete_poll_result(poll_id: int, user_id: int) -> None: ) +async def fetch_user(username=None, id=None, first_name=None, full_name=None, last_name=None): + # returns full player info by given info + conditions = [] + if not id is None: + conditions.append(f'id = {id.__repr__()}') + if not username is None: + conditions.append(f'username = {username.__repr__()}') + if not first_name is None: + conditions.append(f'first_name = {first_name.__repr__()}') + if not last_name is None: + conditions.append(f'last_name = {last_name.__repr__()}') + if not full_name is None: + conditions.append(f'full_name = {full_name.__repr__()}') + query = f"""SELECT * FROM players + WHERE {' AND '.join(conditions)};""" + result = await fetch_one(query, []) + return result + + async def fetch_poll_data(poll_id: int) -> Poll | None: sql = """SELECT id, message_id, chat_id, chat_name, creator_id, creator_username FROM polls @@ -176,7 +195,7 @@ async def fetch_player_answers(username): return res -async def fetch_players_stats(order='DESC', top=None): +async def fetch_players_stats(order='DESC', mingames=None, top=None): """ Returns table containing number of wins and loses for each role and winrate @@ -199,17 +218,23 @@ async def fetch_players_stats(order='DESC', top=None): HL (Hitler loses), winrate """ - query = f"""SELECT username, full_name, - SUM(CASE WHEN role = 'LW' THEN 1 ELSE 0 END) AS LW, - SUM(CASE WHEN role = 'FW' THEN 1 ELSE 0 END) AS FW, - SUM(CASE WHEN role IN ('HW', 'HC') THEN 1 ELSE 0 END) AS HW, - SUM(CASE WHEN role = 'LL' THEN 1 ELSE 0 END) AS LL, - SUM(CASE WHEN role = 'FL' THEN 1 ELSE 0 END) AS FL, - SUM(CASE WHEN role IN ('HL', 'HD') THEN 1 ELSE 0 END) AS HL, - AVG(CASE WHEN role IN ('LW', 'FW', 'HW', 'HC') THEN 1 ELSE 0 END) AS winrate + if mingames is None: + condition = "" + else: + condition = f"HAVING games >= {mingames}" + query = f"""SELECT players.id, players.username, players.full_name, + SUM(CASE WHEN records.role = 'LW' THEN 1 ELSE 0 END) AS LW, + SUM(CASE WHEN records.role = 'FW' THEN 1 ELSE 0 END) AS FW, + SUM(CASE WHEN records.role IN ('HW', 'HC') THEN 1 ELSE 0 END) AS HW, + SUM(CASE WHEN records.role = 'LL' THEN 1 ELSE 0 END) AS LL, + SUM(CASE WHEN records.role = 'FL' THEN 1 ELSE 0 END) AS FL, + SUM(CASE WHEN records.role IN ('HL', 'HD') THEN 1 ELSE 0 END) AS HL, + COUNT(records.role) AS games, + AVG(CASE WHEN records.role IN ('LW', 'FW', 'HW', 'HC') THEN 1 ELSE 0 END) AS winrate FROM records INNER JOIN players ON players.id = records.player_id - GROUP BY player_id ORDER BY winrate {order};""" + GROUP BY player_id {condition} + ORDER BY winrate {order};""" if top is not None: query = query[:-1] + f'\nLIMIT {top};' res = await fetch_all(query, []) diff --git a/src/services/draw_graphs.py b/src/services/draw_graphs.py index 9c26178..ae5eb9f 100644 --- a/src/services/draw_graphs.py +++ b/src/services/draw_graphs.py @@ -1,12 +1,15 @@ import asyncio - import pandas as pd -import matplotlib.pyplot as plt -from src.services.db_service import fetch_player_answers, fetch_players_stats, fetch_connection_stats +from matplotlib import pyplot as plt +from matplotlib import image +from matplotlib.offsetbox import (OffsetImage, AnnotationBbox)#The OffsetBox is a simple container artist. +from telegram.ext import ContextTypes +from src.services.db_service import fetch_user, fetch_player_answers, fetch_players_stats, fetch_connection_stats +from src.services.draw_result_image import get_user_profile_photo -async def draw_username_winrate(username, ax=None): +async def draw_user_winrate_bins(username, ax=None, return_bins=False): """ Plots a winrate statistics for given player. @@ -25,4 +28,53 @@ async def draw_username_winrate(username, ax=None): columns=['Wins', 'Loses'], index=['Liberal', 'Fascist', 'Hitler']).transpose() - bins.plot(kind='bar', stacked=True, color=['deepskyblue', 'orangered', 'darkred'], rot='horizontal', ax=ax) \ No newline at end of file + bins.plot(kind='bar', stacked=True, color=['deepskyblue', 'orangered', 'darkred'], rot='horizontal', ax=ax) + if return_bins: + return bins + + + +async def draw_user_winrate(username, outcome=None, context=ContextTypes.DEFAULT_TYPE): + """ + Draw a winrate statistics for given player and saves this as svg + + Parameters: + ----------- + username : string from table Players.username + Username of given player. + + outcome: str + Outcome svg-file name + Just show the figure, if that's None + + context : telegram.ext.CallbackContext + """ + user = await fetch_user(username=username) + + fig, ax = plt.subplots(1, 1) + fig.set_figwidth(8) + fig.set_figheight(6) + fig.suptitle(user['full_name']) + + # Define a logo (an avatar) position + bins = await draw_user_winrate_bins(username, ax=ax, return_bins=True) + heights = bins.sum(axis=1) + if heights['Wins'] < 0.75*heights['Loses']: + ab_posx = 0 + elif heights['Loses'] < 0.75*heights['Wins']: + ab_posx = 1 + else: + ab_posx = 0.5 + ab_posy = 0.85*heights.max() + + # Set a logo (an avatar) + file = await get_user_profile_photo(context=context, player_id=user['id']) + logo = image.imread(file) + imagebox = OffsetImage(logo, zoom = 0.25) + ab = AnnotationBbox(imagebox, (ab_posx, ab_posy), frameon = True) + ax.add_artist(ab) + + if outcome is None: + plt.show(fig) + else: + fig.savefig(outcome, format="svg") \ No newline at end of file From 3b82232bbaa02e6275d23d7f54e94668c48fa5b7 Mon Sep 17 00:00:00 2001 From: zimfv Date: Tue, 12 Mar 2024 21:47:59 +0300 Subject: [PATCH 07/14] Function draw_user_stats returns png as str --- src/services/draw_graphs.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/services/draw_graphs.py b/src/services/draw_graphs.py index ae5eb9f..b793594 100644 --- a/src/services/draw_graphs.py +++ b/src/services/draw_graphs.py @@ -1,3 +1,4 @@ +import io import asyncio import pandas as pd @@ -6,7 +7,7 @@ from matplotlib.offsetbox import (OffsetImage, AnnotationBbox)#The OffsetBox is a simple container artist. from telegram.ext import ContextTypes from src.services.db_service import fetch_user, fetch_player_answers, fetch_players_stats, fetch_connection_stats -from src.services.draw_result_image import get_user_profile_photo +from src.services.draw_result_image import get_user_profile_photo, svg2png async def draw_user_winrate_bins(username, ax=None, return_bins=False): @@ -44,8 +45,8 @@ async def draw_user_winrate(username, outcome=None, context=ContextTypes.DEFAULT Username of given player. outcome: str - Outcome svg-file name - Just show the figure, if that's None + Outcome file name + Returns svg-string if this is None context : telegram.ext.CallbackContext """ @@ -74,7 +75,10 @@ async def draw_user_winrate(username, outcome=None, context=ContextTypes.DEFAULT ab = AnnotationBbox(imagebox, (ab_posx, ab_posy), frameon = True) ax.add_artist(ab) - if outcome is None: - plt.show(fig) + if not (outcome is None): + fig.savefig(outcome) else: - fig.savefig(outcome, format="svg") \ No newline at end of file + svg = io.StringIO() + fig.savefig(svg, format='svg') + svg = svg.getvalue() + return svg2png(svg) \ No newline at end of file From 2b6003e08b64dfb467943eb286fa0ea3919d32f7 Mon Sep 17 00:00:00 2001 From: zimfv Date: Wed, 13 Mar 2024 19:21:47 +0300 Subject: [PATCH 08/14] Made mystats handler --- src/get_handlers.py | 1 + src/handlers/mystats.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/handlers/mystats.py diff --git a/src/get_handlers.py b/src/get_handlers.py index 159b118..6625ffe 100644 --- a/src/get_handlers.py +++ b/src/get_handlers.py @@ -11,6 +11,7 @@ def get_handlers() -> tuple: CommandHandler("help", handlers.help), CommandHandler("game", handlers.game), CommandHandler("save", handlers.save), + CommandHandler("mystats", handlers.mystats), # Poll answer handler PollAnswerHandler(poll_callback_receiver), ) diff --git a/src/handlers/mystats.py b/src/handlers/mystats.py new file mode 100644 index 0000000..eec8a64 --- /dev/null +++ b/src/handlers/mystats.py @@ -0,0 +1,19 @@ +import asyncio + +from telegram import Update +from telegram.ext import ContextTypes + +from src.services.draw_graphs import draw_user_winrate +from src.config import AppConfig + + +async def mystats( + update: Update, context: ContextTypes.DEFAULT_TYPE, config: AppConfig = AppConfig() +) -> None: + # Draws personal stats of asking user + username = update.effective_user.username + chat_id = update.my_chat_member + await context.bot.send_photo(chat_id=chat_id, + photo=await draw_user_winrate(username), + disable_notification=True) + #await update.effective_message.delete() \ No newline at end of file From 3b7dd15ac9fbe87cf113d8cbc9ced2d2a5a2a26a Mon Sep 17 00:00:00 2001 From: zimfv Date: Thu, 14 Mar 2024 22:03:03 +0300 Subject: [PATCH 09/14] Testing mystats - sqlite3.OperationalError: no such table: players --- pyproject.toml | 3 ++- src/handlers/__init__.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b317259..629b9a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,8 @@ python-telegram-bot = "^20.7" pydantic = "^2.5.3" cairosvg = "^2.7.1" pydantic-settings = "^2.2.1" - +pandas = "^1.5.3" +matplotlib = "^3.7.4" [build-system] requires = ["poetry-core"] diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py index e4b8252..82c7942 100644 --- a/src/handlers/__init__.py +++ b/src/handlers/__init__.py @@ -2,6 +2,7 @@ from .help import help from .game import game from .save import save +from .mystats import mystats from .error_handler import error_handler __all__ = [ @@ -9,5 +10,6 @@ "help", "game", "save", + "mystats", "error_handler", ] From 75ee55a726ed9a5f94af7d76b45da9f28c8af1a4 Mon Sep 17 00:00:00 2001 From: zimfv Date: Fri, 29 Mar 2024 00:09:58 +0300 Subject: [PATCH 10/14] /mystats command works in chats, but shows the default user photo --- src/handlers/mystats.py | 2 +- src/services/draw_graphs.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/handlers/mystats.py b/src/handlers/mystats.py index eec8a64..8054875 100644 --- a/src/handlers/mystats.py +++ b/src/handlers/mystats.py @@ -12,7 +12,7 @@ async def mystats( ) -> None: # Draws personal stats of asking user username = update.effective_user.username - chat_id = update.my_chat_member + chat_id = update.effective_chat.id await context.bot.send_photo(chat_id=chat_id, photo=await draw_user_winrate(username), disable_notification=True) diff --git a/src/services/draw_graphs.py b/src/services/draw_graphs.py index b793594..8c89127 100644 --- a/src/services/draw_graphs.py +++ b/src/services/draw_graphs.py @@ -21,6 +21,10 @@ async def draw_user_winrate_bins(username, ax=None, return_bins=False): ax : matplotlib axes object, default None An axes of the current figure + + Returns: + -------- + bins : pd.DataFrame """ info = await fetch_player_answers(username) bins = pd.DataFrame([[info['LW'], info['LL']], From 2a78f24a4252f6d7f5cb881cf69fef2c7ba6a668 Mon Sep 17 00:00:00 2001 From: zimfv Date: Fri, 29 Mar 2024 01:58:29 +0300 Subject: [PATCH 11/14] Change argument in functions connected with mystats: username -> user_id --- src/handlers/mystats.py | 4 ++-- src/services/db_service.py | 14 ++++++-------- src/services/draw_graphs.py | 19 ++++++++++--------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/handlers/mystats.py b/src/handlers/mystats.py index 8054875..67b9523 100644 --- a/src/handlers/mystats.py +++ b/src/handlers/mystats.py @@ -11,9 +11,9 @@ async def mystats( update: Update, context: ContextTypes.DEFAULT_TYPE, config: AppConfig = AppConfig() ) -> None: # Draws personal stats of asking user - username = update.effective_user.username + user_id = update.effective_user.id chat_id = update.effective_chat.id await context.bot.send_photo(chat_id=chat_id, - photo=await draw_user_winrate(username), + photo=await draw_user_winrate(user_id), disable_notification=True) #await update.effective_message.delete() \ No newline at end of file diff --git a/src/services/db_service.py b/src/services/db_service.py index 103e151..46fffa6 100644 --- a/src/services/db_service.py +++ b/src/services/db_service.py @@ -162,7 +162,7 @@ async def fetch_poll_results(poll_id: int) -> tuple[PollResult]: return tuple(PollResult(**result) for result in results) -async def fetch_player_answers(username): +async def fetch_player_answers(user_id): """ Returns table of player with given username results grouped by his answers @@ -170,9 +170,9 @@ async def fetch_player_answers(username): ----------- cur : sqlite3 cursor Cursor to the given database. - - username : string from table Players.username - Username of given player. + + user_id : int + Id of a given player. Returns: -------- @@ -188,10 +188,8 @@ async def fetch_player_answers(username): SUM(CASE WHEN records.role = 'LW' THEN 1 ELSE 0 END) AS LW, SUM(CASE WHEN records.role = 'FW' THEN 1 ELSE 0 END) AS FW FROM records - INNER JOIN players ON players.id = records.player_id - WHERE players.username = ? - GROUP BY players.id;""" - res = await fetch_one(query, [username]) + WHERE records.player_id = ?;""" + res = await fetch_one(query, [user_id]) return res diff --git a/src/services/draw_graphs.py b/src/services/draw_graphs.py index 8c89127..68c1391 100644 --- a/src/services/draw_graphs.py +++ b/src/services/draw_graphs.py @@ -10,14 +10,14 @@ from src.services.draw_result_image import get_user_profile_photo, svg2png -async def draw_user_winrate_bins(username, ax=None, return_bins=False): +async def draw_user_winrate_bins(user_id, ax=None, return_bins=False): """ Plots a winrate statistics for given player. Parameters: ----------- - username : string from table Players.username - Username of given player. + user_id : int from table Players.id + Id of a given player. ax : matplotlib axes object, default None An axes of the current figure @@ -26,7 +26,7 @@ async def draw_user_winrate_bins(username, ax=None, return_bins=False): -------- bins : pd.DataFrame """ - info = await fetch_player_answers(username) + info = await fetch_player_answers(user_id) bins = pd.DataFrame([[info['LW'], info['LL']], [info['FW'], info['FL']], [info['HC'] + info['HW'], info['HD'] + info['HL']]], @@ -39,14 +39,14 @@ async def draw_user_winrate_bins(username, ax=None, return_bins=False): -async def draw_user_winrate(username, outcome=None, context=ContextTypes.DEFAULT_TYPE): +async def draw_user_winrate(user_id, outcome=None, context=ContextTypes.DEFAULT_TYPE): """ Draw a winrate statistics for given player and saves this as svg Parameters: ----------- - username : string from table Players.username - Username of given player. + user_id : int from table Players.id + Id of a given player. outcome: str Outcome file name @@ -54,7 +54,8 @@ async def draw_user_winrate(username, outcome=None, context=ContextTypes.DEFAULT context : telegram.ext.CallbackContext """ - user = await fetch_user(username=username) + user = await fetch_user(id=user_id) + print(user) fig, ax = plt.subplots(1, 1) fig.set_figwidth(8) @@ -62,7 +63,7 @@ async def draw_user_winrate(username, outcome=None, context=ContextTypes.DEFAULT fig.suptitle(user['full_name']) # Define a logo (an avatar) position - bins = await draw_user_winrate_bins(username, ax=ax, return_bins=True) + bins = await draw_user_winrate_bins(user_id, ax=ax, return_bins=True) heights = bins.sum(axis=1) if heights['Wins'] < 0.75*heights['Loses']: ab_posx = 0 From 704426c284a40fb828515ba97e7a1a85e9448cfb Mon Sep 17 00:00:00 2001 From: zimfv Date: Sat, 30 Mar 2024 00:53:14 +0300 Subject: [PATCH 12/14] The /mystats command tested and works correctly. Few non-tested questions and comments from Alex: 1) How this command will worl for user with an empty stats? 2) In the chat bot should give info only about this chat. --- pyproject.toml | 1 + src/handlers/mystats.py | 2 +- src/services/draw_graphs.py | 13 +++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 629b9a5..2e2943f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ cairosvg = "^2.7.1" pydantic-settings = "^2.2.1" pandas = "^1.5.3" matplotlib = "^3.7.4" +scikit-image = "^0.22.0" [build-system] requires = ["poetry-core"] diff --git a/src/handlers/mystats.py b/src/handlers/mystats.py index 67b9523..e9b37b7 100644 --- a/src/handlers/mystats.py +++ b/src/handlers/mystats.py @@ -14,6 +14,6 @@ async def mystats( user_id = update.effective_user.id chat_id = update.effective_chat.id await context.bot.send_photo(chat_id=chat_id, - photo=await draw_user_winrate(user_id), + photo=await draw_user_winrate(user_id, context=context), disable_notification=True) #await update.effective_message.delete() \ No newline at end of file diff --git a/src/services/draw_graphs.py b/src/services/draw_graphs.py index 68c1391..7a7e9ef 100644 --- a/src/services/draw_graphs.py +++ b/src/services/draw_graphs.py @@ -1,4 +1,5 @@ import io +import skimage import asyncio import pandas as pd @@ -55,7 +56,6 @@ async def draw_user_winrate(user_id, outcome=None, context=ContextTypes.DEFAULT_ context : telegram.ext.CallbackContext """ user = await fetch_user(id=user_id) - print(user) fig, ax = plt.subplots(1, 1) fig.set_figwidth(8) @@ -65,18 +65,19 @@ async def draw_user_winrate(user_id, outcome=None, context=ContextTypes.DEFAULT_ # Define a logo (an avatar) position bins = await draw_user_winrate_bins(user_id, ax=ax, return_bins=True) heights = bins.sum(axis=1) - if heights['Wins'] < 0.75*heights['Loses']: + if heights['Wins'] < 0.66*heights['Loses']: ab_posx = 0 - elif heights['Loses'] < 0.75*heights['Wins']: + elif heights['Loses'] < 0.66*heights['Wins']: ab_posx = 1 else: ab_posx = 0.5 ab_posy = 0.85*heights.max() # Set a logo (an avatar) - file = await get_user_profile_photo(context=context, player_id=user['id']) - logo = image.imread(file) - imagebox = OffsetImage(logo, zoom = 0.25) + photo_path = await get_user_profile_photo(context=context, player_id=user_id) + logo = skimage.io.imread(photo_path) + zoom = 90/logo.shape[1] + imagebox = OffsetImage(logo, zoom=zoom) ab = AnnotationBbox(imagebox, (ab_posx, ab_posy), frameon = True) ax.add_artist(ab) From 3539f6fb885fe3ad8496dce565994cfcc6738f04 Mon Sep 17 00:00:00 2001 From: zimfv Date: Wed, 3 Apr 2024 21:20:19 +0300 Subject: [PATCH 13/14] Add rating system --- src/services/db_service.py | 118 +++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/src/services/db_service.py b/src/services/db_service.py index 46fffa6..cbf8613 100644 --- a/src/services/db_service.py +++ b/src/services/db_service.py @@ -239,6 +239,124 @@ async def fetch_players_stats(order='DESC', mingames=None, top=None): return res + +async def fetch_result_distribution(playroom_id=None): + # returns + if playroom_id is None: + condition = '' + else: + condition = f'WHERE playroom_id = {playroom_id}' + query = f"""WITH roles_in_games(Games, FW, LL, LW, FL, HW, HL, CH, DH) AS + (SELECT 1 AS Games, + SUM(CASE WHEN role = 'FW' THEN 1 ELSE 0 END) > 0 AS FW, + SUM(CASE WHEN role = 'LL' THEN 1 ELSE 0 END) > 0 AS LL, + SUM(CASE WHEN role = 'LW' THEN 1 ELSE 0 END) > 0 AS LW, + SUM(CASE WHEN role = 'FL' THEN 1 ELSE 0 END) > 0 AS FL, + SUM(CASE WHEN role = 'HW' THEN 1 ELSE 0 END) > 0 AS HW, + SUM(CASE WHEN role = 'HL' THEN 1 ELSE 0 END) > 0 AS HL, + SUM(CASE WHEN role = 'CH' THEN 1 ELSE 0 END) > 0 AS CH, + SUM(CASE WHEN role = 'DH' THEN 1 ELSE 0 END) > 0 AS DH + FROM records + {condition} + GROUP BY game_id) + SELECT SUM(Games) as Games, + SUM(FW) AS FW, + SUM(LL) AS LL, + SUM(LW) AS LW, + SUM(FL) AS FL, + SUM(HW) AS HW, + SUM(HL) AS HL, + SUM(CH) AS CH, + SUM(DH) AS DH + FROM roles_in_games; + """ + result = await fetch_one(query, []) + return result + + +async def get_answer_coeffs(playroom_id=None): + # calculate the coeffs for rating + d = await fetch_result_distribution(playroom_id) + coeffs = {'FW' : 0.5*d['Games']/d['FW'], + 'LW' : 0.5*d['Games']/d['LW'], + 'HW' : 0.5*d['Games']/d['HW'], + 'CH' : 0.5*d['Games']/d['CH'], + 'FL' : -0.5*d['Games']/d['FL'], + 'LL' : -0.5*d['Games']/d['LL'], + 'HL' : -0.5*d['Games']/d['HL'], + 'DH' : -0.5*d['Games']/d['DH'] + } + return coeffs + + +async def get_players_rating(playroom_id=None, order='DESC', mingames=None, top=None): + """ + Returns table containing number of wins and loses for each role and winrate + + Parameters: + ----------- + playroom_id : int or None + + order : str values 'DESC' or 'ASC' + + top : uint or None + function returns top number of results, if that's not None + + Retuens: + -------- + res : DataFrame + Columns: username, + LW (liberal wins), + FW (fascist wins), + HW (Hitler wins), + LL (liberal loses), + FL (fascist loses), + HL (Hitler loses), + winrate + """ + answer_coeffs = await get_answer_coeffs(playroom_id=playroom_id) + + if playroom_id is None: condition = '' + else: condition = f'WHERE playroom_id = {playroom_id}' + + if mingames is None: having = "" + else: having = f"HAVING games >= {mingames}" + + query = f""" + SELECT player_id, players.username, players.full_name, + AVG(CASE WHEN role IN ('FW', 'LW', 'HW', 'CH') THEN 1 ELSE 0 END) AS winrate, + COUNT(role) AS games, + SUM(CASE WHEN role = 'FW' THEN {answer_coeffs['FW']} + WHEN role = 'LW' THEN {answer_coeffs['LW']} + WHEN role = 'HW' THEN {answer_coeffs['HW']} + WHEN role = 'CH' THEN {answer_coeffs['CH']} + WHEN role = 'FL' THEN {answer_coeffs['FL']} + WHEN role = 'LL' THEN {answer_coeffs['LL']} + WHEN role = 'HL' THEN {answer_coeffs['HL']} + WHEN role = 'DH' THEN {answer_coeffs['DH']} END) AS rating, + SUM(CASE WHEN records.role = 'LW' THEN 1 ELSE 0 END) AS LW, + SUM(CASE WHEN records.role = 'FW' THEN 1 ELSE 0 END) AS FW, + SUM(CASE WHEN records.role IN ('HW', 'HC') THEN 1 ELSE 0 END) AS HW, + SUM(CASE WHEN records.role = 'LL' THEN 1 ELSE 0 END) AS LL, + SUM(CASE WHEN records.role = 'FL' THEN 1 ELSE 0 END) AS FL, + SUM(CASE WHEN records.role IN ('HL', 'HD') THEN 1 ELSE 0 END) AS HL + FROM records + INNER JOIN players ON players.id = records.player_id + {condition} + GROUP BY player_id {having} + ORDER BY rating {order} + """ + if top is not None: + query = query[:-1] + f'\nLIMIT {top};' + result = await fetch_all(query, []) + return result + + + + + + + async def fetch_connection_stats(username, order='DESC', top=None, which='teammate'): """ Returns table containing number of wins and loses for each role and winrate From 0ce17ee90dfc83dd72fb54aaaf8f76a38945f807 Mon Sep 17 00:00:00 2001 From: zimfv Date: Thu, 4 Apr 2024 19:12:37 +0300 Subject: [PATCH 14/14] Update /mystats functions using Rabbit's recomendations with Alex's comments: add arguments to draw_graphs.py draw_user_winrate; Add try-catch construction to draw_graphs.py draw_user_winrate and handlers/mystats.py mystats --- src/handlers/mystats.py | 12 ++++-- src/services/draw_graphs.py | 78 +++++++++++++++++++++---------------- 2 files changed, 53 insertions(+), 37 deletions(-) diff --git a/src/handlers/mystats.py b/src/handlers/mystats.py index e9b37b7..3968d32 100644 --- a/src/handlers/mystats.py +++ b/src/handlers/mystats.py @@ -13,7 +13,11 @@ async def mystats( # Draws personal stats of asking user user_id = update.effective_user.id chat_id = update.effective_chat.id - await context.bot.send_photo(chat_id=chat_id, - photo=await draw_user_winrate(user_id, context=context), - disable_notification=True) - #await update.effective_message.delete() \ No newline at end of file + try: + await context.bot.send_photo(chat_id=chat_id, + photo=await draw_user_winrate(user_id, context=context), + disable_notification=True) + #await update.effective_message.delete() + except TelegramError as e: + logging.error(f"Failed to send photo: {e}") + await context.bot.send_message(chat_id=chat_id, text="Sorry, there was an error processing your request.") \ No newline at end of file diff --git a/src/services/draw_graphs.py b/src/services/draw_graphs.py index 7a7e9ef..a929415 100644 --- a/src/services/draw_graphs.py +++ b/src/services/draw_graphs.py @@ -40,7 +40,8 @@ async def draw_user_winrate_bins(user_id, ax=None, return_bins=False): -async def draw_user_winrate(user_id, outcome=None, context=ContextTypes.DEFAULT_TYPE): +async def draw_user_winrate(user_id, outcome=None, context=ContextTypes.DEFAULT_TYPE, + border_logo_height=0.66, logo_size=90): """ Draw a winrate statistics for given player and saves this as svg @@ -54,37 +55,48 @@ async def draw_user_winrate(user_id, outcome=None, context=ContextTypes.DEFAULT_ Returns svg-string if this is None context : telegram.ext.CallbackContext - """ - user = await fetch_user(id=user_id) - - fig, ax = plt.subplots(1, 1) - fig.set_figwidth(8) - fig.set_figheight(6) - fig.suptitle(user['full_name']) - # Define a logo (an avatar) position - bins = await draw_user_winrate_bins(user_id, ax=ax, return_bins=True) - heights = bins.sum(axis=1) - if heights['Wins'] < 0.66*heights['Loses']: - ab_posx = 0 - elif heights['Loses'] < 0.66*heights['Wins']: - ab_posx = 1 - else: - ab_posx = 0.5 - ab_posy = 0.85*heights.max() - - # Set a logo (an avatar) - photo_path = await get_user_profile_photo(context=context, player_id=user_id) - logo = skimage.io.imread(photo_path) - zoom = 90/logo.shape[1] - imagebox = OffsetImage(logo, zoom=zoom) - ab = AnnotationBbox(imagebox, (ab_posx, ab_posy), frameon = True) - ax.add_artist(ab) + border_logo_height : float + That's possible to put user photo over the bar if bar height is that size + + logo_szie : int + The size of user photo + """ + try: + user = await fetch_user(id=user_id) + + fig, ax = plt.subplots(1, 1) + fig.set_figwidth(8) + fig.set_figheight(6) + fig.suptitle(user['full_name']) + + # Define a logo (an avatar) position + bins = await draw_user_winrate_bins(user_id, ax=ax, return_bins=True) + heights = bins.sum(axis=1) + if heights['Wins'] < border_logo_height*heights['Loses']: + ab_posx = 0 + elif heights['Loses'] < border_logo_height*heights['Wins']: + ab_posx = 1 + else: + ab_posx = 0.5 + ab_posy = 0.85*heights.max() + + # Set a logo (an avatar) + photo_path = await get_user_profile_photo(context=context, player_id=user_id) + logo = skimage.io.imread(photo_path) + zoom = logo_size/logo.shape[1] + imagebox = OffsetImage(logo, zoom=zoom) + ab = AnnotationBbox(imagebox, (ab_posx, ab_posy), frameon = True) + ax.add_artist(ab) - if not (outcome is None): - fig.savefig(outcome) - else: - svg = io.StringIO() - fig.savefig(svg, format='svg') - svg = svg.getvalue() - return svg2png(svg) \ No newline at end of file + if not (outcome is None): + fig.savefig(outcome) + else: + svg = io.StringIO() + fig.savefig(svg, format='svg') + svg = svg.getvalue() + return svg2png(svg) + except Exception as e: + # Log the error message + print(f"Failed to draw winrate statistics for {username}: {str(e)}") + # Optionally, return or handle the error further \ No newline at end of file