from manim_imports_ext import *
import scipy.stats


CASE_DATA = [
    9,
    15,
    30,
    40,
    56,
    66,
    84,
    102,
    131,
    159,
    173,
    186,
    190,
    221,
    248,
    278,
    330,
    354,
    382,
    461,
    481,
    526,
    587,
    608,
    697,
    781,
    896,
    999,
    1124,
    1212,
    1385,
    1715,
    2055,
    2429,
    2764,
    3323,
    4288,
    5364,
    6780,
    8555,
    10288,
    12742,
    14901,
    17865,
    21395,
    # 25404,
    # 29256,
    # 33627,
    # 38170,
    # 45421,
    # 53873,
]
SICKLY_GREEN = "#9BBD37"


class IntroducePlot(Scene):
    def construct(self):
        axes = self.get_axes()
        self.add(axes)

        # Dots
        dots = VGroup()
        for day, nc in zip(it.count(1), CASE_DATA):
            dot = Dot()
            dot.set_height(0.075)
            dot.x = day
            dot.y = nc
            dot.axes = axes
            dot.add_updater(lambda d: d.move_to(d.axes.c2p(d.x, d.y)))
            dots.add(dot)
        dots.set_color(YELLOW)

        # Rescale y axis
        origin = axes.c2p(0, 0)
        axes.y_axis.tick_marks.save_state()
        for tick in axes.y_axis.tick_marks:
            tick.match_width(axes.y_axis.tick_marks[0])
        axes.y_axis.add(
            axes.h_lines,
            axes.small_h_lines,
            axes.tiny_h_lines,
            axes.tiny_ticks,
        )
        axes.y_axis.stretch(25, 1, about_point=origin)
        dots.update()

        self.add(axes.small_y_labels)
        self.add(axes.tiny_y_labels)

        # Add title
        title = self.get_title(axes)
        self.add(title)

        # Introduce the data
        day = 10
        self.add(*dots[:day + 1])

        dot = Dot()
        dot.match_style(dots[day])
        dot.replace(dots[day])
        count = Integer(CASE_DATA[day])
        count.add_updater(lambda m: m.next_to(dot, UP))
        count.add_updater(lambda m: m.set_stroke(BLACK, 5, background=True))

        v_line = Line(DOWN, UP)
        v_line.set_stroke(YELLOW, 1)
        v_line.add_updater(
            lambda m: m.put_start_and_end_on(
                axes.c2p(
                    axes.x_axis.p2n(dot.get_center()),
                    0,
                ),
                dot.get_bottom(),
            )
        )

        self.add(dot)
        self.add(count)
        self.add(v_line)

        for new_day in range(day + 1, len(dots)):
            new_dot = dots[new_day]
            new_dot.update()
            line = Line(dot.get_center(), new_dot.get_center())
            line.set_stroke(PINK, 3)

            self.add(line, dot)
            self.play(
                dot.move_to, new_dot.get_center(),
                dot.set_color, RED,
                ChangeDecimalToValue(count, CASE_DATA[new_day]),
                ShowCreation(line),
            )
            line.rotate(PI)
            self.play(
                dot.set_color, YELLOW,
                Uncreate(line),
                run_time=0.5
            )
            self.add(dots[new_day])

            day = new_day

            if day == 27:
                self.add(
                    axes.y_axis, axes.tiny_y_labels, axes.tiny_h_lines, axes.tiny_ticks,
                    title
                )
                self.play(
                    axes.y_axis.stretch, 0.2, 1, {"about_point": origin},
                    VFadeOut(axes.tiny_y_labels),
                    VFadeOut(axes.tiny_h_lines),
                    VFadeOut(axes.tiny_ticks),
                    MaintainPositionRelativeTo(dot, dots[new_day]),
                    run_time=2,
                )
                self.add(axes, title, *dots[:new_day])
            if day == 36:
                self.add(axes.y_axis, axes.small_y_labels, axes.small_h_lines, title)
                self.play(
                    axes.y_axis.stretch, 0.2, 1, {"about_point": origin},
                    VFadeOut(axes.small_y_labels),
                    VFadeOut(axes.small_h_lines),
                    MaintainPositionRelativeTo(dot, dots[new_day]),
                    run_time=2,
                )
                self.add(axes, title, *dots[:new_day])

        count.add_background_rectangle()
        count.background_rectangle.stretch(1.1, 0)
        self.add(count)

        # Show multiplications
        last_label = VectorizedPoint(dots[25].get_center())
        last_line = VMobject()
        for d1, d2 in zip(dots[25:], dots[26:]):
            line = Line(
                d1.get_top(),
                d2.get_corner(UL),
                path_arc=-90 * DEGREES,
            )
            line.set_stroke(PINK, 2)

            label = VGroup(
                Tex("\\times"),
                DecimalNumber(
                    axes.y_axis.p2n(d2.get_center()) /
                    axes.y_axis.p2n(d1.get_center()),
                )
            )
            label.arrange(RIGHT, buff=SMALL_BUFF)
            label.set_height(0.25)
            label.next_to(line.point_from_proportion(0.5), UL, SMALL_BUFF)
            label.match_color(line)
            label.add_background_rectangle()
            label.save_state()
            label.move_to(last_label)
            label.set_opacity(0)

            self.play(
                ShowCreation(line),
                Restore(label),
                last_label.move_to, label.saved_state,
                VFadeOut(last_label),
                FadeOut(last_line),
            )
            last_line = line
            last_label = label
        self.wait()
        self.play(
            FadeOut(last_label),
            FadeOut(last_line),
        )

    #
    def get_title(self, axes):
        title = TexText(
            "Recorded COVID-19 cases\\\\outside mainland China",
            tex_to_color_map={"COVID-19": RED}
        )
        title.next_to(axes.c2p(0, 1e3), RIGHT, LARGE_BUFF)
        title.to_edge(UP)
        title.add_background_rectangle()
        return title

    def get_axes(self, width=12, height=6):
        n_cases = len(CASE_DATA)
        axes = Axes(
            x_range=(0, n_cases),
            y_range=(0, 25000, 1000),
            width=width,
            height=height,
        )
        axes.center()
        axes.to_edge(DOWN, buff=LARGE_BUFF)

        # Add dates
        text_pos_pairs = [
            ("Mar 6", 0),
            ("Feb 23", -12),
            ("Feb 12", -23),
            ("Feb 1", -34),
            ("Jan 22", -44),
        ]
        labels = VGroup()
        extra_ticks = VGroup()
        for text, pos in text_pos_pairs:
            label = TexText(text)
            label.set_height(0.2)
            label.rotate(45 * DEGREES)
            axis_point = axes.c2p(n_cases + pos, 0)
            label.move_to(axis_point, UR)
            label.shift(MED_SMALL_BUFF * DOWN)
            label.shift(SMALL_BUFF * RIGHT)
            labels.add(label)

            tick = Line(UP, DOWN)
            tick.set_stroke(GREEN, 3)
            tick.set_height(0.25)
            tick.move_to(axis_point)
            extra_ticks.add(tick)

        axes.x_labels = labels
        axes.extra_x_ticks = extra_ticks
        axes.add(labels, extra_ticks)

        # Adjust y ticks
        axes.y_axis.ticks.stretch(0.5, 0)
        axes.y_axis.ticks[0::5].stretch(2, 0)

        # Add y_axis_labels
        def get_y_labels(axes, y_values):
            labels = VGroup()
            for y in y_values:
                try:
                    label = TexText(f"{y}k")
                    label.set_height(0.25)
                    tick = axes.y_axis.ticks[y]
                    always(label.next_to, tick, LEFT, SMALL_BUFF)
                    labels.add(label)
                except IndexError:
                    pass
            return labels

        main_labels = get_y_labels(axes, range(5, 30, 5))
        axes.y_labels = main_labels
        axes.add(main_labels)
        axes.small_y_labels = get_y_labels(axes, range(1, 6))

        tiny_labels = VGroup()
        tiny_ticks = VGroup()
        for y in range(200, 1000, 200):
            tick = axes.y_axis.ticks[0].copy()
            point = axes.c2p(0, y)
            tick.move_to(point)
            label = Integer(y)
            label.set_height(0.25)
            always(label.next_to, tick, LEFT, SMALL_BUFF)
            tiny_labels.add(label)
            tiny_ticks.add(tick)

        axes.tiny_y_labels = tiny_labels
        axes.tiny_ticks = tiny_ticks

        # Horizontal lines
        axes.h_lines = VGroup()
        axes.small_h_lines = VGroup()
        axes.tiny_h_lines = VGroup()
        group_range_pairs = [
            (axes.h_lines, 5e3 * np.arange(1, 6)),
            (axes.small_h_lines, 1e3 * np.arange(1, 5)),
            (axes.tiny_h_lines, 200 * np.arange(1, 5)),
        ]
        for group, _range in group_range_pairs:
            for y in _range:
                group.add(
                    Line(
                        axes.c2p(0, y),
                        axes.c2p(n_cases, y),
                    )
                )
            group.set_stroke(WHITE, 1, opacity=0.5)

        return axes


