PageRenderTime 58ms CodeModel.GetById 28ms RepoModel.GetById 1ms app.codeStats 0ms

/qgrid/grid.py

https://gitlab.com/e0/qgrid
Python | 387 lines | 344 code | 21 blank | 22 comment | 21 complexity | 9d16bde310275372e3b1679acb4f0c2e MD5 | raw file
  1. import pandas as pd
  2. import numpy as np
  3. import uuid
  4. import os
  5. import json
  6. from numbers import Integral
  7. from IPython.display import display_html, display_javascript
  8. try:
  9. from ipywidgets import widgets
  10. except ImportError:
  11. from IPython.html import widgets
  12. from IPython.display import display, Javascript
  13. try:
  14. from traitlets import Unicode, Instance, Bool, Integer, Dict, List
  15. except ImportError:
  16. from IPython.utils.traitlets import (
  17. Unicode, Instance, Bool, Integer, Dict, List
  18. )
  19. def template_contents(filename):
  20. template_filepath = os.path.join(
  21. os.path.dirname(__file__),
  22. 'templates',
  23. filename,
  24. )
  25. with open(template_filepath) as f:
  26. return f.read()
  27. SLICK_GRID_CSS = template_contents('slickgrid.css.template')
  28. SLICK_GRID_JS = template_contents('slickgrid.js.template')
  29. REMOTE_URL = ("https://cdn.rawgit.com/quantopian/qgrid/"
  30. "73eaa7adf1762f66eaf4d30ed9cbf385a7e9d9fa/qgrid/qgridjs/")
  31. LOCAL_URL = "/nbextensions/qgridjs"
  32. class _DefaultSettings(object):
  33. def __init__(self):
  34. self._grid_options = {
  35. 'fullWidthRows': True,
  36. 'syncColumnCellResize': True,
  37. 'forceFitColumns': True,
  38. 'defaultColumnWidth': 150,
  39. 'rowHeight': 28,
  40. 'enableColumnReorder': False,
  41. 'enableTextSelectionOnCells': True,
  42. 'editable': True,
  43. 'autoEdit': False
  44. }
  45. self._show_toolbar = False
  46. self._remote_js = False
  47. self._precision = None # Defer to pandas.get_option
  48. def set_grid_option(self, optname, optvalue):
  49. self._grid_options[optname] = optvalue
  50. def set_defaults(self, show_toolbar=None, remote_js=None, precision=None, grid_options=None):
  51. if show_toolbar is not None:
  52. self._show_toolbar = show_toolbar
  53. if remote_js is not None:
  54. self._remote_js = remote_js
  55. if precision is not None:
  56. self._precision = precision
  57. if grid_options is not None:
  58. self._grid_options = grid_options
  59. @property
  60. def show_toolbar(self):
  61. return self._show_toolbar
  62. @property
  63. def grid_options(self):
  64. return self._grid_options
  65. @property
  66. def remote_js(self):
  67. return self._remote_js
  68. @property
  69. def precision(self):
  70. return self._precision or pd.get_option('display.precision') - 1
  71. defaults = _DefaultSettings()
  72. def set_defaults(show_toolbar=None, remote_js=None, precision=None, grid_options=None):
  73. """
  74. Set the default qgrid options. The options that you can set here are the
  75. same ones that you can pass into ``show_grid``. See the documentation
  76. for ``show_grid`` for more information.
  77. Notes
  78. -----
  79. This function will be useful to you if you find yourself
  80. setting the same options every time you make a call to ``show_grid``.
  81. Calling this ``set_defaults`` function once sets the options for the
  82. lifetime of the kernel, so you won't have to include the same options
  83. every time you call ``show_grid``.
  84. See Also
  85. --------
  86. show_grid :
  87. The function whose default behavior is changed by ``set_defaults``.
  88. """
  89. defaults.set_defaults(show_toolbar, remote_js, precision, grid_options)
  90. def set_grid_option(optname, optvalue):
  91. """
  92. Set the default value for one of the options that gets passed into the
  93. SlickGrid constructor.
  94. Parameters
  95. ----------
  96. optname : str
  97. The name of the option to set.
  98. optvalue : object
  99. The new value to set.
  100. Notes
  101. -----
  102. The options you can set here are the same ones
  103. that you can set via the ``grid_options`` parameter of the ``set_defaults``
  104. or ``show_grid`` functions. See the `SlickGrid documentation
  105. <https://github.com/mleibman/SlickGrid/wiki/Grid-Options>`_ for the full
  106. list of available options.
  107. """
  108. defaults.grid_options[optname] = optvalue
  109. def show_grid(data_frame, show_toolbar=None, remote_js=None, precision=None, grid_options=None):
  110. """
  111. Main entry point for rendering DataFrames as SlickGrids.
  112. Parameters
  113. ----------
  114. grid_options : dict
  115. Options to use when creating javascript SlickGrid instances. See the Notes section below for
  116. more information on the available options, as well as the default options that qgrid uses.
  117. remote_js : bool
  118. Whether to load slickgrid.js from a local filesystem or from a
  119. remote CDN. Loading from the local filesystem means that SlickGrid
  120. will function even when not connected to the internet, but grid
  121. cells created with local filesystem loading will not render
  122. correctly on external sharing services like NBViewer.
  123. precision : integer
  124. The number of digits of precision to display for floating-point
  125. values. If unset, we use the value of
  126. `pandas.get_option('display.precision')`.
  127. show_toolbar : bool
  128. Whether to show a toolbar with options for adding/removing rows and
  129. exporting the widget to a static view. Adding/removing rows is an
  130. experimental feature which only works with DataFrames that have an
  131. integer index. The export feature is used to generate a copy of the
  132. grid that will be mostly functional when rendered in nbviewer.jupyter.org
  133. or when exported to html via the notebook's File menu.
  134. Notes
  135. -----
  136. By default, the following options get passed into SlickGrid when
  137. ``show_grid`` is called. See the `SlickGrid documentation
  138. <https://github.com/mleibman/SlickGrid/wiki/Grid-Options>`_ for information
  139. about these options::
  140. {
  141. 'fullWidthRows': True,
  142. 'syncColumnCellResize': True,
  143. 'forceFitColumns': True,
  144. 'rowHeight': 28,
  145. 'enableColumnReorder': False,
  146. 'enableTextSelectionOnCells': True,
  147. 'editable': True,
  148. 'autoEdit': False
  149. }
  150. See Also
  151. --------
  152. set_defaults : Permanently set global defaults for `show_grid`.
  153. set_grid_option : Permanently set individual SlickGrid options.
  154. """
  155. if show_toolbar is None:
  156. show_toolbar = defaults.show_toolbar
  157. if remote_js is None:
  158. remote_js = defaults.remote_js
  159. if precision is None:
  160. precision = defaults.precision
  161. if not isinstance(precision, Integral):
  162. raise TypeError("precision must be int, not %s" % type(precision))
  163. if grid_options is None:
  164. grid_options = defaults.grid_options
  165. else:
  166. options = defaults.grid_options.copy()
  167. options.update(grid_options)
  168. grid_options = options
  169. if not isinstance(grid_options, dict):
  170. raise TypeError(
  171. "grid_options must be dict, not %s" % type(grid_options)
  172. )
  173. # create a visualization for the dataframe
  174. grid = QGridWidget(df=data_frame, precision=precision,
  175. grid_options=grid_options,
  176. remote_js=remote_js)
  177. if show_toolbar:
  178. add_row = widgets.Button(description="Add Row")
  179. add_row.on_click(grid.add_row)
  180. rem_row = widgets.Button(description="Remove Row")
  181. rem_row.on_click(grid.remove_row)
  182. export = widgets.Button(description="Export")
  183. export.on_click(grid.export)
  184. display(widgets.HBox((add_row, rem_row, export)), grid)
  185. else:
  186. display(grid)
  187. class QGridWidget(widgets.DOMWidget):
  188. _view_module = Unicode("nbextensions/qgridjs/qgrid.widget", sync=True)
  189. _view_name = Unicode('QGridView', sync=True)
  190. _df_json = Unicode('', sync=True)
  191. _column_types_json = Unicode('', sync=True)
  192. _index_name = Unicode('')
  193. _initialized = Bool(False)
  194. _dirty = Bool(False)
  195. _cdn_base_url = Unicode(LOCAL_URL, sync=True)
  196. _multi_index = Bool(False)
  197. _selected_rows = List()
  198. df = Instance(pd.DataFrame)
  199. precision = Integer(6)
  200. grid_options = Dict(sync=True)
  201. remote_js = Bool(False)
  202. def __init__(self, *args, **kwargs):
  203. """Initialize all variables before building the table."""
  204. self._initialized = False
  205. super(QGridWidget, self).__init__(*args, **kwargs)
  206. # register a callback for custom messages
  207. self.on_msg(self._handle_qgrid_msg)
  208. self._initialized = True
  209. self._selected_rows = []
  210. if self.df is not None:
  211. self._update_table()
  212. def _grid_options_default(self):
  213. return defaults.grid_options
  214. def _remote_js_default(self):
  215. return defaults.remote_js
  216. def _precision_default(self):
  217. return defaults.precision
  218. def _df_changed(self):
  219. """Build the Data Table for the DataFrame."""
  220. if not self._initialized:
  221. return
  222. self._update_table()
  223. self.send({'type': 'draw_table'})
  224. def _update_table(self):
  225. df = self.df.copy()
  226. if not df.index.name:
  227. df.index.name = 'Index'
  228. if type(df.index) == pd.core.index.MultiIndex:
  229. df.reset_index(inplace=True)
  230. self._multi_index = True
  231. else:
  232. df.insert(0, df.index.name, df.index)
  233. self._multi_index = False
  234. self._index_name = df.index.name or 'Index'
  235. tc = dict(np.typecodes)
  236. for key in np.typecodes.keys():
  237. if "All" in key:
  238. del tc[key]
  239. column_types = []
  240. for col_name, dtype in df.dtypes.iteritems():
  241. if str(dtype) == 'category':
  242. categories = list(df[col_name].cat.categories)
  243. column_type = {'field': col_name,
  244. 'categories': ','.join(categories)}
  245. # XXXX: work around bug in to_json for categorical types
  246. # https://github.com/pydata/pandas/issues/10778
  247. df[col_name] = df[col_name].astype(str)
  248. column_types.append(column_type)
  249. continue
  250. column_type = {'field': col_name}
  251. for type_name, type_codes in tc.items():
  252. if dtype.kind in type_codes:
  253. column_type['type'] = type_name
  254. break
  255. column_types.append(column_type)
  256. self._column_types_json = json.dumps(column_types)
  257. self._df_json = df.to_json(
  258. orient='records',
  259. date_format='iso',
  260. double_precision=self.precision,
  261. )
  262. self._cdn_base_url = REMOTE_URL if self.remote_js else LOCAL_URL
  263. self._dirty = False
  264. def add_row(self, value=None):
  265. """Append a row at the end of the dataframe."""
  266. df = self.df
  267. if not df.index.is_integer():
  268. msg = "Cannot add a row to a table with a non-integer index"
  269. display(Javascript('alert("%s")' % msg))
  270. return
  271. last = df.iloc[-1]
  272. last.name += 1
  273. df.loc[last.name] = last.values
  274. precision = pd.get_option('display.precision') - 1
  275. row_data = last.to_json(date_format='iso',
  276. double_precision=precision)
  277. msg = json.loads(row_data)
  278. msg[self._index_name] = str(last.name)
  279. msg['slick_grid_id'] = str(last.name)
  280. msg['type'] = 'add_row'
  281. self._dirty = True
  282. self.send(msg)
  283. def remove_row(self, value=None):
  284. """Remove the current row from the table"""
  285. if self._multi_index:
  286. msg = "Cannot remove a row from a table with a multi index"
  287. display(Javascript('alert("%s")' % msg))
  288. return
  289. self.send({'type': 'remove_row'})
  290. def _handle_qgrid_msg(self, widget, content, buffers=None):
  291. """Handle incoming messages from the QGridView"""
  292. if 'type' not in content:
  293. return
  294. if content['type'] == 'remove_row':
  295. self.df.drop(content['row'], inplace=True)
  296. self._dirty = True
  297. elif content['type'] == 'cell_change':
  298. try:
  299. self.df.set_value(self.df.index[content['row']],
  300. content['column'], content['value'])
  301. self._dirty = True
  302. except ValueError:
  303. pass
  304. elif content['type'] == 'selection_change':
  305. self._selected_rows = content['rows']
  306. def get_selected_rows(self):
  307. """Get the currently selected rows"""
  308. return self._selected_rows
  309. def export(self, value=None):
  310. if self._dirty:
  311. self._update_table()
  312. base_url = REMOTE_URL
  313. div_id = str(uuid.uuid4())
  314. grid_options = self.grid_options
  315. grid_options['editable'] = False
  316. raw_html = SLICK_GRID_CSS.format(
  317. div_id=div_id,
  318. cdn_base_url=base_url,
  319. )
  320. raw_js = SLICK_GRID_JS.format(
  321. cdn_base_url=base_url,
  322. div_id=div_id,
  323. data_frame_json=self._df_json,
  324. column_types_json=self._column_types_json,
  325. options_json=json.dumps(grid_options),
  326. )
  327. display_html(raw_html, raw=True)
  328. display_javascript(raw_js, raw=True)