Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.DS_Store

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -113,3 +115,7 @@ videos/
.vscode/
*.swp
*.swo

# MacOS
.DS_Store

12 changes: 4 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,12 @@ You can add or remove rows and columns with animations.

```python
# Add a new row
new_row, animations = table.add_row(["Charlie", "35", "London"])
self.play(*animations)
new_row, anims = table.add_row(["Charlie", "35", "London"])
self.play(AnimationGroup(*anims, lag_ratio=0.05))

# Delete a row (index starts at 1 for data rows)
deleted_row, shift_anims, resize_anims = table.delete_row(1)

self.play(FadeOut(deleted_row))
self.play(*shift_anims)
if resize_anims:
self.play(*resize_anims)
deleted_row, anims = table.delete_row(1)
self.play(AnimationGroup(*anims, lag_ratio=0.05))
```

### Styling
Expand Down
19 changes: 9 additions & 10 deletions docs/Table.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,34 +129,33 @@ def add_row(self, values: List[str]) -> Tuple[Row, List]

**Returns:** `(new_row, animations)`
- `new_row`: The created Row object.
- `animations`: List of `FadeIn` animations.
- `animations`: List of animations (resize transforms + FadeIn for new cells).

**Example:**
```python
new_row, anims = table.add_row(["New", "Entry"])
self.play(*anims)
self.play(AnimationGroup(*anims, lag_ratio=0.05))
```

#### `delete_row`

Delete a row from the table (animates shifting subsequent rows).
Delete a row from the table (animates fading out, shifting, and resizing).

```python
def delete_row(self, index: int) -> Tuple[Row, List, List]
def delete_row(self, index: int) -> Tuple[Row, List]
```

**Arguments:**
- `index`: Row index to delete (1-indexed; cannot delete header).

**Returns:** `(deleted_row, shift_animations, resize_animations)`
**Returns:** `(deleted_row, animations)`
- `deleted_row`: The deleted Row object.
- `animations`: List of animations (FadeOut + shift + resize).

**Example:**
```python
deleted, shift, resize = table.delete_row(1)
self.play(FadeOut(deleted))
self.play(*shift)
if resize:
self.play(*resize)
deleted, anims = table.delete_row(1)
self.play(AnimationGroup(*anims, lag_ratio=0.05))
```

