Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 72 additions & 14 deletions manim/mobject/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,8 +354,16 @@ def _add_labels(self, mob_table: list[list[VMobject]]) -> list[list[VMobject]]:

def _add_horizontal_lines(self) -> Table:
"""Adds the horizontal lines to the table."""
anchor_left = self.get_left()[0] - 0.5 * self.h_buff
anchor_right = self.get_right()[0] + 0.5 * self.h_buff
anchor_left = (
min(self.get_left()[0], self._get_column_x_edges(0)[0]) - 0.5 * self.h_buff
)
anchor_right = (
max(
self.get_right()[0],
self._get_column_x_edges(len(self.get_columns()) - 1)[1],
)
+ 0.5 * self.h_buff
)
line_group = VGroup()
if self.include_outer_lines:
anchor = self.get_rows()[0].get_top()[1] + 0.5 * self.v_buff
Expand Down Expand Up @@ -386,30 +394,80 @@ def _add_horizontal_lines(self) -> Table:
self.horizontal_lines = line_group
return self

def _get_column_x_edges(self, col_index: int) -> tuple[float, float]:
"""Return the ``(left, right)`` x-coordinates of a column's arranged slot.

When ``col_widths`` (in :attr:`arrange_in_grid_config`) makes a slot wider
than its contents, grid lines should follow the slot edges rather than the
content bounding box. The column's horizontal alignment (``col_alignments``
or ``cell_alignment``) determines where the content sits within the slot.
With no width set, the slot equals the content, so the previous behaviour
is preserved.

Parameters
----------
col_index
Index of the column whose slot edges are returned.
"""
col = self.get_columns()[col_index]
left, right = col.get_left()[0], col.get_right()[0]

col_widths = self.arrange_in_grid_config.get("col_widths")
if (
col_widths is None
or col_index >= len(col_widths)
or col_widths[col_index] is None
):
return left, right

width = col_widths[col_index]

# Determine the column's horizontal alignment the same way
# ``arrange_in_grid`` does: an explicit ``col_alignments`` entry wins,
# otherwise fall back to the horizontal component of ``cell_alignment``
# (which defaults to centered).
col_alignments = self.arrange_in_grid_config.get("col_alignments")
if col_alignments is not None and col_index < len(col_alignments):
align = col_alignments[col_index]
else:
cell_alignment = self.arrange_in_grid_config.get("cell_alignment")
x = cell_alignment[0] if cell_alignment is not None else 0
align = "l" if x < 0 else "r" if x > 0 else "c"

if align == "l":
# left-aligned: the content hugs the left edge of the slot
return left, left + width
if align == "r":
# right-aligned: the content hugs the right edge of the slot
return right - width, right
# centered: the content centre coincides with the slot centre
center_x = col.get_center()[0]
return center_x - width / 2, center_x + width / 2

