Skip to content
Draft
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
90 changes: 88 additions & 2 deletions bigframes/display/anywidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,78 @@
except Exception:
_ANYWIDGET_INSTALLED = False

class _DummyTraitlet:
def __init__(self, default_value=None):
self.default_value = default_value
self.name = None

def tag(self, **kwargs):
return self

def __set_name__(self, owner, name):
self.name = name

def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name, self.default_value)

def __set__(self, instance, value):
# Basic mimic of traitlets validation/observation
# Look for validators registered via @validate
for attr_name in dir(instance):
attr = getattr(type(instance), attr_name, None)
if (
attr is not None
and hasattr(attr, "_validated_trait")
and attr._validated_trait == self.name
):
value = getattr(instance, attr_name)({"value": value})

instance.__dict__[self.name] = value

# Look for observers registered via @observe
for attr_name in dir(instance):
attr = getattr(type(instance), attr_name, None)
if (
attr is not None
and hasattr(attr, "_observed_trait")
and attr._observed_trait == self.name
):
getattr(instance, attr_name)({"new": value})

class _DummyTraitletsModule:
def Int(self, default_value=0, **kwargs):
return _DummyTraitlet(default_value)

def Unicode(self, default_value="", **kwargs):
return _DummyTraitlet(default_value)

def List(self, trait=None, default_value=None, **kwargs):
return _DummyTraitlet(default_value if default_value is not None else [])

def Bool(self, default_value=False, **kwargs):
return _DummyTraitlet(default_value)

def Dict(self, *args, **kwargs):
return _DummyTraitlet({})

def observe(self, trait_name, **kwargs):
def decorator(func):
func._observed_trait = trait_name
return func

return decorator

def validate(self, trait_name, **kwargs):
def decorator(func):
func._validated_trait = trait_name
return func

return decorator

traitlets = _DummyTraitletsModule() # type: ignore[assignment]

_WIDGET_BASE: type[Any]
if _ANYWIDGET_INSTALLED:
_WIDGET_BASE = anywidget.AnyWidget
Expand Down Expand Up @@ -76,12 +148,15 @@ class TableWidget(_WIDGET_BASE):
_error_message = traitlets.Unicode(allow_none=True, default_value=None).tag(
sync=True
)
start_execution = traitlets.Bool(False).tag(sync=True)
is_deferred_mode = traitlets.Bool(False).tag(sync=True)

def __init__(self, dataframe: bigframes.dataframe.DataFrame):
def __init__(self, dataframe: bigframes.dataframe.DataFrame, deferred: bool = True):
"""Initialize the TableWidget.

Args:
dataframe: The Bigframes Dataframe to display in the widget.
deferred: Whether to defer the initial data load.
"""
if not _ANYWIDGET_INSTALLED:
raise ImportError(
Expand All @@ -90,6 +165,7 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
)

self._dataframe = dataframe
self.is_deferred_mode = deferred

super().__init__()

Expand Down Expand Up @@ -122,12 +198,22 @@ def __init__(self, dataframe: bigframes.dataframe.DataFrame):
else:
self.orderable_columns = []

self._initial_load()
if not self.is_deferred_mode:
self._initial_load()

# Signals to the frontend that the initial data load is complete.
# Also used as a guard to prevent observers from firing during initialization.
self._initial_load_complete = True

@traitlets.observe("start_execution")
def _on_start_execution(self, change: dict[str, Any]):
if change["new"]:
try:
self._initial_load()
except Exception as e:
self._error_message = str(e)
self.is_deferred_mode = False

