How to create a correct pie chart with manim

Viewed 700

Anyway, I'm kind of trying (this is actually my first manim program).

from manim import *
import copy
import numpy as np
import random

color_palette = [BLUE, GREEN, YELLOW, GREY_BROWN]

class TestPie(Scene):
    def construct(self):
        n_elements = 3
        radius = 1
        weights = np.random.rand(n_elements)
        weights /= weights.sum()
        angles = weights*np.pi*2
        angles_offset = [0]+np.cumsum(weights*np.pi*2)[:-1].tolist()
        arcs = [Arc(angles_offset[i], angles[i]) for i in range(n_elements)]
        arcs2 = copy.deepcopy(arcs)
        triangles = [Polygon(*[
            # first element
            (radius*np.cos(angles_offset[i]), radius*np.sin(angles_offset[i]),0),
            (0, 0, 0), # second element
            # third element
            (radius*np.cos(angles_offset[(i+1)%n_elements]),
             radius*np.sin(angles_offset[(i+1)%n_elements]), 0)], stroke_width=0)
                    for i in range(n_elements)]
        lines = [Line((0,0,0),
                      (radius*np.cos(angles_offset[i]), radius*np.sin(angles_offset[i]), 0))
                 for i in range(n_elements)]
        for i in range(n_elements):
            arcs2[i].set_fill(color_palette[i%len(color_palette)], opacity=0.5)
            triangles[i].set_fill(color_palette[i%len(color_palette)], opacity=0.5)

        self.play(
            *map(lambda obj: ShowCreation(obj, run_time=1), arcs),
            *map(lambda obj: ShowCreation(obj, run_time=1), lines),
        )
        self.play(
            *map(lambda i: Transform(arcs[i], arcs2[i], runtime=1), range(n_elements)),
            *map(lambda obj: FadeIn(obj, run_time=1), triangles),

        )

        self.wait()
        weights = np.random.rand(n_elements)
        weights /= weights.sum()
        angles = weights*np.pi*2
        angles_offset = [0]+np.cumsum(weights*np.pi*2)[:-1].tolist()
        arcs2 = [Arc(angles_offset[i], angles[i]) for i in range(n_elements)]
        lines2 = [Line((0,0,0),
                       (radius*np.cos(angles_offset[i]), radius*np.sin(angles_offset[i]), 0))
                  for i in range(n_elements)]
        triangles2 = [Polygon(*[
            # first element
            (radius*np.cos(angles_offset[i]), radius*np.sin(angles_offset[i]),0),
            (0, 0, 0), # second element
            # third element
            (radius*np.cos(angles_offset[(i+1)%n_elements]),
             radius*np.sin(angles_offset[(i+1)%n_elements]), 0)], stroke_width=0)
                    for i in range(n_elements)]

        for i in range(n_elements):
            arcs2[i].set_fill(color_palette[i%len(color_palette)], opacity=0.5)
            triangles2[i].set_fill(color_palette[i%len(color_palette)], opacity=0.5)

        self.play(
            *map(lambda i: Transform(lines[i], lines2[i],
                                     runtime=1), range(n_elements)),
            *map(lambda i: Transform(arcs[i], arcs2[i],
                                     runtime=1), range(n_elements)),
            *map(lambda i: Transform(triangles[i], triangles2[i],
                                     runtime=1), range(n_elements)),
        )
        self.wait(2)
        

The output:

out

Thus, I have two problems with my current program. And I would appreciate a little help.

1. Since I'm using triangles and arcs, I get an ugly gap as you can see in the following picture.

image

2. I'm getting ugly transformations with the Arc, Triange and Line classes, the transformations should follow the circumference which is not the case right now. You can appreciate more one of the intermediary ugly steps in the image below. (As you can see, it's not round any more)

image

2 Answers

For the first problem, avoid creating shapes by exactly lining up separate shapes. In fact, avoid exactly lining up separate shapes at all: graphics rendering engines often have trouble rendering such situations. Instead of creating a circular sector out of a circular segment and a triangle, create a single shape that will represent the whole sector that will be drawn as a single unit. In this case, use the Sector class to represent the sector instead of separate Arc and Polygon.

For the second problem, the issue is that by default manim computes intermediate shapes pointwise. The interpolation behaviour is controlled by the shape’s interpolate method. By subclassing the shape, you can override the interpolate method and instead compute intermediate shapes from more natural high-level parameters that define the shape: in this case, the centre, angles and radius.

Both of those fixes are incorporated in the sample below.

from manim import *
import numpy as np

class MySector(Sector):
    """ Circular sector shape with a custom interpolation method. """

    def interpolate(self, mobject1, mobject2, alpha, path_func=straight_path):
        if not (isinstance(mobject1, MySector) and isinstance(mobject2, MySector)):
            return super().interpolate(mobject1, mobject2, alpha, path_func=path_func)

        for attr in (
            'start_angle', 'angle',
            'inner_radius', 'outer_radius',
        ):
            v1 = getattr(mobject1, attr)
            v2 = getattr(mobject2, attr)
            setattr(self, attr, path_func(v1, v2, alpha))

        self.arc_center = path_func(
            mobject1.get_arc_center(),
            mobject2.get_arc_center(),
            alpha
        )
        self.interpolate_color(mobject1, mobject2, alpha)
        self.clear_points()
        self.generate_points()
        return self


color_palette = [BLUE, GREEN, YELLOW, GREY_BROWN]

class TestPie(Scene):
    def construct(self):
        weights = np.array([2.0, 3.0, 4.0])
        weights /= weights.sum()

        angles = weights * TAU
        angles_offset = np.cumsum((0, *angles[:-1]))

        sectors1 = [
            MySector(start_angle=ao, angle=a,
                stroke_width=DEFAULT_STROKE_WIDTH,
                fill_opacity=0)
            for ao, a in zip(angles_offset, angles)
        ]

        sectors2 = [
            MySector(start_angle=ao, angle=a,
                stroke_width=DEFAULT_STROKE_WIDTH,
                fill_color=color_palette[i % len(color_palette)], fill_opacity=0.5)
            for i, (ao, a) in enumerate(zip(angles_offset, angles))
        ]

        self.play(
            *(ShowCreation(a1, run_time=1) for a1 in sectors1)
        )

        self.play(
            *(Transform(a1, a2, runtime=1) for (a1, a2) in zip(sectors1, sectors2))
        )

        self.wait()

        weights = np.array([4.0, 3.0, 2.0])
        weights /= weights.sum()

        angles = weights * TAU
        angles_offset = np.cumsum((0, *angles[:-1]))

        sectors2 = [
            MySector(start_angle=ao, angle=a,
                stroke_width=DEFAULT_STROKE_WIDTH,
                fill_color=color_palette[i % len(color_palette)], fill_opacity=0.5)
            for i, (ao, a) in enumerate(zip(angles_offset, angles))
        ]

        self.play(
            *(Transform(a1, a2, runtime=1) for (a1, a2) in zip(sectors1, sectors2))
        )
        self.wait(2)

And here’s the resulting animation:

animation

The above doesn’t preserve the initial drawing animation for the lines that make up the pie chart sectors. You can override the pointwise_become_partial method to bring it back, or you may simply restore the Line shapes from your original code.

I know this was made a month ago, but for anyone looking, I have a better solution. I have created a manim graphing plugin (manim-graphing), that supports pie charts at a really high level.

Related