rephorm 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rephorm/__init__.py +19 -0
- rephorm/decorators/__init__.py +0 -0
- rephorm/decorators/settings_validation.py +49 -0
- rephorm/dict/__init__.py +1 -0
- rephorm/dict/colors.py +22 -0
- rephorm/dict/legend_positions.py +136 -0
- rephorm/dict/styles.py +206 -0
- rephorm/dict/update_layout.py +129 -0
- rephorm/dict/update_traces.py +90 -0
- rephorm/object_mappers/__init__.py +0 -0
- rephorm/object_mappers/_globals.py +16 -0
- rephorm/object_mappers/_utilities/__init__.py +0 -0
- rephorm/object_mappers/_utilities/chart_mapper_utility.py +34 -0
- rephorm/object_mappers/_utilities/grid_mapper_utility.py +11 -0
- rephorm/object_mappers/_utilities/table_mapper_utility.py +59 -0
- rephorm/object_mappers/chapter_mapper.py +82 -0
- rephorm/object_mappers/chart_mapper.py +120 -0
- rephorm/object_mappers/chart_series_mapper.py +52 -0
- rephorm/object_mappers/grid_mapper.py +106 -0
- rephorm/object_mappers/page_break_mapper.py +8 -0
- rephorm/object_mappers/report_mapper.py +76 -0
- rephorm/object_mappers/table_mapper.py +191 -0
- rephorm/object_mappers/table_section_mapper.py +41 -0
- rephorm/object_mappers/table_series_mapper.py +79 -0
- rephorm/object_mappers/text_mapper.py +36 -0
- rephorm/object_params/__init__.py +0 -0
- rephorm/object_params/settings.py +184 -0
- rephorm/objects/__init__.py +0 -0
- rephorm/objects/_utilities/__init__.py +0 -0
- rephorm/objects/_utilities/settings_container.py +8 -0
- rephorm/objects/chapter.py +44 -0
- rephorm/objects/chart.py +70 -0
- rephorm/objects/chart_series.py +40 -0
- rephorm/objects/footnote.py +13 -0
- rephorm/objects/grid.py +57 -0
- rephorm/objects/page_break.py +13 -0
- rephorm/objects/report.py +196 -0
- rephorm/objects/table.py +76 -0
- rephorm/objects/table_section.py +48 -0
- rephorm/objects/table_series.py +36 -0
- rephorm/objects/text.py +35 -0
- rephorm/utility/PDF.py +80 -0
- rephorm/utility/__init__.py +0 -0
- rephorm/utility/add_style_prefix.py +30 -0
- rephorm/utility/fonts/OpenSans-Bold.ttf +0 -0
- rephorm/utility/fonts/OpenSans-BoldItalic.ttf +0 -0
- rephorm/utility/fonts/OpenSans-Italic.ttf +0 -0
- rephorm/utility/fonts/OpenSans.ttf +0 -0
- rephorm/utility/fonts/__init__.py +0 -0
- rephorm/utility/fonts/font_loading.py +74 -0
- rephorm/utility/is_set.py +9 -0
- rephorm/utility/merge/__init__.py +0 -0
- rephorm/utility/merge/chart_properties_manager.py +80 -0
- rephorm/utility/merge/merge_settings.py +134 -0
- rephorm/utility/merge/merge_styles.py +79 -0
- rephorm/utility/merge/pdf_merger.py +29 -0
- rephorm/utility/report/__init__.py +0 -0
- rephorm/utility/report/add_footnotes.py +46 -0
- rephorm/utility/report/cleanup_utility.py +19 -0
- rephorm/utility/report/footnotes_counter.py +15 -0
- rephorm/utility/report/image_utility.py +14 -0
- rephorm/utility/report/layout_utility.py +59 -0
- rephorm/utility/report/range_utility.py +94 -0
- rephorm/utility/report/report_utility.py +93 -0
- rephorm/utility/report/resolve_color.py +39 -0
- rephorm/utility/report/table_utility.py +71 -0
- rephorm/utility/report/title_utility.py +49 -0
- rephorm/utility/unit_converter.py +12 -0
- rephorm-1.0.1.dist-info/METADATA +41 -0
- rephorm-1.0.1.dist-info/RECORD +73 -0
- rephorm-1.0.1.dist-info/WHEEL +5 -0
- rephorm-1.0.1.dist-info/licenses/LICENSE +21 -0
- rephorm-1.0.1.dist-info/top_level.txt +1 -0
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def process_bar_data(neg_data, pos_data, flattened):
|
|
4
|
+
"""
|
|
5
|
+
Processes a flattened array by slicing it into chunks if its length differs
|
|
6
|
+
from the target data arrays, then applies np.where per chunk and accumulates
|
|
7
|
+
the negative or positive values into the corresponding data arrays.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
neg_data (np.ndarray): negative data array.
|
|
11
|
+
pos_data (np.ndarray): positive data array.
|
|
12
|
+
flattened (np.ndarray): Flattened input array. (The actual series data)
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Tuple[np.ndarray, np.ndarray]: Updated negative and positive data arrays.
|
|
16
|
+
"""
|
|
17
|
+
target_length = neg_data.shape[0]
|
|
18
|
+
total_length = flattened.shape[0]
|
|
19
|
+
|
|
20
|
+
# If equal, direct add
|
|
21
|
+
if total_length == target_length:
|
|
22
|
+
neg_data += np.where(flattened < 0, flattened, 0)
|
|
23
|
+
pos_data += np.where(flattened > 0, flattened, 0)
|
|
24
|
+
|
|
25
|
+
# If longer, break into chunks
|
|
26
|
+
else:
|
|
27
|
+
num_full_chunks = total_length // target_length
|
|
28
|
+
|
|
29
|
+
for i in range(num_full_chunks):
|
|
30
|
+
chunk = flattened[i * target_length : (i + 1) * target_length]
|
|
31
|
+
neg_data += np.where(chunk < 0, chunk, 0)
|
|
32
|
+
pos_data += np.where(chunk > 0, chunk, 0)
|
|
33
|
+
|
|
34
|
+
return neg_data, pos_data
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
def compute_grid_layout(nrow = 1, ncol = 1):
|
|
2
|
+
"""
|
|
3
|
+
Computes the layout of a grid based on the number of rows and columns.
|
|
4
|
+
"""
|
|
5
|
+
layout = []
|
|
6
|
+
for indx in range(nrow):
|
|
7
|
+
row_layout = []
|
|
8
|
+
for indx in range(ncol):
|
|
9
|
+
row_layout.append({})
|
|
10
|
+
layout.append(row_layout)
|
|
11
|
+
return layout
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#TODO: extract this function to external module.
|
|
2
|
+
from typing import Literal
|
|
3
|
+
from rephorm.objects.table_section import TableSection
|
|
4
|
+
from rephorm.objects.table_series import TableSeries
|
|
5
|
+
|
|
6
|
+
# Adjustment for text width, because fpdf does not calculate it correctly
|
|
7
|
+
# Todo: Investigate why, and if it is possible to fix it
|
|
8
|
+
magic_number = 10
|
|
9
|
+
|
|
10
|
+
def get_max_numerical_width(table, pdf, font_family, font_size, biggest_nr, table_element=None, max_width = 0):
|
|
11
|
+
"""
|
|
12
|
+
Determine the maximum width needed for numerical values in the table.
|
|
13
|
+
Ensures width does not exceed max_width.
|
|
14
|
+
|
|
15
|
+
How it works: Finds the longest formatted numerical string within the whole table
|
|
16
|
+
"""
|
|
17
|
+
pdf.set_font(font_family, size=font_size)
|
|
18
|
+
max_cap = pdf.get_string_width(str(biggest_nr)) + magic_number
|
|
19
|
+
|
|
20
|
+
table_element = table_element if table_element else table
|
|
21
|
+
|
|
22
|
+
for child in table_element.CHILDREN:
|
|
23
|
+
if isinstance(child, TableSeries):
|
|
24
|
+
series_data = child.data.get_values(child.settings.span)
|
|
25
|
+
for value in series_data:
|
|
26
|
+
formatted_value = f"{value:.{child.settings.decimal_precision}f}"
|
|
27
|
+
text_width = pdf.get_string_width(str(formatted_value)) + magic_number
|
|
28
|
+
if text_width > max_width:
|
|
29
|
+
max_width = min(text_width, max_cap)
|
|
30
|
+
|
|
31
|
+
elif isinstance(child, TableSection):
|
|
32
|
+
section_max_width = get_max_numerical_width(table, pdf, font_family, font_size, biggest_nr, child, max_width)
|
|
33
|
+
max_width = max(max_width, section_max_width)
|
|
34
|
+
|
|
35
|
+
return max_width
|
|
36
|
+
|
|
37
|
+
def get_max_width(table, pdf, font_family, font_size, table_element = None, parameter: Literal["title", "unit"] = "unit", max_width = 0):
|
|
38
|
+
|
|
39
|
+
pdf.set_font(font_family, size=font_size)
|
|
40
|
+
|
|
41
|
+
table_element = table_element if table_element else table
|
|
42
|
+
|
|
43
|
+
for child in table_element.CHILDREN:
|
|
44
|
+
|
|
45
|
+
if isinstance(child, TableSeries):
|
|
46
|
+
attr_value = getattr(child, parameter)
|
|
47
|
+
if attr_value is not None:
|
|
48
|
+
text_width = pdf.get_string_width(attr_value) + magic_number
|
|
49
|
+
if text_width > max_width:
|
|
50
|
+
max_width = text_width
|
|
51
|
+
|
|
52
|
+
elif isinstance(child, TableSection):
|
|
53
|
+
if parameter == "title":
|
|
54
|
+
section_title_width = pdf.get_string_width(child.TITLE) + magic_number
|
|
55
|
+
if section_title_width > max_width:
|
|
56
|
+
max_width = section_title_width
|
|
57
|
+
section_max_width = get_max_width(table, pdf, font_family, font_size, child, parameter)
|
|
58
|
+
max_width = max(max_width, section_max_width)
|
|
59
|
+
return max_width
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from rephorm.utility.report.add_footnotes import add_footnotes_at_bottom
|
|
2
|
+
from rephorm.utility.report.footnotes_counter import generate_footnote_numbers
|
|
3
|
+
from rephorm.utility.report.layout_utility import LayoutManager
|
|
4
|
+
from rephorm.utility.report.title_utility import add_title_with_references
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ChapterMapper:
|
|
8
|
+
def __init__(self, chapter):
|
|
9
|
+
self.chapter = chapter
|
|
10
|
+
|
|
11
|
+
# Todo: Reconsider add_to_pdf method naming. it would make more sense "render" or "map" because we do
|
|
12
|
+
# add things to the pdf and then go through the children here and eventually render them too...
|
|
13
|
+
def add_to_pdf(self, **kwargs):
|
|
14
|
+
|
|
15
|
+
pdf = kwargs["pdf"]
|
|
16
|
+
footer_last_y = kwargs["footer_last_y"] if "footer_last_y" in kwargs else None
|
|
17
|
+
starting_page = pdf.page_no()
|
|
18
|
+
n_children = len(self.chapter.CHILDREN)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
rs = self.chapter.settings.styles["report"]
|
|
22
|
+
used_top_height = rs["header"]["height"] + rs["header"]["bottom_margin"]
|
|
23
|
+
footer_top_padding = rs["footer"]["top_padding"]
|
|
24
|
+
footer_top_margin = rs["footer"]["top_margin"]
|
|
25
|
+
|
|
26
|
+
pdf.set_y(used_top_height)
|
|
27
|
+
|
|
28
|
+
pdf.set_font(
|
|
29
|
+
family=self.chapter.settings.styles["chapter"]["font_family"],
|
|
30
|
+
size=self.chapter.settings.styles["chapter"]["font_size"],
|
|
31
|
+
style=self.chapter.settings.styles["chapter"]["font_style"],
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if self.chapter.title is not None:
|
|
35
|
+
footnote_references = generate_footnote_numbers(self.chapter.footnotes)
|
|
36
|
+
title_y_pos = pdf.h / 2 - pdf.font_size / 2 # Todo: consider multiline title in the future (would need to calculate h of the text cell)
|
|
37
|
+
add_title_with_references(pdf, self.chapter.title, footnote_references,
|
|
38
|
+
font_family=self.chapter.settings.styles["chapter"]["title"]["font_family"],
|
|
39
|
+
font_size=self.chapter.settings.styles["chapter"]["title"]["font_size"],
|
|
40
|
+
font_style=self.chapter.settings.styles["chapter"]["title"]["font_style"],
|
|
41
|
+
title_y=title_y_pos, cell_width=pdf.epw)
|
|
42
|
+
if n_children > 0:
|
|
43
|
+
pdf.add_page()
|
|
44
|
+
|
|
45
|
+
if bool(self.chapter.footnotes):
|
|
46
|
+
footer_last_y = add_footnotes_at_bottom(
|
|
47
|
+
pdf,
|
|
48
|
+
self.chapter.footnotes,
|
|
49
|
+
footnote_references,
|
|
50
|
+
last_y=footer_last_y,
|
|
51
|
+
font_family=self.chapter.settings.styles["footnotes"]["font_family"],
|
|
52
|
+
font_size=self.chapter.settings.styles["footnotes"]["font_size"],
|
|
53
|
+
font_style=self.chapter.settings.styles["footnotes"]["font_style"],
|
|
54
|
+
footer_padding=footer_top_padding,
|
|
55
|
+
footer_top_margin=footer_top_margin,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
layout_manager = LayoutManager(
|
|
59
|
+
pdf,
|
|
60
|
+
used_top_height=used_top_height,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
for i, item in enumerate(self.chapter.CHILDREN):
|
|
64
|
+
|
|
65
|
+
x, y, width, height = layout_manager.get_default_layout()
|
|
66
|
+
|
|
67
|
+
current_page_nr = pdf.page_no()
|
|
68
|
+
if current_page_nr > starting_page:
|
|
69
|
+
starting_page = current_page_nr
|
|
70
|
+
# footer_last_y = None
|
|
71
|
+
|
|
72
|
+
mapper = item._get_mapper()
|
|
73
|
+
|
|
74
|
+
# TODO: IMPORTANT: Review the design and implement everything in a way where we pass single Y position
|
|
75
|
+
# across the object mappers, so we do not need Y - to set the position of chapter elements/objects,
|
|
76
|
+
# then title_y to correctly set the titles. We should pass Y along, and render elements/titles from the
|
|
77
|
+
# LAST known Y position (which is the last element rendered). This way we can avoid the need for
|
|
78
|
+
# title_y and footer_last_y, and god knows how many more Y's :D
|
|
79
|
+
mapper.add_to_pdf(pdf=pdf, x=x, y=y, w=width, h=height, footer_last_y=footer_last_y)
|
|
80
|
+
|
|
81
|
+
if i < n_children - 1:
|
|
82
|
+
pdf.add_page()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import irispie
|
|
4
|
+
import numpy as np
|
|
5
|
+
from plotly.graph_objects import Figure
|
|
6
|
+
|
|
7
|
+
from rephorm.object_mappers._globals import set_figure_map
|
|
8
|
+
from rephorm.object_mappers._utilities.chart_mapper_utility import process_bar_data
|
|
9
|
+
from rephorm.utility.report.image_utility import to_image
|
|
10
|
+
from rephorm.dict.update_layout import update_layout
|
|
11
|
+
from rephorm.utility.report.resolve_color import resolve_highlight_color
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ChartMapper:
|
|
15
|
+
|
|
16
|
+
def __init__(self, chart):
|
|
17
|
+
self.chart = chart
|
|
18
|
+
self.figure = Figure()
|
|
19
|
+
self.apply_report_layout = chart.settings.apply_report_layout
|
|
20
|
+
|
|
21
|
+
def add_to_pdf(self, **kwargs):
|
|
22
|
+
grid_size = kwargs.get("grid_size", 1)
|
|
23
|
+
pdf = kwargs["pdf"]
|
|
24
|
+
x = kwargs["x"]
|
|
25
|
+
y = kwargs["y"]
|
|
26
|
+
w = kwargs["w"]
|
|
27
|
+
h = kwargs["h"]
|
|
28
|
+
|
|
29
|
+
scale_f = pdf.k
|
|
30
|
+
|
|
31
|
+
img_w = w * scale_f
|
|
32
|
+
img_h = h * scale_f
|
|
33
|
+
|
|
34
|
+
if grid_size > 9:
|
|
35
|
+
img_w = None
|
|
36
|
+
img_h = None
|
|
37
|
+
|
|
38
|
+
# Todo: Directory creation and check, should be extracted from here, too many concerns...
|
|
39
|
+
directory = "./tmp/pdf_figures"
|
|
40
|
+
os.makedirs(directory, exist_ok=True)
|
|
41
|
+
|
|
42
|
+
self.figure = self.chart.figure or self._add_internal_figure()
|
|
43
|
+
|
|
44
|
+
if self.apply_report_layout and bool(self.chart.figure):
|
|
45
|
+
update_layout(self.chart.figure.layout.title.text, self.chart.settings, self.figure)
|
|
46
|
+
|
|
47
|
+
data = to_image(self.figure, image_format="pdf", width=img_w, height=img_h)
|
|
48
|
+
|
|
49
|
+
if data is None:
|
|
50
|
+
raise ValueError(f"ChartMapper: Failed to render chart image. Data is: {data}")
|
|
51
|
+
|
|
52
|
+
file_path = f"{directory}/{id(self.chart)}.pdf"
|
|
53
|
+
with open(file_path, "wb") as f:
|
|
54
|
+
f.write(data)
|
|
55
|
+
|
|
56
|
+
fig_map = ({"page": pdf.page_no(),
|
|
57
|
+
"pdf_path": file_path,
|
|
58
|
+
"x": x * scale_f,
|
|
59
|
+
"y": y * scale_f,
|
|
60
|
+
"width": w * scale_f,
|
|
61
|
+
"height": h * scale_f})
|
|
62
|
+
|
|
63
|
+
set_figure_map(fig_map)
|
|
64
|
+
|
|
65
|
+
def _add_internal_figure(self):
|
|
66
|
+
|
|
67
|
+
min_y = None
|
|
68
|
+
max_y = None
|
|
69
|
+
|
|
70
|
+
span = self.chart._get_span()
|
|
71
|
+
span_length = len(span)
|
|
72
|
+
neg_stacked_bars_data = np.zeros(span_length)
|
|
73
|
+
pos_stacked_bars_data = np.zeros(span_length)
|
|
74
|
+
stacked_bars = False
|
|
75
|
+
|
|
76
|
+
for item in self.chart.CHILDREN:
|
|
77
|
+
for variant in range(1, item.data.num_variants + 1):
|
|
78
|
+
y_values = item.data.get_data_variant_from_until(span, variant).flatten()
|
|
79
|
+
if item.settings.series_type in ("barcon", "conbar", "contribution_bar", "bar_relative"):
|
|
80
|
+
stacked_bars = True
|
|
81
|
+
neg_stacked_bars_data, pos_stacked_bars_data = process_bar_data(
|
|
82
|
+
neg_stacked_bars_data,
|
|
83
|
+
pos_stacked_bars_data,
|
|
84
|
+
y_values
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
# Direct min/max calculation for non-barcon series
|
|
88
|
+
current_min = y_values.min()
|
|
89
|
+
current_max = y_values.max()
|
|
90
|
+
|
|
91
|
+
min_y = current_min if min_y is None else min(min_y, current_min)
|
|
92
|
+
max_y = current_max if max_y is None else max(max_y, current_max)
|
|
93
|
+
|
|
94
|
+
series_mapper = item._get_mapper()
|
|
95
|
+
series_mapper.add_to_pdf(
|
|
96
|
+
figure=self.figure) # Todo: This add_to_pdf call is confusing, yes, we wanted to unify function names, but in this case it does not make sense.
|
|
97
|
+
|
|
98
|
+
if stacked_bars:
|
|
99
|
+
min_y = min(neg_stacked_bars_data) if min_y is None else min(min_y, min(neg_stacked_bars_data))
|
|
100
|
+
max_y = max(pos_stacked_bars_data) if max_y is None else max(max_y, max(pos_stacked_bars_data))
|
|
101
|
+
|
|
102
|
+
dy = max_y - min_y
|
|
103
|
+
y_range = [min_y - dy * 0.01, max_y + dy * 0.01]
|
|
104
|
+
|
|
105
|
+
update_layout(self.chart.title, self.chart.settings, self.figure, y_range)
|
|
106
|
+
|
|
107
|
+
# Adjusts view SPAN on charts. "Slices" any padding between.
|
|
108
|
+
# self.figure.layout.xaxis.range = [span.start.to_python_date(position="start"),
|
|
109
|
+
# span.end.to_python_date(position="end")]
|
|
110
|
+
|
|
111
|
+
if self.chart.settings.highlight is not None:
|
|
112
|
+
irispie.plotly.highlight(self.figure, self.chart._get_highlight(),
|
|
113
|
+
color=resolve_highlight_color(
|
|
114
|
+
self.chart.settings.styles["chart"]["highlight_color"], alpha=0.25))
|
|
115
|
+
|
|
116
|
+
# Todo: This is a workaround for the issue with the highlight. It should be fixed in the irispie library.
|
|
117
|
+
# therefore, delete update_shapes() call here, when it's fixed in iris-pie
|
|
118
|
+
self.figure.update_shapes({"layer": "below"})
|
|
119
|
+
|
|
120
|
+
return self.figure
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from rephorm.dict.update_traces import update_base_traces, apply_per_trace_styles
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ChartSeriesMapper:
|
|
5
|
+
def __init__(self, series):
|
|
6
|
+
self.series = series
|
|
7
|
+
|
|
8
|
+
def add_to_pdf(self, **kwargs):
|
|
9
|
+
|
|
10
|
+
figure = kwargs["figure"]
|
|
11
|
+
num_variants = self.series.data.num_variants
|
|
12
|
+
# Here we will get either single value or a list of values, depends if multivariate or not:
|
|
13
|
+
line_colors = self.series.settings.styles["chart"]["series"]["line_color"]
|
|
14
|
+
line_widths = self.series.settings.styles["chart"]["series"]["line_width"]
|
|
15
|
+
line_styles = self.series.settings.styles["chart"]["series"]["line_style"]
|
|
16
|
+
bar_colors = self.series.settings.styles["chart"]["series"]["bar_face_color"]
|
|
17
|
+
|
|
18
|
+
def get_series_type(key):
|
|
19
|
+
mapping = {
|
|
20
|
+
"line": "line",
|
|
21
|
+
"bar": "bar",
|
|
22
|
+
"bar_stack": "bar_stack",
|
|
23
|
+
"contribution_bar": "bar_relative",
|
|
24
|
+
"conbar": "bar_relative",
|
|
25
|
+
"barcon": "bar_relative",
|
|
26
|
+
"bar_relative": "bar_relative",
|
|
27
|
+
"bar_group": "bar_group",
|
|
28
|
+
"bar_overlay": "bar_overlay",
|
|
29
|
+
}
|
|
30
|
+
return mapping.get(key)
|
|
31
|
+
|
|
32
|
+
fig = self.series.data.plot(
|
|
33
|
+
span=self.series.settings.span,
|
|
34
|
+
date_format_style="compact",
|
|
35
|
+
date_axis_mode="instant",
|
|
36
|
+
figure=figure,
|
|
37
|
+
chart_type=get_series_type(self.series.settings.series_type),
|
|
38
|
+
legend=self.series.settings.legend,
|
|
39
|
+
show_legend=self.series.settings.show_legend,
|
|
40
|
+
return_info=True,
|
|
41
|
+
show_figure=False,
|
|
42
|
+
update_traces=update_base_traces(self.series.settings)
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
apply_per_trace_styles(
|
|
46
|
+
fig["figure"],
|
|
47
|
+
line_colors,
|
|
48
|
+
line_widths,
|
|
49
|
+
line_styles,
|
|
50
|
+
bar_colors,
|
|
51
|
+
num_variants,
|
|
52
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from rephorm.object_mappers._utilities.grid_mapper_utility import compute_grid_layout
|
|
2
|
+
from rephorm.utility.report.add_footnotes import add_footnotes_at_bottom
|
|
3
|
+
from rephorm.utility.report.footnotes_counter import generate_footnote_numbers
|
|
4
|
+
from rephorm.utility.report.layout_utility import LayoutManager
|
|
5
|
+
from rephorm.utility.report.title_utility import add_title_with_references
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GridMapper:
|
|
9
|
+
def __init__(self, grid):
|
|
10
|
+
self.grid = grid
|
|
11
|
+
|
|
12
|
+
def add_to_pdf(self, **kwargs):
|
|
13
|
+
|
|
14
|
+
pdf = kwargs["pdf"]
|
|
15
|
+
layout = self.grid.settings.layout
|
|
16
|
+
nrow = self.grid.settings.nrow
|
|
17
|
+
ncol = self.grid.settings.ncol
|
|
18
|
+
footer_last_y = kwargs["footer_last_y"] if "footer_last_y" in kwargs else None
|
|
19
|
+
starting_page = pdf.page_no()
|
|
20
|
+
rs = self.grid.settings.styles["report"]
|
|
21
|
+
footer_top_padding = rs["footer"]["top_padding"]
|
|
22
|
+
footer_top_margin = rs["footer"]["top_margin"]
|
|
23
|
+
title_bottom_padding = rs["title_bottom_padding"]
|
|
24
|
+
# Todo: For grid why we dont take as in chapter? The used top height calculation?
|
|
25
|
+
# What is the reason to have Y coming from kwargs? Because then it takes Y from chapter/report layout calculations,
|
|
26
|
+
# and here it passes to layout... So I guess this is what offsets it differently, when adding chart to
|
|
27
|
+
# report/chapter directly and when having it inside 1x1 grid
|
|
28
|
+
y = kwargs["y"]
|
|
29
|
+
grid_size = self.grid.settings.ncol * self.grid.settings.nrow
|
|
30
|
+
|
|
31
|
+
if layout is None:
|
|
32
|
+
layout = compute_grid_layout(nrow=nrow, ncol=ncol)
|
|
33
|
+
|
|
34
|
+
if len(self.grid.CHILDREN) > grid_size:
|
|
35
|
+
raise Exception(
|
|
36
|
+
f"Grid capacity exceeded: attempting to add {len(self.grid.CHILDREN)} object(-s) to a grid with capacity for only "
|
|
37
|
+
f"{grid_size} object(-s) ({self.grid.settings.ncol}x{self.grid.settings.nrow}). "
|
|
38
|
+
f"Please either increase grid size or reduce the number of objects."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
footnote_references = generate_footnote_numbers(self.grid.footnotes)
|
|
42
|
+
y = add_title_with_references(pdf, self.grid.title, footnote_references,
|
|
43
|
+
self.grid.settings.styles["grid"]["title"]["font_family"],
|
|
44
|
+
self.grid.settings.styles["grid"]["title"]["font_size"],
|
|
45
|
+
self.grid.settings.styles["grid"]["title"]["font_style"], title_y=y)
|
|
46
|
+
|
|
47
|
+
if bool(self.grid.footnotes):
|
|
48
|
+
|
|
49
|
+
footer_last_y = add_footnotes_at_bottom(
|
|
50
|
+
pdf,
|
|
51
|
+
self.grid.footnotes,
|
|
52
|
+
footnote_references,
|
|
53
|
+
last_y=footer_last_y,
|
|
54
|
+
font_family=self.grid.settings.styles["footnotes"]["font_family"],
|
|
55
|
+
font_size=self.grid.settings.styles["footnotes"]["font_size"],
|
|
56
|
+
font_style=self.grid.settings.styles["footnotes"]["font_style"],
|
|
57
|
+
footer_padding=footer_top_padding,
|
|
58
|
+
footer_top_margin=footer_top_margin,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
layout_manager = LayoutManager(
|
|
62
|
+
pdf=pdf,
|
|
63
|
+
used_top_height=y+title_bottom_padding,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
positions = layout_manager.calculate_position(layout=layout, nrow=nrow, ncol=ncol)
|
|
67
|
+
valid_positions = list(positions.keys())
|
|
68
|
+
# Todo: set to the last chart | Do not remove, yet
|
|
69
|
+
# last_element = valid_positions[-1]
|
|
70
|
+
# last_element_data = positions[last_element]
|
|
71
|
+
# pdf.line(pdf.l_margin, last_element_data["y"] + last_element_data["height"], pdf.w - pdf.r_margin, last_element_data["y"] + last_element_data["height"])
|
|
72
|
+
|
|
73
|
+
for i, item in enumerate(self.grid.CHILDREN):
|
|
74
|
+
|
|
75
|
+
position = valid_positions[i]
|
|
76
|
+
pos_data = positions[position]
|
|
77
|
+
|
|
78
|
+
current_page = pdf.page_no()
|
|
79
|
+
if current_page > starting_page:
|
|
80
|
+
starting_page = current_page
|
|
81
|
+
footer_last_y = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
grid_size = self.grid.settings.ncol * self.grid.settings.nrow
|
|
85
|
+
|
|
86
|
+
mapper = item._get_mapper()
|
|
87
|
+
mapper.add_to_pdf(
|
|
88
|
+
pdf=pdf,
|
|
89
|
+
x=pos_data["x"],
|
|
90
|
+
y=pos_data["y"],
|
|
91
|
+
w=pos_data["width"],
|
|
92
|
+
h=pos_data["height"],
|
|
93
|
+
grid_size=grid_size,
|
|
94
|
+
footer_last_y=footer_last_y,
|
|
95
|
+
context="grid",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# For debugging
|
|
99
|
+
# pdf.set_draw_color(255, 0, 0)
|
|
100
|
+
# pdf.rect(
|
|
101
|
+
# x=pos_data["x"],
|
|
102
|
+
# y=pos_data["y"],
|
|
103
|
+
# w=pos_data["width"],
|
|
104
|
+
# h=pos_data["height"],
|
|
105
|
+
# )
|
|
106
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from logging import warning
|
|
2
|
+
|
|
3
|
+
from fpdf import FPDF
|
|
4
|
+
|
|
5
|
+
from rephorm.object_mappers._globals import get_figure_map
|
|
6
|
+
from rephorm.utility.report.cleanup_utility import perform_cleanup
|
|
7
|
+
from rephorm.utility.report.layout_utility import LayoutManager
|
|
8
|
+
from rephorm.utility.merge.pdf_merger import overlay_pdfs
|
|
9
|
+
from rephorm.utility.report.report_utility import set_title_page
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReportMapper:
|
|
13
|
+
|
|
14
|
+
def __init__(self, pdf: FPDF):
|
|
15
|
+
self.pdf = pdf
|
|
16
|
+
|
|
17
|
+
def compile(self, report, file_name: str, cleanup:bool = False):
|
|
18
|
+
|
|
19
|
+
rs = report.settings.styles["report"]
|
|
20
|
+
|
|
21
|
+
self.pdf.t_margin = rs["page_margin_top"]
|
|
22
|
+
|
|
23
|
+
# TODO: Currently, we offset all bottom spacing to the bottom margin because FPDF2
|
|
24
|
+
# automatically breaks tables as soon as it reaches the bottom margin. This prevents
|
|
25
|
+
# any overlap with the footer, since the footer is included in fpdf.b_margin.
|
|
26
|
+
# We need to apply the same approach for the top margin and top spacing
|
|
27
|
+
# (e.g., header, title padding, etc.).
|
|
28
|
+
# Also, make sure to verify whether any top padding or margin we have is actually
|
|
29
|
+
# padding or margin. Padding refers to inner spacing, while margin is outer spacing.
|
|
30
|
+
# Follow the standard convention.
|
|
31
|
+
|
|
32
|
+
self.pdf.b_margin = rs["page_margin_bottom"] + rs["footer"]["height"] + rs["footer"]["top_margin"]
|
|
33
|
+
self.pdf.l_margin = rs["page_margin_left"]
|
|
34
|
+
self.pdf.r_margin = rs["page_margin_right"]
|
|
35
|
+
self.pdf.c_margin = 0
|
|
36
|
+
|
|
37
|
+
self.pdf.add_page()
|
|
38
|
+
base_file_path = f"./tmp/report_tmp.pdf"
|
|
39
|
+
directory_path = "./tmp/pdf_figures"
|
|
40
|
+
|
|
41
|
+
used_top_height = rs["header"]["height"] + rs["header"]["bottom_margin"]
|
|
42
|
+
|
|
43
|
+
layout_manager = LayoutManager(pdf=self.pdf, used_top_height=used_top_height)
|
|
44
|
+
|
|
45
|
+
set_title_page(self.pdf, report)
|
|
46
|
+
|
|
47
|
+
n_children = len(report.CHILDREN)
|
|
48
|
+
|
|
49
|
+
for i, item in enumerate(report.CHILDREN):
|
|
50
|
+
|
|
51
|
+
x, y, w, h = layout_manager.get_default_layout()
|
|
52
|
+
mapper = item._get_mapper()
|
|
53
|
+
# reset width to default
|
|
54
|
+
# if children does not have width set by user, use the default width, if they do:
|
|
55
|
+
# check if it does not exceed pdf.w and then use children's width instead.
|
|
56
|
+
if hasattr(item, "width") and item.width is not None:
|
|
57
|
+
if item.width > self.pdf.w:
|
|
58
|
+
warning(
|
|
59
|
+
f"{repr(item)} width: ({item.width}) | exceeds the PDF page width: ({self.pdf.w}). Falling back to defaults"
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
w = item.width
|
|
63
|
+
|
|
64
|
+
mapper.add_to_pdf( pdf = self.pdf, w = w, h = h, x = x, y = y)
|
|
65
|
+
|
|
66
|
+
if i < n_children-1:
|
|
67
|
+
self.pdf.add_page()
|
|
68
|
+
|
|
69
|
+
if not get_figure_map():
|
|
70
|
+
self.pdf.output(f"{file_name}.pdf")
|
|
71
|
+
else:
|
|
72
|
+
self.pdf.output(base_file_path)
|
|
73
|
+
overlay_pdfs(base_pdf_path=base_file_path, pdf_data=get_figure_map(),
|
|
74
|
+
output_pdf_path=f"{file_name}.pdf")
|
|
75
|
+
perform_cleanup(cleanup, directory_path, base_file_path)
|
|
76
|
+
|