def _initial_load(self) -> None:
"""Get initial data and row count."""
# obtain the row counts
Expand Down
2 changes: 1 addition & 1 deletion bigframes/display/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ def get_anywidget_bundle(
else:
df, blob_cols = obj._get_display_df_and_blob_cols()

widget = display.TableWidget(df)
widget = display.TableWidget(df, deferred=options.display.anywidget_deferred)
widget_repr_result = widget._repr_mimebundle_(include=include, exclude=exclude)

if isinstance(widget_repr_result, tuple):
Expand Down
26 changes: 26 additions & 0 deletions bigframes/display/table_widget.css
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,32 @@ body[data-theme='dark'] .bigframes-widget.bigframes-widget {
color: var(--bf-null-fg);
}

.bigframes-widget .deferred-message {
align-items: center;
background-color: var(--bf-bg);
border: 1px solid var(--bf-border-color);
display: flex;
flex-direction: column;
gap: 12px;
justify-content: center;
padding: 24px;
text-align: center;
}

.bigframes-widget .deferred-message p {
margin: 0;
}

.bigframes-widget .run-button {
background-color: var(--bf-bg);
border: 1px solid var(--bf-border-color);
padding: 6px 12px;
}

.bigframes-widget .run-button:hover {
background-color: var(--bf-header-bg);
}

.bigframes-widget .debug-info {
border-top: 1px solid var(--bf-border-color);
}
51 changes: 50 additions & 1 deletion bigframes/display/table_widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ const ModelProperty = {
SORT_CONTEXT: 'sort_context',
TABLE_HTML: 'table_html',
MAX_COLUMNS: 'max_columns',
START_EXECUTION: 'start_execution',
IS_DEFERRED_MODE: 'is_deferred_mode',
};

const Event = {
Expand All @@ -41,6 +43,23 @@ function render({ model, el }) {
const errorContainer = document.createElement('div');
errorContainer.classList.add('error-message');

function createDeferredView() {
const container = document.createElement('div');
container.classList.add('deferred-message');
const text = document.createElement('p');
text.textContent =
'This is a preview of the widget. The SQL query has not been executed yet.';
const button = document.createElement('button');
button.textContent = 'Run Query and Display Widget';
button.classList.add('run-button');
container.appendChild(text);
container.appendChild(button);
return { container, button };
}

const { container: deferredContainer, button: runButton } =
createDeferredView();

const tableContainer = document.createElement('div');
tableContainer.classList.add('table-container');
const footer = document.createElement('footer');
Expand Down Expand Up @@ -171,9 +190,14 @@ function render({ model, el }) {
}

let isHeightInitialized = false;
let lastHTML = '';

function handleTableHTMLChange() {
tableContainer.innerHTML = model.get(ModelProperty.TABLE_HTML);
const newHTML = model.get(ModelProperty.TABLE_HTML);
if (newHTML && newHTML !== lastHTML) {
tableContainer.innerHTML = newHTML;
lastHTML = newHTML;
}

// After the first render, dynamically set the container height to fit the
// initial page (usually 10 rows) and then lock it.
Expand Down Expand Up @@ -299,6 +323,28 @@ function render({ model, el }) {
}
}

function updateDeferredMode() {
const isDeferred = model.get(ModelProperty.IS_DEFERRED_MODE);
if (isDeferred) {
deferredContainer.style.display = 'flex';
tableContainer.style.display = 'none';
footer.style.display = 'none';
} else {
deferredContainer.style.display = 'none';
tableContainer.style.display = 'block';
footer.style.display = 'flex';
handleTableHTMLChange();
}
}

runButton.addEventListener(Event.CLICK, () => {
model.set(ModelProperty.START_EXECUTION, true);
model.save_changes();
// Update button state to indicate loading.
runButton.textContent = 'Running...';
runButton.disabled = true;
});

prevPage.addEventListener(Event.CLICK, () => handlePageChange(-1));
nextPage.addEventListener(Event.CLICK, () => handlePageChange(1));
pageSizeInput.addEventListener(Event.CHANGE, (e) => {
Expand All @@ -321,6 +367,7 @@ function render({ model, el }) {
if (val) updateButtonStates();
});
model.on(`change:${ModelProperty.PAGE}`, updateButtonStates);
model.on(`change:${ModelProperty.IS_DEFERRED_MODE}`, updateDeferredMode);

paginationContainer.appendChild(prevPage);
paginationContainer.appendChild(pageIndicator);
Expand All @@ -340,11 +387,13 @@ function render({ model, el }) {
footer.appendChild(settingsContainer);

el.appendChild(errorContainer);
el.appendChild(deferredContainer);
el.appendChild(tableContainer);
el.appendChild(footer);

handleTableHTMLChange();
handleErrorMessageChange();
updateDeferredMode();
}

export default { render };
Loading
Loading