Exemplo n.º 1
0
    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)
Exemplo n.º 2
0
    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)
Exemplo n.º 3
0
    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)
Exemplo n.º 4
0
    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)
Exemplo n.º 5
0
    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)
Exemplo n.º 6
0
    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)
Exemplo n.º 7
0
    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)
Exemplo n.º 8
0
    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)
Exemplo n.º 9
0
    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)
Exemplo n.º 10
0
    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)
Exemplo n.º 11
0
    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)
Exemplo n.º 12
0
    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)