7

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

silgon
  • 6,890
  • 7
  • 46
  • 67
  • the pie chart is not an element in `manim`, I asked some time ago in the github repository with no reply. (https://github.com/ManimCommunity/manim/issues/857) – silgon Jan 19 '21 at 00:17

2 Answers2

8

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.

user3840170
  • 26,597
  • 4
  • 30
  • 62
  • Thank you @user3840170 . Sorry for the delay, I just got the time to check the reply which is already nice. However when I tested, I realized that the `interpolation` procedure, make the object lose some animation transformation (you are not able to move the object anymore). You can check that out in https://pastebin.com/yBAEYa4J . Is there a way to avoid that? – silgon Jan 26 '21 at 14:46
  • Ugh. I managed to work it around in this case, but the real problem is that the data flows in this library are not designed well enough for this use case. (You’re going to have this problem again with scaling/shearing transformations.) I assume `interpolate` is not meant to be overridden in the first place anyway. This answer is more of a hack than a proper solution. – user3840170 Jan 27 '21 at 09:11
2

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.

Edit: Here's some code for it

from manim import *
import typing

class PieChart(VMobject):
    def __init__(self, data: dict, display_labels: bool = True):
        super().__init__()

        self.animations = None

        self.pie_colors = [YELLOW, GREEN, BLUE, ORANGE, RED]

        self.labels = list()
        self.values = list()

        for key, value in data.items():
            self.labels.append(key)
            self.values.append(value)

        angles = [x / sum(self.values) * 360 * DEGREES for x in self.values]
        sectors = list()
        labels = list()
        current_angle = 0

        pie = Circle(stroke_color=WHITE, fill_opacity=0)

        center_point = Circle(color=WHITE, radius=0.01, fill_opacity=1).move_to(
            pie.get_center()
        )
        up_line = Line(pie.get_center(), UP)

        center_point.z_index = 2
        up_line.z_index = 1

        for i, angle in enumerate(angles):
            a_point = pie.point_at_angle(current_angle)
            a2_point = pie.point_at_angle(angle)
            color = self.pie_colors[i + 1 % len(self.pie_colors)]

            piece = ArcPolygonFromArcs(
                ArcBetweenPoints(pie.get_center(), a_point, angle=0),
                Arc(start_angle=current_angle, angle=angle, radius=1),
                ArcBetweenPoints(pie.get_center(), a2_point, angle=0, stroke_opacity=0),
                color=color,
                stroke_color=WHITE,
                fill_opacity=1,
                stroke_opacity=0,
            )

            if display_labels:
                labels.append(
                    Text(self.labels[i])
                    .scale(0.4)
                    .move_to(pie.point_at_angle(current_angle + (angle / 2)) * 1.5)
                    .rotate(90 * DEGREES, about_point=pie.get_center())
                    .rotate(-90 * DEGREES)
                )
                print(self.labels[i])

            piece.rotate(90 * DEGREES, about_point=pie.get_center())

            current_angle += angle
            sectors.append(piece)

        self.add(center_point)
        self.add(up_line)
        self.add(*sectors)
        if labels:
            self.add(*labels)
GameDungeon
  • 195
  • 1
  • 11