Skip to content

BUG+DOC: Recent Styler Enhancements #39317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 24, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 231 additions & 36 deletions doc/source/user_guide/style.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,19 @@
"\n",
"The styling is accomplished using CSS.\n",
"You write \"style functions\" that take scalars, `DataFrame`s or `Series`, and return *like-indexed* DataFrames or Series with CSS `\"attribute: value\"` pairs for the values.\n",
"These functions can be incrementally passed to the `Styler` which collects the styles before rendering."
"These functions can be incrementally passed to the `Styler` which collects the styles before rendering.\n",
"\n",
"CSS is a flexible language and as such there may be multiple ways of achieving the same result, with potential\n",
"advantages or disadvantages, which we try to illustrate."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Building styles\n",
"\n",
"Pass your style functions into one of the following methods:\n",
"\n",
"- ``Styler.applymap``: elementwise\n",
"- ``Styler.apply``: column-/row-/table-wise\n",
"\n",
"Both of those methods take a function (and some other keyword arguments) and applies your function to the DataFrame in a certain way.\n",
"`Styler.applymap` works through the DataFrame elementwise.\n",
"`Styler.apply` passes each column or row into your DataFrame one-at-a-time or the entire table at once, depending on the `axis` keyword argument.\n",
"For columnwise use `axis=0`, rowwise use `axis=1`, and for the entire table at once use `axis=None`.\n",
"## Styler Object\n",
"\n",
"For `Styler.applymap` your function should take a scalar and return a single string with the CSS attribute-value pair.\n",
"\n",
"For `Styler.apply` your function should take a Series or DataFrame (depending on the axis parameter), and return a Series or DataFrame with an identical shape where each value is a string with a CSS attribute-value pair.\n",
"\n",
"Let's see some examples."
"The `DataFrame.style` attribute is a property that returns a `Styler` object. `Styler` has a `_repr_html_` method defined on it so they are rendered automatically. If you want the actual HTML back for further processing or for writing to file call the `.render()` method which returns a string."
]
},
{
Expand All @@ -61,21 +50,23 @@
"outputs": [],
"source": [
"import pandas as pd\n",
"from pandas.io.formats.style import Styler\n",
"import numpy as np\n",
"\n",
"np.random.seed(24)\n",
"df = pd.DataFrame({'A': np.linspace(1, 10, 10)})\n",
"df = pd.concat([df, pd.DataFrame(np.random.randn(10, 4), columns=list('BCDE'))],\n",
" axis=1)\n",
"df.iloc[3, 3] = np.nan\n",
"df.iloc[0, 2] = np.nan"
"df.iloc[0, 2] = np.nan\n",
"df.style"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here's a boring example of rendering a DataFrame, without any (visible) styles:"
"The above output looks very similar to the standard DataFrame HTML representation. But we've done some work behind the scenes to attach CSS classes to each cell. We can view these by calling the `.render` method."
]
},
{
Expand All @@ -84,16 +75,40 @@
"metadata": {},
"outputs": [],
"source": [
"df.style"
"df.style.render().split('\\n')[:10]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `row0_col2` is the identifier for that particular cell. We've also prepended each row/column identifier with a UUID unique to each DataFrame so that the style from one doesn't collide with the styling from another within the same notebook or page (you can set the `uuid` if you'd like to tie together the styling of two DataFrames, or remove it if you want to optimise HTML transfer for larger tables)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*Note*: The `DataFrame.style` attribute is a property that returns a `Styler` object. `Styler` has a `_repr_html_` method defined on it so they are rendered automatically. If you want the actual HTML back for further processing or for writing to file call the `.render()` method which returns a string.\n",
"## Building styles\n",
"\n",
"The above output looks very similar to the standard DataFrame HTML representation. But we've done some work behind the scenes to attach CSS classes to each cell. We can view these by calling the `.render` method."
"There are 3 primary methods of adding custom styles to DataFrames using CSS and matching it to cells:\n",
"\n",
"- Directly linking external CSS classes to your individual cells using `Styler.set_td_classes`.\n",
"- Using `table_styles` to control broader areas of the DataFrame with internal CSS.\n",
"- Using the `Styler.apply` and `Styler.applymap` functions for more specific control with internal CSS. \n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Linking External CSS\n",
"\n",
"*New in version 1.2.0*\n",
"\n",
"If you have designed a website then it is likely you will already have an external CSS file that controls the styling of table and cell objects within your website.\n",
"\n",
"For example, suppose we have an external CSS which controls table properties and has some additional classes to style individual elements (here we manually add one to this notebook):"
]
},
{
Expand All @@ -102,16 +117,110 @@
"metadata": {},
"outputs": [],
"source": [
"df.style.highlight_null().render().split('\\n')[:10]"
"from IPython.display import HTML\n",
"style = \\\n",
"\"<style>\"\\\n",
"\".table-cls {color: grey;}\"\\\n",
"\".cls1 {background-color: red; color: white;}\"\\\n",
"\".cls2 {background-color: blue; color: white;}\"\\\n",
"\".cls3 {font-weight: bold; font-style: italic; font-size:1.8em}\"\\\n",
"\"</style>\"\n",
"HTML(style)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `row0_col2` is the identifier for that particular cell. We've also prepended each row/column identifier with a UUID unique to each DataFrame so that the style from one doesn't collide with the styling from another within the same notebook or page (you can set the `uuid` if you'd like to tie together the styling of two DataFrames).\n",
"Now we can manually link these to our DataFrame using the `Styler.set_table_attributes` and `Styler.set_td_classes` methods (note that table level 'table-cls' is overwritten here by Jupyters own CSS, but in HTML the default text color will be grey)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"s = df.style.set_table_attributes('class=\"table-cls\"')\n",
"cls = pd.DataFrame(data=[['cls1', None], ['cls3', 'cls2 cls3']], index=[0,2], columns=['A', 'C'])\n",
"s.set_td_classes(cls)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The **advantage** of linking to external CSS is that it can be applied very easily. One can build a DataFrame of (multiple) CSS classes to add to each cell dynamically using traditional `DataFrame.apply` and `DataFrame.applymap` methods, or otherwise, and then add those to the Styler. It will integrate with your website's existing CSS styling.\n",
"\n",
"The **disadvantage** of this approach is that it is not easy to transmit files standalone. For example the external CSS must be included or the styling will simply be lost. It is also, as this example shows, not well suited (at a table level) for Jupyter Notebooks. Also this method cannot be used for exporting to Excel, for example, since the external CSS cannot be referenced either by the exporters or by Excel itself."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Using Table Styles\n",
"\n",
"Table styles allow you to control broader areas of the DataFrame styling with minimal HTML transfer. Much of the functionality of `Styler` uses individual HTML id tags to manipulate the output, which may be inefficient for very large tables. Using `table_styles` and otherwise avoiding using id tags can greatly reduce the rendered HTML.\n",
"\n",
"Table styles are also used to control features which can apply to the whole table at once such as greating a generic hover functionality:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"s = Styler(df, cell_ids=False, uuid_len=1)\n",
"s.set_table_styles([{'selector': 'tr:hover',\n",
" 'props': [('background-color', 'yellow')]}])\n",
"s.set_table_styles({\n",
" 'A': [{'selector': '',\n",
" 'props': [('color', 'red')]}],\n",
" 'B': [{'selector': 'td',\n",
" 'props': [('color', 'blue')]}]\n",
"}, axis=0, overwrite=False)\n",
"s.set_table_styles({\n",
" 3: [{'selector': 'td',\n",
" 'props': [('color', 'green')]}]\n",
"}, axis=1, overwrite=False)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The **advantage** of table styles is obviously the reduced HTML that it can create and the relative ease with which more general parts of the table can be quickly styled, e.g. by applying a generic hover, rather than having to apply a hover to each cell individually. Rows and columns as individual objects can only be styled in this way.\n",
"\n",
"The **disadvantage** of being restricted solely to table styles is that you have very limited ability to target and style individual cells based on dynamic criteria. For this one must use either of the other two methods. Also table level styles cannot be exported to Excel: to format cells for Excel output you must use the Styler Functions method below."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Styler Functions\n",
"\n",
"Thirdly we can use the method to pass your style functions into one of the following methods:\n",
"\n",
"When writing style functions, you take care of producing the CSS attribute / value pairs you want. Pandas matches those up with the CSS classes that identify each cell."
"- ``Styler.applymap``: elementwise\n",
"- ``Styler.apply``: column-/row-/table-wise\n",
"\n",
"Both of those methods take a function (and some other keyword arguments) and applies your function to the DataFrame in a certain way.\n",
"`Styler.applymap` works through the DataFrame elementwise.\n",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure how easy this is to link to the actual docs for these but would be great if possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i tried. i think it is possible but the relative path structure is quite complicated and i couldn't get it to work. never done it before so I have left out.

"`Styler.apply` passes each column or row into your DataFrame one-at-a-time or the entire table at once, depending on the `axis` keyword argument.\n",
"For columnwise use `axis=0`, rowwise use `axis=1`, and for the entire table at once use `axis=None`.\n",
"\n",
"For `Styler.applymap` your function should take a scalar and return a single string with the CSS attribute-value pair.\n",
"\n",
"For `Styler.apply` your function should take a Series or DataFrame (depending on the axis parameter), and return a Series or DataFrame with an identical shape where each value is a string with a CSS attribute-value pair.\n",
"\n",
"The **advantage** of this method is that there is full granular control and the output is isolated and easily transferrable, especially in Jupyter Notebooks.\n",
"\n",
"The **disadvantage** is that the HTML/CSS required to produce this needs to be directly generated from the Python code and it can lead to inefficient data transfer for large tables.\n",
"\n",
"Let's see some examples."
]
},
{
Expand Down Expand Up @@ -210,7 +319,34 @@
"cell_type": "markdown",
"metadata": {},
"source": [
"We encourage you to use method chains to build up a style piecewise, before finally rending at the end of the chain."
"A common use case is also to highlight values based on comparison between columns. Suppose we wish to highlight those cells in columns 'B' and 'C' which are lower than respective values in 'E' then we can write a comparator function. (You can read a little more below in 'Finer Control: Slicing')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def compare_col(s, comparator=None):\n",
" attr = 'background-color: #00BFFF;'\n",
" return np.where(s < comparator, attr, '')"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"df.style.apply(compare_col, subset=['B', 'C'], comparator=df['E'])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We encourage you to use method chains to build up a style piecewise, before finally rending at the end of the chain. Note the ordering of application will affect styles that overlap."
]
},
{
Expand All @@ -220,6 +356,7 @@
"outputs": [],
"source": [
"df.style.\\\n",
" apply(compare_col, subset=['B', 'C'], comparator=df['E']).\\\n",
" applymap(color_negative_red).\\\n",
" apply(highlight_max)"
]
Expand Down Expand Up @@ -271,7 +408,8 @@
"metadata": {},
"outputs": [],
"source": [
"df.style.apply(highlight_max, color='darkorange', axis=None)"
"s = df.style.apply(highlight_max, color='darkorange', axis=None)\n",
"s"
]
},
{
Expand All @@ -290,6 +428,62 @@
"And crucially the input and output shapes of `func` must match. If `x` is the input then ``func(x).shape == x.shape``."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Tooltips\n",
"\n",
"*New in version 1.3.0*\n",
"\n",
"You can now add tooltips in the same way you can add external CSS classes to datacells by providing a string based DataFrame with intersecting indices and columns."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"tt = pd.DataFrame(data=[[None, 'No Data', None], \n",
" [None, None, 'Missing Data'], \n",
" ['Maximum value across entire DataFrame', None, None]], \n",
" index=[0, 3, 9], \n",
" columns=['A', 'C', 'D'])\n",
"s.set_tooltips(tt)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The tooltips are added with a default CSS styling, however, you have full control of the tooltips in the following way. The name of the class can be integrated with your existing website's CSS so you do not need to set any properties within Python if you have the external CSS files. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"s.set_tooltips_class(name='pd-tt', properties=[\n",
" ('visibility', 'hidden'),\n",
" ('position', 'absolute'),\n",
" ('z-index', '1'),\n",
" ('background-color', 'blue'),\n",
" ('color', 'white'),\n",
" ('font-size', '1.5em'),\n",
" ('transform', 'translate(3px, -11px)'),\n",
" ('padding', '0.5em'),\n",
" ('border', '1px solid red'),\n",
" ('border-radius', '0.5em')\n",
"])"
]
},
{
"cell_type": "markdown",
"metadata": {},
Expand Down Expand Up @@ -843,20 +1037,19 @@
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"pycharm": {
"name": "#%%\n"
}
},
"outputs": [],
"source": [
"html = html.set_table_styles({\n",
" 'B': [dict(selector='', props=[('color', 'green')])],\n",
" 'C': [dict(selector='td', props=[('color', 'red')])], \n",
" }, overwrite=False)\n",
"html"
],
"metadata": {
"collapsed": false,
"pycharm": {
"name": "#%%\n"
}
}
]
},
{
"cell_type": "markdown",
Expand Down Expand Up @@ -1053,7 +1246,9 @@
"\n",
"- Only CSS2 named colors and hex colors of the form `#rgb` or `#rrggbb` are currently supported.\n",
"- The following pseudo CSS properties are also available to set excel specific style properties:\n",
" - `number-format`\n"
" - `number-format`\n",
"\n",
"Table level styles are not included in the export to Excel: individual cells must have their properties mapped by the `Styler.apply` and/or `Styler.applymap` methods."
]
},
{
Expand Down Expand Up @@ -1262,7 +1457,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.0"
"version": "3.8.6"
}
},
"nbformat": 4,
Expand Down
Loading