class Thumbnail(IntroducePlot):
    def construct(self):
        axes = self.get_axes()
        self.add(axes)

        dots = VGroup()
        data = CASE_DATA
        data.append(25398)
        for day, nc in zip(it.count(1), CASE_DATA):
            dot = Dot()
            dot.set_height(0.2)
            dot.x = day
            dot.y = nc
            dot.axes = axes
            dot.add_updater(lambda d: d.move_to(d.axes.c2p(d.x, d.y)))
            dots.add(dot)
        dots.set_color(YELLOW)
        dots.set_submobject_colors_by_gradient(BLUE, GREEN, RED)

        self.add(dots)

        title = TexText("COVID-19")
        title.set_height(1)
        title.set_color(RED)
        title.to_edge(UP, buff=LARGE_BUFF)

        subtitle = TexText("and exponential growth")
        subtitle.match_width(title)
        subtitle.next_to(title, DOWN)

        # self.add(title)
        # self.add(subtitle)

        title = TexText("The early warning\\\\of ", "COVID-19\\\\")
        title.set_color_by_tex("COVID", RED)
        title.set_height(2.5)
        title.to_edge(UP, buff=LARGE_BUFF)
        self.add(title)

        # self.remove(words)
        # words = TexText("Exponential growth")
        # words.move_to(ORIGIN, DL)
        # words.apply_function(
        #     lambda p: [
        #         p[0], p[1] + np.exp(0.2 * p[0]), p[2]
        #     ]
        # )
        # self.add(words)

        self.embed()


class IntroQuestion(Scene):
    def construct(self):
        questions = VGroup(
            TexText("What is exponential growth?"),
            TexText("Where does it come from?"),
            TexText("What does it imply?"),
            TexText("When does it stop?"),
        )
        questions.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)

        for question in questions:
            self.play(FadeIn(question, RIGHT))
            self.wait()
        self.play(LaggedStartMap(
            FadeOutAndShift, questions,
            lambda m: (m, DOWN),
        ))


class ViralSpreadModel(Scene):
    CONFIG = {
        "num_neighbors": 5,
        "infection_probability": 0.3,
        "random_seed": 1,
    }

    def construct(self):
        # Init population
        randys = self.get_randys()
        self.add(*randys)

        # Show the sicko
        self.show_patient0(randys)

        # Repeatedly spread
        for x in range(20):
            self.spread_infection(randys)

    def get_randys(self):
        randys = VGroup(*[
            Randolph()
            for x in range(150)
        ])
        for randy in randys:
            randy.set_height(0.5)
        randys.arrange_in_grid(10, 15, buff=0.5)
        randys.set_height(FRAME_HEIGHT - 1)

        for i in range(0, 10, 2):
            randys[i * 15:(i + 1) * 15].shift(0.25 * RIGHT)
        for randy in randys:
            randy.shift(0.2 * random.random() * RIGHT)
            randy.shift(0.2 * random.random() * UP)
            randy.infected = False
        randys.center()
        return randys

    def show_patient0(self, randys):
        patient0 = random.choice(randys)
        patient0.infected = True

        circle = Circle()
        circle.set_stroke(SICKLY_GREEN)
        circle.replace(patient0)
        circle.scale(1.5)
        self.play(
            patient0.change, "sick",
            patient0.set_color, SICKLY_GREEN,
            ShowCreationThenFadeOut(circle),
        )

    def spread_infection(self, randys):
        E = self.num_neighbors
        inf_p = self.infection_probability

        cough_anims = []
        new_infection_anims = []

        for randy in randys:
            if randy.infected:
                cough_anims.append(Flash(
                    randy,
                    color=SICKLY_GREEN,
                    num_lines=16,
                    line_stroke_width=1,
                    flash_radius=0.5,
                    line_length=0.1,
                ))
        random.shuffle(cough_anims)
        self.play(LaggedStart(
            *cough_anims,
            run_time=1,
            lag_ratio=1 / len(cough_anims),
        ))

        newly_infected = []
        for randy in randys:
            if randy.infected:
                distances = [
                    get_norm(r2.get_center() - randy.get_center())
                    for r2 in randys
                ]
                for i in np.argsort(distances)[1:E + 1]:
                    r2 = randys[i]
                    if random.random() < inf_p and not r2.infected and r2 not in newly_infected:
                        newly_infected.append(r2)
                        r2.generate_target()
                        r2.target.change("sick")
                        r2.target.set_color(SICKLY_GREEN)
                        new_infection_anims.append(MoveToTarget(r2))
        random.shuffle(new_infection_anims)
        self.play(LaggedStart(*new_infection_anims, run_time=1))

        for randy in newly_infected:
            randy.infected = True


