Skip to content

Instantly share code, notes, and snippets.

@ozgurkalan
Last active January 29, 2023 13:39
Show Gist options
  • Save ozgurkalan/7032266bcc032d7705f47091375ff777 to your computer and use it in GitHub Desktop.
Save ozgurkalan/7032266bcc032d7705f47091375ff777 to your computer and use it in GitHub Desktop.
qgrid
"""
Base class for qgrid settings
"""
import pandas as pd
import qgrid
import ipywidgets as widgets
from IPython.display import display, Javascript
import time
class Base:
"""
base class for qgrid settings
"""
def __init__(self):
self.qgrid_options = None # options are set in class method
# qgrid info
self.qgrid_shape = None
self.qgrid_object = None
self.qgrid_df_slice = None
# initiate display widget Outputs
self._output_header = widgets.Output()
self._output_before_header = widgets.Output()
self._output_after_header = widgets.Output()
self._output_qgrid = widgets.Output()
self._output_footer = widgets.Output()
self._output_before_footer = widgets.Output()
self._output_after_footer = widgets.Output()
def _display_qgrid(self, q):
"""
build qgrid display for Jupyter Notebook
usage::
q = self._qgrid(df_slice=df_slice, **kwargs)
display_qgrid(q)
# define before any headers to receive qgrid info, e.g. shape
"""
with self._output_qgrid:
self._output_qgrid.clear_output() # first clear existing objects
display(q)
display(self._output_qgrid)
time.sleep(0.5) # sleep short for js injection
self._clean_qgrid_js()
def _set_grid_options(self, **kwargs):
"""
qgrid grid_options
kwargs apply to values
parameters:
kwargs: option values are updated by kwargs
usage::
grid_options(editable=True)
"""
options = {
# SlickGrid options
"fullWidthRows": False,
"syncColumnCellResize": False,
"forceFitColumns": False,
"defaultColumnWidth": 150,
"rowHeight": 70,
"enableColumnReorder": False, # causes glitch
"enableTextSelectionOnCells": True,
"editable": False,
"autoEdit": False,
"explicitInitialization": True,
# Qgrid options
"maxVisibleRows": 15,
"minVisibleRows": 8,
"sortable": True, # causes glitch when editable, should be opposite of editable
"filterable": True, # causes glitch when editable, should be opposite of editable
"highlightSelectedCell": False,
"highlightSelectedRow": True,
"height": "250px",
}
# update options with kwargs
options.update({k: v for k, v in kwargs.items() if k in options})
# causes glitch when editable, should be opposite of editable
options.update(
sortable=(not options["editable"]), filterable=(not options["editable"])
)
self.qgrid_options = options
def _qgrid(self, df_slice=None, minor=None, **kwargs):
"""
Custom qgrid wrapper
parameters:
df_slice: Dataframe to be displayed
minor: list, columns to display
kwargs: arguments for qgrid settings
"""
# check df_slice
if not (isinstance(df_slice, pd.core.frame.DataFrame)):
raise TypeError("df_slice must be a valid DataFrame!")
elif df_slice.empty:
raise TypeError("df_slice is empty!")
else:
self.qgrid_df_slice = df_slice # given df, to be used in rec details
# check minor
if minor is not None:
if not (isinstance(minor, list)):
raise TypeError("minor columns must be a valid list object!!")
elif df_slice.columns.intersection(minor).empty:
raise Exception("df_slice columns do not match with minor columns!")
else:
df_slice = df_slice[df_slice.columns.intersection(minor)]
# kwargs can set grid options
self._set_grid_options(**kwargs)
# column_definitions assigned during show_grid
# get column_definitions from kwargs
column_definitions = kwargs.get("column_definitions", {})
# set all columns read-only unless defined in column_definitions
for col in df_slice.columns:
if not col in column_definitions.keys():
column_definitions[col] = {"editable": False}
# show qgrid
q = qgrid.show_grid(df_slice, column_definitions=column_definitions)
q.show_toolbar = kwargs.get("show_toolbar", True)
# set grid options
q.grid_options = self.qgrid_options
# assign global properties
self.qgrid_object = q # for object functions stored
self.qgrid_shape = df_slice.shape # shape of final df displayed
return q
def _clean_qgrid_js(self):
"""
Javascript injection for qgrid
returns:
* JS integrated in Jupyter Notebook for Qgrid
* removes default buttons,
* adds a js button,
* adds js to scroll action of qgrid
"""
js = Javascript(
"""
function cleanIt() {
$('.slick-cell').attr('style','white-space:normal');
}
$('.q-grid-toolbar .btn[data-btn-text="Remove Row"]').remove();
$('.q-grid-toolbar .btn[data-btn-text="Add Row"]').remove();
$('.q-grid-toolbar').append('<button class="btn btn-default btn-sm fa fa-paint-brush" name="js_clean_grid">js</button>');
$('.q-grid-toolbar .btn[name="js_clean_grid"]').click(cleanIt);
slick_grid.onScroll.subscribe(cleanIt);
"""
)
display(js)
"""
qgrid abstract class
"""
from .view import View
class Grid(View):
"""
abstract class for calling qgrid
inherits:
Base class
View class
methods:
* view: displays with settings in Base class
* edit: abstract method, to be implemented
helper methods:
| _handler_view_selection_changed()
| already implemented in View class, can be altered by re-defining
usage::
# simple usage:
from ozcore import core
core.gridq.view(dataframe)
# integrate with inner class:
from ozcore.core.qgrid_.grid import Grid
class MySampleClass:
....
def grid(self):
# call grid's inner class
_grid = self._View()
_grid.view(df_slice=self.df)
class _View(Grid):
# inner class to inherit core > qgrid > view method
def __init__(self, cards=False):
super().__init__()
def _handler_view_selection_changed(self, event, qgrid_widget):
# .... override handler
def edit()
# .... override edit
"""
def __init__(self):
super().__init__()
def edit(self, df_slice, **kwargs):
"""
edit qgrid fields
NotImplemented
"""
raise NotImplementedError("edit is not implemented yet!")
"""
Qgrid module tests
"""
import pytest
import pandas as pd
from ozcore import core
def test_exceptions():
# GIVEN a dummy Dataframe with 5 columns and 5 records
df = core.dummy.df1
with pytest.raises(TypeError):
# WHEN minor is not a valid list object
core.gridq.view(df_slice=df, handler=False, minor="col1")
with pytest.raises(Exception):
# WHEN col7 does not exist
core.gridq.view(df_slice=df, handler=False, minor=["col7"])
with pytest.raises(NotImplementedError):
# WHEN edit called from abstract class
core.gridq.edit(df)
with pytest.raises(Exception):
# WHEN Series given
core.gridq.view(df.col1)
with pytest.raises(Exception):
# WHEN Dataframe is empty
core.gridq.view(pd.DataFrame())
"""
View class for qgrid
"""
import ipywidgets as widgets
from IPython.display import display
from .base import Base
class View(Base):
"""
class with displaying options for dataframe
view:
view method to call
_handler_view_selection_changed():
| helper handler method to display a record below the grid
| display content can be overriden in your own class
usage::
view(df_slice, handler=True)
"""
def __init__(self):
super().__init__()
def view(self, df_slice, handler=True, minor=None, **kwargs):
"""
display dataframe with qgrid
qgrid display settings defined in inherited Base class
parameters:
df_slice: Dataframe to be displayed
handler: bool, default True, turns on/off preview below grid
minor: list, columns to display, default None
Keyword Arguments:
show_toolbar: bool, default True
sortable: bool, default True, causes glitch with editable
filterable: bool, default True, causes glitch with editable
forceFitColumns: bool, default True
rowHeight: int, default 70
defaultColumnWidth: int, default 150
maxVisibleRows: int, default 15
minVisibleRows: int, default 8
height: str, default "250px"
returns:
Jupyter Notebook display with a header, qgrid, footer and selected record block
usage::
from ozcore import core
core.grid.view(core.dummy.emp, rowHeight=30, handler=False)
"""
# call qgrid
q = self._qgrid(df_slice=df_slice, minor=minor, **kwargs)
# observe cell change
if handler:
q.on(names="selection_changed", handler=self._handler_view_selection_changed)
# Label for displaying shape
shape_label=widgets.Label(value=f"read-only view => shape: {self.qgrid_shape}")
# put label in header output
with self._output_header:
self._output_header.clear_output()
display(widgets.Box([shape_label], box_style="info"))
before_footer_label = widgets.Label(
value="end of df_slice: select a record to view below in detail")
with self._output_before_footer:
self._output_before_footer.clear_output()
display(widgets.Box([before_footer_label], box_style="danger"))
after_footer_label = widgets.Label(
value="~end of display~")
with self._output_after_footer:
self._output_after_footer.clear_output()
display(widgets.Box([after_footer_label], box_style="success"))
display(self._output_header)
self._display_qgrid(q)
if handler:
display(self._output_before_footer)
display(self._output_footer)
display(self._output_after_footer)
def _handler_view_selection_changed(self, event, qgrid_widget):
"""
event handler for view when qgrid on selection_changed
returns:
displays selected row in footer single rec Output
"""
row = event["new"]
if len(row)<1: # if no keys return None
return
# selected df
df = self.qgrid_object.get_selected_df()
# the given df_slice
df_slice = self.qgrid_df_slice
# get the detailed record from given df_slice
rec = df_slice.loc[df.index.to_list()[0]].to_frame()
# output to footer
with self._output_footer:
self._output_footer.clear_output()
display(rec)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment