async def curve(self, ctx, *args: str): """Plots the count of problems solved over time on Codeforces for the handles provided.""" filt = cf_common.SubFilter() args = filt.parse(args) handles = args or ('!' + str(ctx.author),) handles = await cf_common.resolve_handles(ctx, self.converter, handles) resp = [await cf.user.status(handle=handle) for handle in handles] all_solved_subs = [filt.filter_subs(submissions) for submissions in resp] if not any(all_solved_subs): raise GraphCogError(f'There are no problems within the specified parameters.') plt.clf() plt.xlabel('Time') plt.ylabel('Cumulative solve count') all_times = [[dt.datetime.fromtimestamp(sub.creationTimeSeconds) for sub in solved_subs] for solved_subs in all_solved_subs] for times in all_times: cumulative_solve_count = list(range(1, len(times)+1)) + [len(times)] timestretched = times + [min(dt.datetime.now(), dt.datetime.fromtimestamp(filt.dhi))] plt.plot(timestretched, cumulative_solve_count) labels = [gc.StrWrap(f'{handle}: {len(times)}') for handle, times in zip(handles, all_times)] plt.legend(labels) plt.gcf().autofmt_xdate() discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed(title='Curve of number of solved problems over time') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def howgud(self, ctx, *members: discord.Member): members = members or (ctx.author, ) if len(members) > 5: raise GraphCogError('Please specify at most 5 gudgitters.') # shift the [-300, 300] gitgud range to center the text hist_bins = list(range(-300 - 50, 300 + 50 + 1, 100)) deltas = [[x[0] for x in cf_common.user_db.howgud(member.id)] for member in members] labels = [ gc.StrWrap(f'{member.display_name}: {len(delta)}') for member, delta in zip(members, deltas) ] plt.clf() plt.margins(x=0) plt.hist(deltas, bins=hist_bins, label=labels, rwidth=1) plt.xlabel('Problem delta') plt.ylabel('Number solved') plt.legend(prop=self.fontprop) discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed(title='Histogram of gudgitting') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def hist(self, ctx, *args: str): """Shows the histogram of problems solved over time on Codeforces for the handles provided.""" filt = cf_common.SubFilter() args = filt.parse(args) handles = args or ('!' + str(ctx.author), ) handles = await cf_common.resolve_handles(ctx, self.converter, handles) resp = [await cf.user.status(handle=handle) for handle in handles] all_solved_subs = [ filt.filter_subs(submissions) for submissions in resp ] if not any(all_solved_subs): raise GraphCogError( f'There are no problems within the specified parameters.') plt.clf() plt.xlabel('Time') plt.ylabel('Number solved') if len(handles) == 1: handle, solved_by_type = handles[0], _classify_submissions( all_solved_subs[0]) all_times = [[ dt.datetime.fromtimestamp(sub.creationTimeSeconds) for sub in solved_by_type[sub_type] ] for sub_type in filt.types] nice_names = nice_sub_type(filt.types) labels = [ name.format(len(times)) for name, times in zip(nice_names, all_times) ] plt.hist(all_times, stacked=True, label=labels, bins=34) total = sum(map(len, all_times)) plt.legend(title=f'{handle}: {total}', title_fontsize=plt.rcParams['legend.fontsize']) else: all_times = [[ dt.datetime.fromtimestamp(sub.creationTimeSeconds) for sub in solved_subs ] for solved_subs in all_solved_subs] # NOTE: matplotlib ignores labels that begin with _ # https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.legend # Add zero-width space to work around this labels = [ gc.StrWrap(f'{handle}: {len(times)}') for handle, times in zip(handles, all_times) ] plt.hist(all_times) plt.legend(labels) plt.gcf().autofmt_xdate() discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed( title='Histogram of number of solved problems over time') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def rating(self, ctx, *args: str): """Plots Codeforces rating graph for the handles provided.""" (zoom, peak), args = cf_common.filter_flags(args, ['+zoom' , '+peak']) filt = cf_common.SubFilter() args = filt.parse(args) handles = args or ('!' + str(ctx.author),) handles = await cf_common.resolve_handles(ctx, self.converter, handles) resp = [await cf.user.rating(handle=handle) for handle in handles] resp = [filt.filter_rating_changes(rating_changes) for rating_changes in resp] if not any(resp): handles_str = ', '.join(f'`{handle}`' for handle in handles) if len(handles) == 1: message = f'User {handles_str} is not rated' else: message = f'None of the given users {handles_str} are rated' raise GraphCogError(message) def max_prefix(user): max_rate = 0 res = [] for data in user: old_rating = data.oldRating if old_rating == 0: old_rating = 1500 if data.newRating - old_rating >= 0 and data.newRating >= max_rate: max_rate = data.newRating res.append(data) return(res) if peak: resp = [max_prefix(user) for user in resp] plt.clf() plt.axes().set_prop_cycle(gc.rating_color_cycler) _plot_rating(resp) current_ratings = [rating_changes[-1].newRating if rating_changes else 'Unrated' for rating_changes in resp] labels = [gc.StrWrap(f'{handle} ({rating})') for handle, rating in zip(handles, current_ratings)] plt.legend(labels, loc='upper left') if not zoom: min_rating = 1100 max_rating = 1800 for rating_changes in resp: for rating in rating_changes: min_rating = min(min_rating, rating.newRating) max_rating = max(max_rating, rating.newRating) plt.ylim(min_rating - 100, max_rating + 200) discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed(title='Rating graph on Codeforces') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def solved(self, ctx, *args: str): """Shows a histogram of problems solved on Codeforces for the handles provided. e.g. ;plot solved meooow +contest +virtual +outof +dp""" filt = cf_common.SubFilter() args = filt.parse(args) handles = args or ('!' + str(ctx.author),) handles = await cf_common.resolve_handles(ctx, self.converter, handles) resp = [await cf.user.status(handle=handle) for handle in handles] all_solved_subs = [filt.filter_subs( submissions) for submissions in resp] if not any(all_solved_subs): raise GraphCogError(f'There are no problems within the specified parameters.') plt.clf() plt.xlabel('Problem rating') plt.ylabel('Number solved') if len(handles) == 1: # Display solved problem separately by type for a single user. handle, solved_by_type = handles[0], _classify_submissions( all_solved_subs[0]) all_ratings = [[sub.problem.rating for sub in solved_by_type[sub_type]] for sub_type in filt.types] nice_names = nice_sub_type(filt.types) labels = [name.format(len(ratings)) for name, ratings in zip(nice_names, all_ratings)] step = 100 # shift the range to center the text hist_bins = list(range(filt.rlo - step // 2, filt.rhi + step // 2 + 1, step)) plt.hist(all_ratings, stacked=True, bins=hist_bins, label=labels) total = sum(map(len, all_ratings)) plt.legend(title=f'{handle}: {total}', title_fontsize=plt.rcParams['legend.fontsize'], loc='upper right') else: all_ratings = [[sub.problem.rating for sub in solved_subs] for solved_subs in all_solved_subs] labels = [gc.StrWrap(f'{handle}: {len(ratings)}') for handle, ratings in zip(handles, all_ratings)] step = 200 hist_bins = list(range(filt.rlo - step // 2, filt.rhi + step // 2 + 1, step)) plt.hist(all_ratings, bins=hist_bins) plt.legend(labels, loc='upper right') discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed( title='Histogram of problems solved on Codeforces') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def performance(self, ctx, *args: str): """Plots Codeforces performance graph for the handles provided.""" (zoom, peak), args = cf_common.filter_flags(args, ['+zoom', '+asdfgdsafefsdve']) filt = cf_common.SubFilter() args = filt.parse(args) handles = args or ('!' + str(ctx.author), ) handles = await cf_common.resolve_handles(ctx, self.converter, handles) resp = [await cf.user.rating(handle=handle) for handle in handles] # extract last rating before corrections current_ratings = [ rating_changes[-1].newRating if rating_changes else 'Unrated' for rating_changes in resp ] resp = cf.user.correct_rating_changes(resp=resp) resp = [ filt.filter_rating_changes(rating_changes) for rating_changes in resp ] if not any(resp): handles_str = ', '.join(f'`{handle}`' for handle in handles) if len(handles) == 1: message = f'User {handles_str} is not rated' else: message = f'None of the given users {handles_str} are rated' raise GraphCogError(message) plt.clf() plt.axes().set_prop_cycle(gc.rating_color_cycler) _plot_perf(resp) labels = [ gc.StrWrap(f'{handle} ({rating})') for handle, rating in zip(handles, current_ratings) ] plt.legend(labels, loc='upper left') if not zoom: min_rating = 1100 max_rating = 1800 for rating_changes in resp: for rating in rating_changes: min_rating = min(min_rating, rating.oldRating) max_rating = max(max_rating, rating.oldRating) plt.ylim(min_rating - 100, max_rating + 200) discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed( title='Performance graph on Codeforces') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def vcperformance(self, ctx, *members: discord.Member): """Plots VC performance for at most 5 users.""" members = members or (ctx.author, ) if len(members) > 5: raise ContestCogError('Cannot plot more than 5 VCers at once.') plot_data = defaultdict(list) min_rating = 1100 max_rating = 1800 for member in members: rating_history = cf_common.user_db.get_vc_rating_history(member.id) if not rating_history: raise ContestCogError(f'{member.mention} has no vc history.') ratingbefore = 1500 for vc_id, rating in rating_history: vc = cf_common.user_db.get_rated_vc(vc_id) perf = ratingbefore + (rating - ratingbefore)*4 date = dt.datetime.fromtimestamp(vc.finish_time) plot_data[member.display_name].append((date, perf)) min_rating = min(min_rating, perf) max_rating = max(max_rating, perf) ratingbefore = rating plt.clf() # plot at least from mid gray to mid purple for rating_data in plot_data.values(): x, y = zip(*rating_data) plt.plot(x, y, linestyle='-', marker='o', markersize=4, markerfacecolor='white', markeredgewidth=0.5) gc.plot_rating_bg(cf.RATED_RANKS) plt.gcf().autofmt_xdate() plt.ylim(min_rating - 100, max_rating + 200) labels = [ gc.StrWrap('{} ({})'.format( member_display_name, ratingbefore)) for member_display_name, rating_data in plot_data.items() ] plt.legend(labels, loc='upper left', prop=gc.fontprop) discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed(title='VC performance graph') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def rating(self, ctx, *args: str): """Plots Codeforces rating graph for the handles provided.""" (zoom, ), args = cf_common.filter_flags(args, ["+zoom"]) filt = cf_common.SubFilter() args = filt.parse(args) args = _mention_to_handle(args, ctx) handles = args or ("!" + str(ctx.author), ) handles = await cf_common.resolve_handles(ctx, self.converter, handles) resp = [await cf.user.rating(handle=handle) for handle in handles] resp = [ filt.filter_rating_changes(rating_changes) for rating_changes in resp ] if not any(resp): handles_str = ", ".join(f"`{handle}`" for handle in handles) if len(handles) == 1: message = f"User {handles_str} is not rated" else: message = f"None of the given users {handles_str} are rated" raise GraphCogError(message) plt.clf() _plot_rating(resp) current_ratings = [ rating_changes[-1].newRating if rating_changes else "Unrated" for rating_changes in resp ] labels = [ gc.StrWrap(f"{handle} ({rating})") for handle, rating in zip(handles, current_ratings) ] plt.legend(labels, loc="upper left") if not zoom: min_rating = 1100 max_rating = 1800 for rating_changes in resp: for rating in rating_changes: min_rating = min(min_rating, rating.newRating) max_rating = max(max_rating, rating.newRating) plt.ylim(min_rating - 100, max_rating + 200) discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed( title="Rating graph on Codeforces") discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def rating(self, ctx, *args: str): """Plots Codeforces rating graph for the handles provided.""" (zoom, ), args = cf_common.filter_flags(args, ['+zoom']) handles = args or ('!' + str(ctx.author), ) handles = await cf_common.resolve_handles(ctx, self.converter, handles) resp = [await cf.user.rating(handle=handle) for handle in handles] if not any(resp): handles_str = ', '.join(f'`{handle}`' for handle in handles) if len(handles) == 1: message = f'User {handles_str} is not rated' else: message = f'None of the given users {handles_str} are rated' raise GraphCogError(message) plt.clf() _plot_rating(resp) current_ratings = [ rating_changes[-1].newRating if rating_changes else 'Unrated' for rating_changes in resp ] labels = [ gc.StrWrap(f'{handle} ({rating})') for handle, rating in zip(handles, current_ratings) ] plt.legend(labels, loc='upper left') if not zoom: min_rating = 1100 max_rating = 1800 for rating_changes in resp: for rating in rating_changes: min_rating = min(min_rating, rating.newRating) max_rating = max(max_rating, rating.newRating) plt.ylim(min_rating - 100, max_rating + 200) discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed( title='Rating graph on Codeforces') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def hist(self, ctx, *args: str): """Shows the histogram of problems solved on Codeforces over time for the handles provided""" filt = cf_common.SubFilter() args = filt.parse(args) phase_days = 1 handles = [] for arg in args: if arg[0:11] == 'phase_days=': phase_days = int(arg[11:]) else: handles.append(arg) if phase_days < 1: raise GraphCogError('Invalid parameters') phase_time = dt.timedelta(days=phase_days) handles = handles or ['!' + str(ctx.author)] handles = await cf_common.resolve_handles(ctx, self.converter, handles) resp = [await cf.user.status(handle=handle) for handle in handles] all_solved_subs = [filt.filter_subs(submissions) for submissions in resp] if not any(all_solved_subs): raise GraphCogError(f'There are no problems within the specified parameters.') plt.clf() plt.xlabel('Time') plt.ylabel('Number solved') if len(handles) == 1: handle, solved_by_type = handles[0], _classify_submissions(all_solved_subs[0]) all_times = [[dt.datetime.fromtimestamp(sub.creationTimeSeconds) for sub in solved_by_type[sub_type]] for sub_type in filt.types] nice_names = nice_sub_type(filt.types) labels = [name.format(len(times)) for name, times in zip(nice_names, all_times)] dlo = min(itertools.chain.from_iterable(all_times)).date() dhi = min(dt.datetime.today() + dt.timedelta(days=1), dt.datetime.fromtimestamp(filt.dhi)).date() phase_cnt = math.ceil((dhi - dlo) / phase_time) plt.hist( all_times, stacked=True, label=labels, range=(dhi - phase_cnt * phase_time, dhi), bins=min(40, phase_cnt)) total = sum(map(len, all_times)) plt.legend(title=f'{handle}: {total}', title_fontsize=plt.rcParams['legend.fontsize']) else: all_times = [[dt.datetime.fromtimestamp(sub.creationTimeSeconds) for sub in solved_subs] for solved_subs in all_solved_subs] # NOTE: matplotlib ignores labels that begin with _ # https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.legend # Add zero-width space to work around this labels = [gc.StrWrap(f'{handle}: {len(times)}') for handle, times in zip(handles, all_times)] dlo = min(itertools.chain.from_iterable(all_times)).date() dhi = min(dt.datetime.today() + dt.timedelta(days=1), dt.datetime.fromtimestamp(filt.dhi)).date() phase_cnt = math.ceil((dhi - dlo) / phase_time) plt.hist( all_times, range=(dhi - phase_cnt * phase_time, dhi), bins=min(40 // len(handles), phase_cnt)) plt.legend(labels) # NOTE: In case of nested list, matplotlib decides type using 1st sublist, # it assumes float when 1st sublist is empty. # Hence explicitly assigning locator and formatter is must here. locator = mdates.AutoDateLocator() plt.gca().xaxis.set_major_locator(locator) plt.gca().xaxis.set_major_formatter(mdates.AutoDateFormatter(locator)) plt.gcf().autofmt_xdate() discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed(title='Histogram of number of solved problems over time') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def rating(self, ctx, *members: discord.Member): """Plot duelist's rating.""" members = members or (ctx.author, ) if len(members) > 5: raise DuelCogError(f'Cannot plot more than 5 duelists at once.') duelists = [member.id for member in members] duels = cf_common.user_db.get_complete_official_duels() rating = dict() plot_data = defaultdict(list) time_tick = 0 for challenger, challengee, winner, finish_time in duels: challenger_r = rating.get(challenger, 1500) challengee_r = rating.get(challengee, 1500) if winner == Winner.CHALLENGER: delta = round(elo_delta(challenger_r, challengee_r, 1)) elif winner == Winner.CHALLENGEE: delta = round(elo_delta(challenger_r, challengee_r, 0)) else: delta = round(elo_delta(challenger_r, challengee_r, 0.5)) rating[challenger] = challenger_r + delta rating[challengee] = challengee_r - delta if challenger in duelists or challengee in duelists: if challenger in duelists: plot_data[challenger].append( (time_tick, rating[challenger])) if challengee in duelists: plot_data[challengee].append( (time_tick, rating[challengee])) time_tick += 1 if time_tick == 0: raise DuelCogError(f'Nothing to plot.') plt.clf() # plot at least from mid gray to mid purple min_rating = 1350 max_rating = 1550 for rating_data in plot_data.values(): for tick, rating in rating_data: min_rating = min(min_rating, rating) max_rating = max(max_rating, rating) x, y = zip(*rating_data) plt.plot(x, y, linestyle='-', marker='o', markersize=2, markerfacecolor='white', markeredgewidth=0.5) gc.plot_rating_bg(DUEL_RANKS) plt.xlim(0, time_tick - 1) plt.ylim(min_rating - 100, max_rating + 100) labels = [ gc.StrWrap('{} ({})'.format( ctx.guild.get_member(duelist).display_name, rating_data[-1][1])) for duelist, rating_data in plot_data.items() ] plt.legend(labels, loc='upper left', prop=gc.fontprop) discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed(title='Duel rating graph') discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)
async def speed(self, ctx, *args): """Plot time spent on problems of particular rating during contest.""" (add_scatter, use_median), args = cf_common.filter_flags(args, ['+scatter', '+median']) filt = cf_common.SubFilter() args = filt.parse(args) if 'PRACTICE' in filt.types: filt.types.remove( 'PRACTICE') # can't estimate time for practice submissions handles, point_size = [], 3 for arg in args: if arg[0:2] == 's=': point_size = int(arg[2:]) else: handles.append(arg) handles = handles or ['!' + str(ctx.author)] handles = await cf_common.resolve_handles(ctx, self.converter, handles) resp = [await cf.user.status(handle=handle) for handle in handles] all_solved_subs = [ filt.filter_subs(submissions) for submissions in resp ] plt.clf() plt.xlabel('Rating') plt.ylabel('Minutes spent') max_time = 0 # for ylim for submissions in all_solved_subs: scatter_points = [] # only matters if +scatter solved_by_contest = collections.defaultdict(lambda: []) for submission in submissions: # [solve_time, problem rating, problem index] for each solved problem solved_by_contest[submission.contestId].append([ submission.relativeTimeSeconds, submission.problem.rating, submission.problem.index ]) time_by_rating = collections.defaultdict(lambda: []) for events in solved_by_contest.values(): events.sort() solved_subproblems = dict() last_ac_time = 0 for (current_ac_time, rating, problem_index) in events: time_to_solve = current_ac_time - last_ac_time last_ac_time = current_ac_time # if there are subproblems, add total time for previous subproblems to current one if len(problem_index) == 2 and problem_index[1].isdigit(): time_to_solve += solved_subproblems.get( problem_index[0], 0) solved_subproblems[problem_index[0]] = time_to_solve time_by_rating[rating].append(time_to_solve / 60) # in minutes for rating in time_by_rating.keys(): times = time_by_rating[rating] if use_median: time_by_rating[rating] = np.median(times) else: time_by_rating[rating] = sum(times) / len(times) if add_scatter: for t in times: scatter_points.append([rating, t]) max_time = max(max_time, t) xs = sorted(time_by_rating.keys()) ys = [time_by_rating[rating] for rating in xs] max_time = max(max_time, max(ys, default=0)) plt.plot(xs, ys) if add_scatter: plt.scatter(*zip(*scatter_points), s=point_size) labels = [gc.StrWrap(handle) for handle in handles] plt.legend(labels) plt.ylim(0, max_time + 5) # make xticks divisible by 100 ticks = plt.gca().get_xticks() base = ticks[1] - ticks[0] plt.gca().get_xaxis().set_major_locator( MultipleLocator(base=max(base // 100 * 100, 100))) discord_file = gc.get_current_figure_as_file() title = f'Plot of {"median" if use_median else "average"} time spent on a problem' embed = discord_common.cf_color_embed(title=title) discord_common.attach_image(embed, discord_file) discord_common.set_author_footer(embed, ctx.author) await ctx.send(embed=embed, file=discord_file)