diff --git a/qf_lib/documents_utils/document_exporting/element/df_table.py b/qf_lib/documents_utils/document_exporting/element/df_table.py index e93d9ef2..745da3e8 100644 --- a/qf_lib/documents_utils/document_exporting/element/df_table.py +++ b/qf_lib/documents_utils/document_exporting/element/df_table.py @@ -29,7 +29,20 @@ class DFTable(Element): def __init__(self, data: QFDataFrame = None, columns: Sequence[str] = None, css_classes: Union[str, Sequence[str]] = "table", title: str = "", - grid_proportion: GridProportion = GridProportion.Eight, include_index=False): + grid_proportion: GridProportion = GridProportion.Eight, index_name: str = None): + """ + Main method that modifies the css style and/or class of different elements in the ModelController + Parameters + ---------- + data: QFDataFrame + columns: Sequence[str] + css_classes: Union[str, Sequence[str]] + title: str + grid_proportion: GridProportion + index_name: str + If it is None, then the dftable won't show the index + If it is any string (empty string included), the most upper level will take the name + """ super().__init__(grid_proportion) self.model = ModelController(data=data, index=data.index, @@ -42,9 +55,7 @@ def __init__(self, data: QFDataFrame = None, columns: Sequence[str] = None, self.model.table_styles.add_css_class(css_classes) self.title = title - - if include_index: - self.model.index_styling = Style() + self.index_name = index_name def generate_html(self, document: Optional[Document] = None) -> str: """ @@ -66,15 +77,18 @@ def generate_html(self, document: Optional[Document] = None) -> str: for level in range(self.columns.nlevels) ] - if self.model.index_styling: + if self.index_name is not None: index_levels = self.model.data.index.nlevels - columns_to_occurrences[0] = [("Index", index_levels)] + columns_to_occurrences[0] + self.index_name = " " if len(self.index_name) == 0 else self.index_name + + columns_to_occurrences[0] = [(self.index_name, index_levels)] + columns_to_occurrences[0] for index, occurence in enumerate(columns_to_occurrences[1:]): columns_to_occurrences[index + 1] = [("", index_levels)] + occurence return template.render(css_class=self.model.table_styles.classes(), table=self, - columns=columns_to_occurrences, - index_styling=self.model.index_styling) + columns=enumerate(columns_to_occurrences), + include_index=self.index_name is not None, index_styling=self.model.index_styling, + header_styling=self.model.header_styles) @property def columns(self): @@ -140,17 +154,30 @@ def remove_cells_classes(self, columns: Union[str, Sequence[str]], rows: Union[A css_classes: Union[str, Sequence[str]]): self.model.modify_data((columns, rows), css_classes, DataType.CELL, StylingType.CLASS, False) - def add_index_style(self, styles: Union[Dict[str, str], Sequence[str]]): - self.model.modify_data(None, styles, DataType.INDEX, StylingType.STYLE) + def add_index_style(self, styles: Union[Dict[str, str], Sequence[str]], level: Union[int, Sequence[int]] = None): + self.model.modify_data(level, styles, DataType.INDEX, StylingType.STYLE) - def add_index_class(self, css_classes: str): - self.model.modify_data(None, css_classes, DataType.INDEX, StylingType.CLASS) + def add_index_class(self, css_classes: str, level: Union[int, Sequence[int]] = None): + self.model.modify_data(level, css_classes, DataType.INDEX, StylingType.CLASS) - def remove_index_style(self, styles: Union[Dict[str, str], Sequence[str]]): - self.model.modify_data(None, styles, DataType.INDEX, StylingType.STYLE, False) + def remove_index_style(self, styles: Union[Dict[str, str], Sequence[str]], level: Union[int, Sequence[int]] = None): + self.model.modify_data(level, styles, DataType.INDEX, StylingType.STYLE, False) - def remove_index_class(self, css_classes: str): - self.model.modify_data(None, css_classes, DataType.INDEX, StylingType.CLASS, False) + def remove_index_class(self, css_classes: str, level: Union[int, Sequence[int]] = None): + self.model.modify_data(level, css_classes, DataType.INDEX, StylingType.CLASS, False) + + def add_header_style(self, styles: Union[Dict[str, str], Sequence[str]], level: Union[int, Sequence[int]] = None): + self.model.modify_data(level, styles, DataType.HEADER, StylingType.STYLE) + + def add_header_class(self, css_classes: str, level: Union[int, Sequence[int]] = None): + self.model.modify_data(level, css_classes, DataType.HEADER, StylingType.CLASS) + + def remove_header_style(self, styles: Union[Dict[str, str], Sequence[str]], + level: Union[int, Sequence[int]] = None): + self.model.modify_data(level, styles, DataType.HEADER, StylingType.STYLE, False) + + def remove_header_class(self, css_classes: str, level: Union[int, Sequence[int]] = None): + self.model.modify_data(level, css_classes, DataType.HEADER, StylingType.CLASS, False) class ModelController: @@ -175,7 +202,8 @@ def __init__(self, data=None, index=None, columns=None, dtype=None, copy=False): ] for column_name, column_style in self.columns_styles.items() }, index=self.data.index, columns=self.data.columns) self.table_styles = Style() - self.index_styling = None + self.index_styling = [Style() for level in range(0, index.nlevels)] + self.header_styles = [Style() for level in range(0, columns.nlevels)] def modify_data(self, location: Optional[Union[Any, Sequence[Any], Tuple[Any, Any]]] = None, data_to_update: Union[str, Dict[str, str], Sequence[str]] = None, @@ -192,7 +220,8 @@ def modify_data(self, location: Optional[Union[Any, Sequence[Any], Tuple[Any, An - rows: Union[Any, Sequence[Any]] - cells: Tuple[column, rows] - table: None - - index: None + - index: Union[int, Sequence[int], None] + - header: Union[int, Sequence[int], None] Default is None data_to_update: Union[str, Dict[str, str], Sequence[str]] The actual css information that will be inserted/deleted from the model. @@ -209,9 +238,12 @@ def modify_data(self, location: Optional[Union[Any, Sequence[Any], Tuple[Any, An - if False, then it is removing from the current list of css """ if data_type == DataType.INDEX: - if not self.index_styling: - self.index_styling = Style() - list_of_modified_elements = [self.index_styling] + if location is None: + list_of_modified_elements = self.index_styling + else: + location, _ = convert_to_list(location, int) + list_of_modified_elements = [self.index_styling[i] for i in location if + 0 <= i < len(self.index_styling)] elif data_type == DataType.ROW: list_of_modified_elements = self.rows_styles.loc[location].tolist() elif data_type == DataType.COLUMN: @@ -219,9 +251,17 @@ def modify_data(self, location: Optional[Union[Any, Sequence[Any], Tuple[Any, An list_of_modified_elements = [self.columns_styles[column_name] for column_name in location] elif data_type == DataType.CELL: location = tuple([item] if not isinstance(item, list) else item for item in location) - list_of_modified_elements = [self.styles.loc[row, column_name] for column_name in location[0] for row in location[1]] + list_of_modified_elements = [self.styles.loc[row, column_name] for column_name in location[0] for row in + location[1]] elif data_type == DataType.TABLE: list_of_modified_elements = [self.table_styles] + elif data_type == DataType.HEADER: + if location is None: + list_of_modified_elements = self.header_styles + else: + location, _ = convert_to_list(location, int) + list_of_modified_elements = [self.header_styles[i] for i in location if + 0 <= i < len(self.header_styles)] else: list_of_modified_elements = [] diff --git a/qf_lib/documents_utils/document_exporting/element/helpers/style_enums.py b/qf_lib/documents_utils/document_exporting/element/helpers/style_enums.py index a3e40a22..001a6b7e 100644 --- a/qf_lib/documents_utils/document_exporting/element/helpers/style_enums.py +++ b/qf_lib/documents_utils/document_exporting/element/helpers/style_enums.py @@ -7,6 +7,7 @@ class DataType(Enum): INDEX = 3 CELL = 4 TABLE = 5 + HEADER = 6 class StylingType(Enum): diff --git a/qf_lib/documents_utils/document_exporting/templates/df_table.html b/qf_lib/documents_utils/document_exporting/templates/df_table.html index 9016ce8f..0f1161c8 100644 --- a/qf_lib/documents_utils/document_exporting/templates/df_table.html +++ b/qf_lib/documents_utils/document_exporting/templates/df_table.html @@ -17,10 +17,11 @@ - {% for level in columns: %} + {% for level, content in columns: %} - {% for col, multiplier in level: %} - {% endfor %} @@ -30,18 +31,19 @@ {% for (indices, row), (_, styles) in table.model.iterrows(): %} - {% if index_styling%} - {% if indices is iterable %} - {% for index in indices %} - {% endfor %} {% else %} - - {% endif%} + {% endif %} + {% endif %} {% for i in range(0, row|length): %} diff --git a/qf_lib/tests/unit_tests/document_utils/document_exporting/element/test_df_table.py b/qf_lib/tests/unit_tests/document_utils/document_exporting/element/test_df_table.py index a2b5a076..fb111af3 100644 --- a/qf_lib/tests/unit_tests/document_utils/document_exporting/element/test_df_table.py +++ b/qf_lib/tests/unit_tests/document_utils/document_exporting/element/test_df_table.py @@ -25,12 +25,13 @@ def setUp(self): names=['Category', 'ID']) # Create a DataFrame with a MultiIndex for columns - data_nested_html = DFTable(data_nested, css_classes=["table", "wide-first-column"]) + data_nested_html = DFTable(data_nested, css_classes=["table", "wide-first-column"], index_name="Sth custom") data_nested_html.add_index_class("wider-second-column") data_nested_html.add_index_class("center-align") - data_nested_html.add_index_style({"background-color": "rgb(225, 225, 225)"}) - data_nested_html.add_index_style({"border-color": "rgb(100, 0, 0)"}) - data_nested_html.add_index_style({"color": "rgb(0, 1, 0)"}) + data_nested_html.add_index_style({"background-color": "rgb(225, 0, 225)"}, 1) + data_nested_html.add_index_style({"background-color": "rgb(100, 100, 5)"}, 0) + data_nested_html.add_index_style({"color": "rgb(100, 0, 0)"}) + data_nested_html.add_index_style({"color": "rgb(0, 1, 0)"}, 2) data_nested_html.remove_index_style({"color": "rgb(0, 1, 0)"}) dark_red = "#8B0000" data_nested_html.add_rows_styles(data_nested_html.model.data.index.tolist()[::2], @@ -43,6 +44,10 @@ def setUp(self): data_nested_html.add_cells_styles([data_nested_html.columns[0], data_nested_html.columns[4]], [("Person", 0), ("Person", 1)], {"background-color": "rgb(100, 255, 255)", "color": dark_red}) + + data_nested_html.add_header_style({"background-color": "rgb(100, 100, 255)"}, [1, 0]) + data_nested_html.remove_header_style({"background-color": "rgb(200, 255, 255)"}, 1) + self.data_nested_html = data_nested_html # Create a DataFrame with MultiIndex for both rows and columns @@ -51,20 +56,26 @@ def setUp(self): names=['Category', 'ID']) # Create a DataFrame with a MultiIndex for columns - data_nested_2_html = DFTable(data_nested_2, css_classes=["table", "wide-first-column"], include_index=False) + data_nested_2_html = DFTable(data_nested_2, css_classes=["table", "wide-first-column"]) dark_red = "#8B08B0" data_nested_2_html.add_cells_styles([data_nested_2_html.columns[1], data_nested_2_html.columns[3]], [("Person", 1), ("Person", 2)], {"background-color": "rgb(255, 100, 255)", "color": dark_red}) + data_nested_2_html.add_header_style({"background-color": "rgb(0, 255, 0)"}) self.data_nested_2_html = data_nested_2_html def test_index(self): self.assertTrue(self.data_nested_html.model.index_styling is not None) - self.assertEqual(self.data_nested_html.model.index_styling.css_class, ['wider-second-column', 'center-align']) - self.assertEqual(self.data_nested_html.model.index_styling.style, {'background-color': 'rgb(225,225,225)', - 'border-color': 'rgb(100,0,0)'}) + self.assertEqual(self.data_nested_html.model.index_styling[0].css_class, + ['wider-second-column', 'center-align']) + self.assertEqual(self.data_nested_html.model.index_styling[0].css_class, + self.data_nested_html.model.index_styling[1].css_class) + + self.assertEqual(self.data_nested_html.model.index_styling[0].style, {'background-color': 'rgb(100,100,5)'}) + self.assertEqual(self.data_nested_html.model.index_styling[1].style, {'background-color': 'rgb(225,0,225)'}) - self.assertTrue(self.data_nested_2_html.model.index_styling is None) + self.assertTrue(self.data_nested_2_html.model.index_styling[0].style == {}) + self.assertTrue(self.data_nested_2_html.model.index_styling[1].style == {}) def test_columns(self): self.assertTrue(self.data_nested_html.model.columns_styles is not None) @@ -83,6 +94,16 @@ def test_individual_cells(self): self.assertEqual(self.data_nested_html.model.styles[("General", "Age")].iloc[1].style, {'background-color': 'rgb(100,100,255)', 'color': '#8B0000'}) + def test_header(self): + self.assertTrue(isinstance(self.data_nested_html.model.header_styles, list)) + self.assertTrue(isinstance(self.data_nested_2_html.model.header_styles, list)) + + self.assertEqual(self.data_nested_html.model.header_styles[0].style, {'background-color': 'rgb(100,100,255)'}) + self.assertEqual(self.data_nested_html.model.header_styles[1].style, {}) + + self.assertEqual(self.data_nested_2_html.model.header_styles[0].style, + self.data_nested_2_html.model.header_styles[1].style) + if __name__ == '__main__': unittest.main()
{{ table.title }}
+ {% for col, multiplier in content: %} + {{ col }}
- {{index}} + {% if include_index%} + {% if indices is iterable and indices is not string %} + {% for index in range(index_styling | length) %} + + {{indices[index]}} + {{indices}}