Optimized Datagrid.render()
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import html
|
||||
import re
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
import pandas as pd
|
||||
from fasthtml.common import NotStr
|
||||
from fasthtml.components import *
|
||||
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
@@ -11,10 +14,26 @@ 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.optimized_ft import OptimizedDiv
|
||||
from myfasthtml.core.utils import make_safe_id
|
||||
from myfasthtml.icons.fluent import checkbox_unchecked16_regular
|
||||
from myfasthtml.icons.fluent_p2 import checkbox_checked16_regular
|
||||
|
||||
# OPTIMIZATION: Pre-compiled regex to detect HTML special characters
|
||||
_HTML_SPECIAL_CHARS_REGEX = re.compile(r'[<>&"\']')
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def _mk_bool_cached(_value):
|
||||
"""
|
||||
OPTIMIZED: Cached boolean checkbox HTML generator.
|
||||
Since there are only 2 possible values (True/False), this will only generate HTML twice.
|
||||
"""
|
||||
return NotStr(str(
|
||||
Div(mk.icon(checkbox_checked16_regular if _value else checkbox_unchecked16_regular, can_select=False),
|
||||
cls="dt2-cell-content-checkbox")
|
||||
))
|
||||
|
||||
|
||||
class DatagridState(DbObject):
|
||||
def __init__(self, owner, save_state):
|
||||
@@ -148,64 +167,84 @@ class DataGrid(MultipleInstance):
|
||||
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(checkbox_checked16_regular if _value else checkbox_unchecked16_regular, 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())
|
||||
def mk_body_cell_content(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
||||
"""
|
||||
OPTIMIZED: Generate cell content with minimal object creation.
|
||||
- Uses plain strings instead of Label objects when possible
|
||||
- Accepts pre-computed filter_keyword_lower to avoid repeated dict lookups
|
||||
- Avoids html.escape when not necessary
|
||||
- Uses cached boolean HTML (_mk_bool_cached)
|
||||
"""
|
||||
|
||||
def mk_highlighted_text(value_str, css_class):
|
||||
"""Return highlighted text as raw HTML string or tuple of Spans."""
|
||||
if not filter_keyword_lower:
|
||||
# OPTIMIZATION: Return plain HTML string instead of Label object
|
||||
# Include "truncate text-sm" to match mk.label() behavior (ellipsis + font size)
|
||||
return NotStr(f'<span class="{css_class} truncate text-sm">{value_str}</span>')
|
||||
|
||||
index = value_str.lower().find(filter_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)
|
||||
|
||||
return NotStr(f'<span class="{css_class} truncate text-sm">{value_str}</span>')
|
||||
|
||||
# Has highlighting - need to use Span objects
|
||||
# Add "truncate text-sm" to match mk.label() behavior
|
||||
len_keyword = len(filter_keyword_lower)
|
||||
res = []
|
||||
if index > 0:
|
||||
res.append(Span(value_str[:index], cls=f"{css_class} text-sm"))
|
||||
res.append(Span(value_str[index:index + len_keyword], cls=f"{css_class} text-sm dt2-highlight-1"))
|
||||
if index + len_keyword < len(value_str):
|
||||
res.append(Span(value_str[index + len_keyword:], cls=f"{css_class} text-sm"))
|
||||
return Span(*res, cls=f"{css_class} truncate") if len(res) > 1 else res[0]
|
||||
|
||||
column_type = col_def.type
|
||||
value = self._state.ns_fast_access[col_def.col_id][row_index]
|
||||
|
||||
|
||||
# Boolean type - uses cached HTML (only 2 possible values)
|
||||
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)
|
||||
return _mk_bool_cached(value)
|
||||
|
||||
# RowIndex - simplest case, just return the number as plain HTML
|
||||
if column_type == ColumnType.RowIndex:
|
||||
return NotStr(f'<span class="dt2-cell-content-number truncate text-sm">{row_index}</span>')
|
||||
|
||||
# Convert value to string
|
||||
value_str = str(value)
|
||||
|
||||
# OPTIMIZATION: Only escape if necessary (check for HTML special chars with pre-compiled regex)
|
||||
if _HTML_SPECIAL_CHARS_REGEX.search(value_str):
|
||||
value_str = html.escape(value_str)
|
||||
|
||||
# Number or Text type
|
||||
if column_type == ColumnType.Number:
|
||||
return mk_highlighted_text(value_str, "dt2-cell-content-number")
|
||||
else:
|
||||
content = mk_text(process_cell_content(value))
|
||||
|
||||
return content
|
||||
return mk_highlighted_text(value_str, "dt2-cell-content-text")
|
||||
|
||||
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState):
|
||||
def mk_body_cell(self, col_pos, row_index, col_def: DataGridColumnState, filter_keyword_lower=None):
|
||||
"""
|
||||
OPTIMIZED: Accepts pre-computed filter_keyword_lower to avoid repeated dict lookups.
|
||||
OPTIMIZED: Uses OptimizedDiv instead of Div for faster rendering.
|
||||
"""
|
||||
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")
|
||||
return OptimizedDiv(cls="dt2-col-hidden")
|
||||
|
||||
content = self.mk_body_cell_content(col_pos, row_index, col_def, filter_keyword_lower)
|
||||
|
||||
return OptimizedDiv(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):
|
||||
"""
|
||||
OPTIMIZED: Extract filter keyword once instead of 10,000 times.
|
||||
OPTIMIZED: Uses OptimizedDiv for rows instead of Div for faster rendering.
|
||||
"""
|
||||
df = self._df # self._get_filtered_df()
|
||||
start = page_index * DATAGRID_PAGE_SIZE
|
||||
end = start + DATAGRID_PAGE_SIZE
|
||||
@@ -213,14 +252,19 @@ class DataGrid(MultipleInstance):
|
||||
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)],
|
||||
|
||||
# OPTIMIZATION: Extract filter keyword once (was being checked 10,000 times)
|
||||
filter_keyword = self._state.filtered.get(FILTER_INPUT_CID)
|
||||
filter_keyword_lower = filter_keyword.lower() if filter_keyword else None
|
||||
|
||||
rows = [OptimizedDiv(
|
||||
*[self.mk_body_cell(col_pos, row_index, col_def, filter_keyword_lower)
|
||||
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}",
|
||||
_id=f"tr_{self._id}-{row_index}",
|
||||
) for row_index in df.index[start:end]]
|
||||
|
||||
|
||||
return rows
|
||||
|
||||
def mk_body(self):
|
||||
|
||||
Reference in New Issue
Block a user