Skip to content

Commit 05ddfc9

Browse files
committed
feat: enhance table data handling to support PolarsLazyFrame and add dataframe2list utility function
1 parent b2cda27 commit 05ddfc9

File tree

3 files changed

+143
-31
lines changed

3 files changed

+143
-31
lines changed

src/pptxr/_pptx/shape/table.py

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
from . import Shape
1919

2020
# Define DataFrame type alias
21-
DataFrame: TypeAlias = list[list[str]] | PandasDataFrame | PolarsDataFrame
21+
DataFrame: TypeAlias = (
22+
list[list[str]] | PandasDataFrame | PolarsDataFrame | PolarsLazyFrame
23+
)
2224

2325

2426
class TableCellStyle(TypedDict):
@@ -48,7 +50,7 @@ class TableData(TableProps):
4850

4951
type: Literal["table"]
5052

51-
data: DataFrame
53+
data: list[list[str]]
5254

5355

5456
class Table(Shape[GraphicFrame]):
@@ -68,63 +70,46 @@ def __init__(
6870
table = pptx_obj.table
6971

7072
# Apply first row as header if specified
71-
if props.get("first_row_header"):
72-
table.first_row = True
73+
if (first_row := props.get("first_row_header")) is not None:
74+
table.first_row = first_row
7375

7476
# Apply table data if provided
75-
if "data" in props:
76-
data = props["data"]
77-
78-
# Convert different DataFrame types to list of lists
79-
if USE_POLARS and issubclass(
80-
data.__class__, (PolarsDataFrame, PolarsLazyFrame)
81-
):
82-
# Convert pandas DataFrame to list of lists
83-
data = cast(PandasDataFrame, data)
84-
table_data = [data.columns.tolist()] + data.values.tolist()
85-
elif USE_PANDAS and issubclass(data.__class__, PandasDataFrame): # type: ignore
86-
# Convert polars DataFrame to list of lists
87-
data = cast(PandasDataFrame, data)
88-
table_data = [data.columns] + data.to_numpy().tolist()
89-
else:
90-
table_data = data
91-
77+
if data := props.get("data"):
9278
# Now apply the data to the table
93-
for i, row in enumerate(table_data):
79+
for i, row in enumerate(data):
9480
if i < len(table.rows):
9581
for j, cell_text in enumerate(row):
9682
if j < len(table.columns):
9783
table.cell(i, j).text = str(cell_text)
9884

9985
# Apply cell styles if provided
100-
if "cell_styles" in props:
101-
cell_styles = props["cell_styles"]
86+
if (cell_styles := props.get("cell_styles")) is not None:
10287
for i, row_styles in enumerate(cell_styles):
10388
if i < len(table.rows):
10489
for j, cell_style in enumerate(row_styles):
10590
if j < len(table.columns):
10691
cell = table.cell(i, j)
10792

108-
if "text_align" in cell_style:
93+
if (text_align := cell_style.get("text_align")) is not None:
10994
align_map = {
11095
"left": PP_ALIGN.LEFT,
11196
"center": PP_ALIGN.CENTER,
11297
"right": PP_ALIGN.RIGHT,
11398
"justify": PP_ALIGN.JUSTIFY,
11499
}
115100
paragraph = cell.text_frame.paragraphs[0]
116-
paragraph.alignment = align_map[
117-
cell_style["text_align"]
118-
]
101+
paragraph.alignment = align_map[text_align]
119102

120-
if "vertical_align" in cell_style:
103+
if (
104+
vertical_align := cell_style.get("vertical_align")
105+
) is not None:
121106
valign_map = {
122107
"top": MSO_VERTICAL_ANCHOR.TOP,
123108
"middle": MSO_VERTICAL_ANCHOR.MIDDLE,
124109
"bottom": MSO_VERTICAL_ANCHOR.BOTTOM,
125110
}
126111
cell.text_frame.vertical_anchor = valign_map[
127-
cell_style["vertical_align"]
112+
vertical_align
128113
]
129114

130115
# Apply text formatting
@@ -161,3 +146,29 @@ def to_pptx(self) -> GraphicFrame:
161146
def from_pptx(cls, pptx_obj: GraphicFrame) -> Self:
162147
"""Create from pptx table frame."""
163148
return cls(pptx_obj)
149+
150+
151+
def dataframe2list(data: DataFrame) -> list[list[str]]:
152+
"""Convert different DataFrame types to list of lists."""
153+
if USE_POLARS:
154+
if isinstance(data, PolarsLazyFrame):
155+
# For LazyFrame, collect it first
156+
polars_df = data.collect()
157+
columns = list(polars_df.columns)
158+
rows = polars_df.to_numpy().tolist()
159+
return [columns] + rows
160+
elif isinstance(data, PolarsDataFrame):
161+
polars_df = data
162+
columns = list(polars_df.columns)
163+
rows = polars_df.to_numpy().tolist()
164+
return [columns] + rows
165+
166+
if USE_PANDAS and isinstance(data, PandasDataFrame): # type: ignore
167+
# Convert pandas DataFrame to list of lists
168+
pandas_df = data
169+
columns = pandas_df.columns.tolist()
170+
rows = pandas_df.values.tolist()
171+
return [columns] + rows
172+
173+
# Assume it's a list of lists
174+
return cast(list[list[str]], data)

src/pptxr/_pptx/slide.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .converter import PptxConvertible, to_pptx_length
1313
from .shape import Shape
1414
from .shape.picture import Picture, PictureData, PictureProps
15-
from .shape.table import DataFrame, Table, TableData, TableProps
15+
from .shape.table import DataFrame, Table, TableData, TableProps, dataframe2list
1616
from .shape.text import Text, TextData, TextProps
1717
from .shape.title import Title
1818

@@ -102,6 +102,7 @@ def picture(
102102
return self
103103

104104
def table(self, data: DataFrame, **kwargs: Unpack[TableProps]) -> Self:
105+
data = dataframe2list(data)
105106
rows, cols = len(data), len(data[0])
106107
table_data: TableData = {"type": "table", "data": data, **kwargs}
107108

tests/test_table.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Tests for table module."""
2+
3+
import pathlib
4+
5+
import pytest
6+
7+
import pptxr
8+
from pptxr._features import USE_PANDAS, USE_POLARS
9+
10+
11+
def test_create_table_with_list_data(output: pathlib.Path) -> None:
12+
"""Test creating a table with list data."""
13+
table_data = [
14+
["Header 1", "Header 2", "Header 3"],
15+
["Cell 1,1", "Cell 1,2", "Cell 1,3"],
16+
["Cell 2,1", "Cell 2,2", "Cell 2,3"],
17+
]
18+
19+
presentation = (
20+
pptxr.Presentation.builder()
21+
.slide(
22+
pptxr.SlideBuilder().table(
23+
table_data,
24+
left=(100, "pt"),
25+
top=(100, "pt"),
26+
width=(400, "pt"),
27+
height=(200, "pt"),
28+
)
29+
)
30+
.build()
31+
)
32+
presentation.save(output / "table_list_data.pptx")
33+
34+
35+
@pytest.mark.skipif(not USE_PANDAS, reason="Pandas not installed")
36+
def test_create_table_with_pandas_dataframe(output: pathlib.Path) -> None:
37+
"""Test creating a table with pandas DataFrame."""
38+
# Type checking is done at runtime when pandas is available
39+
import pandas as pd # type: ignore[import]
40+
41+
# Create pandas DataFrame with simple data using dictionary
42+
data = {
43+
"名前": ["田中", "佐藤", "鈴木"],
44+
"年齢": [25, 30, 22],
45+
"都市": ["東京", "大阪", "名古屋"],
46+
}
47+
df = pd.DataFrame(data)
48+
49+
# Convert DataFrame to a list of lists for table creation
50+
table_data = [df.columns.tolist()] + df.values.tolist()
51+
52+
presentation = (
53+
pptxr.Presentation.builder()
54+
.slide(
55+
pptxr.SlideBuilder().table(
56+
table_data, # 変換したリストを使用
57+
left=(100, "pt"),
58+
top=(100, "pt"),
59+
width=(400, "pt"),
60+
height=(200, "pt"),
61+
first_row_header=True,
62+
)
63+
)
64+
.build()
65+
)
66+
presentation.save(output / "table_pandas_data.pptx")
67+
68+
69+
@pytest.mark.skipif(not USE_POLARS, reason="Polars not installed")
70+
def test_create_table_with_polars_dataframe(output: pathlib.Path) -> None:
71+
"""Test creating a table with polars DataFrame."""
72+
# Type checking is done at runtime when polars is available
73+
import polars as pl # type: ignore[import]
74+
75+
# Create polars DataFrame with simple data using dictionary
76+
data = {
77+
"製品": ["A製品", "B製品", "C製品"],
78+
"価格": [1000, 2000, 3000],
79+
"在庫": [50, 30, 10],
80+
}
81+
df = pl.DataFrame(data)
82+
83+
# Convert DataFrame to a list of lists for table creation
84+
table_data = [df.columns] + df.to_numpy().tolist()
85+
86+
presentation = (
87+
pptxr.Presentation.builder()
88+
.slide(
89+
pptxr.SlideBuilder().table(
90+
table_data, # 変換したリストを使用
91+
left=(100, "pt"),
92+
top=(100, "pt"),
93+
width=(400, "pt"),
94+
height=(200, "pt"),
95+
first_row_header=True,
96+
)
97+
)
98+
.build()
99+
)
100+
presentation.save(output / "table_polars_data.pptx")

0 commit comments

Comments
 (0)