Skip to content

Commit a434fe1

Browse files
authored
Merge 955937b into da2eda9
2 parents da2eda9 + 955937b commit a434fe1

File tree

4 files changed

+72
-18
lines changed

4 files changed

+72
-18
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
1919
## [2.7.7] - Not released yet
2020
### Added
2121
* SVG importing now supports clipping paths, and `defs` tags anywhere in the SVG file
22+
* Tables can now handle multi-index formatting similar to how multi-column formatted is currently implemented.
23+
* Code added to format multi-index and multi-header to match Pandas (repeat labels are now omitted but spaced correctly)
24+
2225

2326
## [2.7.6] - 2023-10-11
2427
This release is the first performed from the [@py-pdf GitHub org](https://github.com/py-pdf), where `fpdf2` migrated.

docs/Maths.md

+3-8
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Result:
110110
Create a table with pandas [DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html):
111111
```python
112112
from fpdf import FPDF
113+
from fpdf.table import format_dataframe
113114
import pandas as pd
114115

115116
df = pd.DataFrame(
@@ -121,11 +122,7 @@ df = pd.DataFrame(
121122
}
122123
)
123124

124-
df = df.applymap(str) # Convert all data inside dataframe into string type
125-
126-
columns = [list(df)] # Get list of dataframe columns
127-
rows = df.values.tolist() # Get list of dataframe rows
128-
data = columns + rows # Combine columns and rows in one list
125+
data = format_dataframe(df) # converts data to string and formats column and index labels
129126

130127
pdf = FPDF()
131128
pdf.add_page()
@@ -137,9 +134,7 @@ with pdf.table(borders_layout="MINIMAL",
137134
text_align="CENTER",
138135
width=160) as table:
139136
for data_row in data:
140-
row = table.row()
141-
for datum in data_row:
142-
row.cell(datum)
137+
table.row(data_row)
143138
pdf.output("table_from_pandas.pdf")
144139
```
145140

docs/Tables.md

+2
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,8 @@ Result:
352352

353353
![](table_with_multiple_headings.png)
354354

355+
This also works with index columns. Pass any integer to the `num_index_columns` argument when calling `Table()` and that many columns will be formatted according to the `index_style` argument.
356+
355357
## Table from pandas DataFrame
356358

357359
_cf._ [Maths documentation page](Maths.md#using-pandas)

fpdf/table.py

+64-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from .util import Padding
1010

1111
DEFAULT_HEADINGS_STYLE = FontFace(emphasis="BOLD")
12+
DEFAULT_INDEX_STYLE = FontFace(emphasis="BOLD")
1213

1314

1415
def draw_box_borders(pdf, x1, y1, x2, y2, border, fill_color=None):
@@ -89,6 +90,7 @@ def __init__(
8990
gutter_height=0,
9091
gutter_width=0,
9192
headings_style=DEFAULT_HEADINGS_STYLE,
93+
index_style=DEFAULT_INDEX_STYLE,
9294
line_height=None,
9395
markdown=False,
9496
text_align="JUSTIFY",
@@ -97,6 +99,7 @@ def __init__(
9799
padding=None,
98100
outer_border_width=None,
99101
num_heading_rows=1,
102+
num_index_columns=0,
100103
):
101104
"""
102105
Args:
@@ -149,6 +152,8 @@ def __init__(
149152
self._wrapmode = wrapmode
150153
self._num_heading_rows = num_heading_rows
151154
self.rows = []
155+
self.index_style = index_style
156+
self.n_index_columns = num_index_columns
152157

153158
if padding is None:
154159
self._padding = Padding.new(0)
@@ -185,11 +190,14 @@ def __init__(
185190
self.row(row)
186191

187192
def row(self, cells=()):
188-
"Adds a row to the table. Yields a `Row` object."
193+
"Adds a row to the table. Yields a `Row` object. Styles first `self.n_index_columns` cells with `self.index_style`"
189194
row = Row(self._fpdf)
190195
self.rows.append(row)
191-
for cell in cells:
192-
row.cell(cell)
196+
for n, cell in enumerate(cells):
197+
if n < self.n_index_columns:
198+
row.cell(cell, style=self.index_style)
199+
else:
200+
row.cell(cell)
193201
return row
194202

195203
def render(self):
@@ -255,10 +263,10 @@ def render(self):
255263
# pylint: disable=protected-access
256264
self._fpdf._perform_page_break()
257265
# repeat headings on top:
258-
for row_idx in range(self._num_heading_rows):
266+
for row_lbl in range(self._num_heading_rows):
259267
self._render_table_row(
260-
row_idx,
261-
self._get_row_layout_info(row_idx),
268+
row_lbl,
269+
self._get_row_layout_info(row_lbl),
262270
cell_x_positions=cell_x_positions,
263271
)
264272
elif i and self._gutter_height:
@@ -646,11 +654,11 @@ def cols_count(self):
646654
@property
647655
def column_indices(self):
648656
columns_count = len(self.cells)
649-
colidx = 0
650-
indices = [colidx]
657+
collbl = 0
658+
indices = [collbl]
651659
for jj in range(columns_count - 1):
652-
colidx += self.cells[jj].colspan
653-
indices.append(colidx)
660+
collbl += self.cells[jj].colspan
661+
indices.append(collbl)
654662
return indices
655663

656664
def cell(
@@ -726,3 +734,49 @@ class Cell:
726734

727735
def write(self, text, align=None):
728736
raise NotImplementedError("Not implemented yet")
737+
738+
739+
def format_label_tuples(lbl, char=" "):
740+
"""
741+
Formats columns and indexes to match DataFrame formatting.
742+
"""
743+
indexes = [lbl[0]]
744+
for i, j in zip(lbl, lbl[1:]):
745+
next_label = []
746+
for i_, j_ in zip(i, j):
747+
if j_ == i_:
748+
next_label.append(char)
749+
else:
750+
next_label.append(j_)
751+
indexes.append(tuple(next_label))
752+
return indexes
753+
754+
755+
def add_labels_to_data(data, indexes, columns, include_index: bool = True, char=" "):
756+
"""Combines index and column labels with data for table output"""
757+
if include_index:
758+
index_header_padding = [tuple(char) * len(indexes[0])] * len(columns[0])
759+
formatted_indexes = format_label_tuples(indexes)
760+
new_values = []
761+
for i, v in zip(formatted_indexes, data):
762+
new_values.append(list(i) + list(v))
763+
formatted_columns = [
764+
list(c)
765+
for c in zip(*format_label_tuples(index_header_padding + list(columns)))
766+
]
767+
new_data = formatted_columns + new_values
768+
769+
else:
770+
formatted_columns = [list(c) for c in zip(*format_label_tuples(list(columns)))]
771+
new_data = formatted_columns + data.tolist()
772+
773+
return new_data
774+
775+
776+
def format_dataframe(df, include_index: bool = True):
777+
"""Fully formats a dataframe for conversion into pdf"""
778+
data = df.map(str).values
779+
columns = df.columns
780+
indexes = df.index
781+
table_data = add_labels_to_data(data, indexes, columns, include_index=include_index)
782+
return table_data

0 commit comments

Comments
 (0)