plotly / dash-table

OBSOLETE: now part of https://github.com/plotly/dash
https://dash.plotly.com
MIT License
419 stars 74 forks source link

CSS Import Module for DataTable Styling #843

Open kr-hansen opened 3 years ago

kr-hansen commented 3 years ago

A feature I think would be helpful is a way to import styles from a CSS file to use in the DataTable styling.

It seems difficult to style a DataTable using straight CSS as highlighted in these forum posts: https://community.plotly.com/t/dash-datatable-css-from-page/16633 https://community.plotly.com/t/how-to-use-css-and-js-with-dash/27587/4

Ultimately, it looks like DataTable styling should be done via style_* components in the DataTable, which are essentially CSS components. My use case that this is annoying is I'm working in a setting that we have group CSS files to adhere to that we work with and share via scss files that get compiled into css. This includes a broad array of colors and other standards that are "variable-ized" using scss that we need to adhere to for our projects.

I have created some HTML tables in our apps in Dash that work great and easily leverage these CSS standards. However, I am wanting to get some of the added functionality of the DataTable while maintaining the same CSS styling from my standard HTML tables. I'm guessing there are multiple dependencies in DataTables that overwrite various CSS, since adding CSS formatting from inspecting elements for table {} or .cell-table tbody tr th {} doesn't seem to make it through to the DataTable, hence I imagine the docs recommendation to use style_* components to address those dependencies. Is that correct, or can this be simply addressed in some form in this manner?

I don't have much experience with Javascript, but am pretty proficient in Python. Assuming there is no simple solution above that I haven't been able to figure out, I would propose to write a python class for dash_table (something like css) that would parse a CSS file and save each CSS selector as a descriptor of the class, which would simply be a Dash-adjusted dictionary (camelCase selectors) of the raw CSS selector.

A use case would look something like this.

my.css

.my-header {
  background-color: #aaaaaa;
  color: #ffffff;
}

my_app.py - Simplified without data to highlight use

import dash_table
import dash
import dash_html_components as html

app = dash.Dash(__name__)

myCSS = dash_table.css('assets/my.css')

