Skip to content

Commit

Permalink
DFTable - style header per level and style index per level in multiin…
Browse files Browse the repository at this point in the history
…dex (#121)

* Added header styling
  • Loading branch information
tavisit authored Aug 2, 2023
1 parent de50334 commit 397a3e0
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 41 deletions.
84 changes: 62 additions & 22 deletions qf_lib/documents_utils/document_exporting/element/df_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:
"""
Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -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.
Expand All @@ -209,19 +238,30 @@ 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:
location = location if isinstance(location, list) else [location]
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 = []

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class DataType(Enum):
INDEX = 3
CELL = 4
TABLE = 5
HEADER = 6


class StylingType(Enum):
Expand Down
22 changes: 12 additions & 10 deletions qf_lib/documents_utils/document_exporting/templates/df_table.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@
<table class="{{ css_class }}">
<caption>{{ table.title }}</caption>
<thead>
{% for level in columns: %}
{% for level, content in columns: %}
<tr>
{% for col, multiplier in level: %}
<th colspan="{{multiplier}}">
{% for col, multiplier in content: %}
<th colspan="{{multiplier}}" style={{header_styling[level].styles()}}
class={{header_styling[level].classes()}}>
{{ col }}
</th>
{% endfor %}
Expand All @@ -30,18 +31,19 @@
<tbody>
{% for (indices, row), (_, styles) in table.model.iterrows(): %}
<tr>
{% if index_styling%}
{% if indices is iterable %}
{% for index in indices %}
<td style={{index_styling.styles()}} class={{index_styling.classes()}}>
{{index}}
{% if include_index%}
{% if indices is iterable and indices is not string %}
{% for index in range(index_styling | length) %}
<td style={{index_styling[index].styles()}} class={{index_styling[index].classes()}}>
{{indices[index]}}
</td>
{% endfor %}
{% else %}
<td style={{index_styling.styles()}} class={{index_styling.classes()}}>
<td style={{index_styling[0].styles()}} class={{index_styling[0].classes()}}>
{{indices}}
</td>
{% endif%}
{% endif %}

{% endif %}

{% for i in range(0, row|length): %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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()

0 comments on commit 397a3e0

Please sign in to comment.