Fixed performance issues by creating a dedicated store for dataframe and optimizing
This commit is contained in:
@@ -638,10 +638,17 @@ function updateDatagridSelection(datagridId) {
|
|||||||
if (wrapper) wrapper.removeAttribute('mf-no-tooltip');
|
if (wrapper) wrapper.removeAttribute('mf-no-tooltip');
|
||||||
|
|
||||||
// Clear browser text selection to prevent stale ranges from reappearing
|
// Clear browser text selection to prevent stale ranges from reappearing
|
||||||
window.getSelection()?.removeAllRanges();
|
// But skip if an input/textarea/contenteditable has focus (would clear text cursor)
|
||||||
|
if (!document.activeElement?.closest('input, textarea, [contenteditable]')) {
|
||||||
|
window.getSelection()?.removeAllRanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIMIZATION: scope to table instead of scanning the entire document
|
||||||
|
const table = document.getElementById(`t_${datagridId}`);
|
||||||
|
const searchRoot = table ?? document;
|
||||||
|
|
||||||
// Clear previous selections and drag preview
|
// Clear previous selections and drag preview
|
||||||
document.querySelectorAll('.dt2-selected-focus, .dt2-selected-cell, .dt2-selected-row, .dt2-selected-column, .dt2-drag-preview, .dt2-selection-border-top, .dt2-selection-border-bottom, .dt2-selection-border-left, .dt2-selection-border-right').forEach((element) => {
|
searchRoot.querySelectorAll('.dt2-selected-focus, .dt2-selected-cell, .dt2-selected-row, .dt2-selected-column, .dt2-drag-preview, .dt2-selection-border-top, .dt2-selection-border-bottom, .dt2-selection-border-left, .dt2-selection-border-right').forEach((element) => {
|
||||||
element.classList.remove('dt2-selected-focus', 'dt2-selected-cell', 'dt2-selected-row', 'dt2-selected-column', 'dt2-drag-preview', 'dt2-selection-border-top', 'dt2-selection-border-bottom', 'dt2-selection-border-left', 'dt2-selection-border-right');
|
element.classList.remove('dt2-selected-focus', 'dt2-selected-cell', 'dt2-selected-row', 'dt2-selected-column', 'dt2-drag-preview', 'dt2-selection-border-top', 'dt2-selection-border-bottom', 'dt2-selection-border-left', 'dt2-selection-border-right');
|
||||||
element.style.userSelect = '';
|
element.style.userSelect = '';
|
||||||
});
|
});
|
||||||
@@ -669,7 +676,7 @@ function updateDatagridSelection(datagridId) {
|
|||||||
}
|
}
|
||||||
} else if (selectionType === 'column') {
|
} else if (selectionType === 'column') {
|
||||||
// Select all elements in the specified column
|
// Select all elements in the specified column
|
||||||
document.querySelectorAll(`[data-col="${elementId}"]`).forEach((columnElement) => {
|
searchRoot.querySelectorAll(`[data-col="${elementId}"]`).forEach((columnElement) => {
|
||||||
columnElement.classList.add('dt2-selected-column');
|
columnElement.classList.add('dt2-selected-column');
|
||||||
});
|
});
|
||||||
} else if (selectionType === 'range') {
|
} else if (selectionType === 'range') {
|
||||||
@@ -721,6 +728,9 @@ function getCellId(event) {
|
|||||||
return {cell_id: null};
|
return {cell_id: null};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OPTIMIZATION: Cache of highlighted cells per grid to avoid querySelectorAll on every animation frame
|
||||||
|
const _dragHighlightCache = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Highlight the drag selection range in real time during a mousedown>mouseup drag.
|
* Highlight the drag selection range in real time during a mousedown>mouseup drag.
|
||||||
* Called by mouse.js on each animation frame while dragging.
|
* Called by mouse.js on each animation frame while dragging.
|
||||||
@@ -765,16 +775,19 @@ function highlightDatagridDragRange(event, combination, mousedownResult) {
|
|||||||
|
|
||||||
if (isNaN(startCol) || isNaN(startRow) || isNaN(endCol) || isNaN(endRow)) return;
|
if (isNaN(startCol) || isNaN(startRow) || isNaN(endCol) || isNaN(endRow)) return;
|
||||||
|
|
||||||
// Clear previous selection and drag preview within this table
|
// OPTIMIZATION: Clear only previously highlighted cells instead of querySelectorAll on all table cells
|
||||||
table.querySelectorAll('.dt2-drag-preview, .dt2-selected-cell, .dt2-selected-row, .dt2-selected-column, .dt2-selected-focus, .dt2-selection-border-top, .dt2-selection-border-bottom, .dt2-selection-border-left, .dt2-selection-border-right')
|
const prevHighlighted = _dragHighlightCache.get(gridId);
|
||||||
.forEach(c => c.classList.remove('dt2-drag-preview', 'dt2-selected-cell', 'dt2-selected-row', 'dt2-selected-column', 'dt2-selected-focus', 'dt2-selection-border-top', 'dt2-selection-border-bottom', 'dt2-selection-border-left', 'dt2-selection-border-right'));
|
if (prevHighlighted) {
|
||||||
|
prevHighlighted.forEach(c => c.classList.remove('dt2-drag-preview', 'dt2-selected-cell', 'dt2-selected-row', 'dt2-selected-column', 'dt2-selected-focus', 'dt2-selection-border-top', 'dt2-selection-border-bottom', 'dt2-selection-border-left', 'dt2-selection-border-right'));
|
||||||
|
}
|
||||||
|
|
||||||
// Apply preview to all cells in the rectangular range
|
// Apply preview to all cells in the rectangular range and track them
|
||||||
const minCol = Math.min(startCol, endCol);
|
const minCol = Math.min(startCol, endCol);
|
||||||
const maxCol = Math.max(startCol, endCol);
|
const maxCol = Math.max(startCol, endCol);
|
||||||
const minRow = Math.min(startRow, endRow);
|
const minRow = Math.min(startRow, endRow);
|
||||||
const maxRow = Math.max(startRow, endRow);
|
const maxRow = Math.max(startRow, endRow);
|
||||||
|
|
||||||
|
const newHighlighted = [];
|
||||||
for (let col = minCol; col <= maxCol; col++) {
|
for (let col = minCol; col <= maxCol; col++) {
|
||||||
for (let row = minRow; row <= maxRow; row++) {
|
for (let row = minRow; row <= maxRow; row++) {
|
||||||
const cell = document.getElementById(`tcell_${gridId}-${col}-${row}`);
|
const cell = document.getElementById(`tcell_${gridId}-${col}-${row}`);
|
||||||
@@ -784,7 +797,9 @@ function highlightDatagridDragRange(event, combination, mousedownResult) {
|
|||||||
if (row === maxRow) cell.classList.add('dt2-selection-border-bottom');
|
if (row === maxRow) cell.classList.add('dt2-selection-border-bottom');
|
||||||
if (col === minCol) cell.classList.add('dt2-selection-border-left');
|
if (col === minCol) cell.classList.add('dt2-selection-border-left');
|
||||||
if (col === maxCol) cell.classList.add('dt2-selection-border-right');
|
if (col === maxCol) cell.classList.add('dt2-selection-border-right');
|
||||||
|
newHighlighted.push(cell);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_dragHighlightCache.set(gridId, newHighlighted);
|
||||||
}
|
}
|
||||||
@@ -79,11 +79,6 @@ class DatagridState(DbObject):
|
|||||||
self.selection: DatagridSelectionState = DatagridSelectionState()
|
self.selection: DatagridSelectionState = DatagridSelectionState()
|
||||||
self.cell_formats: dict = {}
|
self.cell_formats: dict = {}
|
||||||
self.table_format: list = []
|
self.table_format: list = []
|
||||||
self.ne_df = None
|
|
||||||
|
|
||||||
self.ns_fast_access = None
|
|
||||||
self.ns_row_data = None
|
|
||||||
self.ns_total_rows = None
|
|
||||||
|
|
||||||
|
|
||||||
class DatagridSettings(DbObject):
|
class DatagridSettings(DbObject):
|
||||||
@@ -104,6 +99,16 @@ class DatagridSettings(DbObject):
|
|||||||
self.enable_formatting: bool = True
|
self.enable_formatting: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
class DatagridStore(DbObject):
|
||||||
|
def __init__(self, owner, save_state):
|
||||||
|
with self.initializing():
|
||||||
|
super().__init__(owner, name=f"{owner.get_id()}#df", save_state=save_state)
|
||||||
|
self.ne_df = None
|
||||||
|
self.ns_fast_access = None
|
||||||
|
self.ns_row_data = None
|
||||||
|
self.ns_total_rows = None
|
||||||
|
|
||||||
|
|
||||||
class Commands(BaseCommands):
|
class Commands(BaseCommands):
|
||||||
def get_page(self, page_index: int):
|
def get_page(self, page_index: int):
|
||||||
return Command("GetPage",
|
return Command("GetPage",
|
||||||
@@ -187,9 +192,10 @@ class DataGrid(MultipleInstance):
|
|||||||
name, namespace = (conf.name, conf.namespace) if conf else ("No name", "__default__")
|
name, namespace = (conf.name, conf.namespace) if conf else ("No name", "__default__")
|
||||||
self._settings = DatagridSettings(self, save_state=save_state, name=name, namespace=namespace)
|
self._settings = DatagridSettings(self, save_state=save_state, name=name, namespace=namespace)
|
||||||
self._state = DatagridState(self, save_state=self._settings.save_state)
|
self._state = DatagridState(self, save_state=self._settings.save_state)
|
||||||
|
self._df_store = DatagridStore(self, save_state=self._settings.save_state)
|
||||||
self._formatting_engine = FormattingEngine()
|
self._formatting_engine = FormattingEngine()
|
||||||
self.commands = Commands(self)
|
self.commands = Commands(self)
|
||||||
self.init_from_dataframe(self._state.ne_df, init_state=False) # state comes from DatagridState
|
self.init_from_dataframe(self._df_store.ne_df, init_state=False) # data comes from DatagridStore
|
||||||
|
|
||||||
# add Panel
|
# add Panel
|
||||||
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="-panel")
|
self._panel = Panel(self, conf=PanelConf(show_display_right=False), _id="-panel")
|
||||||
@@ -250,7 +256,7 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def _df(self):
|
def _df(self):
|
||||||
return self._state.ne_df
|
return self._df_store.ne_df
|
||||||
|
|
||||||
def _apply_sort(self, df):
|
def _apply_sort(self, df):
|
||||||
if df is None:
|
if df is None:
|
||||||
@@ -293,7 +299,7 @@ class DataGrid(MultipleInstance):
|
|||||||
df = self._df.copy()
|
df = self._df.copy()
|
||||||
df = self._apply_sort(df) # need to keep the real type to sort
|
df = self._apply_sort(df) # need to keep the real type to sort
|
||||||
df = self._apply_filter(df)
|
df = self._apply_filter(df)
|
||||||
self._state.ns_total_rows = len(df)
|
self._df_store.ns_total_rows = len(df)
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
@@ -391,14 +397,15 @@ class DataGrid(MultipleInstance):
|
|||||||
return _df.to_dict(orient='records')
|
return _df.to_dict(orient='records')
|
||||||
|
|
||||||
if df is not None:
|
if df is not None:
|
||||||
self._state.ne_df = df
|
self._df_store.ne_df = df
|
||||||
if init_state:
|
if init_state:
|
||||||
self._df.columns = self._df.columns.map(make_safe_id) # make sure column names are trimmed
|
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._df_store.save()
|
||||||
|
self._state.rows = [] # sparse: only rows with non-default state are stored
|
||||||
self._state.columns = _init_columns(df) # use df not self._df to keep the original title
|
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._df_store.ns_fast_access = _init_fast_access(self._df)
|
||||||
self._state.ns_row_data = _init_row_data(self._df)
|
self._df_store.ns_row_data = _init_row_data(self._df)
|
||||||
self._state.ns_total_rows = len(self._df) if self._df is not None else 0
|
self._df_store.ns_total_rows = len(self._df) if self._df is not None else 0
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@@ -427,10 +434,9 @@ class DataGrid(MultipleInstance):
|
|||||||
if cell_id in self._state.cell_formats:
|
if cell_id in self._state.cell_formats:
|
||||||
return self._state.cell_formats[cell_id]
|
return self._state.cell_formats[cell_id]
|
||||||
|
|
||||||
if row_index < len(self._state.rows):
|
row_state = next((r for r in self._state.rows if r.row_id == row_index), None)
|
||||||
row_state = self._state.rows[row_index]
|
if row_state and row_state.format:
|
||||||
if row_state.format:
|
return row_state.format
|
||||||
return row_state.format
|
|
||||||
|
|
||||||
if col_def.format:
|
if col_def.format:
|
||||||
return col_def.format
|
return col_def.format
|
||||||
@@ -502,7 +508,6 @@ class DataGrid(MultipleInstance):
|
|||||||
if (is_inside and
|
if (is_inside and
|
||||||
cell_id_mousedown and cell_id_mouseup and
|
cell_id_mousedown and cell_id_mouseup and
|
||||||
cell_id_mousedown.startswith("tcell_") and cell_id_mouseup.startswith("tcell_")):
|
cell_id_mousedown.startswith("tcell_") and cell_id_mouseup.startswith("tcell_")):
|
||||||
|
|
||||||
self._update_current_position(None)
|
self._update_current_position(None)
|
||||||
|
|
||||||
pos_mouse_down = self._get_pos_from_element_id(cell_id_mousedown)
|
pos_mouse_down = self._get_pos_from_element_id(cell_id_mousedown)
|
||||||
@@ -615,7 +620,7 @@ class DataGrid(MultipleInstance):
|
|||||||
return Span(*res, cls=f"{css_class} truncate", style=style) if len(res) > 1 else res[0]
|
return Span(*res, cls=f"{css_class} truncate", style=style) if len(res) > 1 else res[0]
|
||||||
|
|
||||||
column_type = col_def.type
|
column_type = col_def.type
|
||||||
value = self._state.ns_fast_access[col_def.col_id][row_index]
|
value = self._df_store.ns_fast_access[col_def.col_id][row_index]
|
||||||
|
|
||||||
# Boolean type - uses cached HTML (only 2 possible values)
|
# Boolean type - uses cached HTML (only 2 possible values)
|
||||||
if column_type == ColumnType.Bool:
|
if column_type == ColumnType.Bool:
|
||||||
@@ -630,7 +635,7 @@ class DataGrid(MultipleInstance):
|
|||||||
formatted_value = None
|
formatted_value = None
|
||||||
rules = self._get_format_rules(col_pos, row_index, col_def)
|
rules = self._get_format_rules(col_pos, row_index, col_def)
|
||||||
if rules:
|
if rules:
|
||||||
row_data = self._state.ns_row_data[row_index] if row_index < len(self._state.ns_row_data) else None
|
row_data = self._df_store.ns_row_data[row_index] if row_index < len(self._df_store.ns_row_data) else None
|
||||||
style, formatted_value = self._formatting_engine.apply_format(rules, value, row_data)
|
style, formatted_value = self._formatting_engine.apply_format(rules, value, row_data)
|
||||||
|
|
||||||
# Use formatted value or convert to string
|
# Use formatted value or convert to string
|
||||||
@@ -661,7 +666,7 @@ class DataGrid(MultipleInstance):
|
|||||||
if not col_def.visible:
|
if not col_def.visible:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
value = self._state.ns_fast_access[col_def.col_id][row_index]
|
value = self._df_store.ns_fast_access[col_def.col_id][row_index]
|
||||||
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
|
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
|
||||||
|
|
||||||
return OptimizedDiv(content,
|
return OptimizedDiv(content,
|
||||||
@@ -682,7 +687,7 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
start = page_index * DATAGRID_PAGE_SIZE
|
start = page_index * DATAGRID_PAGE_SIZE
|
||||||
end = start + DATAGRID_PAGE_SIZE
|
end = start + DATAGRID_PAGE_SIZE
|
||||||
if self._state.ns_total_rows > end:
|
if self._df_store.ns_total_rows > end:
|
||||||
last_row = df.index[end - 1]
|
last_row = df.index[end - 1]
|
||||||
else:
|
else:
|
||||||
last_row = None
|
last_row = None
|
||||||
@@ -853,7 +858,7 @@ class DataGrid(MultipleInstance):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
if self._state.ne_df is None:
|
if self._df_store.ne_df is None:
|
||||||
return Div("No data to display !")
|
return Div("No data to display !")
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import logging
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from myfasthtml.controls.DslEditor import DslEditor
|
from myfasthtml.controls.DslEditor import DslEditor
|
||||||
|
from myfasthtml.controls.datagrid_objects import DataGridRowState
|
||||||
from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError, ColumnScope, RowScope, CellScope, TableScope, TablesScope
|
from myfasthtml.core.formatting.dsl import parse_dsl, DSLSyntaxError, ColumnScope, RowScope, CellScope, TableScope, TablesScope
|
||||||
from myfasthtml.core.instances import InstancesManager
|
from myfasthtml.core.instances import InstancesManager
|
||||||
|
|
||||||
@@ -108,10 +109,11 @@ class DataGridFormattingEditor(DslEditor):
|
|||||||
logger.warning(f"Column '{column_name}' not found, skipping rules")
|
logger.warning(f"Column '{column_name}' not found, skipping rules")
|
||||||
|
|
||||||
for row_index, rules in rows_rules.items():
|
for row_index, rules in rows_rules.items():
|
||||||
if row_index < len(state.rows):
|
row_state = next((r for r in state.rows if r.row_id == row_index), None)
|
||||||
state.rows[row_index].format = rules
|
if row_state is None:
|
||||||
else:
|
row_state = DataGridRowState(row_id=row_index)
|
||||||
logger.warning(f"Row {row_index} out of range, skipping rules")
|
state.rows.append(row_state)
|
||||||
|
row_state.format = rules
|
||||||
|
|
||||||
for cell_id, rules in cells_rules.items():
|
for cell_id, rules in cells_rules.items():
|
||||||
state.cell_formats[cell_id] = rules
|
state.cell_formats[cell_id] = rules
|
||||||
|
|||||||
@@ -46,10 +46,9 @@ class DataGridsRegistry(SingleInstance):
|
|||||||
as_fullname_dict = self._get_entries_as_full_name_dict()
|
as_fullname_dict = self._get_entries_as_full_name_dict()
|
||||||
grid_id = as_fullname_dict[table_name]
|
grid_id = as_fullname_dict[table_name]
|
||||||
|
|
||||||
# load dataframe
|
# load dataframe from dedicated store
|
||||||
state_id = f"{grid_id}#state"
|
store = self._db_manager.load(f"{grid_id}#df")
|
||||||
state = self._db_manager.load(state_id)
|
df = store["ne_df"] if store else None
|
||||||
df = state["ne_df"] if state else None
|
|
||||||
return df[column_name].tolist() if df is not None else []
|
return df[column_name].tolist() if df is not None else []
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -60,10 +59,9 @@ class DataGridsRegistry(SingleInstance):
|
|||||||
as_fullname_dict = self._get_entries_as_full_name_dict()
|
as_fullname_dict = self._get_entries_as_full_name_dict()
|
||||||
grid_id = as_fullname_dict[table_name]
|
grid_id = as_fullname_dict[table_name]
|
||||||
|
|
||||||
# load dataframe
|
# load dataframe from dedicated store
|
||||||
state_id = f"{grid_id}#state"
|
store = self._db_manager.load(f"{grid_id}#df")
|
||||||
state = self._db_manager.load(state_id)
|
df = store["ne_df"] if store else None
|
||||||
df = state["ne_df"] if state else None
|
|
||||||
return len(df) if df is not None else 0
|
return len(df) if df is not None else 0
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|||||||
Reference in New Issue
Block a user