class GrowthEquation(Scene):
    def construct(self):
        # Add labels
        N_label = TexText("$N_d$", " = Number of cases on a given day", )
        E_label = TexText("$E$", " = Average number of people someone infected is exposed to each day")
        p_label = TexText("$p$", " = Probability of each exposure becoming an infection")

        N_label[0].set_color(YELLOW)
        E_label[0].set_color(BLUE)
        p_label[0].set_color(TEAL)

        labels = VGroup(
            N_label,
            E_label,
            p_label
        )
        labels.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)
        labels.set_width(FRAME_WIDTH - 1)
        labels.to_edge(UP)

        for label in labels:
            self.play(FadeInFromDown(label))
            self.wait()

        delta_N = Tex("\\Delta", "N_d")
        delta_N.set_color(YELLOW)
        eq = Tex("=")
        eq.center()
        delta_N.next_to(eq, LEFT)

        delta_N_brace = Brace(delta_N, DOWN)
        delta_N_text = delta_N_brace.get_text("Change over a day")

        nep = Tex("E", "\\cdot", "p", "\\cdot", "N_d")
        nep[4].match_color(N_label[0])
        nep[0].match_color(E_label[0])
        nep[2].match_color(p_label[0])
        nep.next_to(eq, RIGHT)

        self.play(FadeIn(delta_N), FadeIn(eq))
        self.play(
            GrowFromCenter(delta_N_brace),
            FadeIn(delta_N_text, 0.5 * UP),
        )
        self.wait()
        self.play(LaggedStart(
            TransformFromCopy(N_label[0], nep[4]),
            TransformFromCopy(E_label[0], nep[0]),
            TransformFromCopy(p_label[0], nep[2]),
            FadeIn(nep[1]),
            FadeIn(nep[3]),
            lag_ratio=0.2,
            run_time=2,
        ))
        self.wait()
        self.play(ShowCreationThenFadeAround(
            nep[-1],
            surrounding_rectangle_config={"color": RED},
        ))

        # Recursive equation
        lhs = Tex("N_{d + 1}", "=")
        lhs[0].set_color(YELLOW)
        lhs.move_to(eq, RIGHT)
        lhs.shift(DOWN)

        rhs = VGroup(
            nep[-1].copy(),
            Tex("+"),
            nep.copy(),
        )
        rhs.arrange(RIGHT)
        rhs.next_to(lhs, RIGHT)

        self.play(
            FadeOut(delta_N_brace),
            FadeOut(delta_N_text),
            FadeIn(lhs, UP),
        )
        self.play(FadeIn(rhs[:2]))
        self.play(TransformFromCopy(nep, rhs[2]))
        self.wait()

        alt_rhs = Tex(
            "(", "1", "+", "E", "\\cdot", "p", ")", "N_d",
            tex_to_color_map={
                "E": BLUE,
                "p": TEAL,
                "N_d": YELLOW,
            }
        )
        new_lhs = lhs.copy()
        new_lhs.shift(DOWN)
        alt_rhs.next_to(new_lhs, RIGHT)
        self.play(TransformFromCopy(lhs, new_lhs))

        self.play(
            TransformFromCopy(rhs[0], alt_rhs[7].copy()),
            TransformFromCopy(rhs[2][4], alt_rhs[7]),
        )
        self.play(
            TransformFromCopy(rhs[1][0], alt_rhs[2]),
            TransformFromCopy(rhs[2][0], alt_rhs[3]),
            TransformFromCopy(rhs[2][1], alt_rhs[4]),
            TransformFromCopy(rhs[2][2], alt_rhs[5]),
            TransformFromCopy(rhs[2][3], alt_rhs[6]),
            FadeIn(alt_rhs[0]),
            FadeIn(alt_rhs[1]),
        )
        self.wait()

        # Comment on factor
        brace = Brace(alt_rhs[:7], DOWN)
        text = TexText("For example, ", "1.15")
        text.next_to(brace, DOWN)
        self.play(
            GrowFromCenter(brace),
            FadeIn(text, 0.5 * UP)
        )
        self.wait()

        # Show exponential
        eq_group = VGroup(
            delta_N, eq, nep,
            lhs, rhs,
            new_lhs, alt_rhs,
            brace,
            text,
        )
        self.clear()
        self.add(labels, eq_group)

        self.play(ShowCreationThenFadeAround(
            VGroup(delta_N, eq, nep),
            surrounding_rectangle_config={"color": RED},
        ))
        self.play(ShowCreationThenFadeAround(
            VGroup(new_lhs, alt_rhs, brace, text),
            surrounding_rectangle_config={"color": RED},
        ))
        self.wait()
        self.play(eq_group.to_edge, LEFT, LARGE_BUFF)

        exp_eq = Tex(
            "N_d = (1 + E \\cdot p)^{d} \\cdot N_0",
            tex_to_color_map={
                "N_d": YELLOW,
                "E": BLUE,
                "p": TEAL,
                "{d}": YELLOW,
                "N_0": YELLOW,
            }
        )
        exp_eq.next_to(alt_rhs, RIGHT, buff=3)
        arrow = Arrow(alt_rhs.get_right(), exp_eq.get_left())

        self.play(
            GrowArrow(arrow),
            FadeIn(exp_eq, 2 * LEFT)
        )
        self.wait()

        # Discuss factor in front of N
        ep = nep[:3]
        ep_rect = SurroundingRectangle(ep)
        ep_rect.set_stroke(RED, 2)

        ep_label = TexText("This factor will decrease")
        ep_label.next_to(ep_rect, UP, aligned_edge=LEFT)
        ep_label.set_color(RED)

        self.play(
            ShowCreation(ep_rect),
            FadeIn(ep_label, lag_ratio=0.1),
        )
        self.wait()
        self.play(
            FadeOut(ep_rect),
            FadeOut(ep_label),
        )

        # Add carrying capacity factor to p
        p_factors = Tex(
            "\\left(1 - {N_d \\over \\text{pop. size}} \\right)",
            tex_to_color_map={"N_d": YELLOW},
        )
        p_factors.next_to(nep, RIGHT, buff=3)
        p_factors_rect = SurroundingRectangle(p_factors)
        p_factors_rect.set_stroke(TEAL, 2)
        p_arrow = Arrow(
            p_factors_rect.get_corner(UL),
            nep[2].get_top(),
            path_arc=75 * DEGREES,
            color=TEAL,
        )

        self.play(
            ShowCreation(p_factors_rect),
            ShowCreation(p_arrow)
        )
        self.wait()
        self.play(Write(p_factors))
        self.wait()
        self.play(
            FadeOut(p_factors),
            FadeOut(p_arrow),
            FadeOut(p_factors_rect),
        )

        # Ask about ep shrinking
        ep_question = TexText("What makes this shrink?")
        ep_question.set_color(RED)
        ep_question.next_to(ep_rect, UP, aligned_edge=LEFT)

        E_line = Underline(E_label)
        E_line.set_color(BLUE)
        p_line = Underline(p_label)
        p_line.set_color(TEAL)

        self.play(
            ShowCreation(ep_rect),
            FadeIn(ep_question, LEFT)
        )
        self.wait()
        for line in E_line, p_line:
            self.play(ShowCreation(line))
            self.wait()
            self.play(FadeOut(line))
        self.wait()

        # Show alternate projections
        ep_value = DecimalNumber(0.15)
        ep_value.next_to(ep_rect, UP)

        self.play(
            FadeOut(ep_question),
            FadeIn(ep_value),
            FadeOut(text[0]),
            text[1].next_to, brace, DOWN,
        )

        eq1 = Tex("(", "1.15", ")", "^{61}", "\\cdot", "21{,}000", "=")
        eq2 = Tex("(", "1.05", ")", "^{61}", "\\cdot", "21{,}000", "=")
        eq1_rhs = Integer((1.15**61) * (21000))
        eq2_rhs = Integer((1.05**61) * (21000))

        for eq, rhs in (eq1, eq1_rhs), (eq2, eq2_rhs):
            eq[1].set_color(RED)
            eq.move_to(nep)
            eq.to_edge(RIGHT, buff=3)
            rhs.next_to(eq, RIGHT)
            rhs.align_to(eq[-2], UP)

        self.play(FadeIn(eq1))
        for tex in ["21{,}000", "61"]:
            self.play(ShowCreationThenFadeOut(
                Underline(
                    eq1.get_part_by_tex(tex),
                    stroke_color=YELLOW,
                    stroke_width=2,
                    buff=SMALL_BUFF,
                ),
                run_time=2,
            ))
        value = eq1_rhs.get_value()
        eq1_rhs.set_value(0)
        self.play(ChangeDecimalToValue(eq1_rhs, value))
        self.wait()
        eq1.add(eq1_rhs)
        self.play(
            eq1.shift, DOWN,
            FadeIn(eq2),
        )

        new_text = TexText("1.05")
        new_text.move_to(text[1])
        self.play(
            ChangeDecimalToValue(ep_value, 0.05),
            FadeOut(text[1]),
            FadeIn(new_text),
        )

        self.wait()

        eq2_rhs.align_to(eq1_rhs, RIGHT)
        value = eq2_rhs.get_value()
        eq2_rhs.set_value(0)
        self.play(ChangeDecimalToValue(eq2_rhs, value))
        self.wait()

        # Pi creature quote
        morty = Mortimer()
        morty.set_height(1)
        morty.next_to(eq2_rhs, UP)
        bubble = SpeechBubble(
            direction=RIGHT,
            height=2.5,
            width=5,
        )
        bubble.next_to(morty, UL, buff=0)
        bubble.write("The only thing to fear\\\\is the lack of fear itself.")

        self.add(morty)
        self.add(bubble)
        self.add(bubble.content)

        self.play(
            labels.set_opacity, 0.5,
            VFadeIn(morty),
            morty.change, "speaking",
            FadeIn(bubble),
            Write(bubble.content),
        )
        self.play(Blink(morty))
        self.wait()