#### `add_column`
Expand Down
24 changes: 23 additions & 1 deletion manim_table/cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ def __init__(
self.is_header = is_header
self.show_border = show_border

# Store color properties for copying
self._font_color = None
self._background_color = None
self._background_opacity = 0.0
self._border_color = None

# Create invisible bounding box to enforce dimensions
self.invisible_box = Rectangle(
width=width,
Expand Down Expand Up @@ -131,13 +137,14 @@ def get_resized_copy(self, new_width: float) -> "Cell":
"""
Create a copy of this cell with a new width.
The copy is positioned at the same center as this cell.
Preserves font color, background color, and border color.
Useful for animating width changes with Transform.

Args:
new_width: The new width for the cell

Returns:
A new Cell with the same value but different width
A new Cell with the same value and styling but different width
"""
new_cell = Cell(
value=self.value,
Expand All @@ -148,6 +155,15 @@ def get_resized_copy(self, new_width: float) -> "Cell":
show_border=self.show_border,
)
new_cell.move_to(self.get_center())

# Copy styling colors
if self._font_color is not None:
new_cell.set_font_color(self._font_color)
if self._background_color is not None:
new_cell.set_background_color(self._background_color, self._background_opacity)
if self._border_color is not None:
new_cell.set_border_color(self._border_color)

return new_cell

def resize_width(self, new_width: float):
Expand Down Expand Up @@ -185,6 +201,7 @@ def set_font_color(self, color):
Args:
color: A manimlib color (e.g., RED, BLUE, "#FF0000")
"""
self._font_color = color # Store for copying
self.text.set_color(color)
return self

Expand All @@ -195,6 +212,7 @@ def set_border_color(self, color):
Args:
color: A manimlib color (e.g., RED, BLUE, "#FF0000")
"""
self._border_color = color # Store for copying
if self.border is not None:
for line in self.border:
line.set_color(color)
Expand All @@ -209,6 +227,10 @@ def set_background_color(self, color, opacity: float = 0.5):
color: A manimlib color (e.g., RED, BLUE, "#FF0000")
opacity: Opacity of the background (0 to 1)
"""
# Store for copying
self._background_color = color
self._background_opacity = opacity

# Remove existing background if any
if hasattr(self, 'background') and self.background is not None:
self.remove(self.background)
Expand Down
128 changes: 100 additions & 28 deletions manim_table/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,13 +264,65 @@ def add_row(
Add a new row to the bottom of the table.

Returns:
Tuple of (new_row, animations) where animations is a list of
FadeIn animations for animating the new row into view.
Tuple of (new_row, animations) where animations includes both:
- Resize transforms for existing cells (if column widths change)
- FadeIn for the new row cells

All animations can be played together with AnimationGroup.

Example:
new_row, anims = table.add_row(["Alice", "Smith", "30"])
self.play(*anims)
self.play(AnimationGroup(*anims, lag_ratio=0.05))
"""
# First, add the new row values to internal tracking
self.row_values.append(values)

# Check if we need to resize columns (before creating the row)
resize_animations = []
new_widths = self.column_widths # Default to current widths

if self.auto_fit:
new_widths = self.calculate_column_widths()

# Check if any column width changed
width_changed = any(
abs(old_w - new_w) > 0.01
for old_w, new_w in zip(self.column_widths, new_widths)
)

if width_changed:
# Build resize animations for all existing rows
table_left = self.header_row.get_left()[0]

# Create target cells for header
x_offset = table_left
for col_idx, new_w in enumerate(new_widths):
target_x = x_offset + new_w / 2

header_cell = self.header_row[col_idx]
target_header = header_cell.get_resized_copy(new_w)
target_header.move_to([target_x, header_cell.get_center()[1], 0])
resize_animations.append(Transform(header_cell, target_header))

x_offset += new_w

# Create target cells for existing data rows
for row in self.rows:
x_offset = table_left
for col_idx, new_w in enumerate(new_widths):
target_x = x_offset + new_w / 2

cell = row[col_idx]
target_cell = cell.get_resized_copy(new_w)
target_cell.move_to([target_x, cell.get_center()[1], 0])
resize_animations.append(Transform(cell, target_cell))

x_offset += new_w

# Update stored widths
self.column_widths = new_widths

# Now create the new row with the updated column widths
index = len(self.rows) + 1
new_row = Row(
values=values,
Expand All @@ -282,41 +334,54 @@ def add_row(
index=index,
)

# Position below last row
# Position the new row cells at correct positions
table_left = self.header_row.get_left()[0]

# Calculate y position: below the last row (or header if no rows)
if len(self.rows) > 0:
new_row.next_to(self.rows[-1], DOWN, buff=0)
y_pos = self.rows[-1].get_bottom()[1] - self.cell_height / 2
else:
new_row.next_to(self.header_row, DOWN, buff=0)
y_pos = self.header_row.get_bottom()[1] - self.cell_height / 2

self.row_values.append(values)
# Position each cell at the correct x based on new column widths
x_offset = table_left
for col_idx, width in enumerate(self.column_widths):
cell = new_row.cells[col_idx]
target_x = x_offset + width / 2
cell.move_to([target_x, y_pos, 0])
x_offset += width

# Add to table structure
self.rows.append(new_row)
self.add(new_row)

# Return animations for each cell to fade in
animations = [FadeIn(cell) for cell in new_row.cells]
return new_row, animations
# Combine all animations: resize first, then fade in new row
appear_animations = [FadeIn(cell) for cell in new_row.cells]
all_animations = resize_animations + appear_animations

return new_row, all_animations

def delete_row(
self,
index: int
) -> Tuple[Row, List, List]:
) -> Tuple[Row, List]:
"""
Delete a row from the table.

Args:
index: Row index (1-indexed, i.e., header is 0, first data row is 1)

Returns:
Tuple of (deleted_row, shift_animations, resize_animations) where:
- shift_animations move remaining rows up
- resize_animations resize columns if the deleted row had the longest value
Tuple of (deleted_row, animations) where animations includes:
- FadeOut for the deleted row
- Shift animations for remaining rows
- Resize animations if column widths change

All animations can be played together with AnimationGroup.

Example:
deleted, shift_anims, resize_anims = table.delete_row(1)
self.play(FadeOut(deleted))
self.play(*shift_anims)
if resize_anims:
self.play(*resize_anims)
deleted, anims = table.delete_row(1)
self.play(AnimationGroup(*anims, lag_ratio=0.05))
"""
if index == 0:
raise ValueError("Cannot delete header row")
Expand All @@ -335,16 +400,20 @@ def delete_row(
# Track which rows will be shifted (rows at and after the deleted index)
rows_to_shift = set(self.rows[data_index:])

# Calculate actual rendered height (accounts for table scaling)
actual_height = deleted_row.get_height()

# Start with FadeOut for deleted row
all_animations = [FadeOut(deleted_row)]

# Create shift-up animations for remaining rows
shift_animations = []
for row in rows_to_shift:
shift_animations.append(
row.animate.shift([0, self.cell_height, 0])
all_animations.append(
row.animate.shift([0, actual_height, 0])
)
row.index -= 1

# Recalculate column widths if auto_fit is enabled
resize_animations = []
if self.auto_fit:
new_widths = self.calculate_column_widths()

Expand All @@ -369,7 +438,7 @@ def delete_row(
target_header = header_cell.get_resized_copy(new_w)
# Position at target x, same y
target_header.move_to([target_x, header_cell.get_center()[1], 0])
resize_animations.append(Transform(header_cell, target_header))
all_animations.append(Transform(header_cell, target_header))

x_offset += new_w

Expand All @@ -387,14 +456,14 @@ def delete_row(
# Use the y position AFTER shift would complete
target_y = cell.get_center()[1] + y_shift
target_cell.move_to([target_x, target_y, 0])
resize_animations.append(Transform(cell, target_cell))
all_animations.append(Transform(cell, target_cell))

x_offset += new_w

# Update stored widths
self.column_widths = new_widths

return deleted_row, shift_animations, resize_animations
return deleted_row, all_animations

def add_column(
self,
Expand Down Expand Up @@ -535,9 +604,12 @@ def delete_column(
self.header_row.remove(header_cell) # Remove from VGroup
deleted_cells.append(header_cell)

# Calculate actual rendered width (accounts for table scaling)
actual_width = header_cell.get_width()

# Shift remaining cells left
for cell in self.header_row.cells[index:]:
shift_animations.append(cell.animate.shift(LEFT * col_width))
shift_animations.append(cell.animate.shift(LEFT * actual_width))

# 2. Data Rows
for row in self.rows:
Expand All @@ -546,7 +618,7 @@ def delete_column(
deleted_cells.append(cell)

for c in row.cells[index:]:
shift_animations.append(c.animate.shift(LEFT * col_width))
shift_animations.append(c.animate.shift(LEFT * actual_width))

deleted_group = VGroup(*deleted_cells)

Expand Down
Loading