def _make_contest_pages(contests, title): pages = [] chunks = paginator.chunkify(contests, _CONTESTS_PER_PAGE) for chunk in chunks: embed = discord_common.cf_color_embed() for name, value in _get_embed_fields_from_contests(chunk): embed.add_field(name=name, value=value, inline=False) pages.append((title, embed)) return pages
async def mashup(self, ctx, *args): """Create a mashup contest using problems within +-100 of average rating of handles provided. Add tags with "+" before them. """ handles = [arg for arg in args if arg[0] != '+'] tags = [arg[1:] for arg in args if arg[0] == '+' and len(arg) > 1] 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] submissions = [sub for user in resp for sub in user] solved = {sub.problem.name for sub in submissions} info = await cf.user.info(handles=handles) rating = int( round( sum(user.effective_rating for user in info) / len(handles), -2)) problems = [ prob for prob in cf_common.cache2.problem_cache.problems if abs(prob.rating - rating) <= 100 and prob.name not in solved and not any( cf_common.is_contest_writer(prob.contestId, handle) for handle in handles) and not cf_common.is_nonstandard_problem(prob) ] if tags: problems = [prob for prob in problems if prob.tag_matches(tags)] if len(problems) < 4: raise CodeforcesCogError( 'Problems not found within the search parameters') problems.sort(key=lambda problem: cf_common.cache2.contest_cache. get_contest(problem.contestId).startTimeSeconds) choices = [] for i in range(4): k = max(random.randrange(len(problems) - i) for _ in range(2)) for c in choices: if k >= c: k += 1 choices.append(k) choices.sort() problems = reversed([problems[k] for k in choices]) msg = '\n'.join(f'{"ABCD"[i]}: [{p.name}]({p.url}) [{p.rating}]' for i, p in enumerate(problems)) str_handles = '`, `'.join(handles) embed = discord_common.cf_color_embed(description=msg) await ctx.send(f'Mashup contest for `{str_handles}`', 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)
def _make_contest_embed_for_ranklist(ranklist): contest = ranklist.contest assert contest.phase != 'BEFORE', f'Contest {contest.id} has not started.' embed = discord_common.cf_color_embed(title=contest.name, url=contest.url) phase = contest.phase.capitalize().replace('_', ' ') embed.add_field(name='Phase', value=phase) if ranklist.is_rated: embed.add_field(name='Deltas', value=ranklist.deltas_status) now = time.time() en = '\N{EN SPACE}' if contest.phase == 'CODING': elapsed = cf_common.pretty_time_format(now - contest.startTimeSeconds, shorten=True) remaining = cf_common.pretty_time_format(contest.end_time - now, shorten=True) msg = f'{elapsed} elapsed{en}|{en}{remaining} remaining' embed.add_field(name='Tick tock', value=msg, inline=False) else: start = _contest_start_time_format(contest, dt.timezone.utc) duration = _contest_duration_format(contest) since = cf_common.pretty_time_format(now - contest.end_time, only_most_significant=True) msg = f'{start}{en}|{en}{duration}{en}|{en}Ended {since} ago' embed.add_field(name='When', value=msg, inline=False) return embed
async def centile(self, ctx, *args: str): """Show percentile distribution of codeforces and mark given handles in the plot. If +zoom and handles are given, it zooms to the neighborhood of the handles.""" (zoom, nomarker, exact), args = cf_common.filter_flags( args, ['+zoom', '+nomarker', '+exact']) # Prepare data intervals = [(rank.low, rank.high) for rank in cf.RATED_RANKS] colors = [rank.color_graph for rank in cf.RATED_RANKS] ratings = cf_common.cache2.rating_changes_cache.get_all_ratings() ratings = np.array(sorted(ratings)) n = len(ratings) perc = 100 * np.arange(n) / n users_to_mark = {} if not nomarker: handles = args or ('!' + str(ctx.author), ) handles = await cf_common.resolve_handles(ctx, self.converter, handles, mincnt=0, maxcnt=50) infos = await cf.user.info(handles=list(set(handles))) for info in infos: if info.rating is None: raise GraphCogError(f'User `{info.handle}` is not rated') ix = bisect.bisect_left(ratings, info.rating) cent = 100 * ix / len(ratings) users_to_mark[info.handle] = info.rating, cent # Plot plt.clf() fig, ax = plt.subplots(1) ax.plot(ratings, perc, color='#00000099') plt.xlabel('Rating') plt.ylabel('Percentile') for pos in ['right', 'top', 'bottom', 'left']: ax.spines[pos].set_visible(False) ax.tick_params(axis='both', which='both', length=0) # Color intervals by rank for interval, color in zip(intervals, colors): alpha = '99' l, r = interval col = color + alpha rect = patches.Rectangle((l, -50), r - l, 200, edgecolor='none', facecolor=col) ax.add_patch(rect) if users_to_mark: ymin = min(point[1] for point in users_to_mark.values()) ymax = max(point[1] for point in users_to_mark.values()) if zoom: ymargin = max(0.5, (ymax - ymin) * 0.1) ymin -= ymargin ymax += ymargin else: ymin = min(-1.5, ymin - 8) ymax = max(101.5, ymax + 8) else: ymin, ymax = -1.5, 101.5 if users_to_mark and zoom: xmin = min(point[0] for point in users_to_mark.values()) xmax = max(point[0] for point in users_to_mark.values()) xmargin = max(20, (xmax - xmin) * 0.1) xmin -= xmargin xmax += xmargin else: xmin, xmax = ratings[0], ratings[-1] plt.xlim(xmin, xmax) plt.ylim(ymin, ymax) # Mark users in plot for user, point in users_to_mark.items(): astr = f'{user} ({round(point[1], 2)})' if exact else user apos = ('left', 'top') if point[0] <= (xmax + xmin) // 2 else ('right', 'bottom') plt.annotate(astr, xy=point, xytext=(0, 0), textcoords='offset points', ha=apos[0], va=apos[1]) plt.plot(*point, marker='o', markersize=5, color='red', markeredgecolor='darkred') # Draw tick lines linecolor = '#00000022' inf = 10000 def horz_line(y): l = mlines.Line2D([-inf, inf], [y, y], color=linecolor) ax.add_line(l) def vert_line(x): l = mlines.Line2D([x, x], [-inf, inf], color=linecolor) ax.add_line(l) for y in ax.get_yticks(): horz_line(y) for x in ax.get_xticks(): vert_line(x) # Discord stuff discord_file = gc.get_current_figure_as_file() embed = discord_common.cf_color_embed( title=f'Rating/percentile relationship') 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_hist(self, ctx, ratings, mode, binsize, title): if mode not in ('log', 'normal'): raise GraphCogError('Mode should be either `log` or `normal`') ratings = [r for r in ratings if r >= 0] assert ratings, 'Cannot histogram plot empty list of ratings' assert 100 % binsize == 0 # because bins is semi-hardcoded bins = 39 * 100 // binsize colors = [] low, high = 0, binsize * bins for rank in cf.RATED_RANKS: for r in range(max(rank.low, low), min(rank.high, high), binsize): colors.append('#' + '%06x' % rank.color_embed) assert len( colors) == bins, f'Expected {bins} colors, got {len(colors)}' height = [0] * bins for r in ratings: height[r // binsize] += 1 csum = 0 cent = [0] users = sum(height) for h in height: csum += h cent.append(round(100 * csum / users)) x = [k * binsize for k in range(bins)] label = [f'{r} ({c})' for r, c in zip(x, cent)] l, r = 0, bins - 1 while not height[l]: l += 1 while not height[r]: r -= 1 x = x[l:r + 1] cent = cent[l:r + 1] label = label[l:r + 1] colors = colors[l:r + 1] height = height[l:r + 1] plt.clf() fig = plt.figure(figsize=(15, 5)) plt.xticks(rotation=45) plt.xlim(l * binsize - binsize // 2, r * binsize + binsize // 2) plt.bar(x, height, binsize * 0.9, color=colors, linewidth=0, tick_label=label, log=(mode == 'log')) plt.xlabel('Rating') plt.ylabel('Number of users') discord_file = gc.get_current_figure_as_file() plt.close(fig) 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)
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.""" (nolegend, ), args = cf_common.filter_flags(args, ['+nolegend']) legend, = cf_common.negate_flags(nolegend) 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') if legend: 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 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 solved(self, ctx, *args: str): """Shows a histogram of solved problems' rating 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 if filt.rhi - filt.rlo > 3000 // len(handles) else 100 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 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)
def make_page(chunk): str_handles = '`, `'.join(handles) message = f'Recommended contest(s) for `{str_handles}`' vc_str = '\n'.join(make_line(contest) for contest in chunk) embed = discord_common.cf_color_embed(description=vc_str) return message, embed
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