class RescaleToLogarithmic(IntroducePlot):
    def construct(self):
        # Setup axes
        axes = self.get_axes(width=10)
        title = self.get_title(axes)

        dots = VGroup()
        for day, nc in zip(it.count(1), CASE_DATA):
            dot = Dot()
            dot.set_height(0.075)
            dot.move_to(axes.c2p(day, nc))
            dots.add(dot)
        dots.set_color(YELLOW)

        self.add(axes, axes.h_lines, dots, title)

        # Create logarithmic y axis
        log_y_axis = NumberLine(
            x_min=0,
            x_max=9,
        )
        log_y_axis.rotate(90 * DEGREES)
        log_y_axis.move_to(axes.c2p(0, 0), DOWN)

        labels_text = [
            "10", "100",
            "1k", "10k", "100k",
            "1M", "10M", "100M",
            "1B",
        ]
        log_y_labels = VGroup()
        for text, tick in zip(labels_text, log_y_axis.tick_marks[1:]):
            label = TexText(text)
            label.set_height(0.25)
            always(label.next_to, tick, LEFT, SMALL_BUFF)
            log_y_labels.add(label)

        # Animate the rescaling to a logarithmic plot
        logarithm_title = TexText("(Logarithmic scale)")
        logarithm_title.set_color(TEAL)
        logarithm_title.next_to(title, DOWN)
        logarithm_title.add_background_rectangle()

        def scale_logarithmically(p):
            result = np.array(p)
            y = axes.y_axis.p2n(p)
            result[1] = log_y_axis.n2p(np.log10(y))[1]
            return result

        log_h_lines = VGroup()
        for exponent in range(0, 9):
            for mult in range(2, 12, 2):
                y = mult * 10**exponent
                line = Line(
                    axes.c2p(0, y),
                    axes.c2p(axes.x_max, y),
                )
                log_h_lines.add(line)
        log_h_lines.set_stroke(WHITE, 0.5, opacity=0.5)
        log_h_lines[4::5].set_stroke(WHITE, 1, opacity=1)

        movers = [dots, axes.y_axis.tick_marks, axes.h_lines, log_h_lines]
        for group in movers:
            group.generate_target()
            for mob in group.target:
                mob.move_to(scale_logarithmically(mob.get_center()))

        log_y_labels.suspend_updating()
        log_y_labels.save_state()
        for exponent, label in zip(it.count(1), log_y_labels):
            label.set_y(axes.y_axis.n2p(10**exponent)[1])
            label.set_opacity(0)

        self.add(log_y_axis)
        log_y_axis.save_state()
        log_y_axis.tick_marks.set_opacity(0)
        log_h_lines.set_opacity(0)
        self.wait()
        self.add(log_h_lines, title, logarithm_title)
        self.play(
            MoveToTarget(dots),
            MoveToTarget(axes.y_axis.tick_marks),
            MoveToTarget(axes.h_lines),
            MoveToTarget(log_h_lines),
            VFadeOut(axes.y_labels),
            VFadeOut(axes.y_axis.tick_marks),
            VFadeOut(axes.h_lines),
            Restore(log_y_labels),
            FadeIn(logarithm_title),
            run_time=2,
        )
        self.play(Restore(log_y_axis))
        self.wait()

        # Walk up y axis
        brace = Brace(
            log_y_axis.tick_marks[1:3],
            RIGHT,
            buff=SMALL_BUFF,
        )
        brace_label = brace.get_tex(
            "\\times 10",
            buff=SMALL_BUFF
        )
        VGroup(brace, brace_label).set_color(TEAL)
        brace_label.set_stroke(BLACK, 8, background=True)

        self.play(
            GrowFromCenter(brace),
            FadeIn(brace_label)
        )
        brace.add(brace_label)
        for i in range(2, 5):
            self.play(
                brace.next_to,
                log_y_axis.tick_marks[i:i + 2],
                {"buff": SMALL_BUFF}
            )
            self.wait(0.5)
        self.play(FadeOut(brace))
        self.wait()

        # Show order of magnitude jumps
        remove_anims = []
        for i, j in [(7, 27), (27, 40)]:
            line = Line(dots[i].get_center(), dots[j].get_center())
            rect = Rectangle()
            rect.set_fill(TEAL, 0.5)
            rect.set_stroke(width=0)
            rect.replace(line, stretch=True)
            label = TexText(f"{j - i} days")
            label.next_to(rect, UP, SMALL_BUFF)
            label.set_color(TEAL)

            rect.save_state()
            rect.stretch(0, 0, about_edge=LEFT)
            self.play(
                Restore(rect),
                FadeIn(label, LEFT)
            )
            self.wait()

            remove_anims += [
                ApplyMethod(
                    rect.stretch, 0, 0, {"about_edge": RIGHT},
                    remover=True,
                ),
                FadeOut(label, RIGHT),
            ]
        self.wait()

        # Linear regression
        def c2p(x, y):
            xp = axes.x_axis.n2p(x)
            yp = log_y_axis.n2p(np.log10(y))
            return np.array([xp[0], yp[1], 0])

        reg = scipy.stats.linregress(
            range(7, len(CASE_DATA)),
            np.log10(CASE_DATA[7:])
        )
        x_max = axes.x_max
        axes.y_axis = log_y_axis
        reg_line = Line(
            c2p(0, 10**reg.intercept),
            c2p(x_max, 10**(reg.intercept + reg.slope * x_max)),
        )
        reg_line.set_stroke(TEAL, 3)

        self.add(reg_line, dots)
        dots.set_stroke(BLACK, 3, background=True)
        self.play(
            LaggedStart(*remove_anims),
            ShowCreation(reg_line)
        )

        # Describe linear regression
        reg_label = TexText("Linear regression")
        reg_label.move_to(c2p(25, 10), DOWN)
        reg_arrows = VGroup()
        for prop in [0.4, 0.6, 0.5]:
            reg_arrows.add(
                Arrow(
                    reg_label.get_top(),
                    reg_line.point_from_proportion(prop),
                    buff=SMALL_BUFF,
                )
            )

        reg_arrow = reg_arrows[0].copy()
        self.play(
            Write(reg_label, run_time=1),
            Transform(reg_arrow, reg_arrows[1], run_time=2),
            VFadeIn(reg_arrow),
        )
        self.play(Transform(reg_arrow, reg_arrows[2]))
        self.wait()

        # Label slope
        slope_label = TexText("$\\times 10$ every $16$ days (on average)")
        slope_label.set_color(BLUE)
        slope_label.set_stroke(BLACK, 8, background=True)
        slope_label.rotate(reg_line.get_angle())
        slope_label.move_to(reg_line.get_center())
        slope_label.shift(MED_LARGE_BUFF * UP)

        self.play(FadeIn(slope_label, lag_ratio=0.1))
        self.wait()

        # R^2 label
        R2_label = VGroup(
            Tex("R^2 = "),
            DecimalNumber(0, num_decimal_places=3)
        )
        R2_label.arrange(RIGHT, aligned_edge=DOWN)
        R2_label.next_to(reg_label[0][-1], RIGHT, LARGE_BUFF, aligned_edge=DOWN)

        self.play(
            ChangeDecimalToValue(R2_label[1], reg.rvalue**2, run_time=2),
            UpdateFromAlphaFunc(
                R2_label,
                lambda m, a: m.set_opacity(a),
            )
        )
        self.wait()

        rect = SurroundingRectangle(R2_label, buff=0.15)
        rect.set_stroke(YELLOW, 3)
        rect.set_fill(BLACK, 0)
        self.add(rect, R2_label)
        self.play(ShowCreation(rect))
        self.play(
            rect.set_stroke, WHITE, 2,
            rect.set_fill, GREY_E, 1,
        )
        self.wait()
        self.play(
            FadeOut(rect),
            FadeOut(R2_label),
            FadeOut(reg_label),
            FadeOut(reg_arrow),
        )

        # Zoom out
        extended_x_axis = NumberLine(
            x_min=axes.x_axis.x_max,
            x_max=axes.x_axis.x_max + 90,
            unit_size=get_norm(
                axes.x_axis.n2p(1) -
                axes.x_axis.n2p(0)
            ),
            numbers_with_elongated_ticks=[],
        )
        extended_x_axis.move_to(axes.x_axis.get_right(), LEFT)
        self.play(
            self.camera.frame.scale, 2, {"about_edge": DL},
            self.camera.frame.shift, 2.5 * DOWN + RIGHT,
            log_h_lines.stretch, 3, 0, {"about_edge": LEFT},
            ShowCreation(extended_x_axis, rate_func=squish_rate_func(smooth, 0.5, 1)),
            run_time=3,
        )
        self.play(
            reg_line.scale, 3, {"about_point": reg_line.get_start()}
        )
        self.wait()

        # Show future projections
        target_ys = [1e6, 1e7, 1e8, 1e9]
        last_point = dots[-1].get_center()
        last_label = None
        last_rect = None

        date_labels_text = [
            "Apr 5",
            "Apr 22",
            "May 9",
            "May 26",
        ]

        for target_y, date_label_text in zip(target_ys, date_labels_text):
            log_y = np.log10(target_y)
            x = (log_y - reg.intercept) / reg.slope
            line = Line(last_point, c2p(x, target_y))
            rect = Rectangle().replace(line, stretch=True)
            rect.set_stroke(width=0)
            rect.set_fill(TEAL, 0.5)
            label = TexText(f"{int(x) - axes.x_max} days")
            label.scale(1.5)
            label.next_to(rect, UP, SMALL_BUFF)

            date_label = TexText(date_label_text)
            date_label.set_height(0.25)
            date_label.rotate(45 * DEGREES)
            axis_point = axes.c2p(int(x), 0)
            date_label.move_to(axis_point, UR)
            date_label.shift(MED_SMALL_BUFF * DOWN)
            date_label.shift(SMALL_BUFF * RIGHT)

            v_line = DashedLine(
                axes.c2p(x, 0),
                c2p(x, target_y),
            )
            v_line.set_stroke(WHITE, 2)

            if target_y is target_ys[-1]:
                self.play(self.camera.frame.scale, 1.1, {"about_edge": LEFT})

            if last_label:
                self.play(
                    ReplacementTransform(last_label, label),
                    ReplacementTransform(last_rect, rect),
                )
            else:
                rect.save_state()
                rect.stretch(0, 0, about_edge=LEFT)
                self.play(Restore(rect), FadeIn(label, LEFT))
            self.wait()

            self.play(
                ShowCreation(v_line),
                FadeIn(date_label),
            )

            last_label = label
            last_rect = rect

        self.wait()
        self.play(
            FadeOut(last_label, RIGHT),
            ApplyMethod(
                last_rect.stretch, 0, 0, {"about_edge": RIGHT},
                remover=True
            ),
        )

        # Show alternate petering out possibilities
        def get_dots_along_curve(curve):
            x_min = int(axes.x_axis.p2n(curve.get_start()))
            x_max = int(axes.x_axis.p2n(curve.get_end()))
            result = VGroup()
            for x in range(x_min, x_max):
                prop = binary_search(
                    lambda p: axes.x_axis.p2n(
                        curve.point_from_proportion(p),
                    ),
                    x, 0, 1,
                )
                prop = prop or 0
                point = curve.point_from_proportion(prop)
                dot = Dot(point)
                dot.shift(0.02 * (random.random() - 0.5))
                dot.set_height(0.075)
                dot.set_color(RED)
                result.add(dot)
            dots.remove(dots[0])
            return result

        def get_point_from_y(y):
            log_y = np.log10(y)
            x = (log_y - reg.intercept) / reg.slope
            return c2p(x, 10**log_y)

        p100k = get_point_from_y(1e5)
        p100M = get_point_from_y(1e8)
        curve1 = VMobject()
        curve1.append_points([
            dots[-1].get_center(),
            p100k,
            p100k + 5 * RIGHT,
        ])
        curve2 = VMobject()
        curve2.append_points([
            dots[-1].get_center(),
            p100M,
            p100M + 5 * RIGHT + 0.25 * UP,
        ])

        proj_dots1 = get_dots_along_curve(curve1)
        proj_dots2 = get_dots_along_curve(curve2)

        for proj_dots in [proj_dots1, proj_dots2]:
            self.play(FadeIn(proj_dots, lag_ratio=0.1))
            self.wait()
            self.play(FadeOut(proj_dots, lag_ratio=0.1))


class LinRegNote(Scene):
    def construct(self):
        text = TexText("(Starting from when\\\\there were 100 cases)")
        text.set_stroke(BLACK, 8, background=True)
        self.add(text)


class CompareCountries(Scene):
    def construct(self):
        # Introduce
        sk_flag = ImageMobject(os.path.join("flags", "kr"))
        au_flag = ImageMobject(os.path.join("flags", "au"))
        flags = Group(sk_flag, au_flag)
        flags.set_height(3)
        flags.arrange(RIGHT, buff=LARGE_BUFF)
        flags.next_to(ORIGIN, UP)

        labels = VGroup()
        case_numbers = [6593, 64]
        for flag, cn in zip(flags, case_numbers):
            label = VGroup(Integer(cn), TexText("cases"))
            label.arrange(RIGHT, buff=MED_SMALL_BUFF)
            label[1].align_to(label[0][-1], DOWN)
            label.scale(1.5)
            label.next_to(flag, DOWN, MED_LARGE_BUFF)
            label[0].set_value(0)
            labels.add(label)

        self.play(LaggedStartMap(FadeInFromDown, flags, lag_ratio=0.25))
        self.play(
            ChangeDecimalToValue(labels[0][0], case_numbers[0]),
            ChangeDecimalToValue(labels[1][0], case_numbers[1]),
            UpdateFromAlphaFunc(
                labels,
                lambda m, a: m.set_opacity(a),
            )
        )
        self.wait()

        # Compare
        arrow = Arrow(
            labels[1][0].get_bottom(),
            labels[0][0].get_bottom(),
            path_arc=-90 * DEGREES,
        )
        arrow_label = TexText("100x better")
        arrow_label.set_color(YELLOW)
        arrow_label.next_to(arrow, DOWN)

        alt_arrow_label = TexText("1 month behind")
        alt_arrow_label.set_color(RED)
        alt_arrow_label.next_to(arrow, DOWN)

        self.play(ShowCreation(arrow))
        self.play(FadeIn(arrow_label, 0.5 * UP))
        self.wait(2)
        self.play(
            FadeIn(alt_arrow_label, 0.5 * UP),
            FadeOut(arrow_label, 0.5 * DOWN),
        )
        self.wait(2)


class SARSvs1918(Scene):
    def construct(self):
        titles = VGroup(
            TexText("2002 SARS outbreak"),
            TexText("1918 Spanish flu"),
        )
        images = Group(
            ImageMobject("sars_icon"),
            ImageMobject("spanish_flu"),
        )
        for title, vect, color, image in zip(titles, [LEFT, RIGHT], [YELLOW, RED], images):
            image.set_height(4)
            image.move_to(vect * FRAME_WIDTH / 4)
            image.to_edge(UP)
            title.scale(1.25)
            title.next_to(image, DOWN, MED_LARGE_BUFF)
            title.set_color(color)
            title.underline = Underline(title)
            title.underline.set_stroke(WHITE, 1)
            title.add_to_back(title.underline)

        titles[1].underline.match_y(titles[0].underline)

        n_cases_labels = VGroup(
            TexText("8,096 cases"),
            TexText("$\\sim$513{,}000{,}000 cases"),
        )

        for n_cases_label, title in zip(n_cases_labels, titles):
            n_cases_label.scale(1.25)
            n_cases_label.next_to(title, DOWN, MED_LARGE_BUFF)

        for image, title, label in zip(images, titles, n_cases_labels):
            self.play(
                FadeIn(image, DOWN),
                Write(title),
                run_time=1,
            )
            self.play(FadeIn(label, UP))
            self.wait()


