async def scatter(self, ctx, *args): """Plot Codeforces rating overlaid on a scatter plot of problems solved. Also plots a running average of ratings of problems solved in practice.""" filt = cf_common.SubFilter() args = filt.parse(args) handle, bin_size, point_size = None, 10, 3 for arg in args: if arg[0:2] == 'b=': bin_size = int(arg[2:]) elif arg[0:2] == 's=': point_size = int(arg[2:]) else: if handle: raise GraphCogError('Only one handle allowed.') handle = arg if bin_size < 1 or point_size < 1 or point_size > 100: raise GraphCogError('Invalid parameters') handle = handle or '!' + str(ctx.author) handle, = await cf_common.resolve_handles(ctx, self.converter, (handle,)) rating_resp = [await cf.user.rating(handle=handle)] rating_resp = [filt.filter_rating_changes(rating_changes) for rating_changes in rating_resp] submissions = filt.filter_subs(await cf.user.status(handle=handle)) def extract_time_and_rating(submissions): return [(dt.datetime.fromtimestamp(sub.creationTimeSeconds), sub.problem.rating) for sub in submissions] if not any(submissions): raise GraphCogError(f'No submissions for user `{handle}`') solved_by_type = _classify_submissions(submissions) regular = extract_time_and_rating(solved_by_type['CONTESTANT'] + solved_by_type['OUT_OF_COMPETITION']) practice = extract_time_and_rating(solved_by_type['PRACTICE']) virtual = extract_time_and_rating(solved_by_type['VIRTUAL']) plt.clf() _plot_scatter(regular, practice, virtual, point_size) labels = [] if practice: labels.append('Practice') if regular: labels.append('Regular') if virtual: labels.append('Virtual') plt.legend(labels, loc='upper left') _plot_average(practice, bin_size) _plot_rating(rating_resp, mark='') # zoom ymin, ymax = plt.gca().get_ylim() plt.ylim(max(ymin, filt.rlo - 100), min(ymax, filt.rhi + 100)) discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed(title=f'Rating vs solved problem rating for {handle}') 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 stalk(self, ctx, *args): """Print problems solved by user sorted by time (default) or rating. All submission types are included by default (practice, contest, etc.) """ (hardest,), args = cf_common.filter_flags(args, ['+hardest']) filt = cf_common.SubFilter(False) args = filt.parse(args) handles = args or ('!' + str(ctx.author),) handles = await cf_common.resolve_handles(ctx, self.converter, handles) submissions = [await cf.user.status(handle=handle) for handle in handles] submissions = [sub for subs in submissions for sub in subs] submissions = filt.filter(submissions) if not submissions: raise CodeforcesCogError('Submissions not found within the search parameters') if hardest: submissions.sort(key=lambda sub: (sub.problem.rating or 0, sub.creationTimeSeconds), reverse=True) else: submissions.sort(key=lambda sub: sub.creationTimeSeconds, reverse=True) msg = '\n'.join( f'[{sub.problem.name}]({sub.problem.url})\N{EN SPACE}' f'[{sub.problem.rating if sub.problem.rating else "?"}]\N{EN SPACE}' f'({cf_common.days_ago(sub.creationTimeSeconds)})' for sub in submissions[:10] ) title = '{} solved problems by `{}`'.format('Hardest' if hardest else 'Recently', '`, `'.join(handles)) embed = discord_common.cf_color_embed(title=title, description=msg) await ctx.send(embed=embed)
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 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 stalk(self, ctx, *args): """Print problems solved by user sorted by time (default) or rating. All submission types are included by default (practice, contest, etc.) """ (hardest, ), args = cf_common.filter_flags(args, ['+hardest']) filt = cf_common.SubFilter(False) args = filt.parse(args) handles = args or ('!' + str(ctx.author), ) handles = await cf_common.resolve_handles(ctx, self.converter, handles) submissions = [ await cf.user.status(handle=handle) for handle in handles ] submissions = [sub for subs in submissions for sub in subs] submissions = filt.filter_subs(submissions) if not submissions: raise CodeforcesCogError( 'Submissions not found within the search parameters') if hardest: submissions.sort( key=lambda sub: (sub.problem.rating or 0, sub.creationTimeSeconds), reverse=True) else: submissions.sort(key=lambda sub: sub.creationTimeSeconds, reverse=True) def make_line(sub): data = (f'[{sub.problem.name}]({sub.problem.url})', f'[{sub.problem.rating if sub.problem.rating else "?"}]', f'({cf_common.days_ago(sub.creationTimeSeconds)})') return '\N{EN SPACE}'.join(data) def make_page(chunk): title = '{} solved problems by `{}`'.format( 'Hardest' if hardest else 'Recently', '`, `'.join(handles)) hist_str = '\n'.join(make_line(sub) for sub in chunk) embed = discord_common.cf_color_embed(description=hist_str) return title, embed pages = [ make_page(chunk) for chunk in paginator.chunkify(submissions[:100], 10) ] paginator.paginate(self.bot, ctx.channel, pages, wait_time=5 * 60, set_pagenum_footers=True)
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 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 scatter(self, ctx, *args): """Plot Codeforces rating overlaid on a scatter plot of problems solved. Also plots a running average of ratings of problems solved in practice.""" filt = cf_common.SubFilter() args = filt.parse(args) handle, bin_size, point_size = None, 10, 3 for arg in args: if arg[0:2] == "b=": bin_size = int(arg[2:]) elif arg[0:2] == "s=": point_size = int(arg[2:]) else: handle = arg if bin_size < 1 or point_size < 1 or point_size > 100: raise GraphCogError("Invalid parameters") handle = handle or "!" + str(ctx.author) (handle, ) = await cf_common.resolve_handles(ctx, self.converter, (handle, )) rating_resp = [await cf.user.rating(handle=handle)] rating_resp = [ filt.filter_rating_changes(rating_changes) for rating_changes in rating_resp ] submissions = filt.filter_subs(await cf.user.status(handle=handle)) def extract_time_and_rating(submissions): return [( dt.datetime.fromtimestamp(sub.creationTimeSeconds), sub.problem.rating, ) for sub in submissions] if not any(rating_resp) and not any(submissions): raise GraphCogError( f"User `{handle}` is not rated and has not solved any rated problem" ) solved_by_type = _classify_submissions(submissions) regular = extract_time_and_rating(solved_by_type["CONTESTANT"] + solved_by_type["OUT_OF_COMPETITION"]) practice = extract_time_and_rating(solved_by_type["PRACTICE"]) virtual = extract_time_and_rating(solved_by_type["VIRTUAL"]) plt.clf() _plot_scatter(regular, practice, virtual, point_size) labels = [] if practice: labels.append("Practice") if regular: labels.append("Regular") if virtual: labels.append("Virtual") plt.legend(labels, loc="upper left") _plot_average(practice, bin_size) _plot_rating(rating_resp, mark="") # zoom ymin, ymax = plt.gca().get_ylim() plt.ylim(max(ymin, filt.rlo - 100), min(ymax, filt.rhi + 100)) discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed( title=f"Rating vs solved problem rating for {handle}") 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)