jupyter-widgets / ipydatagrid

Fast Datagrid widget for the Jupyter Notebook and JupyterLab
BSD 3-Clause "New" or "Revised" License
580 stars 51 forks source link

Add Styling/Themes #247

Closed ibdafna closed 3 years ago

ibdafna commented 3 years ago

Signed-off-by: Itay Dafna i.b.dafna@gmail.com

This PR adds styling/theming support for ipydatagrid by exposing some styling properties lumino/datagrid has available. There is some overlap with properties such as backgroundColor which can also be set by TextRenderer, but overall it does allow for more customisation of the grid's style by enabling setting style properties such as grid lines, etc.

We will add the following styles:

import pandas as pd
import numpy as np
import ipydatagrid as g

np.random.seed(104)
rang = 20
df = pd.DataFrame(
    data=[np.random.randint(0, 11, rang) for i in range(rang)],
    index=[f'Row {i}' for i in range(rang)],
    columns=[f'Col {i}' for i in range(rang)]
)

style = {
    'voidColor': '#111',
    'backgroundColor': 'purple',
#     'rowBackgroundColor': 'green', // Needs a function (vegaExpr?)
#     'columnBackgroundColor': 'yellow' // Needs a function (vegaExpr?)
      'gridLineColor': 'red',
     'verticalGridLineColor': 'navy',
     'horizontalGridLineColor': 'purple',
     'headerBackgroundColor': 'orange',
     'headerGridLineColor': 'green',
     'headerVerticalGridLineColor': 'yellow',
     'headerHorizontalGridLineColor': 'orange',
     'selectionFillColor': 'yellow',
     'selectionBorderColor': 'limegreen',
     'headerSelectionFillColor': 'darkorange',
      'headerSelectionBorderColor': 'green',
      'cursorFillColor': 'magenta',
      'cursorBorderColor': 'limegreen',
      'scrollShadow': {
          'size': 8,
          'color1': 'salmon',
          'color2': 'pink',
          'color3': 'gray'
      }
}

g.DataGrid(df, layout={'height': '300px'}, selection_mode='cell', grid_style=style)

PLEASE EXCUSE THE HIDEOUS STYLING HERE - THIS IS JUST FOR DEMO PURPOSES 🙈 🙉 🙊 image

I will map all properties so that we use snake_case on the Python side for consistency. I would like to keep this as simple as possible and allow the users to just pass a dictionary with these properties. Properties which are specified will take precedence over any defaults or values set in TextRenderer/BarRenderer, otherwise we fall back to the existing behaviour.

Two of the properties here, rowBackgroundColor and columnBackgroundColor actually take a function (index: number) => string. @martinRenou I am thinking VegaExpr would be perfect here. We just need to whitelist the index arguments and hopefully it should work, what are your thoughts? How would this work when using VegaExpr/Expr as one of the values mapped to by a key in the Dict() traitlet? If there's a way to achieve this then great, otherwise we can create a Theme class which will have all these styles as properties with Union(Dict(), Instance(VegaExpr)).

EDIT: Would also make sense to use Dict() with keys mapping to Color() traitlets.

martinRenou commented 3 years ago

I am thinking VegaExpr would be perfect here. We just need to whitelist the index arguments and hopefully it should work, what are your thoughts?

👍🏽 👍🏽

If there's a way to achieve this then great, otherwise we can create a Theme class which will have all these styles as properties

I am actually wondering if this styling could be done using a custom Style class, the same way we do it for the Map in ipyleaflet https://github.com/jupyter-widgets/ipyleaflet/blob/master/ipyleaflet/leaflet.py#L1917. Although, if I recall correctly, those are for real CSS styling, which is not what we need in ipydatagrid.

The two approaches are fine I guess, but there would be less overhead not having to create another Theme or Style widget, so I would maybe go for a Dict with a custom serializer/deserializer. Did you try just using the widgetserialization on the Dict? Maybe it works out of the box by serializing widgets in the dict without modifying the rest of the dict.

kaiayoung commented 3 years ago

It appears that styling is only applied when a DataGrid widget is instantiated? Since grid_style is a trait, users will likely expect that updating grid_style on an already displayed grid would update it with the new styling.

ibdafna commented 3 years ago

PR ready for review!

Users now pass snake_case parameters to the Dict and these get serialised as camelCase on the front-end. We also have an event listener to handle any changes to the grid_style traitlet.

@martinRenou would appreciate if you could also take a quick look here, especially on how I'm handling Vega Expressions in my custom serializer. I used the code below for testing.

Thanks!

ipydatagrid_styles

import pandas as pd
import numpy as np
import ipydatagrid as g

np.random.seed(104)
rang = 20
df = pd.DataFrame(
    data=[np.random.randint(0, 11, rang) for i in range(rang)],
    index=[f'Row {i}' for i in range(rang)],
    columns=[f'Col {i}' for i in range(rang)]
)

style = {
#     'void_color': '#111',
#     'background_color': 'salmon',
#     'row_background_color': g.VegaExpr("cell % 2 === 0 ? 'yellow' : 'green'"),
    'column_background_color': g.VegaExpr("cell % 2 === 0 ? 'rgb(204, 255, 255)' : 'rgb(204, 220, 255)'"),
#     'grid_line_color': 'rgb(184, 210, 235)',
    'vertical_grid_line_color': 'rgb(0, 51, 102)',
    'horizontal_grid_line_color': 'rgb(245, 245, 245, 0.4)',
#     'header_background_color': 'magenta',
#     'header_grid_line_color': 'rgb(153, 0, 255, 1)',
#     'header_vertical_grid_line_color': 'yellow',
#     'header_horizontal_grid_line_color': 'orange',
    'selection_fill_color': 'rgb(102, 255, 102, 0.5)',
    'selection_border_color': 'yellow',
    'header_selection_fill_color': 'rgb(255, 204, 153, 0.4)',
    'header_selection_border_color': 'lawngreen',
    'cursor_fill_color': 'rgb(255, 51, 204, 0.5)',
    'cursor_border_color': 'limegreen',
#     'scroll_shadow': {
#         'size': 50,
#         'color1': 'salmon',
#         'color2': 'pink',
#         'color3': 'gray'
#     }
}

rend = g.TextRenderer(
    horizontal_alignment='center',
#     background_color='green',
#     text_color='yellow'
)

grid = g.DataGrid(df, layout={'height': '500px'}, selection_mode='cell', 
                  grid_style=style,
                  default_renderer=rend,
#                   header_renderer=rend

)

grid
ibdafna commented 3 years ago

Tested this again and it's working beautifully. Martin is off for three weeks so I'll merge for now.