class ViralSpreadModelWithShuffling(ViralSpreadModel):
    def construct(self):
        # Init population
        randys = self.get_randys()
        self.add(*randys)

        # Show the sicko
        self.show_patient0(randys)

        # Repeatedly spread
        for x in range(15):
            self.spread_infection(randys)
            self.shuffle_randys(randys)

    def shuffle_randys(self, randys):
        indices = list(range(len(randys)))
        np.random.shuffle(indices)

        anims = []
        for i, randy in zip(indices, randys):
            randy.generate_target()
            randy.target.move_to(randys[i])
            anims.append(MoveToTarget(
                randy, path_arc=30 * DEGREES,
            ))

        self.play(LaggedStart(
            *anims,
            lag_ratio=1 / len(randys),
            run_time=3
        ))


class SneezingOnNeighbors(Scene):
    def construct(self):
        randys = VGroup(*[PiCreature() for x in range(3)])
        randys.set_height(1)
        randys.arrange(RIGHT)

        self.add(randys)
        self.play(
            randys[1].change, "sick",
            randys[1].set_color, SICKLY_GREEN,
        )
        self.play(
            Flash(
                randys[1],
                color=SICKLY_GREEN,
                flash_radius=0.8,
            ),
            randys[0].change, "sassy", randys[1],
            randys[2].change, "angry", randys[1],
        )
        self.play(
            randys[0].change, "sick",
            randys[0].set_color, SICKLY_GREEN,
            randys[2].change, "sick",
            randys[2].set_color, SICKLY_GREEN,
        )
        self.play(
            Flash(
                randys[1],
                color=SICKLY_GREEN,
                flash_radius=0.8,
            )
        )
        self.play(
            randys[0].change, "sad", randys[1],
            randys[2].change, "tired", randys[1],
        )
        self.play(
            Flash(
                randys[1],
                color=SICKLY_GREEN,
                flash_radius=0.8,
            )
        )
        self.play(
            randys[0].change, "angry", randys[1],
            randys[2].change, "angry", randys[1],
        )
        self.wait()


class ViralSpreadModelWithClusters(ViralSpreadModel):
    def construct(self):
        randys = self.get_randys()
        self.add(*randys)
        self.show_patient0(randys)

        for x in range(6):
            self.spread_infection(randys)

    def get_randys(self):
        cluster = VGroup(*[Randolph() for x in range(16)])
        cluster.arrange_in_grid(4, 4)
        cluster.set_height(1)
        cluster.space_out_submobjects(1.3)

        clusters = VGroup(*[cluster.copy() for x in range(12)])
        clusters.arrange_in_grid(3, 4, buff=LARGE_BUFF)
        clusters.set_height(FRAME_HEIGHT - 1)

        for cluster in clusters:
            for randy in cluster:
                randy.infected = False

        self.add(clusters)

        self.clusters = clusters
        return VGroup(*it.chain(*clusters))


class ViralSpreadModelWithClustersAndTravel(ViralSpreadModelWithClusters):
    CONFIG = {
        "random_seed": 2,
    }

    def construct(self):
        randys = self.get_randys()
        self.add(*randys)
        self.show_patient0(randys)

        for x in range(20):
            self.spread_infection(randys)
            self.travel_between_clusters()
            self.update_frame(ignore_skipping=True)

    def travel_between_clusters(self):
        reps = VGroup(*[
            random.choice(cluster)
            for cluster in self.clusters
        ])
        targets = list(reps)
        random.shuffle(targets)

        anims = []
        for rep, target in zip(reps, targets):
            rep.generate_target()
            rep.target.move_to(target)
            anims.append(MoveToTarget(
                rep,
                path_arc=30 * DEGREES,
            ))
        self.play(LaggedStart(*anims, run_time=3))


class ShowLogisticCurve(Scene):
    def construct(self):
        # Init axes
        axes = self.get_axes()
        self.add(axes)

        # Add ODE
        ode = Tex(
            "{dN \\over dt} =",
            "c",
            "\\left(1 - {N \\over \\text{pop.}}\\right)",
            "N",
            tex_to_color_map={"N": YELLOW}
        )
        ode.set_height(0.75)
        ode.center()
        ode.to_edge(RIGHT)
        ode.shift(1.5 * UP)
        self.add(ode)

        # Show curve
        curve = axes.get_graph(
            lambda x: 8 * smooth(x / 10) + 0.2,
        )
        curve.set_stroke(YELLOW, 3)

        curve_title = TexText("Logistic curve")
        curve_title.set_height(0.75)
        curve_title.next_to(curve.get_end(), UL)

        self.play(ShowCreation(curve, run_time=3))
        self.play(FadeIn(curve_title, lag_ratio=0.1))
        self.wait()

        # Early part
        line = Line(
            curve.point_from_proportion(0),
            curve.point_from_proportion(0.25),
        )
        rect = Rectangle()
        rect.set_stroke(width=0)
        rect.set_fill(TEAL, 0.5)
        rect.replace(line, stretch=True)

        exp_curve = axes.get_graph(
            lambda x: 0.15 * np.exp(0.68 * x)
        )
        exp_curve.set_stroke(RED, 3)

        rect.save_state()
        rect.stretch(0, 0, about_edge=LEFT)
        self.play(Restore(rect))
        self.play(ShowCreation(exp_curve, run_time=4))

        # Show capacity
        line = DashedLine(
            axes.c2p(0, 8.2),
            axes.c2p(axes.x_max, 8.2),
        )
        line.set_stroke(BLUE, 2)

        self.play(ShowCreation(line))
        self.wait()
        self.play(FadeOut(rect), FadeOut(exp_curve))

        # Show inflection point
        infl_point = axes.input_to_graph_point(5, curve)
        infl_dot = Dot(infl_point)
        infl_dot.set_stroke(WHITE, 3)

        curve_up_part = curve.copy()
        curve_up_part.pointwise_become_partial(curve, 0, 0.4)
        curve_up_part.set_stroke(GREEN)
        curve_down_part = curve.copy()
        curve_down_part.pointwise_become_partial(curve, 0.4, 1)
        curve_down_part.set_stroke(RED)
        for part in curve_up_part, curve_down_part:
            part.save_state()
            part.stretch(0, 1)
            part.set_y(axes.c2p(0, 0)[1])

        pre_dot = curve.copy()
        pre_dot.pointwise_become_partial(curve, 0.375, 0.425)

        infl_name = TexText("Inflection point")
        infl_name.next_to(infl_dot, LEFT)

        self.play(ReplacementTransform(pre_dot, infl_dot, path_arc=90 * DEGREES))
        self.add(curve_up_part, infl_dot)
        self.play(Restore(curve_up_part))
        self.add(curve_down_part, infl_dot)
        self.play(Restore(curve_down_part))
        self.wait()
        self.play(Write(infl_name, run_time=1))
        self.wait()

        # Show tangent line
        x_tracker = ValueTracker(0)
        tan_line = Line(LEFT, RIGHT)
        tan_line.set_width(5)
        tan_line.set_stroke(YELLOW, 2)

        def update_tan_line(line):
            x1 = x_tracker.get_value()
            x2 = x1 + 0.001
            p1 = axes.input_to_graph_point(x1, curve)
            p2 = axes.input_to_graph_point(x2, curve)
            angle = angle_of_vector(p2 - p1)
            line.rotate(angle - line.get_angle())
            line.move_to(p1)

        tan_line.add_updater(update_tan_line)

        dot = Dot()
        dot.scale(0.75)
        dot.set_fill(BLUE, 0.75)
        dot.add_updater(
            lambda m: m.move_to(axes.input_to_graph_point(
                x_tracker.get_value(), curve
            ))
        )

        self.play(
            ShowCreation(tan_line),
            FadeInFromLarge(dot),
        )
        self.play(
            x_tracker.set_value, 5,
            run_time=6,
        )
        self.wait()
        self.play(
            x_tracker.set_value, 9.9,
            run_time=6,
        )
        self.wait()

        # Define growth factor
        gf_label = Tex(
            "\\text{Growth factor} =",
            "{\\Delta N_d \\over \\Delta N_{d - 1}}",
            tex_to_color_map={
                "\\Delta": WHITE,
                "N_d": YELLOW,
                "N_{d - 1}": BLUE,
            }
        )
        gf_label.next_to(infl_dot, RIGHT, LARGE_BUFF)

        numer_label = TexText("New cases one day")
        denom_label = TexText("New cases the\\\\previous day")

        for label, tex, vect in (numer_label, "N_d", UL), (denom_label, "N_{d - 1}", DL):
            part = gf_label.get_part_by_tex(tex)
            label.match_color(part)
            label.next_to(part, vect, LARGE_BUFF)
            label.shift(2 * RIGHT)
            arrow = Arrow(
                label.get_corner(vect[1] * DOWN),
                part.get_corner(vect[1] * UP) + 0.25 * LEFT,
                buff=0.1,
            )
            arrow.match_color(part)
            label.add_to_back(arrow)

        self.play(
            FadeIn(gf_label[0], RIGHT),
            FadeIn(gf_label[1:], LEFT),
            FadeOut(ode)
        )
        self.wait()
        for label in numer_label, denom_label:
            self.play(FadeIn(label, lag_ratio=0.1))
            self.wait()

        # Show example growth factors
        self.play(x_tracker.set_value, 1)

        eq = Tex("=")
        eq.next_to(gf_label, RIGHT)
        gf = DecimalNumber(1.15)
        gf.set_height(0.4)
        gf.next_to(eq, RIGHT)

        def get_growth_factor():
            x1 = x_tracker.get_value()
            x0 = x1 - 0.2
            x2 = x1 + 0.2
            p0, p1, p2 = [
                axes.input_to_graph_point(x, curve)
                for x in [x0, x1, x2]
            ]
            return (p2[1] - p1[1]) / (p1[1] - p0[1])

        gf.add_updater(lambda m: m.set_value(get_growth_factor()))

        self.add(eq, gf)
        self.play(
            x_tracker.set_value, 5,
            run_time=6,
            rate_func=linear,
        )
        self.wait()
        self.play(
            x_tracker.set_value, 9,
            run_time=6,
            rate_func=linear,
        )

    def get_axes(self):
        axes = Axes(
            x_min=0,
            x_max=13,
            y_min=0,
            y_max=10,
            y_axis_config={
                "unit_size": 0.7,
                "include_tip": False,
            }
        )
        axes.center()
        axes.to_edge(DOWN)

        x_label = TexText("Time")
        x_label.next_to(axes.x_axis, UP, aligned_edge=RIGHT)
        y_label = TexText("N cases")
        y_label.next_to(axes.y_axis, RIGHT, aligned_edge=UP)
        axes.add(x_label, y_label)
        return axes


