Repository: ManimCommunity/manim

Issue:
`BarChart` bars of height 0 do not get updated as expected.
## Description of bug / unexpected behavior

`BarChart` bars of height do not get updated as expected.


## Expected behavior
I expected to be able to updated `BarChart` bars using the provided `change_bar_values` method, but this appears not to work for bars of height zero.


## How to reproduce the issue
<!-- Provide a piece of code illustrating the undesired behavior. -->

In the following, the bars are not updated in the `Foo` scene (initial height 0.0), whereas they are updated in the `Bar` scene (initial height 0.0001).

<details><summary>Code for reproducing the problem</summary>

```py
class Foo(Scene):
    def construct(self):
        values=[0 for _ in range(11)]

        chart = BarChart(
            values,
            y_range=[0, 10, 1],
            y_axis_config={"font_size": 24},
        )
        new_values=[1 for _ in range(11)]
        chart.change_bar_values(new_values)
        self.add(chart)


class Bar(Scene):
    def construct(self):
        values=[0.0001 for _ in range(11)]

        chart = BarChart(
            values,
            y_range=[0, 10, 1],
            y_axis_config={"font_size": 24},
        )
        new_values=[1 for _ in range(11)]
        chart.change_bar_values(new_values)
        self.add(chart)
```

</details>


## Additional media files
<!-- Paste in the files manim produced on rendering the code above. -->

<details><summary>Images/GIFs</summary>

Foo:

![Foo_ManimCE_v0 15 1](https://user-images.githubusercontent.com/25851939/160243420-3cea9d04-83bc-40c3-8913-6fa5a6317fb8.png)

Bar:

![Bar_ManimCE_v0 15 1](https://user-images.githubusercontent.com/25851939/160243433-d912348f-2bac-4e07-b3b2-9739fca3eeb2.png)

</details>


## System specifications

<details><summary>System Details</summary>

```
Manjaro Linux 21.2.5
Python 3.10.2
manim 0.15.1
```

</details>




Discussion:

Resolving patch:
diff --git a/manim/mobject/graphing/probability.py b/manim/mobject/graphing/probability.py
--- a/manim/mobject/graphing/probability.py
+++ b/manim/mobject/graphing/probability.py
@@ -5,12 +5,12 @@
 __all__ = ["SampleSpace", "BarChart"]
 
 
-from typing import Iterable, Sequence
+from typing import Iterable, MutableSequence, Sequence
 
 import numpy as np
 from colour import Color
 
-from manim import config
+from manim import config, logger
 from manim.constants import *
 from manim.mobject.geometry.polygram import Rectangle
 from manim.mobject.graphing.coordinate_systems import Axes
@@ -192,9 +192,9 @@ class BarChart(Axes):
     Parameters
     ----------
     values
-        An iterable of values that determines the height of each bar. Accepts negative values.
+        A sequence of values that determines the height of each bar. Accepts negative values.
     bar_names
-        An iterable of names for each bar. Does not have to match the length of ``values``.
+        A sequence of names for each bar. Does not have to match the length of ``values``.
     y_range
         The y_axis range of values. If ``None``, the range will be calculated based on the
         min/max of ``values`` and the step will be calculated based on ``y_length``.
@@ -204,7 +204,7 @@ class BarChart(Axes):
     y_length
         The length of the y-axis.
     bar_colors
-        The color for the bars. Accepts a single color or an iterable of colors.
+        The color for the bars. Accepts a sequence of colors (can contain just one item).
         If the length of``bar_colors`` does not match that of ``values``,
         intermediate colors will be automatically determined.
     bar_width
@@ -237,14 +237,12 @@ def construct(self):
 
     def __init__(
         self,
-        values: Iterable[float],
-        bar_names: Iterable[str] | None = None,
+        values: MutableSequence[float],
+        bar_names: Sequence[str] | None = None,
         y_range: Sequence[float] | None = None,
         x_length: float | None = None,
-        y_length: float | None = config.frame_height - 4,
-        bar_colors: str
-        | Iterable[str]
-        | None = [
+        y_length: float | None = None,
+        bar_colors: Iterable[str] = [
             "#003f5c",
             "#58508d",
             "#bc5090",
@@ -257,6 +255,14 @@ def __init__(
         **kwargs,
     ):
 
+        if isinstance(bar_colors, str):
+            logger.warning(
+                "Passing a string to `bar_colors` has been deprecated since v0.15.2 and will be removed after v0.17.0, the parameter must be a list.  "
+            )
+            bar_colors = list(bar_colors)
+
+        y_length = y_length if y_length is not None else config.frame_height - 4
+
         self.values = values
         self.bar_names = bar_names
         self.bar_colors = bar_colors
@@ -272,6 +278,7 @@ def __init__(
                 max(0, max(self.values)),
                 round(max(self.values) / y_length, 2),
             ]
+
         elif len(y_range) == 2:
             y_range = [*y_range, round(max(self.values) / y_length, 2)]
 
@@ -283,9 +290,9 @@ def __init__(
             (x_axis_config,), (kwargs.pop("x_axis_config", None),)
         )
 
-        self.bars = None
-        self.x_labels = None
-        self.bar_labels = None
+        self.bars: VGroup = VGroup()
+        self.x_labels: VGroup | None = None
+        self.bar_labels: VGroup | None = None
 
         super().__init__(
             x_range=x_range,
@@ -304,6 +311,17 @@ def __init__(
 
         self.y_axis.add_numbers()
 
+    def _update_colors(self):
+        """Initialize the colors of the bars of the chart.
+
+        Sets the color of ``self.bars`` via ``self.bar_colors``.
+
+        Primarily used when the bars are initialized with ``self._add_bars``
+        or updated via ``self.change_bar_values``.
+        """
+
+        self.bars.set_color_by_gradient(*self.bar_colors)
+
     def _add_x_axis_labels(self):
         """Essentially :meth`:~.NumberLine.add_labels`, but differs in that
         the direction of the label with respect to the x_axis changes to UP or DOWN
@@ -339,27 +357,46 @@ def _add_x_axis_labels(self):
         self.x_axis.labels = labels
         self.x_axis.add(labels)
 
-    def _add_bars(self):
-        self.bars = VGroup()
+    def _create_bar(self, bar_number: int, value: float) -> Rectangle:
+        """Creates a positioned bar on the chart.
 
-        for i, value in enumerate(self.values):
-            bar_h = abs(self.c2p(0, value)[1] - self.c2p(0, 0)[1])
-            bar_w = self.c2p(self.bar_width, 0)[0] - self.c2p(0, 0)[0]
-            bar = Rectangle(
-                height=bar_h,
-                width=bar_w,
-                stroke_width=self.bar_stroke_width,
-                fill_opacity=self.bar_fill_opacity,
-            )
+        Parameters
+        ----------
+        bar_number
+            Determines the x-position of the bar.
+        value
+            The value that determines the height of the bar.
+
+        Returns
+        -------
+        Rectangle
+            A positioned rectangle representing a bar on the chart.
+        """
 
-            pos = UP if (value >= 0) else DOWN
-            bar.next_to(self.c2p(i + 0.5, 0), pos, buff=0)
-            self.bars.add(bar)
-        if isinstance(self.bar_colors, str):
-            self.bars.set_color_by_gradient(self.bar_colors)
-        else:
-            self.bars.set_color_by_gradient(*self.bar_colors)
+        # bar measurements relative to the axis
+
+        # distance from between the y-axis and the top of the bar
+        bar_h = abs(self.c2p(0, value)[1] - self.c2p(0, 0)[1])
+        # width of the bar
+        bar_w = self.c2p(self.bar_width, 0)[0] - self.c2p(0, 0)[0]
+
+        bar = Rectangle(
+            height=bar_h,
+            width=bar_w,
+            stroke_width=self.bar_stroke_width,
+            fill_opacity=self.bar_fill_opacity,
+        )
 
+        pos = UP if (value >= 0) else DOWN
+        bar.next_to(self.c2p(bar_number + 0.5, 0), pos, buff=0)
+        return bar
+
+    def _add_bars(self) -> None:
+        for i, value in enumerate(self.values):
+            tmp_bar = self._create_bar(bar_number=i, value=value)
+            self.bars.add(tmp_bar)
+
+        self._update_colors()
         self.add_to_back(self.bars)
 
     def get_bar_labels(
@@ -367,7 +404,7 @@ def get_bar_labels(
         color: Color | None = None,
         font_size: float = 24,
         buff: float = MED_SMALL_BUFF,
-        label_constructor: VMobject = Tex,
+        label_constructor: type[VMobject] = Tex,
     ):
         """Annotates each bar with its corresponding value. Use ``self.bar_labels`` to access the
         labels after creation.
@@ -416,7 +453,7 @@ def construct(self):
 
         return bar_labels
 
-    def change_bar_values(self, values: Iterable[float]):
+    def change_bar_values(self, values: Iterable[float], update_colors: bool = True):
         """Updates the height of the bars of the chart.
 
         Parameters
@@ -424,6 +461,8 @@ def change_bar_values(self, values: Iterable[float]):
         values
             The values that will be used to update the height of the bars.
             Does not have to match the number of bars.
+        update_colors
+            Whether to re-initalize the colors of the bars based on ``self.bar_colors``.
 
         Examples
         --------
@@ -455,7 +494,8 @@ def construct(self):
                 bar_lim = bar.get_top()
                 aligned_edge = UP
 
-            try:
+            # check if the bar has height
+            if chart_val != 0:
                 quotient = value / chart_val
                 if quotient < 0:
 
@@ -468,11 +508,17 @@ def construct(self):
                     # if already negative, then we move the bottom edge of the bar
                     # to the location of the previous top
 
-                bar.stretch_to_fit_height(quotient * bar.height)
+                bar.stretch_to_fit_height(abs(quotient) * bar.height)
 
-            except ZeroDivisionError:
-                bar.height = 0
+            else:
+                # create a new bar since the current one has a height of zero (doesn't exist)
+                temp_bar = self._create_bar(i, value)
+                self.bars.remove(bar)
+                self.bars.insert(i, temp_bar)
 
             bar.move_to(bar_lim, aligned_edge)
 
+        if update_colors:
+            self._update_colors()
+
         self.values[: len(values)] = values
diff --git a/manim/mobject/mobject.py b/manim/mobject/mobject.py
--- a/manim/mobject/mobject.py
+++ b/manim/mobject/mobject.py
@@ -1,5 +1,5 @@
 """Base classes for objects that can be displayed."""
-
+from __future__ import annotations
 
 __all__ = ["Mobject", "Group", "override_animate"]
 
@@ -84,9 +84,9 @@ class Mobject:
     def __init_subclass__(cls, **kwargs):
         super().__init_subclass__(**kwargs)
 
-        cls.animation_overrides: Dict[
-            Type["Animation"],
-            Callable[["Mobject"], "Animation"],
+        cls.animation_overrides: dict[
+            type[Animation],
+            Callable[[Mobject], Animation],
         ] = {}
         cls._add_intrinsic_animation_overrides()
         cls._original__init__ = cls.__init__
@@ -109,8 +109,8 @@ def __init__(self, color=WHITE, name=None, dim=3, target=None, z_index=0):
     @classmethod
     def animation_override_for(
         cls,
-        animation_class: Type["Animation"],
-    ) -> "Optional[Callable[[Mobject, ...], Animation]]":
+        animation_class: type[Animation],
+    ) -> Callable[[Mobject, ...], Animation] | None:
         """Returns the function defining a specific animation override for this class.
 
         Parameters
@@ -147,8 +147,8 @@ def _add_intrinsic_animation_overrides(cls):
     @classmethod
     def add_animation_override(
         cls,
-        animation_class: Type["Animation"],
-        override_func: "Callable[[Mobject, ...], Animation]",
+        animation_class: type[Animation],
+        override_func: Callable[[Mobject, ...], Animation],
     ):
         """Add an animation override.
 
@@ -358,7 +358,7 @@ def generate_points(self):
         """
         pass
 
-    def add(self, *mobjects: "Mobject"):
+    def add(self, *mobjects: Mobject):
         """Add mobjects as submobjects.
 
         The mobjects are added to :attr:`submobjects`.
@@ -425,13 +425,34 @@ def add(self, *mobjects: "Mobject"):
         self.submobjects = list_update(self.submobjects, mobjects)
         return self
 
+    def insert(self, index: int, mobject: Mobject):
+        """Inserts a mobject at a specific position into self.submobjects
+
+        Effectively just calls  ``self.submobjects.insert(index, mobject)``,
+        where ``self.submobjects`` is a list.
+
+        Highly adapted from ``Mobject.add``.
+
+        Parameters
+        ----------
+        index
+            The index at which
+        mobject
+            The mobject to be inserted.
+        """
+        if not isinstance(mobject, Mobject):
+            raise TypeError("All submobjects must be of type Mobject")
+        if mobject is self:
+            raise ValueError("Mobject cannot contain self")
+        self.submobjects.insert(index, mobject)
+
     def __add__(self, mobject):
         raise NotImplementedError
 
     def __iadd__(self, mobject):
         raise NotImplementedError
 
-    def add_to_back(self, *mobjects: "Mobject"):
+    def add_to_back(self, *mobjects: Mobject):
         """Add all passed mobjects to the back of the submobjects.
 
         If :attr:`submobjects` already contains the given mobjects, they just get moved
@@ -487,7 +508,7 @@ def add_to_back(self, *mobjects: "Mobject"):
         self.submobjects = list(dict.fromkeys(mobjects)) + self.submobjects
         return self
 
-    def remove(self, *mobjects: "Mobject"):
+    def remove(self, *mobjects: Mobject):
         """Remove :attr:`submobjects`.
 
         The mobjects are removed from :attr:`submobjects`, if they exist.
@@ -809,7 +830,7 @@ def update(self, dt: float = 0, recursive: bool = True):
                 submob.update(dt, recursive)
         return self
 
-    def get_time_based_updaters(self) -> List[Updater]:
+    def get_time_based_updaters(self) -> list[Updater]:
         """Return all updaters using the ``dt`` parameter.
 
         The updaters use this parameter as the input for difference in time.
@@ -843,7 +864,7 @@ def has_time_based_updater(self) -> bool:
         """
         return any("dt" in get_parameters(updater) for updater in self.updaters)
 
-    def get_updaters(self) -> List[Updater]:
+    def get_updaters(self) -> list[Updater]:
         """Return all updaters.
 
         Returns
@@ -865,7 +886,7 @@ def get_family_updaters(self):
     def add_updater(
         self,
         update_function: Updater,
-        index: Optional[int] = None,
+        index: int | None = None,
         call_updater: bool = False,
     ):
         """Add an update function to this mobject.
@@ -990,7 +1011,7 @@ def clear_updaters(self, recursive: bool = True):
                 submob.clear_updaters()
         return self
 
-    def match_updaters(self, mobject: "Mobject"):
+    def match_updaters(self, mobject: Mobject):
         """Match the updaters of the given mobject.
 
         Parameters
@@ -1075,7 +1096,7 @@ def resume_updating(self, recursive: bool = True):
 
     # Transforming operations
 
-    def apply_to_family(self, func: Callable[["Mobject"], None]):
+    def apply_to_family(self, func: Callable[[Mobject], None]):
         """Apply a function to ``self`` and every submobject with points recursively.
 
         Parameters
@@ -1177,7 +1198,7 @@ def rotate(
         self,
         angle,
         axis=OUT,
-        about_point: Optional[Sequence[float]] = None,
+        about_point: Sequence[float] | None = None,
         **kwargs,
     ):
         """Rotates the :class:`~.Mobject` about a certain point."""
@@ -1611,7 +1632,7 @@ def replace(self, mobject, dim_to_match=0, stretch=False):
 
     def surround(
         self,
-        mobject: "Mobject",
+        mobject: Mobject,
         dim_to_match=0,
         stretch=False,
         buff=MED_SMALL_BUFF,
@@ -1646,7 +1667,7 @@ def put_start_and_end_on(self, start, end):
 
     # Background rectangle
     def add_background_rectangle(
-        self, color: Optional[Colors] = None, opacity: float = 0.75, **kwargs
+        self, color: Colors | None = None, opacity: float = 0.75, **kwargs
     ):
         """Add a BackgroundRectangle as submobject.
 
@@ -2013,27 +2034,27 @@ def has_no_points(self) -> bool:
 
     # Match other mobject properties
 
-    def match_color(self, mobject: "Mobject"):
+    def match_color(self, mobject: Mobject):
         """Match the color with the color of another :class:`~.Mobject`."""
         return self.set_color(mobject.get_color())
 
-    def match_dim_size(self, mobject: "Mobject", dim, **kwargs):
+    def match_dim_size(self, mobject: Mobject, dim, **kwargs):
         """Match the specified dimension with the dimension of another :class:`~.Mobject`."""
         return self.rescale_to_fit(mobject.length_over_dim(dim), dim, **kwargs)
 
-    def match_width(self, mobject: "Mobject", **kwargs):
+    def match_width(self, mobject: Mobject, **kwargs):
         """Match the width with the width of another :class:`~.Mobject`."""
         return self.match_dim_size(mobject, 0, **kwargs)
 
-    def match_height(self, mobject: "Mobject", **kwargs):
+    def match_height(self, mobject: Mobject, **kwargs):
         """Match the height with the height of another :class:`~.Mobject`."""
         return self.match_dim_size(mobject, 1, **kwargs)
 
-    def match_depth(self, mobject: "Mobject", **kwargs):
+    def match_depth(self, mobject: Mobject, **kwargs):
         """Match the depth with the depth of another :class:`~.Mobject`."""
         return self.match_dim_size(mobject, 2, **kwargs)
 
-    def match_coord(self, mobject: "Mobject", dim, direction=ORIGIN):
+    def match_coord(self, mobject: Mobject, dim, direction=ORIGIN):
         """Match the coordinates with the coordinates of another :class:`~.Mobject`."""
         return self.set_coord(
             mobject.get_coord(dim, direction),
@@ -2041,21 +2062,21 @@ def match_coord(self, mobject: "Mobject", dim, direction=ORIGIN):
             direction=direction,
         )
 
-    def match_x(self, mobject: "Mobject", direction=ORIGIN):
+    def match_x(self, mobject: Mobject, direction=ORIGIN):
         """Match x coord. to the x coord. of another :class:`~.Mobject`."""
         return self.match_coord(mobject, 0, direction)
 
-    def match_y(self, mobject: "Mobject", direction=ORIGIN):
+    def match_y(self, mobject: Mobject, direction=ORIGIN):
         """Match y coord. to the x coord. of another :class:`~.Mobject`."""
         return self.match_coord(mobject, 1, direction)
 
-    def match_z(self, mobject: "Mobject", direction=ORIGIN):
+    def match_z(self, mobject: Mobject, direction=ORIGIN):
         """Match z coord. to the x coord. of another :class:`~.Mobject`."""
         return self.match_coord(mobject, 2, direction)
 
     def align_to(
         self,
-        mobject_or_point: Union["Mobject", np.ndarray, List],
+        mobject_or_point: Mobject | np.ndarray | list,
         direction=ORIGIN,
         alignment_vect=UP,
     ):
@@ -2141,14 +2162,14 @@ def construct(self):
 
     def arrange_in_grid(
         self,
-        rows: Optional[int] = None,
-        cols: Optional[int] = None,
-        buff: Union[float, Tuple[float, float]] = MED_SMALL_BUFF,
+        rows: int | None = None,
+        cols: int | None = None,
+        buff: float | tuple[float, float] = MED_SMALL_BUFF,
         cell_alignment: np.ndarray = ORIGIN,
-        row_alignments: Optional[str] = None,  # "ucd"
-        col_alignments: Optional[str] = None,  # "lcr"
-        row_heights: Optional[Iterable[Optional[float]]] = None,
-        col_widths: Optional[Iterable[Optional[float]]] = None,
+        row_alignments: str | None = None,  # "ucd"
+        col_alignments: str | None = None,  # "lcr"
+        row_heights: Iterable[float | None] | None = None,
+        col_widths: Iterable[float | None] | None = None,
         flow_order: str = "rd",
         **kwargs,
     ):
@@ -2468,7 +2489,7 @@ def construct(self):
         return self.shuffle(*args, **kwargs)
 
     # Alignment
-    def align_data(self, mobject: "Mobject", skip_point_alignment: bool = False):
+    def align_data(self, mobject: Mobject, skip_point_alignment: bool = False):
         """Aligns the data of this mobject with another mobject.
 
         Afterwards, the two mobjects will have the same number of submobjects
@@ -2520,7 +2541,7 @@ def align_submobjects(self, mobject):
         mob2.add_n_more_submobjects(max(0, n1 - n2))
         return self
 
-    def null_point_align(self, mobject: "Mobject"):
+    def null_point_align(self, mobject: Mobject):
         """If a :class:`~.Mobject` with points is being aligned to
         one without, treat both as groups, and push
         the one with points into its own submobjects
@@ -2599,7 +2620,7 @@ def interpolate_color(self, mobject1, mobject2, alpha):
 
     def become(
         self,
-        mobject: "Mobject",
+        mobject: Mobject,
         copy_submobjects: bool = True,
         match_height: bool = False,
         match_width: bool = False,
@@ -2663,7 +2684,7 @@ def construct(self):
             sm1.interpolate_color(sm1, sm2, 1)
         return self
 
-    def match_points(self, mobject: "Mobject", copy_submobjects: bool = True):
+    def match_points(self, mobject: Mobject, copy_submobjects: bool = True):
         """Edit points, positions, and submobjects to be identical
         to another :class:`~.Mobject`, while keeping the style unchanged.
 
@@ -2697,7 +2718,7 @@ def set_z_index(
         self,
         z_index_value: float,
         family: bool = True,
-    ) -> "VMobject":
+    ) -> VMobject:
         """Sets the :class:`~.Mobject`'s :attr:`z_index` to the value specified in `z_index_value`.
 
         Parameters
diff --git a/manim/mobject/opengl/opengl_mobject.py b/manim/mobject/opengl/opengl_mobject.py
--- a/manim/mobject/opengl/opengl_mobject.py
+++ b/manim/mobject/opengl/opengl_mobject.py
@@ -736,6 +736,42 @@ def add(
         self.assemble_family()
         return self
 
+    def insert(self, index: int, mobject: OpenGLMobject, update_parent: bool = False):
+        """Inserts a mobject at a specific position into self.submobjects
+
+        Effectively just calls  ``self.submobjects.insert(index, mobject)``,
+        where ``self.submobjects`` is a list.
+
+        Highly adapted from ``OpenGLMobject.add``.
+
+        Parameters
+        ----------
+        index
+            The index at which
+        mobject
+            The mobject to be inserted.
+        update_parent
+            Whether or not to set ``mobject.parent`` to ``self``.
+        """
+
+        if update_parent:
+            mobject.parent = self
+
+        if mobject is self:
+            raise ValueError("OpenGLMobject cannot contain self")
+
+        if not isinstance(mobject, OpenGLMobject):
+            raise TypeError("All submobjects must be of type OpenGLMobject")
+
+        if mobject not in self.submobjects:
+            self.submobjects.insert(index, mobject)
+
+        if self not in mobject.parents:
+            mobject.parents.append(self)
+
+        self.assemble_family()
+        return self
+
     def remove(
         self, *mobjects: OpenGLMobject, update_parent: bool = False
     ) -> OpenGLMobject:


Test patch: