3

I want to make some form on ipywidgets, which show values of particular row of pandas dataframe in its inputs.

To make row selection and displaying in form more interactive I need to handle somehow clicking in displayed dataframe. Virtually, I want to be able to identify index or number of last clicked row of dataframe.


PS
I've seen tools like ipysheet and the others, but I think they still too nascent, though ipysheet could be very powerful in future. So those tools don't applicable to this task.

Alex-droid AD
  • 635
  • 1
  • 6
  • 14

3 Answers3

3

I had similar problem and qgrid was an overkill for me.

Workings in jupyter (inspired by add click feature on each row in html table):

  1. Injected script adds handlers to every cell of the dataframe table (marked by id='T_table' through Styler.set_uuid function).

  2. When a cell is clicked typical handler:

    1. looks for the designated input element (hidden and marked by placeholder "undefined"),

    2. sets the value property of the found input for class names of the clicked cell,

    3. triggers "change" event.
  3. After that the class names (pandas makes them contain the necessary row/column information) of the clicked cell are available for the ipywidgets framework and can be processed in python.

Remarks:

  1. Certain timeout is needed before calling the script. For bigger tables the needed timeout could be bigger.

  2. The logic is a bit convoluted, because the way to bring js-values into the python world that I used was through ipywidgets.Text.

  3. Item 2.1 is added in case the DOM was rerendered after initiation.

import pandas as pd
import ipywidgets as wgt
from IPython.display import display, HTML
import re

# javascript-part
script = """
<script>
var input
var xpath = "//input[contains(@placeholder,'undefined')]";

function addHandlers() {
    input = document.evaluate(xpath, document, null, 
        XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    input.setAttribute("hidden","");

    var table = document.querySelector("#T_table");
    var headcells = [].slice.call(table.getElementsByTagName("th"));
    var datacells = [].slice.call(table.getElementsByTagName("td"));
    var cells = headcells.concat(datacells);
    for (var i=0; i < cells.length; i++) {
       var createClickHandler = function(cell) {
         return function() { 
            input = document.evaluate(xpath, document, null,
                XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
            input.value = cell.className; 
            var event = new Event('change', { bubbles: true });
            input.dispatchEvent(event);
      }}
      cells[i].onclick = createClickHandler(cells[i]);
    };
}

window.onload = setTimeout(addHandlers, 500);
</script>
"""
display(HTML(script))

# ipywidgets-part
newdf = pd.DataFrame(data={
    '1': [11,21,31,41], '2': [12,22,32,42], '3': [13,23,33,43],
    '4': [14,24,34,44], '5': [15,25,35,45], '6': [16,26,36,46],
    '7': [17,27,37,47], '8': [18,28,38,48], '9': [19,29,39,49],
    '10': [110,210,310,410],'11': [111,211,311,411],
})

html = newdf.style.\
    set_uuid('table') 

def on_change(change):
    cls = change['new'].split(' ')
    if len(cls) == 2: 
        place.value, row.value = cls
        col.value = '0'
    elif len(cls) == 3: 
        place.value, txtrow, txtcol = cls
        res = re.search(r'\d+',txtrow).group(0)
        row.value = str(int(res)+1)
        res = re.search(r'\d+',txtcol).group(0)
        col.value = str(int(res)+1)
    else:
        place.value, row.value, col.value = ['unknown']*3

status = wgt.Text(placeholder='undefined',layout={'font-size':'6px'}) 
status.observe(on_change,names=['value'])

table = wgt.Output()
with table: display(html)

layout = {'width':'192px'}
row = wgt.Text(layout=layout,description='row')
col = wgt.Text(layout=layout,description='col')
place = wgt.Text(layout=layout,description='place')
body = wgt.HBox([table,wgt.VBox([place,row,col])])

wgt.VBox([body,status])
dzenny
  • 31
  • 3
  • this is a super nice implementation, I am just wondering if the clicked rows could stay highlighted after being clicked – Michel Kluger Jun 15 '21 at 13:03
0

Did you try qgrid?

It's definition on github is: An interactive grid for sorting, filtering, and editing DataFrames in Jupyter notebooks.

Ke Zhang
  • 937
  • 1
  • 10
  • 24
0

Pandas output in Jupyter with on on_click event handler

When row clicked:

  • row highlighed
  • row number provided to on_click
  • as an example, row number and content printed

Based on @dzenny answer

import pandas as pd
import ipywidgets as wgt
from IPython.display import display, HTML
import re

# javascript-part
script = """
<style>tr.selected {background-color:#00FFFF!important}</style>
<script>
(function () {
    var input = document.evaluate("//input[contains(@placeholder,'undefined')]", document, null, 
        XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    input.setAttribute("hidden","");

    var table = document.querySelector("#T_table");
    var rows = Array.from(table.querySelectorAll('tbody > tr'));    
    var cells = Array.from(table.querySelectorAll("td"));
    for (var i=0; i < cells.length; i++) {
       var createClickHandler = function(cell) {
         return function() { 
            input.value = cell.className; 
            var event = new Event('change', { bubbles: true });
            input.dispatchEvent(event);
            rows.forEach(el => el.classList.remove('selected')) 
            cell.parentElement.classList.add('selected')
      }}
      cells[i].onclick = createClickHandler(cells[i]);
    };
})();
</script>
"""

# ipywidgets-part
df = pd.DataFrame(data={
    '1': [11,21,31,41], '2': [12,22,32,42], '3': [13,23,33,43],
    '4': [14,24,34,44], '5': [15,25,35,45], '6': [16,26,36,46],
    '7': [17,27,37,47], '8': [18,28,38,48], '9': [19,29,39,49],
    '10': [110,210,310,410],'11': [111,211,311,411]})

html = df.style.set_uuid('table')

def on_click(change):
    cls = change['new'].split(' ')
    row = int(re.search(r'\d+',cls[1]).group(0))
    print('row=', row)
    print(df.iloc[[row]])

click_handler = wgt.Text(placeholder='undefined')
click_handler.observe(on_click,names=['value'])

out = wgt.Output()
with out: display(html)

display(out,click_handler,HTML(script))
Sandre
  • 498
  • 6
  • 10
  • Really helpful. I wasn't seeing the prints in JupyterLab 3.2.5, but having read Issue 2148 on GitHub I found I needed to reorder the code so `out` is created before `on_click` and to add `@out.capture()` before the definition of the callback so the prints in the callback displayed. – Chris Pointon Jan 19 '22 at 16:25