class SubtltyOfGrowthFactorShift(Scene):
    def construct(self):
        # Set up totals
        total_title = TexText("Totals")
        total_title.add(Underline(total_title))
        total_title.to_edge(UP)
        total_title.scale(1.25)
        total_title.shift(LEFT)
        total_title.set_color(YELLOW)
        total_title.shift(LEFT)

        data = CASE_DATA[-4:]
        data.append(int(data[-1] + 1.15 * (data[-1] - data[-2])))
        totals = VGroup(*[Integer(value) for value in data])
        totals.scale(1.25)
        totals.arrange(DOWN, buff=0.6, aligned_edge=LEFT)
        totals.next_to(total_title, DOWN, buff=0.6)
        totals[-1].set_color(BLUE)

        # Set up dates
        dates = VGroup(
            TexText("March 3, 2020"),
            TexText("March 4, 2020"),
            TexText("March 5, 2020"),
            TexText("March 6, 2020"),
        )
        for date, total in zip(dates, totals):
            date.scale(0.75)
            date.set_color(GREY_B)
            date.next_to(total, LEFT, buff=0.75, aligned_edge=DOWN)

        # Set up changes
        change_arrows = VGroup()
        change_labels = VGroup()
        for t1, t2 in zip(totals, totals[1:]):
            arrow = Arrow(
                t1.get_right(),
                t2.get_right(),
                path_arc=-150 * DEGREES,
                buff=0.1,
                max_tip_length_to_length_ratio=0.15,
            )
            arrow.shift(MED_SMALL_BUFF * RIGHT)
            arrow.set_stroke(width=3)
            change_arrows.add(arrow)

            diff = t2.get_value() - t1.get_value()
            label = Integer(diff, include_sign=True)
            label.set_color(GREEN)
            label.next_to(arrow, RIGHT)
            change_labels.add(label)

        change_labels[-1].set_color(BLUE)

        change_title = TexText("Changes")
        change_title.add(Underline(change_title).shift(0.128 * UP))
        change_title.scale(1.25)
        change_title.set_color(GREEN)
        change_title.move_to(change_labels)
        change_title.align_to(total_title, UP)

        # Set up growth factors
        gf_labels = VGroup()
        gf_arrows = VGroup()
        for c1, c2 in zip(change_labels, change_labels[1:]):
            arrow = Arrow(
                c1.get_right(),
                c2.get_right(),
                path_arc=-150 * DEGREES,
                buff=0.1,
                max_tip_length_to_length_ratio=0.15,
            )
            arrow.set_stroke(width=1)
            gf_arrows.add(arrow)

            line = Line(LEFT, RIGHT)
            line.match_width(c2)
            line.set_stroke(WHITE, 2)
            numer = c2.deepcopy()
            denom = c1.deepcopy()
            frac = VGroup(numer, line, denom)
            frac.arrange(DOWN, buff=SMALL_BUFF)
            frac.scale(0.7)
            frac.next_to(arrow, RIGHT)
            eq = Tex("=")
            eq.next_to(frac, RIGHT)
            gf = DecimalNumber(c2.get_value() / c1.get_value())
            gf.next_to(eq, RIGHT)
            gf_labels.add(VGroup(frac, eq, gf))

        gf_title = TexText("Growth factors")
        gf_title.add(Underline(gf_title))
        gf_title.scale(1.25)
        gf_title.move_to(gf_labels[0][-1])
        gf_title.align_to(total_title, DOWN)

        # Add things
        self.add(dates, total_title)
        self.play(
            LaggedStartMap(
                FadeInFrom, totals[:-1],
                lambda m: (m, UP),
            )
        )
        self.wait()
        self.play(
            ShowCreation(change_arrows[:-1]),
            LaggedStartMap(
                FadeInFrom, change_labels[:-1],
                lambda m: (m, LEFT),
            ),
            FadeIn(change_title),
        )
        self.wait()
        self.play(
            ShowCreation(gf_arrows[:-1]),
            LaggedStartMap(FadeIn, gf_labels[:-1]),
            FadeIn(gf_title),
        )
        self.wait()

        # Show hypothetical new value
        self.play(LaggedStart(
            FadeIn(gf_labels[-1]),
            FadeIn(gf_arrows[-1]),
            FadeIn(change_labels[-1]),
            FadeIn(change_arrows[-1]),
            FadeIn(totals[-1]),
        ))
        self.wait()

        # Change it
        alt_change = data[-2] - data[-3]
        alt_total = data[-2] + alt_change
        alt_gf = 1

        self.play(
            ChangeDecimalToValue(gf_labels[-1][-1], alt_gf),
            ChangeDecimalToValue(gf_labels[-1][0][0], alt_change),
            ChangeDecimalToValue(change_labels[-1], alt_change),
            ChangeDecimalToValue(totals[-1], alt_total),
        )
        self.wait()


