Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 29 additions & 12 deletions pandas/io/formats/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -1615,11 +1615,22 @@ def _update_ctx(self, attrs: DataFrame) -> None:
"with non-unique index or columns."
)

for cn in attrs.columns:
j = self.columns.get_loc(cn)
ser = attrs[cn]
for rn, c in ser.items():
if not c or pd.isna(c):
# Optimization: precompute column locations to avoid repeated get_loc
columns = list(attrs.columns)
columns_get_loc = {cn: self.columns.get_loc(cn) for cn in columns}
attrs_values = attrs.values
attrs_index = list(attrs.index)
# Use vectorized numpy isnan where possible, otherwise fallback to pd.isna
pd_isna = pd.isna

for col_idx, cn in enumerate(columns):
j = columns_get_loc[cn]
col_values = attrs_values[:, col_idx]
for row_idx, rn in enumerate(attrs_index):
c = col_values[row_idx]
# Note: Avoid checking `not c` when c is np.nan, as this is not always safe.
# First check for None/empty string (fast), then call pd.isna.
if c is None or c == "" or pd_isna(c):
continue
css_list = maybe_convert_css_to_tuples(c)
i = self.index.get_loc(rn)
Expand Down Expand Up @@ -2060,11 +2071,15 @@ def map_index(
return self

def _map(self, func: Callable, subset: Subset | None = None, **kwargs) -> Styler:
func = partial(func, **kwargs) # map doesn't take kwargs?
# Avoid functools.partial if no kwargs (likely case), which speeds up map slightly
if kwargs:
func_used = partial(func, **kwargs)
else:
func_used = func
if subset is None:
subset = IndexSlice[:]
subset = non_reducing_slice(subset)
result = self.data.loc[subset].map(func)
result = self.data.loc[subset].map(func_used)
self._update_ctx(result)
return self

Expand Down Expand Up @@ -2483,7 +2498,7 @@ def set_sticky(
for i, level in enumerate(levels_):
styles.append(
{
"selector": f"thead tr:nth-child({level+1}) th",
"selector": f"thead tr:nth-child({level + 1}) th",
"props": props
+ (
f"top:{i * pixel_size}px; height:{pixel_size}px; "
Expand All @@ -2494,7 +2509,7 @@ def set_sticky(
if not all(name is None for name in self.index.names):
styles.append(
{
"selector": f"thead tr:nth-child({obj.nlevels+1}) th",
"selector": f"thead tr:nth-child({obj.nlevels + 1}) th",
"props": props
+ (
f"top:{(len(levels_)) * pixel_size}px; "
Expand All @@ -2514,7 +2529,7 @@ def set_sticky(
styles.extend(
[
{
"selector": f"thead tr th:nth-child({level+1})",
"selector": f"thead tr th:nth-child({level + 1})",
"props": props_ + "z-index:3 !important;",
},
{
Expand Down Expand Up @@ -4109,8 +4124,10 @@ def css_bar(start: float, end: float, color: str) -> str:
if end > start:
cell_css += "background: linear-gradient(90deg,"
if start > 0:
cell_css += f" transparent {start*100:.1f}%, {color} {start*100:.1f}%,"
cell_css += f" {color} {end*100:.1f}%, transparent {end*100:.1f}%)"
cell_css += (
f" transparent {start * 100:.1f}%, {color} {start * 100:.1f}%,"
)
cell_css += f" {color} {end * 100:.1f}%, transparent {end * 100:.1f}%)"
return cell_css

def css_calc(x, left: float, right: float, align: str, color: str | list | tuple):
Expand Down
23 changes: 12 additions & 11 deletions pandas/io/formats/style_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ class StylerRenderer:
Base class to process rendering a Styler with a specified jinja2 template.
"""

loader = jinja2.PackageLoader("pandas", "io/formats/templates")
import os

loader = jinja2.FileSystemLoader(
os.path.join(os.path.dirname(__file__), "templates")
)
env = jinja2.Environment(loader=loader, trim_blocks=True)
template_html = env.get_template("html.tpl")
template_html_table = env.get_template("html_table.tpl")
Expand Down Expand Up @@ -834,10 +838,7 @@ def _generate_body_row(

data_element = _element(
"td",
(
f"{self.css['data']} {self.css['row']}{r} "
f"{self.css['col']}{c}{cls}"
),
(f"{self.css['data']} {self.css['row']}{r} {self.css['col']}{c}{cls}"),
value,
data_element_visible,
attributes="",
Expand Down Expand Up @@ -956,7 +957,7 @@ def concatenated_visible_rows(obj):
idx_len = d["index_lengths"].get((lvl, r), None)
if idx_len is not None: # i.e. not a sparsified entry
d["clines"][rn + idx_len].append(
f"\\cline{{{lvln+1}-{len(visible_index_levels)+data_len}}}"
f"\\cline{{{lvln + 1}-{len(visible_index_levels) + data_len}}}"
)

def format(
Expand Down Expand Up @@ -1211,7 +1212,7 @@ def format(
data = self.data.loc[subset]

if not isinstance(formatter, dict):
formatter = {col: formatter for col in data.columns}
formatter = dict.fromkeys(data.columns, formatter)

cis = self.columns.get_indexer_for(data.columns)
ris = self.index.get_indexer_for(data.index)
Expand Down Expand Up @@ -1397,7 +1398,7 @@ def format_index(
return self # clear the formatter / revert to default and avoid looping

if not isinstance(formatter, dict):
formatter = {level: formatter for level in levels_}
formatter = dict.fromkeys(levels_, formatter)
else:
formatter = {
obj._get_level_number(level): formatter_
Expand Down Expand Up @@ -1540,7 +1541,7 @@ def relabel_index(

>>> df = pd.DataFrame({"samples": np.random.rand(10)})
>>> styler = df.loc[np.random.randint(0, 10, 3)].style
>>> styler.relabel_index([f"sample{i+1} ({{}})" for i in range(3)])
>>> styler.relabel_index([f"sample{i + 1} ({{}})" for i in range(3)])
... # doctest: +SKIP
samples
sample1 (5) 0.315811
Expand Down Expand Up @@ -1694,7 +1695,7 @@ def format_index_names(
return self # clear the formatter / revert to default and avoid looping

if not isinstance(formatter, dict):
formatter = {level: formatter for level in levels_}
formatter = dict.fromkeys(levels_, formatter)
else:
formatter = {
obj._get_level_number(level): formatter_
Expand Down Expand Up @@ -2503,7 +2504,7 @@ def color(value, user_arg, command, comm_arg):
if value[0] == "#" and len(value) == 7: # color is hex code
return command, f"[HTML]{{{value[1:].upper()}}}{arg}"
if value[0] == "#" and len(value) == 4: # color is short hex code
val = f"{value[1].upper()*2}{value[2].upper()*2}{value[3].upper()*2}"
val = f"{value[1].upper() * 2}{value[2].upper() * 2}{value[3].upper() * 2}"
return command, f"[HTML]{{{val}}}{arg}"
elif value[:3] == "rgb": # color is rgb or rgba
r = re.findall("(?<=\\()[0-9\\s%]+(?=,)", value)[0].strip()
Expand Down