I can display datagrid content
This commit is contained in:
@@ -1,18 +1,22 @@
|
||||
import html
|
||||
from typing import Optional
|
||||
|
||||
from fastcore.basics import NotStr
|
||||
from fasthtml.components import Div
|
||||
import pandas as pd
|
||||
from fasthtml.components import *
|
||||
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
||||
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
||||
from myfasthtml.controls.helpers import mk
|
||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
from myfasthtml.core.utils import make_safe_id
|
||||
|
||||
|
||||
class DatagridState(DbObject):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner, name=f"{owner.get_full_id()}-state")
|
||||
def __init__(self, owner, save_state):
|
||||
super().__init__(owner, name=f"{owner.get_full_id()}-state", save_state=save_state)
|
||||
with self.initializing():
|
||||
self.sidebar_visible: bool = False
|
||||
self.selected_view: str = None
|
||||
@@ -25,12 +29,15 @@ class DatagridState(DbObject):
|
||||
self.filtered: dict = {}
|
||||
self.edition: DatagridEditionState = DatagridEditionState()
|
||||
self.selection: DatagridSelectionState = DatagridSelectionState()
|
||||
self.ne_data = None
|
||||
self.ne_df = None
|
||||
|
||||
self.ns_fast_access = None
|
||||
self.ns_total_rows = None
|
||||
|
||||
|
||||
class DatagridSettings(DbObject):
|
||||
def __init__(self, owner):
|
||||
super().__init__(owner, name=f"{owner.get_full_id()}-settings")
|
||||
def __init__(self, owner, save_state):
|
||||
super().__init__(owner, name=f"{owner.get_full_id()}-settings", save_state=save_state)
|
||||
with self.initializing():
|
||||
self.file_name: Optional[str] = None
|
||||
self.selected_sheet_name: Optional[str] = None
|
||||
@@ -46,19 +53,271 @@ class Commands(BaseCommands):
|
||||
|
||||
|
||||
class DataGrid(MultipleInstance):
|
||||
def __init__(self, parent, settings=None, _id=None):
|
||||
def __init__(self, parent, settings=None, save_state=False, _id=None):
|
||||
super().__init__(parent, _id=_id)
|
||||
self._settings = DatagridSettings(self)
|
||||
self._state = DatagridState(self)
|
||||
self._settings = settings or DatagridSettings(self, save_state=save_state)
|
||||
self._state = DatagridState(self, save_state=save_state)
|
||||
self.commands = Commands(self)
|
||||
self.init_from_dataframe(self._state.ne_df)
|
||||
|
||||
@property
|
||||
def _df(self):
|
||||
return self._state.ne_df
|
||||
|
||||
def init_from_dataframe(self, df):
|
||||
self._state.ne_data = df
|
||||
|
||||
def _get_column_type(dtype):
|
||||
if pd.api.types.is_integer_dtype(dtype):
|
||||
return ColumnType.Number
|
||||
elif pd.api.types.is_float_dtype(dtype):
|
||||
return ColumnType.Number
|
||||
elif pd.api.types.is_bool_dtype(dtype):
|
||||
return ColumnType.Bool
|
||||
elif pd.api.types.is_datetime64_any_dtype(dtype):
|
||||
return ColumnType.Datetime
|
||||
else:
|
||||
return ColumnType.Text # Default to Text if no match
|
||||
|
||||
def _init_columns(_df):
|
||||
columns = [DataGridColumnState(make_safe_id(col_id),
|
||||
col_index,
|
||||
col_id,
|
||||
_get_column_type(self._df[make_safe_id(col_id)].dtype))
|
||||
for col_index, col_id in enumerate(_df.columns)]
|
||||
if self._state.row_index:
|
||||
columns.insert(0, DataGridColumnState(make_safe_id(ROW_INDEX_ID), -1, " ", ColumnType.RowIndex))
|
||||
|
||||
return columns
|
||||
|
||||
def _init_fast_access(_df):
|
||||
"""
|
||||
Generates a fast-access dictionary for a DataFrame.
|
||||
|
||||
This method converts the columns of the provided DataFrame into NumPy arrays
|
||||
and stores them as values in a dictionary, using the column names as keys.
|
||||
This allows for efficient access to the data stored in the DataFrame.
|
||||
|
||||
Args:
|
||||
_df (DataFrame): The input pandas DataFrame whose columns are to be converted
|
||||
into a dictionary of NumPy arrays.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary where the keys are the column names of the input DataFrame
|
||||
and the values are the corresponding column values as NumPy arrays.
|
||||
"""
|
||||
if _df is None:
|
||||
return {}
|
||||
|
||||
res = {col: _df[col].to_numpy() for col in _df.columns}
|
||||
res[ROW_INDEX_ID] = _df.index.to_numpy()
|
||||
return res
|
||||
|
||||
if df is not None:
|
||||
self._state.ne_df = df
|
||||
self._df.columns = self._df.columns.map(make_safe_id) # make sure column names are trimmed
|
||||
self._state.rows = [DataGridRowState(row_id) for row_id in self._df.index]
|
||||
self._state.columns = _init_columns(df) # use df not self._df to keep the original title
|
||||
self._state.ns_fast_access = _init_fast_access(self._df)
|
||||
self._state.ns_total_rows = len(self._df) if self._df is not None else 0
|
||||
|
||||
return self
|
||||
|
||||
def mk_headers(self):
|
||||
def _mk_header_name(col_def: DataGridColumnState):
|
||||
return Div(
|
||||
mk.label(col_def.title, name="dt2-header-title"),
|
||||
cls="flex truncate cursor-default",
|
||||
)
|
||||
|
||||
def _mk_header(col_def: DataGridColumnState):
|
||||
return Div(
|
||||
_mk_header_name(col_def),
|
||||
Div(cls="dt2-resize-handle"),
|
||||
style=f"width:{col_def.width}px;",
|
||||
data_col=col_def.col_id,
|
||||
data_tooltip=col_def.title,
|
||||
cls="dt2-cell dt2-resizable flex",
|
||||
)
|
||||
|
||||
header_class = "dt2-row dt2-header" + "" if self._settings.header_visible else " hidden"
|
||||
return Div(
|
||||
*[_mk_header(col_def) for col_def in self._state.columns],
|
||||
cls=header_class,
|
||||
id=f"th_{self._id}"
|
||||
)
|
||||
|
||||
def mk_body_cell_content(self, col_pos, row_index, col_def: DataGridColumnState):
|
||||
|
||||
def mk_bool(_value):
|
||||
return Div(mk.icon(icon_checked if _value else icon_unchecked, can_select=False),
|
||||
cls="dt2-cell-content-checkbox")
|
||||
|
||||
def mk_text(_value):
|
||||
return mk.label(_value, cls="dt2-cell-content-text")
|
||||
|
||||
def mk_number(_value):
|
||||
return mk.label(_value, cls="dt2-cell-content-number")
|
||||
|
||||
def process_cell_content(_value):
|
||||
value_str = html.escape(str(_value))
|
||||
|
||||
if FILTER_INPUT_CID not in self._state.filtered or (
|
||||
keyword := self._state.filtered[FILTER_INPUT_CID]) is None:
|
||||
return value_str
|
||||
|
||||
index = value_str.lower().find(keyword.lower())
|
||||
if index < 0:
|
||||
return value_str
|
||||
|
||||
len_keyword = len(keyword)
|
||||
res = [Span(value_str[:index])] if index > 0 else []
|
||||
res += [Span(value_str[index:index + len_keyword], cls="dt2-highlight-1")]
|
||||
res += [Span(value_str[index + len_keyword:])] if len(value_str) > len_keyword else []
|
||||
return tuple(res)
|
||||
|
||||
column_type = col_def.type
|
||||
value = self._state.ns_fast_access[col_def.col_id][row_index]
|
||||
|
||||
if column_type == ColumnType.Bool:
|
||||
content = mk_bool(value)
|
||||
elif column_type == ColumnType.Number:
|
||||
content = mk_number(process_cell_content(value))
|
||||
elif column_type == ColumnType.RowIndex:
|
||||
content = mk_number(row_index)
|
||||
else:
|
||||
content = mk_text(process_cell_content(value))
|
||||
|
||||
return content
|
||||
|
||||
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState):
|
||||
if not col_def.usable:
|
||||
return None
|
||||
|
||||
if not col_def.visible:
|
||||
return Div(cls="dt2-col-hidden")
|
||||
|
||||
content = self.mk_body_cell_content(col_pos, row_index, col_def)
|
||||
|
||||
return Div(content,
|
||||
data_col=col_def.col_id,
|
||||
style=f"width:{col_def.width}px;",
|
||||
cls="dt2-cell")
|
||||
|
||||
def mk_body_content_page(self, page_index: int):
|
||||
df = self._df # self._get_filtered_df()
|
||||
start = page_index * DATAGRID_PAGE_SIZE
|
||||
end = start + DATAGRID_PAGE_SIZE
|
||||
if self._state.ns_total_rows > end:
|
||||
last_row = df.index[end - 1]
|
||||
else:
|
||||
last_row = None
|
||||
|
||||
rows = [Div(
|
||||
*[self.mk_body_cell(col_pos, row_index, col_def) for col_pos, col_def in enumerate(self._state.columns)],
|
||||
cls="dt2-row",
|
||||
data_row=f"{row_index}",
|
||||
id=f"tr_{self._id}-{row_index}",
|
||||
) for row_index in df.index[start:end]]
|
||||
|
||||
return rows
|
||||
|
||||
def mk_body(self):
|
||||
return Div(
|
||||
*self.mk_body_content_page(0),
|
||||
cls="dt2-body",
|
||||
id=f"tb_{self._id}",
|
||||
)
|
||||
|
||||
def mk_footers(self):
|
||||
return Div(
|
||||
*[Div(
|
||||
*[self.mk_aggregation_cell(col_def, row_index, footer) for col_def in self._state.columns],
|
||||
id=f"tf_{self._id}",
|
||||
data_row=f"{row_index}",
|
||||
cls="dt2-row dt2-row-footer",
|
||||
) for row_index, footer in enumerate(self._state.footers)],
|
||||
cls="dt2-footer",
|
||||
id=f"tf_{self._id}"
|
||||
)
|
||||
|
||||
def mk_table(self):
|
||||
return Div(
|
||||
self.mk_headers(),
|
||||
self.mk_body(),
|
||||
self.mk_footers()
|
||||
)
|
||||
|
||||
def mk_aggregation_cell(self, col_def, row_index: int, footer_conf, oob=False):
|
||||
"""
|
||||
Generates a footer cell for a data table based on the provided column definition,
|
||||
row index, footer configuration, and optional out-of-bound setting. This method
|
||||
applies appropriate aggregation functions, determines visibility, and structures
|
||||
the cell's elements accordingly.
|
||||
|
||||
:param col_def: Details of the column state, including its usability, visibility,
|
||||
and column ID, which are necessary to determine how the footer
|
||||
cell should be created.
|
||||
:type col_def: DataGridColumnState
|
||||
:param row_index: The specific index of the footer row where this cell will be
|
||||
added. This parameter is used to uniquely identify the cell
|
||||
within the footer.
|
||||
:type row_index: int
|
||||
:param footer_conf: Configuration for the footer that contains mapping of column
|
||||
IDs to their corresponding aggregation functions. This is
|
||||
critical for calculating aggregated values for the cell content.
|
||||
:type footer_conf: DataGridFooterConf
|
||||
:param oob: A boolean flag indicating whether the configuration involves any
|
||||
out-of-bound parameters that must be handled specifically. This
|
||||
parameter is optional and defaults to False.
|
||||
:type oob: bool
|
||||
:return: Returns an instance of `Div`, containing the visually structured footer
|
||||
cell content, including the calculated aggregation if applicable. If
|
||||
the column is not usable, it returns None. For non-visible columns, it
|
||||
returns a hidden cell `Div`. The aggregation value is displayed for valid
|
||||
aggregations. If none is applicable or the configuration is invalid,
|
||||
appropriate default content or styling is applied.
|
||||
:rtype: Div | None
|
||||
"""
|
||||
if not col_def.usable:
|
||||
return None
|
||||
|
||||
if not col_def.visible:
|
||||
return Div(cls="dt2-col-hidden")
|
||||
|
||||
if col_def.col_id in footer_conf.conf:
|
||||
agg_function = footer_conf.conf[col_def.col_id]
|
||||
if agg_function == FooterAggregation.Sum.value:
|
||||
value = self._df[col_def.col_id].sum()
|
||||
elif agg_function == FooterAggregation.Min.value:
|
||||
value = self._df[col_def.col_id].min()
|
||||
elif agg_function == FooterAggregation.Max.value:
|
||||
value = self._df[col_def.col_id].max()
|
||||
elif agg_function == FooterAggregation.Mean.value:
|
||||
value = self._df[col_def.col_id].mean()
|
||||
elif agg_function == FooterAggregation.Count.value:
|
||||
value = self._df[col_def.col_id].count()
|
||||
else:
|
||||
value = "** Invalid aggregation function **"
|
||||
else:
|
||||
value = None
|
||||
|
||||
return Div(mk_ellipsis(value, cls="dt2-cell-content-number"),
|
||||
data_col=col_def.col_id,
|
||||
style=f"width:{col_def.width}px;",
|
||||
cls="dt2-cell dt2-footer-cell",
|
||||
id=f"tf_{self._id}-{col_def.col_id}-{row_index}",
|
||||
hx_swap_oob='true' if oob else None,
|
||||
)
|
||||
|
||||
def render(self):
|
||||
html = self._state.ne_data.to_html(index=False) if self._state.ne_data is not None else "Content lost !"
|
||||
if self._state.ne_df is None:
|
||||
return Div("No data to display !")
|
||||
|
||||
return Div(
|
||||
NotStr(html),
|
||||
Div(
|
||||
self.mk_table(),
|
||||
# Script(f"bindDatagrid('{self._id}', false);"),
|
||||
),
|
||||
id=self._id
|
||||
)
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ class DataGridsManager(MultipleInstance):
|
||||
def open_from_excel(self, tab_id, file_upload: FileUpload):
|
||||
excel_content = file_upload.get_content()
|
||||
df = pd.read_excel(excel_content, file_upload.get_sheet_name())
|
||||
dg = DataGrid(self._tabs_manager)
|
||||
dg = DataGrid(self._tabs_manager, save_state=True)
|
||||
dg.init_from_dataframe(df)
|
||||
document = DocumentDefinition(
|
||||
document_id=str(uuid.uuid4()),
|
||||
|
||||
@@ -95,7 +95,7 @@ class mk:
|
||||
command: Command | CommandTemplate = None,
|
||||
binding: Binding = None,
|
||||
**kwargs):
|
||||
merged_cls = merge_classes("flex", cls, kwargs)
|
||||
merged_cls = merge_classes("flex truncate", cls, kwargs)
|
||||
icon_part = Span(icon, cls=f"mf-icon-{mk.convert_size(size)} mr-1") if icon else None
|
||||
text_part = Span(text, cls=f"text-{size}")
|
||||
return mk.mk(Label(icon_part, text_part, cls=merged_cls, **kwargs), command=command, binding=binding)
|
||||
|
||||
Reference in New Issue
Block a user