Last active
January 29, 2023 13:39
-
-
Save ozgurkalan/7032266bcc032d7705f47091375ff777 to your computer and use it in GitHub Desktop.
qgrid
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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!") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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()) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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