def _add_vertical_lines(self) -> Table:
"""Adds the vertical lines to the table"""
anchor_top = self.get_rows().get_top()[1] + 0.5 * self.v_buff
anchor_bottom = self.get_rows().get_bottom()[1] - 0.5 * self.v_buff
line_group = VGroup()
num_cols = len(self.get_columns())
if self.include_outer_lines:
anchor = self.get_columns()[0].get_left()[0] - 0.5 * self.h_buff
anchor = self._get_column_x_edges(0)[0] - 0.5 * self.h_buff
line = Line(
[anchor, anchor_top, 0], [anchor, anchor_bottom, 0], **self.line_config
)
line_group.add(line)
self.add(line)
anchor = self.get_columns()[-1].get_right()[0] + 0.5 * self.h_buff
anchor = self._get_column_x_edges(num_cols - 1)[1] + 0.5 * self.h_buff
line = Line(
[anchor, anchor_top, 0], [anchor, anchor_bottom, 0], **self.line_config
)
line_group.add(line)
self.add(line)
if self.include_inner_lines:
for k in range(len(self.mob_table[0]) - 1):
anchor = self.get_columns()[k + 1].get_left()[0] + 0.5 * (
self.get_columns()[k].get_right()[0]
- self.get_columns()[k + 1].get_left()[0]
)
for k in range(num_cols - 1):
left_col_right = self._get_column_x_edges(k)[1]
right_col_left = self._get_column_x_edges(k + 1)[0]
anchor = 0.5 * (left_col_right + right_col_left)
line = Line(
[anchor, anchor_bottom, 0],
[anchor, anchor_top, 0],
Expand Down Expand Up @@ -818,24 +876,24 @@ def construct(self):
self.add(table, cell)
"""
row = self.get_rows()[pos[0] - 1]
col = self.get_columns()[pos[1] - 1]
col_left, col_right = self._get_column_x_edges(pos[1] - 1)
edge_UL = [
col.get_left()[0] - self.h_buff / 2,
col_left - self.h_buff / 2,
row.get_top()[1] + self.v_buff / 2,
0,
]
edge_UR = [
col.get_right()[0] + self.h_buff / 2,
col_right + self.h_buff / 2,
row.get_top()[1] + self.v_buff / 2,
0,
]
edge_DL = [
col.get_left()[0] - self.h_buff / 2,
col_left - self.h_buff / 2,
row.get_bottom()[1] - self.v_buff / 2,
0,
]
edge_DR = [
col.get_right()[0] + self.h_buff / 2,
col_right + self.h_buff / 2,
row.get_bottom()[1] - self.v_buff / 2,
0,
]
Expand Down
63 changes: 62 additions & 1 deletion tests/module/mobject/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import numpy as np

from manim import Circle, Table
from manim import LEFT, Circle, Table, Text
from manim.utils.color import GREEN


Expand Down Expand Up @@ -45,6 +45,67 @@ def test_table_include_inner_lines_true():
assert len(table.get_vertical_lines()) == 3


def test_table_col_widths_are_honored():
"""Fixed ``col_widths`` should be applied to every column, including the
first and last, instead of being truncated to the cell contents.

Regression test for https://github.com/ManimCommunity/manim/issues/3446
"""
table = Table(
[["1", "10", "100", "1000"], ["0", "0", "0", "0"]],
h_buff=0.1,
v_buff=0.1,
include_outer_lines=True,
arrange_in_grid_config={"col_widths": [3] * 4, "col_alignments": ["c"] * 4},
)
line_xs = sorted(line.get_center()[0] for line in table.get_vertical_lines())
drawn_widths = [line_xs[i + 1] - line_xs[i] for i in range(len(line_xs) - 1)]

# All columns, outer ones included, must have the same drawn width.
assert max(drawn_widths) - min(drawn_widths) < 1e-6
# Each drawn column spans its slot width plus the horizontal buffer.
for width in drawn_widths:
assert abs(width - (3 + 0.1)) < 1e-6


def test_table_col_widths_with_labels():
"""``col_widths`` should be honored even when row/column labels are present.

Regression test for https://github.com/ManimCommunity/manim/issues/3446
"""
table = Table(
[["a", "b"], ["c", "d"]],
row_labels=[Text("r1"), Text("r2")],
col_labels=[Text("c1"), Text("c2")],
top_left_entry=Text(""),
h_buff=0.1,
include_outer_lines=True,
arrange_in_grid_config={"col_widths": [2, 3, 3], "col_alignments": "ccc"},
)
line_xs = sorted(line.get_center()[0] for line in table.get_vertical_lines())
drawn_widths = [line_xs[i + 1] - line_xs[i] for i in range(len(line_xs) - 1)]
for width, expected in zip(drawn_widths, [2 + 0.1, 3 + 0.1, 3 + 0.1], strict=True):
assert abs(width - expected) < 1e-6


def test_table_col_widths_respects_cell_alignment():
"""A non-centered ``cell_alignment`` must not be treated as centered when
positioning the grid lines around a fixed-width column.

Regression test for https://github.com/ManimCommunity/manim/issues/3446
"""
table = Table(
[["a"]],
h_buff=0.1,
include_outer_lines=True,
arrange_in_grid_config={"col_widths": [3], "cell_alignment": LEFT},
)
column = table.get_columns()[0]
actual_left = min(line.get_center()[0] for line in table.get_vertical_lines())
expected_left = column.get_left()[0] - table.h_buff / 2
assert abs(actual_left - expected_left) < 1e-6


def test_table_accepts_iterable_data():
"""Table data can be any iterable of iterables."""
data = (iter(row) for row in [["A", "B"], ["C", "D"]])
Expand Down