Skip to content
Merged
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
117 changes: 40 additions & 77 deletions python/datafusion/dataframe_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,6 @@ class DataFrameHtmlFormatter:
session
"""

# Class variable to track if styles have been loaded in the notebook
_styles_loaded = False

def __init__(
self,
max_cell_length: int = 25,
Expand Down Expand Up @@ -260,23 +257,6 @@ def set_custom_header_builder(self, builder: Callable[[Any], str]) -> None:
"""
self._custom_header_builder = builder

@classmethod
def is_styles_loaded(cls) -> bool:
"""Check if HTML styles have been loaded in the current session.

This method is primarily intended for debugging UI rendering issues
related to style loading.

Returns:
True if styles have been loaded, False otherwise

Example:
>>> from datafusion.dataframe_formatter import DataFrameHtmlFormatter
>>> DataFrameHtmlFormatter.is_styles_loaded()
False
"""
return cls._styles_loaded

def format_html(
self,
batches: list,
Expand Down Expand Up @@ -315,18 +295,7 @@ def format_html(
# Build HTML components
html = []

# Only include styles and scripts if:
# 1. Not using shared styles, OR
# 2. Using shared styles but they haven't been loaded yet
include_styles = (
not self.use_shared_styles or not DataFrameHtmlFormatter._styles_loaded
)

if include_styles:
html.extend(self._build_html_header())
# If we're using shared styles, mark them as loaded
if self.use_shared_styles:
DataFrameHtmlFormatter._styles_loaded = True
html.extend(self._build_html_header())

html.extend(self._build_table_container_start())

Expand All @@ -338,7 +307,7 @@ def format_html(
html.append("</div>")

# Add footer (JavaScript and messages)
if include_styles and self.enable_cell_expansion:
if self.enable_cell_expansion:
html.append(self._get_javascript())

# Always add truncation message if needed (independent of styles)
Expand Down Expand Up @@ -375,14 +344,20 @@ def format_str(

def _build_html_header(self) -> list[str]:
"""Build the HTML header with CSS styles."""
html = []
html.append("<style>")
# Only include expandable CSS if cell expansion is enabled
if self.enable_cell_expansion:
html.append(self._get_default_css())
default_css = self._get_default_css() if self.enable_cell_expansion else ""
script = f"""
<script>
if (!document.getElementById('df-styles')) {{
const style = document.createElement('style');
style.id = 'df-styles';
style.textContent = `{default_css}`;
document.head.appendChild(style);
}}
</script>
"""
html = [script]
if self.custom_css:
html.append(self.custom_css)
html.append("</style>")
html.append(f"<style>{self.custom_css}</style>")
return html

def _build_table_container_start(self) -> list[str]:
Expand Down Expand Up @@ -570,28 +545,31 @@ def _get_default_css(self) -> str:
def _get_javascript(self) -> str:
"""Get JavaScript code for interactive elements."""
return """
<script>
function toggleDataFrameCellText(table_uuid, row, col) {
var shortText = document.getElementById(
table_uuid + "-min-text-" + row + "-" + col
);
var fullText = document.getElementById(
table_uuid + "-full-text-" + row + "-" + col
);
var button = event.target;

if (fullText.style.display === "none") {
shortText.style.display = "none";
fullText.style.display = "inline";
button.textContent = "(less)";
} else {
shortText.style.display = "inline";
fullText.style.display = "none";
button.textContent = "...";
}
}
</script>
"""
<script>
if (!window.__df_formatter_js_loaded__) {
window.__df_formatter_js_loaded__ = true;
window.toggleDataFrameCellText = function (table_uuid, row, col) {
var shortText = document.getElementById(
table_uuid + "-min-text-" + row + "-" + col
);
var fullText = document.getElementById(
table_uuid + "-full-text-" + row + "-" + col
);
var button = event.target;

if (fullText.style.display === "none") {
shortText.style.display = "none";
fullText.style.display = "inline";
button.textContent = "(less)";
} else {
shortText.style.display = "inline";
fullText.style.display = "none";
button.textContent = "...";
}
};
}
</script>
"""


class FormatterManager:
Expand Down Expand Up @@ -712,24 +690,9 @@ def reset_formatter() -> None:
>>> reset_formatter() # Reset formatter to default settings
"""
formatter = DataFrameHtmlFormatter()
# Reset the styles_loaded flag to ensure styles will be reloaded
DataFrameHtmlFormatter._styles_loaded = False
set_formatter(formatter)


def reset_styles_loaded_state() -> None:
"""Reset the styles loaded state to force reloading of styles.

This can be useful when switching between notebook sessions or
when styles need to be refreshed.

Example:
>>> from datafusion.html_formatter import reset_styles_loaded_state
>>> reset_styles_loaded_state() # Force styles to reload in next render
"""
DataFrameHtmlFormatter._styles_loaded = False


def _refresh_formatter_reference() -> None:
"""Refresh formatter reference in any modules using it.

Expand Down
60 changes: 23 additions & 37 deletions python/tests/test_dataframe.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
configure_formatter,
get_formatter,
reset_formatter,
reset_styles_loaded_state,
)
from datafusion.expr import Window
from pyarrow.csv import write_csv
Expand Down Expand Up @@ -2177,27 +2176,15 @@ def test_html_formatter_shared_styles(df, clean_formatter_state):
# First, ensure we're using shared styles
configure_formatter(use_shared_styles=True)

# Get HTML output for first table - should include styles
html_first = df._repr_html_()

# Verify styles are included in first render
assert "<style>" in html_first
assert ".expandable-container" in html_first

# Get HTML output for second table - should NOT include styles
html_second = df._repr_html_()

# Verify styles are NOT included in second render
assert "<script>" in html_first
assert "df-styles" in html_first
assert "<script>" in html_second
assert "df-styles" in html_second
assert "<style>" not in html_first
assert "<style>" not in html_second
assert ".expandable-container" not in html_second

# Reset the styles loaded state and verify styles are included again
reset_styles_loaded_state()
html_after_reset = df._repr_html_()

# Verify styles are included after reset
assert "<style>" in html_after_reset
assert ".expandable-container" in html_after_reset


def test_html_formatter_no_shared_styles(df, clean_formatter_state):
Expand All @@ -2206,15 +2193,15 @@ def test_html_formatter_no_shared_styles(df, clean_formatter_state):
# Configure formatter to NOT use shared styles
configure_formatter(use_shared_styles=False)

# Generate HTML multiple times
html_first = df._repr_html_()
html_second = df._repr_html_()

# Verify styles are included in both renders
assert "<style>" in html_first
assert "<style>" in html_second
assert ".expandable-container" in html_first
assert ".expandable-container" in html_second
assert "<script>" in html_first
assert "<script>" in html_second
assert "df-styles" in html_first
assert "df-styles" in html_second
assert "<style>" not in html_first
assert "<style>" not in html_second


def test_html_formatter_manual_format_html(clean_formatter_state):
Expand All @@ -2228,20 +2215,15 @@ def test_html_formatter_manual_format_html(clean_formatter_state):

formatter = get_formatter()

# First call should include styles
html_first = formatter.format_html([batch], batch.schema)
assert "<style>" in html_first

# Second call should not include styles (using shared styles by default)
html_second = formatter.format_html([batch], batch.schema)
assert "<style>" not in html_second

# Reset loaded state
reset_styles_loaded_state()

# After reset, styles should be included again
html_reset = formatter.format_html([batch], batch.schema)
assert "<style>" in html_reset
assert "<script>" in html_first
assert "<script>" in html_second
assert "df-styles" in html_first
assert "df-styles" in html_second
assert "<style>" not in html_first
assert "<style>" not in html_second

# Create a new formatter with shared_styles=False
local_formatter = DataFrameHtmlFormatter(use_shared_styles=False)
Expand All @@ -2250,8 +2232,12 @@ def test_html_formatter_manual_format_html(clean_formatter_state):
local_html_1 = local_formatter.format_html([batch], batch.schema)
local_html_2 = local_formatter.format_html([batch], batch.schema)

assert "<style>" in local_html_1
assert "<style>" in local_html_2
assert "<script>" in local_html_1
assert "<script>" in local_html_2
assert "df-styles" in local_html_1
assert "df-styles" in local_html_2
assert "<style>" not in local_html_1
assert "<style>" not in local_html_2


def test_fill_null_basic(null_df):
Expand Down