app.layout = html.Div(
  dash_table.DataTable(style_header=myCSS.myHeader)

The variable myCSS.myHeader would be a Python Dict that looked like this:

{'backgroundColor': '#aaaaaa', 'color': '#ffffff'}

I would also assume that css components with multiple elements would be nested as a list.
For example CSS of: border: 1px solid #aaaaaa

would be in the Python Dict as: {'border': ['1px', 'solid', '#aaaaaa']}

Are there major issues or reasons why this wouldn't work? It wouldn't break DataTable functionality and make it simpler to utilize custom CSS in DataTables in the way DataTables want you to style elements. I also think this module would be a better fit for dash_table than raw dash, as the styling constraints on Dash DataTables don't seem to be the same as for other portions of Dash.

kr-hansen commented 3 years ago

So I have built a python function that more or less implements the above functionality. If I wanted to implement it in dash_table as a function or class, where should I put it? Is there somewhere that the python components of the code are housed, as "src" seems to be the main Javascript components.

Can I provide this function in python within dash_table or does it need to be translated to Javascript?

SoufianeDataFan commented 3 years ago

@kr-hansen I think dash-table has its own styling methods and arguments. check the docs for more details.

kr-hansen commented 3 years ago

@SoufianeDataFan I saw you just edited your comment. I know those styling methods exist. This suggestion is for this specific type of use case.

I'm creating a multi-page app with several different tables that I want to be styled in the same/similar manner, so I load the CSS using this function in a utilities.py piece of code that I import to the pages that I need to style my tables the way I want. I think for my use case this is a cleaner and more consistent way to style the tables and make changes to all the tables at once, rather than needing to go in to each individual page and change it.

Additionally, I need to use a style guide with colors pre-defined in CSS, so rather than trying to convert all of those and hard-code the hex values into Python, I can leverage the existing SCSS scripts with those pre-definitions for my team.

I never heard anything on any interest of rolling this into dash-table itself and I wasn't sure where they'd want it. I had a few ideas, but I was waiting for feedback from the developers before just picking where I'd want it and putting it in. @chriddyp any thoughts on if this would be useful/beneficial in here more broadly?

I'd read somewhere that they want to come up with a longer term solution, so they don't want to expose all the classes and such that people would just change it in their own CSS, which I think makes sense as it is pretty involved from what I've found inspecting it. Additionally, to get a lot of stuff to work with that, you need to use the !important tag, which is kind of messy from my experience.

The solution I came up with and have used is to load the CSS into a python dictionary, then provide that style dictionary to the dash-table as the intended style component. This works pretty well, but the main drawback is it doesn't hot-load in the standard way for debugging, since you can change the CSS, but you need to reload the python again to see those changes, but that is just slightly annoying.

Here is the function I'm using to load in a CSS file, if it is useful for anyone that may have a use case similar to mine.

#Function to Load CSS as dict
def load_css(path_to_css, excludeVals=['@import'], breakVals=['/*']):
    print('Loading CSS into Dictionary')
    #Function to load CSS file as dictionary to use with Dash DataTables
    with open(path_to_css) as f: #Load all CSS file
        alltext = f.readlines()

    openVal='{'
    closeVal='}'
    outDict={}
    inSelector=0

    for line in alltext: #Loop through lines
        if any(exVal not in line for exVal in excludeVals):
            if closeVal in line: #Ending of css selector
                outDict[mainKey] = selDict
                inSelector=0
            elif inSelector: #Inside of css selector
                #Get Property Name (Can't yet handle -webkit props for display:flex, props separated by multiple '-', or multiple 'display' correctly)
                rawProp = line.split(':')[0].lstrip(' ') #Drop extra whitespace at the beginning
                splitProp = rawProp.split('-')
                if len(splitProp) == 1:
                    camelProp = splitProp[0]
                else:
                    camelProp = splitProp[0] + splitProp[1].capitalize() #camelCase
                #Get Property Values
                rawPropVals = line.split(':')[1].split(';')[0] #Drop newline and semicolon
                parsedPropVals = [val.replace('"','').lstrip(' ') for val in rawPropVals.split(',')] #Parse by comma, and drop spaces/quotes
                #Add to selector dictionary
                selDict[camelProp] = parsedPropVals
            if openVal in line: #Beginning of css selector
                mainKey = ' '.join(line.split(' ')[:-1])
                selDict = {}
                inSelector=1
        if any(brVal in line for brVal in breakVals): #Assume stop if any block comments at bottom
            break
    return outDict

I then load my css file using something like cssDict = load_css('assets/main.css') and I now have my CSS as a dictionary in Python.

I will then feed this dictionary into the dash-table and it will address all the correct styles without having to do any messy !important tags and things work more cleanly.

Additional tricks I'll use that this process simplifies includes things like creating my conditional styles for easy load/tweaking/reloading like:

defaultCondDataStyleTable = [
    {'if': {'state': 'active'},
    **cssDict['.table-rows:active']},
    {'if': {'state': 'selected'},
    **cssDict['.table-rows::selection']},
    {'if': {'row_index': 'odd'},
    **cssDict['.table-rows:nth-child(odd)']},
    {'if': {'row_index': 'even'},
    **cssDict['.table-rows:nth-child(even)']}
]

Then, when I create my Dash Table it looks like this:

dash_table.DataTable(
   id='myTable',
   row_selectable='multi',
   style_cell=cssDict['.table-cell'],
   style_header=cssDict['.table-header'],
   style_data_conditional=defaultCondDataStyleTable,
   style_cell_conditional=defaultCondCellStyleTable,
)

To some degree, this makes what the styles are a little opaque from where the Dash-Table lives within the code, which is a negative of this approach. However, for my use case as defined above, with a multi-page/tab app and pre-defined styles in SCSS all ready that I can leverage, this utility function is extremely useful.

If this is more broadly of use, I'd be happy to roll this as a function into the dash_table class and submit a pull request, so you could do something like dash_table.load_css(path_to_css) to get a CSS Dictionary into python to use in this way. However, I wanted to get the input of the developers to see if having this as a utility function within the dash_table class is where it makes the most sense to have a function like this live.