Source code for pyexlatex.table.models.data.table

from typing import Union, List, Optional

import pandas as pd

from pyexlatex.table.models.interfaces import LabelClassOrStrs
from pyexlatex.logic.tools import _add_if_not_none
from mixins.repr import ReprMixin
from pyexlatex.table.models.data.valuestable import ValuesTable
from pyexlatex.table.models.labels.label import Label
from pyexlatex.table.models.labels.table import LabelTable, LabelCollection
from pyexlatex.table.models.spacing.columntable import ColumnPadTable, CellSpacer
from pyexlatex.table.models.table.section import TableSection
from pyexlatex.table.logic.panels.topleft import _set_top_left_corner_labels


[docs]class DataTable(TableSection, ReprMixin): """ Represents a subsection in a panel, but tracks row and column labels, which may be consolidated when assembled into Panels then a Table Use DataTable.from_df to create a DataTable from a pandas DataFrame. """ repr_cols = ['values_table', 'column_labels', 'row_labels']
[docs] def __init__(self, values_table: ValuesTable, column_labels: LabelTable=None, row_labels: LabelTable=None, top_left_corner_labels: LabelClassOrStrs = None, break_size_adjustment: Optional[str] = None, skip_add_top_left_to_column_labels: bool = False): self.top_left_corner_labels = _set_top_left_corner_labels(top_left_corner_labels) self.values_table = values_table self.row_labels = row_labels if skip_add_top_left_to_column_labels: self._column_labels = column_labels else: # Use setter which combines with top left self.column_labels = column_labels self.break_size_adjustment = break_size_adjustment
def __add__(self, other): if isinstance(other, DataTable): row_labels_match = _determine_match(self.row_labels, other.row_labels) if row_labels_match or other.row_labels is None: # if right table has same or None row labels, eliminate right row labels. Just add values values_table = _add_if_not_none(self.values_table, other.values_table) column_labels = _add_if_not_none(self.column_labels, other.column_labels) else: # if right table has unique row labels, absorb them into middle of values table values_table = _add_if_not_none( self.values_table, ValuesTable(other.row_labels.rows), other.values_table ) column_labels = _add_if_not_none( self.column_labels, other.top_left_corner_labels, other.column_labels ) row_labels = self.row_labels elif isinstance(other, ColumnPadTable): values_table = self.values_table + other if self.values_table is not None else None column_labels = self.column_labels + other if self.column_labels is not None else None row_labels = self.row_labels elif isinstance(other, TableSection): values_table = _add_if_not_none(self.values_table, other) column_labels = self.column_labels row_labels = self.row_labels else: raise ValueError(f'must add DataTable or TableSection to type {type(self)}. Got type {type(other)}') return DataTable( values_table=values_table, column_labels=column_labels, row_labels=row_labels, top_left_corner_labels=self.top_left_corner_labels, # should already be added from initial construction, no need to do again skip_add_top_left_to_column_labels=True ) @property def should_add_top_left(self) -> bool: return self.has_column_labels and self.has_row_labels @property def rows(self): try: return self._rows except AttributeError: self._rows = self._create_rows() return self._rows # Following properties/setters exist to recreate rows if user overrides values table or labels @property def column_labels(self): return self._column_labels @column_labels.setter def column_labels(self, labels: LabelTable): if labels is not None and not labels.is_empty and self.has_row_labels: self._column_labels = self.top_left_corner_labels + labels else: self._column_labels = labels self._recreate_rows_if_created() @property def has_column_labels(self) -> bool: return (self.column_labels is not None and not self.column_labels.is_empty) @property def row_labels(self): return self._row_labels @row_labels.setter def row_labels(self, labels: LabelTable): self._row_labels = labels self._recreate_rows_if_created() @property def has_row_labels(self) -> bool: return (self.row_labels is not None and not self.row_labels.is_empty) @property def values_table(self): return self._values_table @values_table.setter def values_table(self, table: ValuesTable): self._values_table = table self._recreate_rows_if_created() def _create_rows(self): rows = [] if self.has_column_labels: rows += self.column_labels.rows # need to add row labels inline with values table if self.has_row_labels: assert len(self.row_labels.rows) == len(self.values_table.rows) for label_row, value_row in zip(self.row_labels.rows, self.values_table.rows): rows.append(label_row + value_row) # no row labels, add values rows as they are else: rows += self.values_table.rows return rows
[docs] @classmethod def from_df(cls, df: pd.DataFrame, include_columns=True, include_index=False, extra_header: str=None, extra_header_underline=True, top_left_corner_labels: LabelClassOrStrs = None, **kwargs): """ Use for the most fine-grained control in creating tables. Construct DataTables from pandas DataFrames, modify labels as needed, assemble them into Panels, then create a latex Table with Table.from_panel_list. :param df: :param include_columns: :param include_index: :param extra_header: extra headers to place over the existing column labels (or over values if there are no column labels). Useful when placing multiple DataTables horizontally in a Panel. If a str is passed, str will become a multicolumn header for the entire DataTable. If a list of as many strs as there are columns is passed, these will be placed above any existing column labels. LabelTable and LabelCollection objects may also be passed for more control. When those objects are passed, the extra_header_underline argument is ignored. :param extra_header_underline: whether to add an underline under the extra header, if the extre header was passed :param top_left_corner_labels: additional labels to place in the top left corner. pass a single string or a list of strings for convenience. a list of strings will be create labels which span the gap horizontally and go downwards, one label per row. pass LabelCollection or LabelTable for more control. :param kwargs: DataTable kwargs :return: """ values_table = ValuesTable.from_df(df) if include_columns: column_label_table = LabelTable.from_df_index(df.columns) else: column_label_table = None if include_index: row_label_table = LabelTable.from_df_index(df.index).T else: row_label_table = None dt = cls( values_table, column_labels=column_label_table, row_labels=row_label_table, top_left_corner_labels=top_left_corner_labels, **kwargs, ) if extra_header is not None: header = _create_header_label_collection_list(extra_header, values_table, extra_header_underline) if include_columns: # add to existing header.reverse() # need to insert end first, so they end up in original order [dt.column_labels.label_collections.insert(0, label_collection) for label_collection in header] else: # create column labels as extra header dt.column_labels = LabelTable(header) if include_index: # need to add to top left num_spacers = len(header) # top left has one by default. if there were already columns, that top left is used up by the columns # and we need to add more. if there are not columns, then this one existing top left can be used # for the new header if not include_columns: num_spacers -= 1 for i in range(num_spacers): dt.top_left_corner_labels.label_collections.insert(0, LabelCollection.from_str_list([' '])) return dt
def _determine_match(labels1: LabelTable, labels2: LabelTable): # handle equality for None if labels1 is None: if labels2 is None: return True else: return False elif labels2 is None: # labels 1 must not be None here return False # here, both are not None return labels1.matches(labels2) def _create_header_label_collection_list(extra_header: LabelClassOrStrs, values_table: ValuesTable, underline: bool) -> List[LabelCollection]: if isinstance(extra_header, LabelTable): return extra_header.label_collections if isinstance(extra_header, LabelCollection): return [extra_header] if isinstance(extra_header, list): if len(extra_header) != values_table.num_columns: raise ValueError(f'passed extra header has {len(extra_header)} columns, while table has ' f'{values_table.num_columns} columns') underline_str_arg: Optional[str] if underline: underline_str_arg = f'0-{len(extra_header) - 1}' else: underline_str_arg = None return [LabelCollection.from_str_list(extra_header, underline=underline_str_arg)] # Got a string or latex item # create multicolumn label label = Label(extra_header, span=values_table.num_columns) # set underline underline_int_arg: Optional[int] if underline: underline_int_arg = 0 # place an underline under the singular label else: underline_int_arg = None # no underline return [LabelCollection([label], underline=underline_int_arg)]