class ContrastRandomShufflingWithClustersAndTravel(Scene):
    def construct(self):
        background = FullScreenFadeRectangle()
        background.set_fill(GREY_E)
        self.add(background)

        squares = VGroup(*[Square() for x in range(2)])
        squares.set_width(FRAME_WIDTH / 2 - 1)
        squares.arrange(RIGHT, buff=0.75)
        squares.to_edge(DOWN)
        squares.set_fill(BLACK, 1)
        squares.stretch(0.8, 1)
        self.add(squares)

        titles = VGroup(
            TexText("Random shuffling"),
            TexText("Clusters with travel"),
        )
        for title, square in zip(titles, squares):
            title.scale(1.4)
            title.next_to(square, UP)
        titles[1].align_to(titles[0], UP)

        self.play(LaggedStartMap(
            FadeInFrom, titles,
            lambda m: (m, 0.25 * DOWN),
        ))
        self.wait()


class ShowVaryingExpFactor(Scene):
    def construct(self):
        factor = DecimalNumber(0.15)
        rect = BackgroundRectangle(factor, buff=SMALL_BUFF)
        rect.set_fill(BLACK, 1)
        arrow = Arrow(
            factor.get_right(),
            factor.get_right() + 4 * RIGHT + 0.5 * DOWN,
        )

        self.add(rect, factor, arrow)
        for value in [0.05, 0.25, 0.15]:
            self.play(
                ChangeDecimalToValue(factor, value),
                run_time=3,
            )
        self.wait()


class ShowVaryingBaseFactor(ShowLogisticCurve):
    def construct(self):
        factor = DecimalNumber(1.15)
        rect = BackgroundRectangle(factor, buff=SMALL_BUFF)
        rect.set_fill(BLACK, 1)

        self.add(rect, factor)
        for value in [1.05, 1.25, 1.15]:
            self.play(
                ChangeDecimalToValue(factor, value),
                run_time=3,
            )
        self.wait()


class ShowVaryingExpCurve(ShowLogisticCurve):
    def construct(self):
        axes = self.get_axes()
        self.add(axes)

        curve = axes.get_graph(lambda x: np.exp(0.15 * x))
        curve.set_stroke([BLUE, YELLOW, RED])
        curve.make_jagged()
        self.add(curve)

        self.camera.frame.scale(2, about_edge=DOWN)
        self.camera.frame.shift(DOWN)
        rect = FullScreenFadeRectangle()
        rect.set_stroke(WHITE, 3)
        rect.set_fill(opacity=0)
        self.add(rect)

        for value in [0.05, 0.25, 0.15]:
            new_curve = axes.get_graph(lambda x: np.exp(value * x))
            new_curve.set_stroke([BLUE, YELLOW, RED])
            new_curve.make_jagged()
            self.play(
                Transform(curve, new_curve),
                run_time=3,
            )


class EndScreen(PatreonEndScreen):
    CONFIG = {
        "specific_patrons": [
            "1stViewMaths",
            "Adam Dřínek",
            "Aidan Shenkman",
            "Alan Stein",
            "Alex Mijalis",
            "Alexis Olson",
            "Ali Yahya",
            "Andrew Busey",
            "Andrew Cary",
            "Andrew R. Whalley",
            "Aravind C V",
            "Arjun Chakroborty",
            "Arthur Zey",
            "Austin Goodman",
            "Avi Finkel",
            "Awoo",
            "AZsorcerer",
            "Barry Fam",
            "Bernd Sing",
            "Boris Veselinovich",
            "Bradley Pirtle",
            "Brian Staroselsky",
            "Britt Selvitelle",
            "Britton Finley",
            "Burt Humburg",
            "Calvin Lin",
            "Charles Southerland",
            "Charlie N",
            "Chenna Kautilya",
            "Chris Connett",
            "Christian Kaiser",
            "cinterloper",
            "Clark Gaebel",
            "Colwyn Fritze-Moor",
            "Cooper Jones",
            "Corey Ogburn",
            "D. Sivakumar",
            "Daniel Herrera C",
            "Dave B",
            "Dave Kester",
            "dave nicponski",
            "David B. Hill",
            "David Clark",
            "David Gow",
            "Delton Ding",
            "Dominik Wagner",
            "Douglas Cantrell",
            "emptymachine",
            "Eric Younge",
            "Eryq Ouithaqueue",
            "Federico Lebron",
            "Frank R. Brown, Jr.",
            "Giovanni Filippi",
            "Hal Hildebrand",
            "Hitoshi Yamauchi",
            "Ivan Sorokin",
            "Jacob Baxter",
            "Jacob Harmon",
            "Jacob Hartmann",
            "Jacob Magnuson",
            "Jake Vartuli - Schonberg",
            "Jameel Syed",
            "Jason Hise",
            "Jayne Gabriele",
            "Jean-Manuel Izaret",
            "Jeff Linse",
            "Jeff Straathof",
            "John C. Vesey",
            "John Haley",
            "John Le",
            "John V Wertheim",
            "Jonathan Heckerman",
            "Jonathan Wilson",
            "Joseph John Cox",
            "Joseph Kelly",
            "Josh Kinnear",
            "Joshua Claeys",
            "Juan Benet",
            "Kai-Siang Ang",
            "Kanan Gill",
            "Karl Niu",
            "Kartik Cating-Subramanian",
            "Kaustuv DeBiswas",
            "Killian McGuinness",
            "Kros Dai",
            "L0j1k",
            "Lambda GPU Workstations",
            "Lee Redden",
            "Linh Tran",
            "Luc Ritchie",
            "Ludwig Schubert",
            "Lukas Biewald",
            "Magister Mugit",
            "Magnus Dahlström",
            "Manoj Rewatkar - RITEK SOLUTIONS",
            "Mark Heising",
            "Mark Mann",
            "Martin Price",
            "Mathias Jansson",
            "Matt Godbolt",
            "Matt Langford",
            "Matt Roveto",
            "Matt Russell",
            "Matteo Delabre",
            "Matthew Bouchard",
            "Matthew Cocke",
            "Mia Parent",
            "Michael Hardel",
            "Michael W White",
            "Mirik Gogri",
            "Mustafa Mahdi",
            "Márton Vaitkus",
            "Nicholas Cahill",
            "Nikita Lesnikov",
            "Oleg Leonov",
            "Oliver Steele",
            "Omar Zrien",
            "Owen Campbell-Moore",
            "Patrick Lucas",
            "Peter Ehrnstrom",
            "Peter Mcinerney",
            "Pierre Lancien",
            "Quantopian",
            "Randy C. Will",
            "rehmi post",
            "Rex Godby",
            "Ripta Pasay",
            "Rish Kundalia",
            "Roman Sergeychik",
            "Roobie",
            "Ryan Atallah",
            "Samuel Judge",
            "SansWord Huang",
            "Scott Gray",
            "Scott Walter, Ph.D.",
            "Sebastian Garcia",
            "soekul",
            "Solara570",
            "Steve Huynh",
            "Steve Sperandeo",
            "Steven Braun",
            "Steven Siddals",
            "Stevie Metke",
            "supershabam",
            "Suteerth Vishnu",
            "Suthen Thomas",
            "Tal Einav",
            "Tauba Auerbach",
            "Ted Suzman",
            "Thomas J Sargent",
            "Thomas Tarler",
            "Tianyu Ge",
            "Tihan Seale",
            "Tyler VanValkenburg",
            "Vassili Philippov",
            "Veritasium",
            "Vinicius Reis",
            "Xuanji Li",
            "Yana Chernobilsky",
            "Yavor Ivanov",
            "YinYangBalance.Asia",
            "Yu Jun",
            "Yurii Monastyrshyn",
        ]
    }
