From a169f6427db559708aadbbf06ba723bf143eadae Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 21 Jan 2021 12:59:11 +0100 Subject: [PATCH 01/34] bug: cell ids included classes bug: set_td_classes loop ignored entries bug: tooltips indexing was dropped doc: added documentation for methods --- doc/source/user_guide/style.ipynb | 267 ++++++++++++++++++++++++++---- pandas/io/formats/style.py | 23 +-- 2 files changed, 244 insertions(+), 46 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 24f344488d1ca..2793bb1dc58d6 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -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." ] }, { @@ -61,6 +50,7 @@ "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", @@ -68,14 +58,15 @@ "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." ] }, { @@ -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):" ] }, { @@ -102,16 +117,110 @@ "metadata": {}, "outputs": [], "source": [ - "df.style.highlight_null().render().split('\\n')[:10]" + "from IPython.display import HTML\n", + "style = \\\n", + "\"\"\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", + "`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." ] }, { @@ -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." ] }, { @@ -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)" ] @@ -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" ] }, { @@ -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": {}, @@ -843,6 +1037,11 @@ { "cell_type": "code", "execution_count": null, + "metadata": { + "pycharm": { + "name": "#%%\n" + } + }, "outputs": [], "source": [ "html = html.set_table_styles({\n", @@ -850,13 +1049,7 @@ " 'C': [dict(selector='td', props=[('color', 'red')])], \n", " }, overwrite=False)\n", "html" - ], - "metadata": { - "collapsed": false, - "pycharm": { - "name": "#%%\n" - } - } + ] }, { "cell_type": "markdown", @@ -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." ] }, { @@ -1262,7 +1457,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.8.6" } }, "nbformat": 4, diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 782562f455607..2776c680ae68c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -511,16 +511,15 @@ def format_attr(pair): for c, col in enumerate(self.data.columns): cs = [DATA_CLASS, f"row{r}", f"col{c}"] - cs.extend(cell_context.get("data", {}).get(r, {}).get(c, [])) formatter = self._display_funcs[(r, c)] value = self.data.iloc[r, c] row_dict = { "type": "td", "value": value, - "class": " ".join(cs), "display_value": formatter(value), "is_visible": (c not in hidden_columns), } + # only add an id if the cell has a style props = [] if self.cell_ids or (r, c) in ctx: @@ -531,6 +530,11 @@ def format_attr(pair): props.append(tuple(x.split(":"))) else: props.append(("", "")) + + # add custom classes from cell context + cs.extend(cell_context.get("data", {}).get(r, {}).get(c, [])) + row_dict["class"] = " ".join(cs) + row_es.append(row_dict) cellstyle_map[tuple(props)].append(f"row{r}_col{c}") body.append(row_es) @@ -690,10 +694,12 @@ def set_td_classes(self, classes: DataFrame) -> Styler: mask = (classes.isna()) | (classes.eq("")) self.cell_context["data"] = { - r: {c: [str(classes.iloc[r, c])]} + r: { + c: [str(classes.iloc[r, c])] + for c, cn in enumerate(classes.columns) + if not mask.iloc[r, c] + } for r, rn in enumerate(classes.index) - for c, cn in enumerate(classes.columns) - if not mask.iloc[r, c] } return self @@ -1914,11 +1920,8 @@ def _translate(self, styler_data: FrameOrSeriesUnion, uuid: str, d: Dict): ------- render_dict : Dict """ - self.tt_data = ( - self.tt_data.reindex_like(styler_data) - .dropna(how="all", axis=0) - .dropna(how="all", axis=1) - ) + self.tt_data = self.tt_data.reindex_like(styler_data) + if self.tt_data.empty: return d From 847ec69f113814eb271bf93996c275b26f9f5188 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Thu, 21 Jan 2021 21:26:58 +0100 Subject: [PATCH 02/34] add tests --- pandas/tests/io/formats/test_style.py | 41 ++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index c61d81d565459..51810786395b5 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1706,11 +1706,34 @@ def test_no_cell_ids(self): def test_set_data_classes(self, classes): # GH 36159 df = DataFrame(data=[[0, 1], [2, 3]], columns=["A", "B"], index=["a", "b"]) - s = Styler(df, uuid="_", cell_ids=False).set_td_classes(classes).render() + s = Styler(df, uuid_len=0, cell_ids=False).set_td_classes(classes).render() assert '0' in s assert '1' in s assert '2' in s assert '3' in s + # GH 39317 + s = Styler(df, uuid_len=0, cell_ids=True).set_td_classes(classes).render() + assert '0' in s + assert '1' in s + assert '2' in s + assert '3' in s + + def test_set_data_classes_reindex(self): + # GH 39317 + df = DataFrame( + data=[[0, 1, 2], [3, 4, 5], [6, 7, 8]], columns=[0, 1, 2], index=[0, 1, 2] + ) + classes = DataFrame( + data=[["mi", "ma"], ["mu", "mo"]], + columns=[0, 2], + index=[0, 2], + ) + s = Styler(df, uuid_len=0).set_td_classes(classes).render() + assert '0' in s + assert '2' in s + assert '4' in s + assert '6' in s + assert '8' in s def test_chaining_table_styles(self): # GH 35607 @@ -1815,6 +1838,22 @@ def test_tooltip_render(self, ttips): in s ) + def test_tooltip_reindex(self): + # GH 39317 + df = DataFrame( + data=[[0, 1, 2], [3, 4, 5], [6, 7, 8]], columns=[0, 1, 2], index=[0, 1, 2] + ) + ttips = DataFrame( + data=[["Mi", "Ma"], ["Mu", "Mo"]], + columns=[0, 2], + index=[0, 2], + ) + s = Styler(df, uuid_len=0).set_tooltips(DataFrame(ttips)).render() + assert '#T__ #T__row0_col0 .pd-t::after {\n content: "Mi";\n }' in s + assert '#T__ #T__row0_col2 .pd-t::after {\n content: "Ma";\n }' in s + assert '#T__ #T__row2_col0 .pd-t::after {\n content: "Mu";\n }' in s + assert '#T__ #T__row2_col2 .pd-t::after {\n content: "Mo";\n }' in s + def test_tooltip_ignored(self): # GH 21266 df = DataFrame(data=[[0, 1], [2, 3]]) From e436a95a9ae4e95943542b49719a4330a60d40b6 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Fri, 22 Jan 2021 09:53:40 +0100 Subject: [PATCH 03/34] doc fixes --- doc/source/user_guide/style.ipynb | 182 +++++++++++++++--------------- 1 file changed, 93 insertions(+), 89 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 2793bb1dc58d6..114b4688fffaf 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -50,7 +50,6 @@ "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", @@ -161,9 +160,38 @@ "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", + "Table styles allow you to control broader areas of the DataFrame, i.e. the whole table or specific columns or rows, 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 in data cells 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. This `:hover` pseudo-selectors, as well as others, can only be used this way.\n", + "\n", + "`table_styles` are extremely flexible, but not as fun to type out by hand.\n", + "We hope to collect some useful ones either in pandas, or preferable in a new package that [builds on top](#Extensibility) the tools here." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def hover(hover_color=\"#ffff99\"):\n", + " return {'selector': \"tr:hover\",\n", + " 'props': [(\"background-color\", \"%s\" % hover_color)]}\n", + "\n", + "styles = [\n", + " hover(),\n", + " {'selector': \"th\", 'props': [(\"font-size\", \"150%\"),\n", + " (\"text-align\", \"center\")]}\n", + "]\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:" + "df.style.set_table_styles(styles)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If `table_styles` is given as a dictionary each key should be a specified column or index value and this will map to specific class CSS selectors of the given column or row." ] }, { @@ -172,19 +200,69 @@ "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", + "df.style.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", + "}, axis=0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.style.set_table_styles({\n", " 3: [{'selector': 'td',\n", " 'props': [('color', 'green')]}]\n", - "}, axis=1, overwrite=False)" + "}, axis=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also chain all of the above by setting the `overwrite` argument to `False` so that it preserves previous settings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pandas.io.formats.style import Styler\n", + "s = Styler(df, cell_ids=False, uuid_len=0).\\\n", + " set_table_styles(styles).\\\n", + " set_table_styles({\n", + " 'A': [{'selector': '',\n", + " 'props': [('color', 'red')]}],\n", + " 'B': [{'selector': 'td',\n", + " 'props': [('color', 'blue')]}]\n", + " }, axis=0, overwrite=False).\\\n", + " set_table_styles({\n", + " 3: [{'selector': 'td',\n", + " 'props': [('color', 'green')]}]\n", + " }, axis=1, overwrite=False)\n", + "s" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By using these `table_styles` and the additional `Styler` arguments to optimize the HTML we have compressed these styles to only a few lines withing the \\ tags and none of the \\ cells require any `id` attributes. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s.render().split('\\n')[:16]" ] }, { @@ -193,7 +271,7 @@ "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." + "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." ] }, { @@ -961,7 +1039,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Regular table captions can be added in a few ways." + "Regular table captions can be added and, if necessary, controlled with CSS." ] }, { @@ -971,86 +1049,12 @@ "outputs": [], "source": [ "df.style.set_caption('Colormaps, with a caption.')\\\n", + " .set_table_styles([{\n", + " 'selector': \"caption\", 'props': [(\"caption-side\", \"bottom\")]\n", + " }])\\\n", " .background_gradient(cmap=cm)" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Table styles" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The next option you have are \"table styles\".\n", - "These are styles that apply to the table as a whole, but don't look at the data.\n", - "Certain stylings, including pseudo-selectors like `:hover` can only be used this way.\n", - "These can also be used to set specific row or column based class selectors, as will be shown." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import HTML\n", - "\n", - "def hover(hover_color=\"#ffff99\"):\n", - " return dict(selector=\"tr:hover\",\n", - " props=[(\"background-color\", \"%s\" % hover_color)])\n", - "\n", - "styles = [\n", - " hover(),\n", - " dict(selector=\"th\", props=[(\"font-size\", \"150%\"),\n", - " (\"text-align\", \"center\")]),\n", - " dict(selector=\"caption\", props=[(\"caption-side\", \"bottom\")])\n", - "]\n", - "html = (df.style.set_table_styles(styles)\n", - " .set_caption(\"Hover to highlight.\"))\n", - "html" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "`table_styles` should be a list of dictionaries.\n", - "Each dictionary should have the `selector` and `props` keys.\n", - "The value for `selector` should be a valid CSS selector.\n", - "Recall that all the styles are already attached to an `id`, unique to\n", - "each `Styler`. This selector is in addition to that `id`.\n", - "The value for `props` should be a list of tuples of `('attribute', 'value')`.\n", - "\n", - "`table_styles` are extremely flexible, but not as fun to type out by hand.\n", - "We hope to collect some useful ones either in pandas, or preferable in a new package that [builds on top](#Extensibility) the tools here.\n", - "\n", - "`table_styles` can be used to add column and row based class descriptors. For large tables this can increase performance by avoiding repetitive individual css for each cell, and it can also simplify style construction in some cases.\n", - "If `table_styles` is given as a dictionary each key should be a specified column or index value and this will map to specific class CSS selectors of the given column or row.\n", - "\n", - "Note that `Styler.set_table_styles` will overwrite existing styles but can be chained by setting the `overwrite` argument to `False`." - ] - }, - { - "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" - ] - }, { "cell_type": "markdown", "metadata": {}, From 48790941b6e18f3202e5b6b88e891e0459b008db Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 23 Jan 2021 15:27:05 +0100 Subject: [PATCH 04/34] Styler.apply permits ndarray with axis=None --- doc/source/user_guide/style.ipynb | 63 +++++++-------------------- pandas/io/formats/style.py | 15 +++++-- pandas/tests/io/formats/test_style.py | 5 ++- 3 files changed, 30 insertions(+), 53 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 114b4688fffaf..f9197e0789499 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -140,9 +140,10 @@ "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)" + "css_classes = pd.DataFrame(data=[['cls1', None], ['cls3', 'cls2 cls3']], index=[0,2], columns=['A', 'C'])\n", + "df.style.\\\n", + " set_table_attributes('class=\"table-cls\"').\\\n", + " set_td_classes(css_classes)" ] }, { @@ -315,13 +316,10 @@ "outputs": [], "source": [ "def color_negative_red(val):\n", - " \"\"\"\n", - " Takes a scalar and returns a string with\n", - " the css property `'color: red'` for negative\n", - " strings, black otherwise.\n", - " \"\"\"\n", - " color = 'red' if val < 0 else 'black'\n", - " return 'color: %s' % color" + " \"\"\"Color negative scalars red.\"\"\"\n", + " css = 'color: red;'\n", + " if val < 0: return css\n", + " return None" ] }, { @@ -369,11 +367,9 @@ "outputs": [], "source": [ "def highlight_max(s):\n", - " '''\n", - " highlight the maximum in a Series yellow.\n", - " '''\n", - " is_max = s == s.max()\n", - " return ['background-color: yellow' if v else '' for v in is_max]" + " \"\"\"Highlight the maximum in a Series bold-orange.\"\"\"\n", + " css = 'background-color: orange; font-weight: bold;'\n", + " return np.where(s == np.nanmax(s.values), css, None)" ] }, { @@ -407,8 +403,8 @@ "outputs": [], "source": [ "def compare_col(s, comparator=None):\n", - " attr = 'background-color: #00BFFF;'\n", - " return np.where(s < comparator, attr, '')" + " css = 'background-color: #00BFFF;'\n", + " return np.where(s < comparator, css, None)" ] }, { @@ -448,36 +444,7 @@ "*Debugging Tip*: If you're having trouble writing your style function, try just passing it into DataFrame.apply. Internally, Styler.apply uses DataFrame.apply so the result should be the same.\n", "\n", "What if you wanted to highlight just the maximum value in the entire table?\n", - "Use `.apply(function, axis=None)` to indicate that your function wants the entire table, not one column or row at a time. Let's try that next.\n", - "\n", - "We'll rewrite our `highlight-max` to handle either Series (from `.apply(axis=0 or 1)`) or DataFrames (from `.apply(axis=None)`). We'll also allow the color to be adjustable, to demonstrate that `.apply`, and `.applymap` pass along keyword arguments." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def highlight_max(data, color='yellow'):\n", - " '''\n", - " highlight the maximum in a Series or DataFrame\n", - " '''\n", - " attr = 'background-color: {}'.format(color)\n", - " if data.ndim == 1: # Series from .apply(axis=0) or axis=1\n", - " is_max = data == data.max()\n", - " return [attr if v else '' for v in is_max]\n", - " else: # from .apply(axis=None)\n", - " is_max = data == data.max().max()\n", - " return pd.DataFrame(np.where(is_max, attr, ''),\n", - " index=data.index, columns=data.columns)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When using ``Styler.apply(func, axis=None)``, the function must return a DataFrame with the same index and column labels." + "Use `.apply(function, axis=None)` to indicate that your function wants the entire table, not one column or row at a time. Let's try that next." ] }, { @@ -486,7 +453,7 @@ "metadata": {}, "outputs": [], "source": [ - "s = df.style.apply(highlight_max, color='darkorange', axis=None)\n", + "s = df.style.apply(highlight_max, axis=None)\n", "s" ] }, diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2776c680ae68c..ba486ddbf5bc8 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -848,10 +848,17 @@ def _apply( else: result = func(data, **kwargs) if not isinstance(result, pd.DataFrame): - raise TypeError( - f"Function {repr(func)} must return a DataFrame when " - f"passed to `Styler.apply` with axis=None" - ) + if not isinstance(result, np.ndarray): + raise TypeError( + f"Function {repr(func)} must return a DataFrame or ndarray when" + f" passed to `Styler.apply` with axis=None" + ) + if not (data.shape == result.shape): + raise TypeError( + f"Function {repr(func)} returned ndarray with shape " + f"{result.shape} but {data.shape} was expected" + ) + result = DataFrame(result, index=data.index, columns=data.columns) if not ( result.index.equals(data.index) and result.columns.equals(data.columns) ): diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 51810786395b5..428861217d635 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1357,7 +1357,10 @@ def f(x): return "" df = DataFrame([[1, 2], [3, 4]]) - msg = "must return a DataFrame when passed to `Styler.apply` with axis=None" + msg = ( + "must return a DataFrame or ndarray when passed to `Styler.apply` " + "with axis=None" + ) with pytest.raises(TypeError, match=msg): df.style._apply(f, axis=None) From 1aa097e14b3707e567c28628154873fd7a8a3947 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 23 Jan 2021 15:43:57 +0100 Subject: [PATCH 05/34] apply with row --- doc/source/user_guide/style.ipynb | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index f9197e0789499..ec251d22f2d28 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -381,11 +381,20 @@ "df.style.apply(highlight_max)" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.style.apply(highlight_max, axis=1)" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In this case the input is a `Series`, one column at a time.\n", + "In this case the input is a `Series`, one column (or row) at a time.\n", "Notice that the output shape of `highlight_max` matches the input shape, an array with `len(s)` items." ] }, @@ -439,12 +448,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Above we used `Styler.apply` to pass in each column one at a time.\n", + "Above we used `Styler.apply` to pass in each column (or row) one at a time.\n", "\n", "*Debugging Tip*: If you're having trouble writing your style function, try just passing it into DataFrame.apply. Internally, Styler.apply uses DataFrame.apply so the result should be the same.\n", "\n", "What if you wanted to highlight just the maximum value in the entire table?\n", - "Use `.apply(function, axis=None)` to indicate that your function wants the entire table, not one column or row at a time. Let's try that next." + "Use `.apply(function, axis=None)` to indicate that your function wants the entire table, not one column or row at a time. In this case the return must be a DataFrame or ndarray of the same shape as the input. Let's try that next. " ] }, { From 228f619a5833c749eb01957ac7c941c07c810772 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 24 Jan 2021 09:32:24 +0100 Subject: [PATCH 06/34] doc updates --- pandas/io/formats/style.py | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index ba486ddbf5bc8..3120988625768 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -886,7 +886,7 @@ def apply( **kwargs, ) -> Styler: """ - Apply a function column-wise, row-wise, or table-wise. + Apply a CSS-styling function column-wise, row-wise, or table-wise. Updates the HTML representation with the result. @@ -896,7 +896,7 @@ def apply( ``func`` should take a Series or DataFrame (depending on ``axis``), and return an object with the same shape. Must return a DataFrame with identical index and - column labels when ``axis=None``. + column labels or an ndarray of appropriate shape when ``axis=None``. axis : {0 or 'index', 1 or 'columns', None}, default 0 Apply to each column (``axis=0`` or ``'index'``), to each row (``axis=1`` or ``'columns'``), or to the entire DataFrame at once @@ -913,9 +913,11 @@ def apply( Notes ----- - The output shape of ``func`` should match the input, i.e. if + The output of ``func`` should be elements having CSS style as string or, + if nothing is to be applied to that element, an empty string or ``None``. + The output shape must match the input, i.e. if ``x`` is the input row, column, or table (depending on ``axis``), - then ``func(x).shape == x.shape`` should be true. + then ``func(x).shape == x.shape`` should be ``True``. This is similar to ``DataFrame.apply``, except that ``axis=None`` applies the function to the entire DataFrame at once, @@ -923,12 +925,12 @@ def apply( Examples -------- - >>> def highlight_max(x): - ... return ['background-color: yellow' if v == x.max() else '' - for v in x] - ... + >>> def highlight_max(x, color): + ... return np.where(x == np.nanmax(x.values), f"color: {color};", None) >>> df = pd.DataFrame(np.random.randn(5, 2)) - >>> df.style.apply(highlight_max) + >>> df.style.apply(highlight_max, color='red') + >>> df.style.apply(highlight_max, color='blue', axis=1) + >>> df.style.apply(highlight_max, color='green', axis=None) """ self._todo.append( (lambda instance: getattr(instance, "_apply"), (func, axis, subset), kwargs) @@ -946,7 +948,7 @@ def _applymap(self, func: Callable, subset=None, **kwargs) -> Styler: def applymap(self, func: Callable, subset=None, **kwargs) -> Styler: """ - Apply a function elementwise. + Apply a CSS-styling function elementwise. Updates the HTML representation with the result. @@ -964,10 +966,22 @@ def applymap(self, func: Callable, subset=None, **kwargs) -> Styler: ------- self : Styler + Notes + ----- + The output of ``func`` should be a CSS style as string or, if nothing is to be + applied, an empty string or ``None``. + See Also -------- Styler.where: Updates the HTML representation with a style which is selected in accordance with the return value of a function. + + Examples + -------- + >>> def color_negative(v, color): + ... return f"color: {color};" if v < 0 else None + >>> df = pd.DataFrame(np.random.randn(5, 2)) + >>> df.style.applymap(color_negative, color='red') """ self._todo.append( (lambda instance: getattr(instance, "_applymap"), (func, subset), kwargs) From 259ca047688da19e2553254f65238963521c50c0 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 25 Jan 2021 08:29:20 +0100 Subject: [PATCH 07/34] tests --- pandas/io/formats/style.py | 13 ++++++------- pandas/tests/io/formats/test_style.py | 4 ++++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 22df17299c69c..e7ab810ff1361 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -854,9 +854,10 @@ def _apply( f" passed to `Styler.apply` with axis=None" ) if not (data.shape == result.shape): - raise TypeError( - f"Function {repr(func)} returned ndarray with shape " - f"{result.shape} but {data.shape} was expected" + raise ValueError( + f"Function {repr(func)} returned ndarray with wrong shape.\n" + f"Result has shape: {result.shape}\n" + f"Expected shape: {data.shape}" ) result = DataFrame(result, index=data.index, columns=data.columns) if not ( @@ -867,13 +868,11 @@ def _apply( f"index and columns as the input" ) - result_shape = result.shape - expected_shape = self.data.loc[subset].shape - if result_shape != expected_shape: + if result.shape != data.shape: raise ValueError( f"Function {repr(func)} returned the wrong shape.\n" f"Result has shape: {result.shape}\n" - f"Expected shape: {expected_shape}" + f"Expected shape: {data.shape}" ) self._update_ctx(result) return self diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 428861217d635..d4e1e3a036cee 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -1352,6 +1352,10 @@ def test_bad_apply_shape(self): with pytest.raises(ValueError, match=msg): df.style._apply(lambda x: ["", "", ""], axis=1) + msg = "returned ndarray with wrong shape" + with pytest.raises(ValueError, match=msg): + df.style._apply(lambda x: np.array([[""], [""]]), axis=None) + def test_apply_bad_return(self): def f(x): return "" From ea6ad925534b1c90510e214e53d83e1560d40ba4 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 25 Jan 2021 09:44:23 +0100 Subject: [PATCH 08/34] whats new --- doc/source/whatsnew/v1.3.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 67f27a75c7071..610e88db86a93 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -53,6 +53,7 @@ Other enhancements - :meth:`DataFrame.apply` can now accept non-callable DataFrame properties as strings, e.g. ``df.apply("size")``, which was already the case for :meth:`Series.apply` (:issue:`39116`) - :meth:`Series.apply` can now accept list-like or dictionary-like arguments that aren't lists or dictionaries, e.g. ``ser.apply(np.array(["sum", "mean"]))``, which was already the case for :meth:`DataFrame.apply` (:issue:`39140`) - :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes. +- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None``. .. --------------------------------------------------------------------------- From 09d6302ffb03c870800863c24d92623752682794 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 25 Jan 2021 09:50:10 +0100 Subject: [PATCH 09/34] op --- pandas/io/formats/style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e7ab810ff1361..3a21984a9cca7 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -860,7 +860,7 @@ def _apply( f"Expected shape: {data.shape}" ) result = DataFrame(result, index=data.index, columns=data.columns) - if not ( + elif not ( result.index.equals(data.index) and result.columns.equals(data.columns) ): raise ValueError( From b9715c9e4d06422f5fa6c6bc105e72f9b570d2f1 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 25 Jan 2021 10:56:28 +0100 Subject: [PATCH 10/34] doc fix --- pandas/io/formats/style.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 3a21984a9cca7..9c16ebf9bf47b 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -965,16 +965,16 @@ def applymap(self, func: Callable, subset=None, **kwargs) -> Styler: ------- self : Styler - Notes - ----- - The output of ``func`` should be a CSS style as string or, if nothing is to be - applied, an empty string or ``None``. - See Also -------- Styler.where: Updates the HTML representation with a style which is selected in accordance with the return value of a function. + Notes + ----- + The output of ``func`` should be a CSS style as string or, if nothing is to be + applied, an empty string or ``None``. + Examples -------- >>> def color_negative(v, color): From c6173b5d9ecff3cda5d0d18f4f6d1fc2a8cb8c6e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 25 Jan 2021 13:08:20 +0100 Subject: [PATCH 11/34] text fix --- pandas/io/formats/style.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 9c16ebf9bf47b..b44bc40b4ccd8 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -850,8 +850,8 @@ def _apply( if not isinstance(result, pd.DataFrame): if not isinstance(result, np.ndarray): raise TypeError( - f"Function {repr(func)} must return a DataFrame or ndarray when" - f" passed to `Styler.apply` with axis=None" + f"Function {repr(func)} must return a DataFrame or ndarray " + f"when passed to `Styler.apply` with axis=None" ) if not (data.shape == result.shape): raise ValueError( From 212e14d77cb9a36612a6999b23bdab68b16e95d1 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 1 Feb 2021 07:11:04 +0100 Subject: [PATCH 12/34] renaming sections --- doc/source/ecosystem.rst | 3 ++- doc/source/user_guide/index.rst | 2 +- doc/source/user_guide/style.ipynb | 8 ++++++-- doc/source/user_guide/visualization.rst | 9 ++++++--- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/doc/source/ecosystem.rst b/doc/source/ecosystem.rst index bb89b91954518..4e099ad20b400 100644 --- a/doc/source/ecosystem.rst +++ b/doc/source/ecosystem.rst @@ -98,7 +98,8 @@ which can be used for a wide variety of time series data mining tasks. Visualization ------------- -While :ref:`pandas has built-in support for data visualization with matplotlib `, +`Pandas has its own Styler class for table visualization `_, and while +:ref:`pandas also has built-in support for data visualization through charts with matplotlib `, there are a number of other pandas-compatible libraries. `Altair `__ diff --git a/doc/source/user_guide/index.rst b/doc/source/user_guide/index.rst index 901f42097b911..6b6e212cde635 100644 --- a/doc/source/user_guide/index.rst +++ b/doc/source/user_guide/index.rst @@ -38,12 +38,12 @@ Further information on any specific method can be obtained in the integer_na boolean visualization + style computation groupby window timeseries timedeltas - style options enhancingperf scale diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 114b4688fffaf..002c87fb5390e 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -4,9 +4,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Styling\n", + "# Table Visualization\n", "\n", - "This document is written as a Jupyter Notebook, and can be viewed or downloaded [here](https://nbviewer.ipython.org/github/pandas-dev/pandas/blob/master/doc/source/user_guide/style.ipynb).\n", + "This section demonstrates visualization of tabular data using the ``Styler`` class. For information on visualization with charting please see the relevant section in the *User Guide*. This document is written as a Jupyter Notebook, and can be viewed or downloaded [here](https://nbviewer.ipython.org/github/pandas-dev/pandas/blob/master/doc/source/user_guide/style.ipynb).\n", + "\n", + "[Chart Visualization](visualization.rst)\n", + "\n", + "Visualization of tabular data is acheived by providing a class which generates a table element, ``, in HTML and the api to manipulate the cascading style sheets (CSS) which impact that HTML rendering in native browsers. This allows a lot of flexibility out of the box, and even enables web developers to integrate `DataFrames` into their exiting user interface designs. \n", "\n", "You can apply **conditional formatting**, the visual styling of a DataFrame\n", "depending on the data within, by using the ``DataFrame.style`` property.\n", diff --git a/doc/source/user_guide/visualization.rst b/doc/source/user_guide/visualization.rst index 7911c58b9867e..8766e7efb7229 100644 --- a/doc/source/user_guide/visualization.rst +++ b/doc/source/user_guide/visualization.rst @@ -2,9 +2,12 @@ {{ header }} -************* -Visualization -************* +******************* +Chart Visualization +******************* + +This section demonstrates visualization through charting. For information on +visualization of tabular data please see the section on `Table Visualization `_. We use the standard convention for referencing the matplotlib API: From 8a664382b2fb627141bdffc755a76f007050cf50 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 6 Feb 2021 14:23:04 +0100 Subject: [PATCH 13/34] doc changes --- doc/source/user_guide/style.ipynb | 356 ++++++++++++++++++++++++------ 1 file changed, 291 insertions(+), 65 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 002c87fb5390e..523e8bb01315d 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -6,32 +6,26 @@ "source": [ "# Table Visualization\n", "\n", - "This section demonstrates visualization of tabular data using the ``Styler`` class. For information on visualization with charting please see the relevant section in the *User Guide*. This document is written as a Jupyter Notebook, and can be viewed or downloaded [here](https://nbviewer.ipython.org/github/pandas-dev/pandas/blob/master/doc/source/user_guide/style.ipynb).\n", + "This section demonstrates visualization of tabular data using the [Styler][styler]\n", + "class. For information on visualization with charting please see [Chart Visualization][viz]. This document is written as a Jupyter Notebook, and can be viewed or downloaded [here][download].\n", "\n", - "[Chart Visualization](visualization.rst)\n", - "\n", - "Visualization of tabular data is acheived by providing a class which generates a table element, `
`, in HTML and the api to manipulate the cascading style sheets (CSS) which impact that HTML rendering in native browsers. This allows a lot of flexibility out of the box, and even enables web developers to integrate `DataFrames` into their exiting user interface designs. \n", - "\n", - "You can apply **conditional formatting**, the visual styling of a DataFrame\n", - "depending on the data within, by using the ``DataFrame.style`` property.\n", - "This is a property that returns a ``Styler`` object, which has\n", - "useful methods for formatting and displaying DataFrames.\n", - "\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.\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." + "[styler]: ../reference/api/pandas.io.formats.style.Styler.html#pandas.io.formats.style.Styler\n", + "[viz]: visualization.rst\n", + "[download]: https://nbviewer.ipython.org/github/pandas-dev/pandas/blob/master/doc/source/user_guide/style.ipynb" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Styler Object\n", + "## Styler Object and HTML \n", + "\n", + "Styling should be performed after the data in a DataFrame has been processed. The [Styler][styler] creates an HTML `
` and leverages CSS styling language to manipulate many parameters including colors, fonts, borders, background, etc. See [here][w3schools] for more information on styling HTML tables. This allows a lot of flexibility out of the box, and even enables web developers to integrate DataFrames into their exiting user interface designs.\n", + " \n", + "The `DataFrame.style` attribute is a property that returns a [Styler][styler] object. It has a `_repr_html_` method defined on it so they are rendered automatically in Jupyter Notebook.\n", "\n", - "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." + "[styler]: ../reference/api/pandas.io.formats.style.Styler.html#pandas.io.formats.style.Styler\n", + "[w3schools]: https://www.w3schools.com/html/html_tables.asp" ] }, { @@ -56,12 +50,11 @@ "import pandas as pd\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\n", + "df = pd.DataFrame({\n", + " 'Income': [75000, 165000, 145000, 125000],\n", + " 'Expenses': [101871, 99643, 107871, 126008],\n", + " 'Employees': [62, 60, 68, np.nan],\n", + "}, index=pd.MultiIndex.from_product([[20],['Jan', 'Feb', 'Mar', 'Apr']], names=['Yr', 'Month']))\n", "df.style" ] }, @@ -69,7 +62,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "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." + "The above output looks very similar to the standard DataFrame HTML representation. But the HTML here has already attached some CSS classes to each cell, even if we haven't yet created any styles. We can view these by calling the [.render()][render] method, which returns the raw HTML as string, which is useful for further processing or adding to a file.\n", + "\n", + "[render]: ../reference/api/pandas.io.formats.style.Styler.render.html#pandas.io.formats.style.Styler.render" ] }, { @@ -85,33 +80,60 @@ "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)." + "The `id` attributes, e.g. `T_uuid_row0_col0`, and the `class` attributes, such as `data row0 col0` are what can be referenced by the CSS to achieve styling. By default 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 read more about the use of UUIDs in [Optimization](#Optimization)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Building styles\n", + "## Formatting Display Values \n", + "\n", + "Before adding styles it is useful to show that the [Styler][styler] can distinguish the *display* value from the *actual* value. To control the display value, the text is printed in each cell, and we can use [.format()][formatfunc] method to manipulate this according to a [format spec string][format] or a callable that takes a single value and returns a string. It is possible to define this for the whole table or for individual columns. \n", "\n", - "There are 3 primary methods of adding custom styles to DataFrames using CSS and matching it to cells:\n", + "Missing data can also be reformatted easily here and we can also [hide data](#Hiding-Data) if necessary.\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" + "[styler]: ../reference/api/pandas.io.formats.style.Styler.html#pandas.io.formats.style.Styler\n", + "[format]: https://docs.python.org/3/library/string.html#format-specification-mini-language\n", + "[formatfunc]: ../reference/api/pandas.io.formats.style.Styler.format.html#pandas.io.formats.style.Styler.format" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.style.format(\"{:,.0f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.style.format({\n", + " 'Income': \"{:.2f}\",\n", + " 'Expenses': lambda x: \"$ {:,.0f}\".format(x),\n", + " \"Employees\": \"{:.0f}\"\n", + "}, na_rep=\"MISSING\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Linking External CSS\n", + "### Hiding Data\n", "\n", - "*New in version 1.2.0*\n", + "The index can be hidden from rendering by calling [.hide_index()][hideidx]. \n", + "\n", + "Columns can be hidden from rendering by calling [.hide_columns()][hidecols] and passing in the name of a column, or a slice of columns.\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", + "Hiding does not change the integer arrangement of CSS classes, i.e. the `Employees` column is not be reindexed to `.col0` but remains as `.col2`.\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):" + "[hideidx]: ../reference/api/pandas.io.formats.style.Styler.hide_index.html#pandas.io.formats.style.Styler.hide_index\n", + "[hidecols]: ../reference/api/pandas.io.formats.style.Styler.hide_columns.html#pandas.io.formats.style.Styler.hide_columns" ] }, { @@ -120,22 +142,54 @@ "metadata": {}, "outputs": [], "source": [ - "from IPython.display import HTML\n", - "style = \\\n", - "\"\"\n", - "HTML(style)" + "df.style.hide_index().hide_columns(['Income', 'Expenses'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "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)." + "## Methods to Add Styles\n", + "\n", + "There are **3 primary methods of adding custom CSS styles to [Styler][styler]**:\n", + "\n", + "- **Using [.set_table_styles()][table]** to control broader areas of the table with specified internal CSS. Although table styles allow the flexibility to add CSS selectors and properties controlling all individual parts of the table, they are unwieldy for individual cell specifications. Also, note that table styles cannot be exported to Excel. \n", + "- **Using [.set_td_classes()][td_class]** to directly link either external CSS classes to your data cells or link the internal CSS classes created by [.set_table_styles()][table]. These cannot be used on column header rows or indexes, and also won't export to Excel. \n", + "- **Using the [.apply()][apply] and [.applymap()][applymap] functions** to add direct internal CSS to specific data cells. These cannot be used on column header rows or indexes, but only these methods add styles that will export to Excel. These methods work in a similar way to [DataFrame.apply()][dfapply] and [DataFrame.applymap()][dfapplymap].\n", + "\n", + "Next we cover the advantages and limitations of each of the above methods.\n", + "\n", + "[table]: ../reference/api/pandas.io.formats.style.Styler.set_table_styles.html#pandas.io.formats.style.Styler.set_table_styles\n", + "[styler]: ../reference/api/pandas.io.formats.style.Styler.html#pandas.io.formats.style.Styler\n", + "[td_class]: ../reference/api/pandas.io.formats.style.Styler.set_td_classes.html#pandas.io.formats.style.Styler.set_td_classes\n", + "[apply]: ../reference/api/pandas.io.formats.style.Styler.apply.html#pandas.io.formats.style.Styler.apply\n", + "[applymap]: ../reference/api/pandas.io.formats.style.Styler.applymap.html#pandas.io.formats.style.Styler.applymap\n", + "[dfapply]: ../reference/api/pandas.DataFrame.apply.html#pandas.DataFrame.apply\n", + "[dfapplymap]: ../reference/api/pandas.DataFrame.applymap.html#pandas.DataFrame.applymap" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Using Table Styles\n", + "\n", + "Table styles are flexible enough to control all individual parts of the table, including column headers and indexes. \n", + "However, they can be unwieldy to type for individual data cells or for any kind of conditional formatting, so we recommend that table styles are used for broad styling, such as rows or columns at a time.\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. This `:hover` pseudo-selectors, as well as others, can only be used this way.\n", + "\n", + "CSS style sheets are typically composed of selectors, e.g. element ids or css classes, and attribute value pairs:\n", + "\n", + "```\n", + "tr:hover {\n", + " background-color: #ffff99;\n", + "}\n", + "```\n", + "\n", + "The necessary format to pass styles to [.set_table_styles()][table] is as a list of dicts, each with a CSS-selector tag and CSS-properties. Properties can either be a list of 2-tuples, or a regular CSS-string For example:\n", + "\n", + "[table]: ../reference/api/pandas.io.formats.style.Styler.set_table_styles.html#pandas.io.formats.style.Styler.set_table_styles" ] }, { @@ -144,32 +198,119 @@ "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)" + "hover_style = {\n", + " 'selector': \"tr:hover\",\n", + " 'props': [(\"background-color\", \"#ffff99\")]\n", + "}\n", + "header_style = {\n", + " 'selector': \"th\", \n", + " 'props': [('font-size','115%'), ('text-align','center'), ('color', '#e83e8c')]\n", + "}\n", + "df.style.format(\"{:,.0f}\").set_table_styles([hover_style, header_style])" ] }, { "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", + "Table styles allows for controlling specific rows and columns by their integer index, for example" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.style.format(\"{:,.0f}\").set_table_styles([hover_style, header_style, {\n", + " 'selector': 'th.level1',\n", + " 'props': [('color', 'darkblue')]\n", + "}])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As a convenience method we can also pass a **dict** to [.set_table_styles()][table] which contains row or column keys. Behind the scenes Styler just indexes the keys and add relevant `.col` or `.row` classes as necessary to th CSS selectors.\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." + "Table styles can be chained provided multiple methods don't *overwrite*.\n", + "\n", + "[table]: ../reference/api/pandas.io.formats.style.Styler.set_table_styles.html#pandas.io.formats.style.Styler.set_table_styles" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "s = df.style.format(\"{:,.0f}\")\\\n", + " .set_table_styles([hover_style, header_style, {\n", + " 'selector': 'th.level1',\n", + " 'props': [('color', 'darkblue')]}])\\\n", + " .set_table_styles({\n", + " 'Employees': [{\n", + " 'selector': 'td',\n", + " 'props': [('font-weight', 'bold')]\n", + " }]}, axis=0, overwrite=False)\\\n", + " .set_table_styles({\n", + " (20, 'Mar'): [{\n", + " 'selector': '',\n", + " 'props': [\n", + " ('background-color', 'darkgrey'),\n", + " ('color', 'white')\n", + " ]}]}, axis=1, overwrite=False)\n", + "s" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Using Table Styles\n", + "## Setting Classes and Linking to External CSS\n", "\n", - "Table styles allow you to control broader areas of the DataFrame, i.e. the whole table or specific columns or rows, 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 in data cells can greatly reduce the rendered HTML.\n", + "*New in version 1.2.0*\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. This `:hover` pseudo-selectors, as well as others, can only be used this way.\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. You may want to use these native files rather than duplicate all the CSS in python.\n", "\n", - "`table_styles` are extremely flexible, but not as fun to type out by hand.\n", - "We hope to collect some useful ones either in pandas, or preferable in a new package that [builds on top](#Extensibility) the tools here." + "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 the HTML of this notebook page for demonstration*):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# from IPython.display import HTML\n", + "# style = \\\n", + "# \"\"\n", + "# HTML(style)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At a minimum we can add a class attribute to our `
` element, which will connect with the external CSS, and in this case change the width of the table." ] }, { @@ -178,24 +319,100 @@ "metadata": {}, "outputs": [], "source": [ - "def hover(hover_color=\"#ffff99\"):\n", - " return {'selector': \"tr:hover\",\n", - " 'props': [(\"background-color\", \"%s\" % hover_color)]}\n", + "s.set_table_attributes('class=\"table-cls\"')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data Cell CSS Classes\n", "\n", - "styles = [\n", - " hover(),\n", - " {'selector': \"th\", 'props': [(\"font-size\", \"150%\"),\n", - " (\"text-align\", \"center\")]}\n", - "]\n", + "The [.set_td_classes()][tdclass] method accepts a DataFrame with matching indices and columns to the underlying [Styler][styler]'s DataFrame. That DataFrame will contain strings as css-classes to add to individual data cells: the ` instead of
` elements of the ``.\n", "\n", - "df.style.set_table_styles(styles)" + "[tdclass]: ../reference/api/pandas.io.formats.style.Styler.set_td_classes.html#pandas.io.formats.style.Styler.set_td_classes\n", + "[styler]: ../reference/api/pandas.io.formats.style.Styler.html#pandas.io.formats.style.Styler" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame(np.random.randn(5,5), columns=[f'Trial {x}' for x in range(5)])\n", + "df.iloc[3,4], df.iloc[4,3] = np.nan, np.nan\n", + "df.style.format('{:,.3f}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "cls_inv = pd.DataFrame('', index=df.index, columns=df.columns)\n", + "cls_bold = pd.DataFrame('', index=df.index, columns=df.columns)\n", + "cls_inv.iloc[0:2, 0:2] = 'cls-invert '\n", + "cls_bold.iloc[1:3, 1:3] = 'cls-bold '\n", + "df.style.format('{:,.3f}').set_td_classes(cls_inv + cls_bold)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "If `table_styles` is given as a dictionary each key should be a specified column or index value and this will map to specific class CSS selectors of the given column or row." + "It is possible for this to be an entirely internal CSS solution by defining the class within table styles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.style.format('{:,.3f}')\\\n", + " .set_table_styles([\n", + " {'selector': '.cls-invert',\n", + " 'props': [('color', 'white'), ('background-color', '#e83e8c')]}])\\\n", + " .set_td_classes(cls_inv)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "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." ] }, { @@ -510,6 +727,15 @@ "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": [ + "## Optimization\n", + "\n", + "The HTML ...." + ] + }, { "cell_type": "markdown", "metadata": {}, From c4e28feeb0a3d0ab0652c6c36dca45d75b8d958c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sat, 6 Feb 2021 20:51:01 +0100 Subject: [PATCH 14/34] doc changes --- doc/source/user_guide/style.ipynb | 138 ++++++++++++++++++++++-------- 1 file changed, 104 insertions(+), 34 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 523e8bb01315d..905150db37692 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -76,13 +76,6 @@ "df.style.render().split('\\n')[:10]" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `id` attributes, e.g. `T_uuid_row0_col0`, and the `class` attributes, such as `data row0 col0` are what can be referenced by the CSS to achieve styling. By default 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 read more about the use of UUIDs in [Optimization](#Optimization)" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -98,6 +91,59 @@ "[formatfunc]: ../reference/api/pandas.io.formats.style.Styler.format.html#pandas.io.formats.style.Styler.format" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pd.DataFrame('',index=[1,2], columns=[1,2]).style.set_table_attributes('style=\"color:red;border:1px solid green\"')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pd.DataFrame('',index=[1,2], columns=[1,2]).style.set_table_styles([{'selector': '', 'props': 'border:1px solid blue'}])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import HTML\n", + "style = \\\n", + "\"\"\n", + "HTML(style)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "pd.DataFrame('',index=[1,2], columns=[1,2])\\\n", + " .style.set_table_attributes('class=\"big-tab\"')\\\n", + " .set_table_styles([{'selector': '', 'props': 'font-size:50%;'}])\\\n", + " .set_table_attributes('style=\"font-size:100%;\"').render()" + ] + }, { "cell_type": "code", "execution_count": null, @@ -204,7 +250,7 @@ "}\n", "header_style = {\n", " 'selector': \"th\", \n", - " 'props': [('font-size','115%'), ('text-align','center'), ('color', '#e83e8c')]\n", + " 'props': 'font-size: 115%; text-align: center; color: #e83e8c;'\n", "}\n", "df.style.format(\"{:,.0f}\").set_table_styles([hover_style, header_style])" ] @@ -224,7 +270,7 @@ "source": [ "df.style.format(\"{:,.0f}\").set_table_styles([hover_style, header_style, {\n", " 'selector': 'th.level1',\n", - " 'props': [('color', 'darkblue')]\n", + " 'props': 'color: darkblue;'\n", "}])" ] }, @@ -232,7 +278,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As a convenience method we can also pass a **dict** to [.set_table_styles()][table] which contains row or column keys. Behind the scenes Styler just indexes the keys and add relevant `.col` or `.row` classes as necessary to th CSS selectors.\n", + "As a convenience method (*since version 1.2.0*) we can also pass a **dict** to [.set_table_styles()][table] which contains row or column keys. Behind the scenes Styler just indexes the keys and add relevant `.col` or `.row` classes as necessary to the CSS selectors.\n", "\n", "Table styles can be chained provided multiple methods don't *overwrite*.\n", "\n", @@ -248,19 +294,17 @@ "s = df.style.format(\"{:,.0f}\")\\\n", " .set_table_styles([hover_style, header_style, {\n", " 'selector': 'th.level1',\n", - " 'props': [('color', 'darkblue')]}])\\\n", + " 'props': 'color: darkblue;'}])\\\n", " .set_table_styles({\n", " 'Employees': [{\n", " 'selector': 'td',\n", - " 'props': [('font-weight', 'bold')]\n", - " }]}, axis=0, overwrite=False)\\\n", + " 'props': 'font-weight: bold;'}]\n", + " }, axis=0, overwrite=False)\\\n", " .set_table_styles({\n", " (20, 'Mar'): [{\n", " 'selector': '',\n", - " 'props': [\n", - " ('background-color', 'darkgrey'),\n", - " ('color', 'white')\n", - " ]}]}, axis=1, overwrite=False)\n", + " 'props': 'background-color: darkgrey; color: white;'}]\n", + " }, axis=1, overwrite=False)\n", "s" ] }, @@ -1346,23 +1390,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "### CSS classes\n", - "\n", - "Certain CSS classes are attached to cells.\n", - "\n", - "- Index and Column names include `index_name` and `level` where `k` is its level in a MultiIndex\n", - "- Index label cells include\n", - " + `row_heading`\n", - " + `row` where `n` is the numeric position of the row\n", - " + `level` where `k` is the level in a MultiIndex\n", - "- Column label cells include\n", - " + `col_heading`\n", - " + `col` where `n` is the numeric position of the column\n", - " + `level` where `k` is the level in a MultiIndex\n", - "- Blank cells include `blank`\n", - "- Data cells include `data`" - ] + "source": [] }, { "cell_type": "markdown", @@ -1673,6 +1701,48 @@ " \n", "HTML(''.format(css))" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## More About CSS\n", + "\n", + "Cascading Style Sheet (CSS) language has its own peculiarities. It never reports errors: it just doesn't render your objects how you intend so can sometimes be frustrating. Here is a very brief primer on how ``Styler`` interacts with CSS and common pitfalls to avoid." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## CSS Classes and Ids\n", + "\n", + "The precise structure of the CSS `class` attached to each cells is as follows.\n", + "\n", + "- Cells with Index and Column names include `index_name` and `level` where `k` is its level in a MultiIndex\n", + "- Index label cells include\n", + " + `row_heading`\n", + " + `level` where `k` is the level in a MultiIndex\n", + " + `row` where `m` is the numeric position of the row\n", + "- Column label cells include\n", + " + `col_heading`\n", + " + `level` where `k` is the level in a MultiIndex\n", + " + `col` where `n` is the numeric position of the column\n", + "- Data cells include\n", + " + `data`\n", + " + `row`, where `m` is the numeric position of the cell.\n", + " + `col`, where `n` is the numeric position of the cell.\n", + "- Blank cells include `blank`\n", + "\n", + "The structure of the `id` is `T_uuid_level_row_col` where `level` is used only on headings, and headings will only have either `row` or `col` whichever is needed. By default 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 read more about the use of UUIDs in [Optimization](#Optimization)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { From 8923dd93c25003071eb74e5cbcbdcbded01522dd Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 7 Feb 2021 14:20:58 +0100 Subject: [PATCH 15/34] cln: reorganise vars --- pandas/io/formats/style.py | 56 ++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6eac9ba87c73d..bd21a4bec8de1 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -157,53 +157,40 @@ def __init__( na_rep: Optional[str] = None, uuid_len: int = 5, ): - self.ctx: DefaultDict[Tuple[int, int], List[str]] = defaultdict(list) - self._todo: List[Tuple[Callable, Tuple, Dict]] = [] - + # validate ordered args if not isinstance(data, (pd.Series, pd.DataFrame)): raise TypeError("``data`` must be a Series or DataFrame") if data.ndim == 1: data = data.to_frame() if not data.index.is_unique or not data.columns.is_unique: raise ValueError("style is not supported for non-unique indices.") - - self.data = data - self.index = data.index - self.columns = data.columns - + assert isinstance(data, DataFrame) + self.data: DataFrame = data + self.index: pd.Index = data.index + self.columns: pd.Index = data.columns + if precision is None: + precision = get_option("display.precision") + self.precision = precision + self.table_styles = table_styles if not isinstance(uuid_len, int) or not uuid_len >= 0: raise TypeError("``uuid_len`` must be an integer in range [0, 32].") self.uuid_len = min(32, uuid_len) self.uuid = (uuid or uuid4().hex[: self.uuid_len]) + "_" - self.table_styles = table_styles self.caption = caption - if precision is None: - precision = get_option("display.precision") - self.precision = precision self.table_attributes = table_attributes - self.hidden_index = False - self.hidden_columns: Sequence[int] = [] self.cell_ids = cell_ids self.na_rep = na_rep - self.tooltips: Optional[_Tooltips] = None - + # assign additional default vars + self.hidden_index: bool = False + self.hidden_columns: Sequence[int] = [] + self.ctx: DefaultDict[Tuple[int, int], List[str]] = defaultdict(list) self.cell_context: Dict[str, Any] = {} - - # display_funcs maps (row, col) -> formatting function - - def default_display_func(x): - if self.na_rep is not None and pd.isna(x): - return self.na_rep - elif is_float(x): - display_format = f"{x:.{self.precision}f}" - return display_format - else: - return x - - self._display_funcs: DefaultDict[ + self._todo: List[Tuple[Callable, Tuple, Dict]] = [] + self.tooltips: Optional[_Tooltips] = None + self._display_funcs: DefaultDict[ # maps (row, col) -> formatting function Tuple[int, int], Callable[[Any], str] - ] = defaultdict(lambda: default_display_func) + ] = defaultdict(lambda: self._default_display_func) def _repr_html_(self) -> str: """ @@ -224,6 +211,15 @@ def _init_tooltips(self): if self.tooltips is None: self.tooltips = _Tooltips() + def _default_display_func(self, x): + if self.na_rep is not None and pd.isna(x): + return self.na_rep + elif is_float(x): + display_format = f"{x:.{self.precision}f}" + return display_format + else: + return x + def set_tooltips(self, ttips: DataFrame) -> Styler: """ Add string based tooltips that will appear in the `Styler` HTML result. These From a463a53f365f4d61ed6af3ad567409bac22aad4c Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 8 Feb 2021 07:45:32 +0100 Subject: [PATCH 16/34] docs --- pandas/io/formats/style.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index e0ea4e722f8bb..8db067cbc4e8f 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -893,7 +893,10 @@ def apply( ``func`` should take a Series or DataFrame (depending on ``axis``), and return an object with the same shape. Must return a DataFrame with identical index and - column labels or an ndarray of appropriate shape when ``axis=None``. + column labels or an ndarray with same shape as input when ``axis=None``. + + .. versionchanged:: 1.3.0 + axis : {0 or 'index', 1 or 'columns', None}, default 0 Apply to each column (``axis=0`` or ``'index'``), to each row (``axis=1`` or ``'columns'``), or to the entire DataFrame at once From 5c54c71e9b504d2b02f717d3d04b59986b8784ed Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Mon, 8 Feb 2021 23:27:08 +0100 Subject: [PATCH 17/34] update --- doc/source/user_guide/style.ipynb | 596 +++++++++++++++++++++++++----- 1 file changed, 509 insertions(+), 87 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 905150db37692..a752417017c20 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -30,7 +30,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 126, "metadata": { "nbsphinx": "hidden" }, @@ -43,18 +43,79 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 127, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Model:Decision TreeRegressionRandom
Predicted:TumourNon-TumourTumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)38.0000002.00000018.00000022.0000002119
Non-Tumour (Negative)19.000000439.0000006.000000452.000000226232
\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 127, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "import pandas as pd\n", "import numpy as np\n", "\n", - "df = pd.DataFrame({\n", - " 'Income': [75000, 165000, 145000, 125000],\n", - " 'Expenses': [101871, 99643, 107871, 126008],\n", - " 'Employees': [62, 60, 68, np.nan],\n", - "}, index=pd.MultiIndex.from_product([[20],['Jan', 'Feb', 'Mar', 'Apr']], names=['Yr', 'Month']))\n", + "df = pd.DataFrame([[38.0, 2.0, 18.0, 22.0, 21,19],[19, 439, 6, 452, 226,232]], \n", + " index=pd.Index(['Tumor (Positive)', 'Non-Tumour (Negative)'], name='Actual Label:'), \n", + " columns=pd.MultiIndex.from_product([['Decision Tree', 'Regression', 'Random'],['Tumour', 'Non-Tumour']], names=['Model:', 'Predicted:']))\n", "df.style" ] }, @@ -62,18 +123,206 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The above output looks very similar to the standard DataFrame HTML representation. But the HTML here has already attached some CSS classes to each cell, even if we haven't yet created any styles. We can view these by calling the [.render()][render] method, which returns the raw HTML as string, which is useful for further processing or adding to a file.\n", + "The above output looks very similar to the standard DataFrame HTML representation. But the HTML here has already attached some CSS classes to each cell, even if we haven't yet created any styles. We can view these by calling the [.render()][render] method, which returns the raw HTML as string, which is useful for further processing or adding to a file - read on in [More about CSS and HTML](#More-About-CSS-and-HTML). Below we will show how we can use these to format the DataFrame to be more communicative. For example how we can build `s`:\n", "\n", "[render]: ../reference/api/pandas.io.formats.style.Styler.render.html#pandas.io.formats.style.Styler.render" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, + "execution_count": 155, + "metadata": { + "nbsphinx": "hidden" + }, "outputs": [], "source": [ - "df.style.render().split('\\n')[:10]" + "s = df.style\\\n", + " .hide_columns([('Random', 'Tumour'), ('Random', 'Non-Tumour')])\\\n", + " .format('{:.0f}')\\\n", + " .set_table_styles([\n", + " {\n", + " 'selector': '',\n", + " 'props': 'caption-side: bottom; border-collapse: separate;'\n", + " },{\n", + " 'selector': '.index_name',\n", + " 'props': 'font-style: italic; color: darkgrey; font-weight:normal;'\n", + " },{\n", + " 'selector': 'th:not(.index_name)',\n", + " 'props': 'background-color: darkblue; color: white;'\n", + " },{\n", + " 'selector': 'th.col_heading',\n", + " 'props': 'text-align: center;'\n", + " },{\n", + " 'selector': 'th.col_heading.level0',\n", + " 'props': 'font-size: 1.5em;'\n", + " },{\n", + " 'selector': 'th.col2',\n", + " 'props': 'border-left: 1px solid white;'\n", + " },{\n", + " 'selector': '.col2',\n", + " 'props': 'border-left: 1px solid darkblue;'\n", + " },{\n", + " 'selector': 'td',\n", + " 'props': 'text-align: center; font-weight:bold;'\n", + " },{\n", + " 'selector': '.true',\n", + " 'props': 'background-color: #e6ffe6;'\n", + " },{\n", + " 'selector': '.false',\n", + " 'props': 'background-color: #ffe6e6;'\n", + " },{\n", + " 'selector': '.border-red',\n", + " 'props': 'border: 2px dashed red;'\n", + " },{\n", + " 'selector': '.border-green',\n", + " 'props': 'border: 2px dashed green;'\n", + " },{\n", + " 'selector': 'td:hover',\n", + " 'props': 'background-color: #ffffb3;'\n", + " }])\\\n", + " .set_td_classes(pd.DataFrame([['true border-green', 'false', 'true', 'false border-red', '', ''],\n", + " ['false', 'true', 'false', 'true', '', '']], \n", + " index=df.index, columns=df.columns))\\\n", + " .set_caption(\"Confusion matrix for multiple cancer prediction models.\")\\\n", + " .set_tooltips(pd.DataFrame([['DT has a very strong true positive rate', '', '', 'Regression total false negatives is unacceptable', '', ''],\n", + " ['', '', '', '', '', '']], \n", + " index=df.index, columns=df.columns))\\\n", + " .set_tooltips_class(name='pd-tt', properties=\n", + " 'visibility: hidden; position: absolute; z-index: 1;'\n", + " 'background-color: lightgrey; color: darkblue; font-size: 0.8em;' \n", + " 'transform: translate(0px, -24px); padding: 0.5em; border-radius: 0.5em;'\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 156, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Confusion matrix for multiple cancer prediction models.
Model:Decision TreeRegression
Predicted:TumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)3821822
Non-Tumour (Negative)194396452
\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 156, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "s" ] }, { @@ -93,77 +342,149 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pd.DataFrame('',index=[1,2], columns=[1,2]).style.set_table_attributes('style=\"color:red;border:1px solid green\"')" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pd.DataFrame('',index=[1,2], columns=[1,2]).style.set_table_styles([{'selector': '', 'props': 'border:1px solid blue'}])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import HTML\n", - "style = \\\n", - "\"\"\n", - "HTML(style)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "pd.DataFrame('',index=[1,2], columns=[1,2])\\\n", - " .style.set_table_attributes('class=\"big-tab\"')\\\n", - " .set_table_styles([{'selector': '', 'props': 'font-size:50%;'}])\\\n", - " .set_table_attributes('style=\"font-size:100%;\"').render()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 157, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Model:Decision TreeRegressionRandom
Predicted:TumourNon-TumourTumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)38218222119
Non-Tumour (Negative)194396452226232
\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 157, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "df.style.format(\"{:,.0f}\")" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 158, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Model:Decision TreeRegressionRandom
Predicted:TumourNon-TumourTumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)38.00218-22,000.02119
Non-Tumour (Negative)19.004396-452,000.0226232
\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 158, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "df.style.format({\n", - " 'Income': \"{:.2f}\",\n", - " 'Expenses': lambda x: \"$ {:,.0f}\".format(x),\n", - " \"Employees\": \"{:.0f}\"\n", - "}, na_rep=\"MISSING\")" + "df.style.format('{:.0f}').format({\n", + " ('Decision Tree', 'Tumour'): \"{:.2f}\",\n", + " ('Regression', 'Non-Tumour'): lambda x: \"{:,.1f}\".format(x*-1e3),\n", + "})" ] }, { @@ -176,7 +497,9 @@ "\n", "Columns can be hidden from rendering by calling [.hide_columns()][hidecols] and passing in the name of a column, or a slice of columns.\n", "\n", - "Hiding does not change the integer arrangement of CSS classes, i.e. the `Employees` column is not be reindexed to `.col0` but remains as `.col2`.\n", + "Hiding does not change the integer arrangement of CSS classes, i.e. the `Employees` column is not be reindexed to `.col0` but remains as `.col2`. It does also not affect the underlying ``Styler.data``.\n", + "\n", + "We can update our `style` object to hide some data and format the values...\n", "\n", "[hideidx]: ../reference/api/pandas.io.formats.style.Styler.hide_index.html#pandas.io.formats.style.Styler.hide_index\n", "[hidecols]: ../reference/api/pandas.io.formats.style.Styler.hide_columns.html#pandas.io.formats.style.Styler.hide_columns" @@ -184,11 +507,66 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 161, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Model:Decision TreeRegression
Predicted:TumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)3821822
Non-Tumour (Negative)194396452
\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 161, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "df.style.hide_index().hide_columns(['Income', 'Expenses'])" + "s = df.style.format('{:.0f}').hide_columns([('Random', 'Tumour'), ('Random', 'Non-Tumour')])\n", + "s" ] }, { @@ -245,7 +623,7 @@ "outputs": [], "source": [ "hover_style = {\n", - " 'selector': \"tr:hover\",\n", + " 'selector': \"tr:hover\", \n", " 'props': [(\"background-color\", \"#ffff99\")]\n", "}\n", "header_style = {\n", @@ -1706,9 +2084,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## More About CSS\n", + "## More About CSS and HTML\n", "\n", - "Cascading Style Sheet (CSS) language has its own peculiarities. It never reports errors: it just doesn't render your objects how you intend so can sometimes be frustrating. Here is a very brief primer on how ``Styler`` interacts with CSS and common pitfalls to avoid." + "Cascading Style Sheet (CSS) language, which is designed to influence how a browser renders HTML elements, has its own peculiarities. It never reports errors: it just silently ignores them and doesn't render your objects how you intend so can sometimes be frustrating. Here is a very brief primer on how ``Styler`` creates HTML and interacts with CSS, with advice on common pitfalls to avoid." ] }, { @@ -1734,7 +2112,51 @@ " + `col`, where `n` is the numeric position of the cell.\n", "- Blank cells include `blank`\n", "\n", - "The structure of the `id` is `T_uuid_level_row_col` where `level` is used only on headings, and headings will only have either `row` or `col` whichever is needed. By default 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 read more about the use of UUIDs in [Optimization](#Optimization)" + "The structure of the `id` is `T_uuid_level_row_col` where `level` is used only on headings, and headings will only have either `row` or `col` whichever is needed. By default 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 read more about the use of UUIDs in [Optimization](#Optimization).\n", + "\n", + "We can see example of the HTML by calling the [.render()][render] method.\n", + "\n", + "[render]: ../reference/api/pandas.io.formats.style.Styler.render.html#pandas.io.formats.style.Styler.render" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
c1c2
i112
i234
\n", + "\n" + ] + } + ], + "source": [ + "print(pd.DataFrame([[1,2],[3,4]], index=['i1', 'i2'], columns=['c1', 'c2']).style.render())" ] }, { @@ -1761,7 +2183,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.6" + "version": "3.8.7" } }, "nbformat": 4, From ca94f1a8ebeeaa0110490f1c33cf4b1543d9f059 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Tue, 9 Feb 2021 22:05:42 +0100 Subject: [PATCH 18/34] update --- doc/source/user_guide/style.ipynb | 1494 +++++++++++------------------ 1 file changed, 561 insertions(+), 933 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index a752417017c20..97ed4d833062d 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -30,7 +30,7 @@ }, { "cell_type": "code", - "execution_count": 126, + "execution_count": null, "metadata": { "nbsphinx": "hidden" }, @@ -43,78 +43,15 @@ }, { "cell_type": "code", - "execution_count": 127, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Model:Decision TreeRegressionRandom
Predicted:TumourNon-TumourTumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)38.0000002.00000018.00000022.0000002119
Non-Tumour (Negative)19.000000439.0000006.000000452.000000226232
\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 127, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "\n", - "df = pd.DataFrame([[38.0, 2.0, 18.0, 22.0, 21,19],[19, 439, 6, 452, 226,232]], \n", - " index=pd.Index(['Tumor (Positive)', 'Non-Tumour (Negative)'], name='Actual Label:'), \n", + "df = pd.DataFrame([[38.0, 2.0, 18.0, 22.0, 21, np.nan],[19, 439, 6, 452, 226,232]], \n", + " index=pd.Index(['Tumour (Positive)', 'Non-Tumour (Negative)'], name='Actual Label:'), \n", " columns=pd.MultiIndex.from_product([['Decision Tree', 'Regression', 'Random'],['Tumour', 'Non-Tumour']], names=['Model:', 'Predicted:']))\n", "df.style" ] @@ -130,25 +67,28 @@ }, { "cell_type": "code", - "execution_count": 155, + "execution_count": null, "metadata": { "nbsphinx": "hidden" }, "outputs": [], "source": [ + "# Hidden cell to just create the below example: code is covered throughout the guide.\n", "s = df.style\\\n", " .hide_columns([('Random', 'Tumour'), ('Random', 'Non-Tumour')])\\\n", " .format('{:.0f}')\\\n", - " .set_table_styles([\n", - " {\n", + " .set_table_styles([{\n", " 'selector': '',\n", - " 'props': 'caption-side: bottom; border-collapse: separate;'\n", + " 'props': 'border-collapse: separate;'\n", + " },{\n", + " 'selector': 'caption',\n", + " 'props': 'caption-side: bottom; font-size:1.3em;'\n", " },{\n", " 'selector': '.index_name',\n", " 'props': 'font-style: italic; color: darkgrey; font-weight:normal;'\n", " },{\n", " 'selector': 'th:not(.index_name)',\n", - " 'props': 'background-color: darkblue; color: white;'\n", + " 'props': 'background-color: #000066; color: white;'\n", " },{\n", " 'selector': 'th.col_heading',\n", " 'props': 'text-align: center;'\n", @@ -160,7 +100,7 @@ " 'props': 'border-left: 1px solid white;'\n", " },{\n", " 'selector': '.col2',\n", - " 'props': 'border-left: 1px solid darkblue;'\n", + " 'props': 'border-left: 1px solid #000066;'\n", " },{\n", " 'selector': 'td',\n", " 'props': 'text-align: center; font-weight:bold;'\n", @@ -184,143 +124,21 @@ " ['false', 'true', 'false', 'true', '', '']], \n", " index=df.index, columns=df.columns))\\\n", " .set_caption(\"Confusion matrix for multiple cancer prediction models.\")\\\n", - " .set_tooltips(pd.DataFrame([['DT has a very strong true positive rate', '', '', 'Regression total false negatives is unacceptable', '', ''],\n", + " .set_tooltips(pd.DataFrame([['This model has a very strong true positive rate', '', '', \"This model's total number of false negatives is too high\", '', ''],\n", " ['', '', '', '', '', '']], \n", " index=df.index, columns=df.columns))\\\n", " .set_tooltips_class(name='pd-tt', properties=\n", - " 'visibility: hidden; position: absolute; z-index: 1;'\n", - " 'background-color: lightgrey; color: darkblue; font-size: 0.8em;' \n", - " 'transform: translate(0px, -24px); padding: 0.5em; border-radius: 0.5em;'\n", + " 'visibility: hidden; position: absolute; z-index: 1; border: 1px solid #000066;'\n", + " 'background-color: white; color: #000066; font-size: 0.8em;' \n", + " 'transform: translate(0px, -24px); padding: 0.6em; border-radius: 0.5em;'\n", ")\n" ] }, { "cell_type": "code", - "execution_count": 156, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Confusion matrix for multiple cancer prediction models.
Model:Decision TreeRegression
Predicted:TumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)3821822
Non-Tumour (Negative)194396452
\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 156, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "s" ] @@ -329,11 +147,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Formatting Display Values \n", + "## Formatting the Display\n", + "\n", + "### Formatting Values\n", "\n", "Before adding styles it is useful to show that the [Styler][styler] can distinguish the *display* value from the *actual* value. To control the display value, the text is printed in each cell, and we can use [.format()][formatfunc] method to manipulate this according to a [format spec string][format] or a callable that takes a single value and returns a string. It is possible to define this for the whole table or for individual columns. \n", "\n", - "Missing data can also be reformatted easily here and we can also [hide data](#Hiding-Data) if necessary.\n", + "Missing data can also be reformatted easily here to a string and we can also [hide data](#Hiding-Data) if necessary.\n", "\n", "[styler]: ../reference/api/pandas.io.formats.style.Styler.html#pandas.io.formats.style.Styler\n", "[format]: https://docs.python.org/3/library/string.html#format-specification-mini-language\n", @@ -342,144 +162,18 @@ }, { "cell_type": "code", - "execution_count": 157, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Model:Decision TreeRegressionRandom
Predicted:TumourNon-TumourTumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)38218222119
Non-Tumour (Negative)194396452226232
\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 157, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "df.style.format(\"{:,.0f}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 158, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Model:Decision TreeRegressionRandom
Predicted:TumourNon-TumourTumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)38.00218-22,000.02119
Non-Tumour (Negative)19.004396-452,000.0226232
\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 158, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.style.format(\"{:,.0f}\", na_rep='MISSING')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "df.style.format('{:.0f}').format({\n", " ('Decision Tree', 'Tumour'): \"{:.2f}\",\n", @@ -487,17 +181,47 @@ "})" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Precision\n", + "\n", + "Alternatively you can control the precision of floats using pandas' regular `display.precision` option or through a [.set_precision()][precis] method.\n", + "\n", + "[precis]: ../reference/api/pandas.io.formats.style.Styler.set_precision.html#pandas.io.formats.style.Styler.set_precision" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "with pd.option_context('display.precision', 2):\n", + " html = df.style" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df.style.set_precision(2)" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Hiding Data\n", "\n", - "The index can be hidden from rendering by calling [.hide_index()][hideidx]. \n", + "The index can be hidden from rendering by calling [.hide_index()][hideidx], which might be useful if your index is integer based.\n", "\n", "Columns can be hidden from rendering by calling [.hide_columns()][hidecols] and passing in the name of a column, or a slice of columns.\n", "\n", - "Hiding does not change the integer arrangement of CSS classes, i.e. the `Employees` column is not be reindexed to `.col0` but remains as `.col2`. It does also not affect the underlying ``Styler.data``.\n", + "Hiding does not change the integer arrangement of CSS classes, i.e. hiding the first two columns of a DataFrame means the column class indexing will start at `col2`, since `col0` and `col1` are simply ignored.\n", "\n", "We can update our `style` object to hide some data and format the values...\n", "\n", @@ -507,81 +231,37 @@ }, { "cell_type": "code", - "execution_count": 161, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
Model:Decision TreeRegression
Predicted:TumourNon-TumourTumourNon-Tumour
Actual Label:
Tumor (Positive)3821822
Non-Tumour (Negative)194396452
\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 161, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "s = df.style.format('{:.0f}').hide_columns([('Random', 'Tumour'), ('Random', 'Non-Tumour')])\n", "s" ] }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s.set_uuid('after_hide')" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Methods to Add Styles\n", "\n", - "There are **3 primary methods of adding custom CSS styles to [Styler][styler]**:\n", + "There are **3 primary methods of adding custom CSS styles** to [Styler][styler]:\n", "\n", - "- **Using [.set_table_styles()][table]** to control broader areas of the table with specified internal CSS. Although table styles allow the flexibility to add CSS selectors and properties controlling all individual parts of the table, they are unwieldy for individual cell specifications. Also, note that table styles cannot be exported to Excel. \n", - "- **Using [.set_td_classes()][td_class]** to directly link either external CSS classes to your data cells or link the internal CSS classes created by [.set_table_styles()][table]. These cannot be used on column header rows or indexes, and also won't export to Excel. \n", - "- **Using the [.apply()][apply] and [.applymap()][applymap] functions** to add direct internal CSS to specific data cells. These cannot be used on column header rows or indexes, but only these methods add styles that will export to Excel. These methods work in a similar way to [DataFrame.apply()][dfapply] and [DataFrame.applymap()][dfapplymap].\n", - "\n", - "Next we cover the advantages and limitations of each of the above methods.\n", + "- Using [.set_table_styles()][table] to control broader areas of the table with specified internal CSS. Although table styles allow the flexibility to add CSS selectors and properties controlling all individual parts of the table, they are unwieldy for individual cell specifications. Also, note that table styles cannot be exported to Excel. \n", + "- Using [.set_td_classes()][td_class] to directly link either external CSS classes to your data cells or link the internal CSS classes created by [.set_table_styles()][table]. See [here](#Setting-Classes-and-Linking-to-External-CSS). These cannot be used on column header rows or indexes, and also won't export to Excel. \n", + "- Using the [.apply()][apply] and [.applymap()][applymap] functions to add direct internal CSS to specific data cells. See [here](#Styler-Methods). These cannot be used on column header rows or indexes, but only these methods add styles that will export to Excel. These methods work in a similar way to [DataFrame.apply()][dfapply] and [DataFrame.applymap()][dfapplymap].\n", "\n", "[table]: ../reference/api/pandas.io.formats.style.Styler.set_table_styles.html#pandas.io.formats.style.Styler.set_table_styles\n", "[styler]: ../reference/api/pandas.io.formats.style.Styler.html#pandas.io.formats.style.Styler\n", @@ -596,14 +276,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Using Table Styles\n", + "## Table Styles\n", "\n", "Table styles are flexible enough to control all individual parts of the table, including column headers and indexes. \n", "However, they can be unwieldy to type for individual data cells or for any kind of conditional formatting, so we recommend that table styles are used for broad styling, such as rows or columns at a time.\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. This `:hover` pseudo-selectors, as well as others, can only be used this way.\n", "\n", - "CSS style sheets are typically composed of selectors, e.g. element ids or css classes, and attribute value pairs:\n", + "To replicate the normal format of CSS selectors and properties (attribute value pairs), e.g. \n", "\n", "```\n", "tr:hover {\n", @@ -611,7 +291,7 @@ "}\n", "```\n", "\n", - "The necessary format to pass styles to [.set_table_styles()][table] is as a list of dicts, each with a CSS-selector tag and CSS-properties. Properties can either be a list of 2-tuples, or a regular CSS-string For example:\n", + "the necessary format to pass styles to [.set_table_styles()][table] is as a list of dicts, each with a CSS-selector tag and CSS-properties. Properties can either be a list of 2-tuples, or a regular CSS-string, for example:\n", "\n", "[table]: ../reference/api/pandas.io.formats.style.Styler.set_table_styles.html#pandas.io.formats.style.Styler.set_table_styles" ] @@ -622,45 +302,38 @@ "metadata": {}, "outputs": [], "source": [ - "hover_style = {\n", - " 'selector': \"tr:hover\", \n", - " 'props': [(\"background-color\", \"#ffff99\")]\n", + "cell_hover = { # for row hover use
\n", + " 'selector': 'td:hover',\n", + " 'props': [('background-color', '#ffffb3')]\n", "}\n", - "header_style = {\n", - " 'selector': \"th\", \n", - " 'props': 'font-size: 115%; text-align: center; color: #e83e8c;'\n", + "index_names = {\n", + " 'selector': '.index_name',\n", + " 'props': 'font-style: italic; color: darkgrey; font-weight:normal;'\n", "}\n", - "df.style.format(\"{:,.0f}\").set_table_styles([hover_style, header_style])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Table styles allows for controlling specific rows and columns by their integer index, for example" + "headers = {\n", + " 'selector': 'th:not(.index_name)',\n", + " 'props': 'background-color: #000066; color: white;'\n", + "}\n", + "s.set_table_styles([cell_hover, index_names, headers])" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "nbsphinx": "hidden" + }, "outputs": [], "source": [ - "df.style.format(\"{:,.0f}\").set_table_styles([hover_style, header_style, {\n", - " 'selector': 'th.level1',\n", - " 'props': 'color: darkblue;'\n", - "}])" + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s.set_uuid('after_tab_styles1')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "As a convenience method (*since version 1.2.0*) we can also pass a **dict** to [.set_table_styles()][table] which contains row or column keys. Behind the scenes Styler just indexes the keys and add relevant `.col` or `.row` classes as necessary to the CSS selectors.\n", - "\n", - "Table styles can be chained provided multiple methods don't *overwrite*.\n", - "\n", - "[table]: ../reference/api/pandas.io.formats.style.Styler.set_table_styles.html#pandas.io.formats.style.Styler.set_table_styles" + "Next we just add a couple more styling artifacts targeting specific parts of the table, and we add some internally defined CSS classes that we need for the next section. Be careful here, since we are *chaining methods* we need to explicitly instruct the method **not to** ``overwrite`` the existing styles." ] }, { @@ -669,34 +342,37 @@ "metadata": {}, "outputs": [], "source": [ - "s = df.style.format(\"{:,.0f}\")\\\n", - " .set_table_styles([hover_style, header_style, {\n", - " 'selector': 'th.level1',\n", - " 'props': 'color: darkblue;'}])\\\n", - " .set_table_styles({\n", - " 'Employees': [{\n", - " 'selector': 'td',\n", - " 'props': 'font-weight: bold;'}]\n", - " }, axis=0, overwrite=False)\\\n", - " .set_table_styles({\n", - " (20, 'Mar'): [{\n", - " 'selector': '',\n", - " 'props': 'background-color: darkgrey; color: white;'}]\n", - " }, axis=1, overwrite=False)\n", - "s" + "s.set_table_styles([\n", + " {'selector': 'th.col_heading', 'props': 'text-align: center;'},\n", + " {'selector': 'th.col_heading.level0', 'props': 'font-size: 1.5em;'},\n", + " {'selector': 'td', 'props': 'text-align: center; font-weight: bold;'},\n", + " # internal CSS classes\n", + " {'selector': '.true', 'props': 'background-color: #e6ffe6;'},\n", + " {'selector': '.false', 'props': 'background-color: #ffe6e6;'},\n", + " {'selector': '.border-red', 'props': 'border: 2px dashed red;'},\n", + " {'selector': '.border-green', 'props': 'border: 2px dashed green;'},\n", + "], overwrite=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s.set_uuid('after_tab_styles2')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Setting Classes and Linking to External CSS\n", + "As a convenience method (*since version 1.2.0*) we can also pass a **dict** to [.set_table_styles()][table] which contains row or column keys. Behind the scenes Styler just indexes the keys and adds relevant `.col` or `.row` classes as necessary to the given CSS selectors.\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. You may want to use these native files rather than duplicate all the CSS in python.\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 the HTML of this notebook page for demonstration*):" + "[table]: ../reference/api/pandas.io.formats.style.Styler.set_table_styles.html#pandas.io.formats.style.Styler.set_table_styles" ] }, { @@ -705,34 +381,37 @@ "metadata": {}, "outputs": [], "source": [ - "# from IPython.display import HTML\n", - "# style = \\\n", - "# \"\"\n", - "# HTML(style)" + "s.set_table_styles({\n", + " ('Regression', 'Tumour'): [{'selector': 'th', 'props': 'border-left: 1px solid white'},\n", + " {'selector': 'td', 'props': 'border-left: 1px solid #000066'}]\n", + "}, overwrite=False, axis=0)" ] }, { - "cell_type": "markdown", - "metadata": {}, + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], "source": [ - " " + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s.set_uuid('xyz01')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "At a minimum we can add a class attribute to our `` element, which will connect with the external CSS, and in this case change the width of the table." + "## Setting Classes and Linking to External CSS\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. You may want to use these native files rather than duplicate all the CSS in python (and duplicate any maintenance work).\n", + "\n", + "### Table Attributes\n", + "\n", + "It is very easy to add a `class` to the main `
` using [.set_table_attributes()][tableatt]. This method can also attach inline styles - read more in [CSS Hierarchies](#CSS-Hierarchies).\n", + "\n", + "[tableatt]: ../reference/api/pandas.io.formats.style.Styler.set_table_attributes.html#pandas.io.formats.style.Styler.set_table_attributes" ] }, { @@ -741,7 +420,8 @@ "metadata": {}, "outputs": [], "source": [ - "s.set_table_attributes('class=\"table-cls\"')" + "out = s.set_table_attributes('class=\"my-table-cls\"').render()\n", + "print(out[out.find('` elements of the `
`.\n", + "*New in version 1.2.0*\n", + "\n", + "The [.set_td_classes()][tdclass] method accepts a DataFrame with matching indices and columns to the underlying [Styler][styler]'s DataFrame. That DataFrame will contain strings as css-classes to add to individual data cells: the `\n", - "
` elements of the ``. Here we add our `.true` and `.false` classes that we created previously. We will save adding the borders until the [section on tooltips](#Tooltips).\n", "\n", "[tdclass]: ../reference/api/pandas.io.formats.style.Styler.set_td_classes.html#pandas.io.formats.style.Styler.set_td_classes\n", "[styler]: ../reference/api/pandas.io.formats.style.Styler.html#pandas.io.formats.style.Styler" @@ -762,29 +444,40 @@ "metadata": {}, "outputs": [], "source": [ - "df = pd.DataFrame(np.random.randn(5,5), columns=[f'Trial {x}' for x in range(5)])\n", - "df.iloc[3,4], df.iloc[4,3] = np.nan, np.nan\n", - "df.style.format('{:,.3f}')" + "cell_color = pd.DataFrame([['true ', 'false ', 'true ', 'false '], \n", + " ['false ', 'true ', 'false ', 'true ']], \n", + " index=df.index, \n", + " columns=df.columns[:4])\n", + "s.set_td_classes(cell_color)" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "nbsphinx": "hidden" + }, "outputs": [], "source": [ - "cls_inv = pd.DataFrame('', index=df.index, columns=df.columns)\n", - "cls_bold = pd.DataFrame('', index=df.index, columns=df.columns)\n", - "cls_inv.iloc[0:2, 0:2] = 'cls-invert '\n", - "cls_bold.iloc[1:3, 1:3] = 'cls-bold '\n", - "df.style.format('{:,.3f}').set_td_classes(cls_inv + cls_bold)" + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s.set_uuid('after_classes')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "It is possible for this to be an entirely internal CSS solution by defining the class within table styles." + "## Styler Functions\n", + "\n", + "We use the following methods to pass your style functions. Both of those methods take a function (and some other keyword arguments) and apply it to the DataFrame in a certain way, rendering CSS styles.\n", + "\n", + "- [.applymap()][applymap] (elementwise): accepts a function that takes a single value and returns a string with the CSS attribute-value pair.\n", + "- [.apply()][apply] (column-/row-/table-wise): accepts a function that takes a Series or DataFrame and returns a Series, DataFrame, or numpy array with an identical shape where each element is a string with a CSS attribute-value pair. This method passes each column or row of your DataFrame one-at-a-time or the entire table at once, depending on the `axis` keyword argument. For columnwise use `axis=0`, rowwise use `axis=1`, and for the entire table at once use `axis=None`.\n", + "\n", + "This method is powerful for applying multiple, complex logic to data cells.\n", + "\n", + "[apply]: ../reference/api/pandas.io.formats.style.Styler.apply.html#pandas.io.formats.style.Styler.apply\n", + "[applymap]: ../reference/api/pandas.io.formats.style.Styler.applymap.html#pandas.io.formats.style.Styler.applymap" ] }, { @@ -793,48 +486,48 @@ "metadata": {}, "outputs": [], "source": [ - "df.style.format('{:,.3f}')\\\n", - " .set_table_styles([\n", - " {'selector': '.cls-invert',\n", - " 'props': [('color', 'white'), ('background-color', '#e83e8c')]}])\\\n", - " .set_td_classes(cls_inv)" + "np.random.seed(0)\n", + "df2 = pd.DataFrame(np.random.randn(10,4), columns=['A','B','C','D'])\n", + "df2.style" ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, + "cell_type": "markdown", "metadata": {}, - "outputs": [], - "source": [] + "source": [ + "For example we can build a function that colors text if it is negative, and chain this with a function that partially fades cells of negligible value. Since this looks at each element in turn we use ``applymap``." + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "def style_negative(v, props=''):\n", + " return props if v < 0 else None\n", + "s2 = df2.style.applymap(style_negative, props='color:red;')\\\n", + " .applymap(lambda v: 'opacity: 20%;' if (v < 0.3) and (v > -0.3) else None)\n", + "s2" + ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "nbsphinx": "hidden" + }, "outputs": [], - "source": [] + "source": [ + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s2.set_uuid('after_applymap')" + ] }, { "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." + "We can also build a function that highlights the maximum value across rows, cols, and the DataFrame all at once. In this case we use ``apply``. Below we highlight the maximum in a column." ] }, { @@ -843,31 +536,28 @@ "metadata": {}, "outputs": [], "source": [ - "df.style.set_table_styles({\n", - " 'A': [{'selector': '',\n", - " 'props': [('color', 'red')]}],\n", - " 'B': [{'selector': 'td',\n", - " 'props': [('color', 'blue')]}]\n", - "}, axis=0)" + "def highlight_max(s, props=''):\n", + " return np.where(s == np.nanmax(s.values), props, '')\n", + "s2.apply(highlight_max, props='color:white;background-color:darkblue', axis=0)" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "nbsphinx": "hidden" + }, "outputs": [], "source": [ - "df.style.set_table_styles({\n", - " 3: [{'selector': 'td',\n", - " 'props': [('color', 'green')]}]\n", - "}, axis=1)" + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s2.set_uuid('after_apply')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can also chain all of the above by setting the `overwrite` argument to `False` so that it preserves previous settings." + "We can use the same function across the different axes, highlight the DataFrame maximum in purple." ] }, { @@ -876,27 +566,34 @@ "metadata": {}, "outputs": [], "source": [ - "from pandas.io.formats.style import Styler\n", - "s = Styler(df, cell_ids=False, uuid_len=0).\\\n", - " set_table_styles(styles).\\\n", - " set_table_styles({\n", - " 'A': [{'selector': '',\n", - " 'props': [('color', 'red')]}],\n", - " 'B': [{'selector': 'td',\n", - " 'props': [('color', 'blue')]}]\n", - " }, axis=0, overwrite=False).\\\n", - " set_table_styles({\n", - " 3: [{'selector': 'td',\n", - " 'props': [('color', 'green')]}]\n", - " }, axis=1, overwrite=False)\n", - "s" + "s2.apply(highlight_max, props='color:white;background-color:pink;', axis=1)\\\n", + " .apply(highlight_max, props='color:white;background-color:purple', axis=None)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "By using these `table_styles` and the additional `Styler` arguments to optimize the HTML we have compressed these styles to only a few lines withing the \\ tags and none of the \\ cells require any `id` attributes. " + "This last example shows how some styles have been overwritten by others. In general the most recent style applied is effective but you can read more in the [section on CSS hierarchies](#CSS-Hierarchies). You can also apply these styles to more granular parts of the DataFrame - read more in section on [subset slicing](#Subset-Slicing).\n", + "\n", + "It is possible to replicate some of this functionality using just classes but it can be more cumbersome. See [iii) as part of Optimization](#Optimization)\n", + "\n", + "
\n", + "\n", + "*Debugging Tip*: If you're having trouble writing your style function, try just passing it into ``DataFrame.apply``. Internally, ``Styler.apply`` uses ``DataFrame.apply`` so the result should be the same.\n", + "\n", + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Tooltips and Captions\n", + "\n", + "Table captions can be added with the [.set_caption()][caption] method. You can use table styles to control the CSS relevant to the caption.\n", + "\n", + "[caption]: ../reference/api/pandas.io.formats.style.Styler.set_caption.html#pandas.io.formats.style.Styler.set_caption" ] }, { @@ -905,74 +602,65 @@ "metadata": {}, "outputs": [], "source": [ - "s.render().split('\\n')[:16]" + "s.set_caption(\"Confusion matrix for multiple cancer prediction models.\")\\\n", + " .set_table_styles([{\n", + " 'selector': 'caption',\n", + " 'props': 'caption-side: bottom; font-size:1.25em;'\n", + " }], overwrite=False)" ] }, { - "cell_type": "markdown", - "metadata": {}, + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], "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." + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s.set_uuid('after_caption')" ] }, { "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", - "- ``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", - "\n", - "For `Styler.applymap` your function should take a scalar and return a single string with the CSS attribute-value pair.\n", + "Adding tooltips (*since version 1.3.0*) can be done using the [.set_tooltips()][tooltips] method in the same way you can add CSS classes to data cells by providing a string based DataFrame with intersecting indices and columns.\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." + "[tooltips]: ../reference/api/pandas.io.formats.style.Styler.set_tooltips.html#pandas.io.formats.style.Styler.set_tooltips" ] }, { - "cell_type": "markdown", + "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ - "Let's write a simple style function that will color negative numbers red and positive numbers black." + "tt = pd.DataFrame([['This model has a very strong true positive rate', \n", + " \"This model's total number of false negatives is too high\"]], \n", + " index=['Tumour (Positive)'], columns=df.columns[[0,3]])\n", + "s.set_tooltips(tt)" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "nbsphinx": "hidden" + }, "outputs": [], "source": [ - "def color_negative_red(val):\n", - " \"\"\"\n", - " Takes a scalar and returns a string with\n", - " the css property `'color: red'` for negative\n", - " strings, black otherwise.\n", - " \"\"\"\n", - " color = 'red' if val < 0 else 'black'\n", - " return 'color: %s' % color" + "# Hidden cell to avoid CSS clashes and latter code upcoding previous formatting \n", + "s.set_uuid('after_tooltips')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In this case, the cell's style depends only on its own value.\n", - "That means we should use the `Styler.applymap` method which works elementwise." + "The tooltips are added with a default CSS styling, however, you can custom style them using the [.set_tooltips_class()][tooltipsclass] method. Alternatively the name of the tooltips CSS class can be integrated with your existing website's so you do not need to set any properties within Python if you have the external CSS files. \n", + "\n", + "[tooltipsclass]: ../reference/api/pandas.io.formats.style.Styler.set_tooltips_class.html#pandas.io.formats.style.Styler.set_tooltips_class" ] }, { @@ -981,28 +669,30 @@ "metadata": {}, "outputs": [], "source": [ - "s = df.style.applymap(color_negative_red)\n", - "s" + "s.set_tooltips_class(name='pd-tt', properties=\n", + " 'visibility: hidden; position: absolute; z-index: 1; border: 1px solid #000066;'\n", + " 'background-color: white; color: #000066; font-size: 0.8em;' \n", + " 'transform: translate(0px, -24px); padding: 0.6em; border-radius: 0.5em;'\n", + ")" ] }, { - "cell_type": "markdown", - "metadata": {}, + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], "source": [ - "Notice the similarity with the standard `df.applymap`, which operates on DataFrames elementwise. We want you to be able to reuse your existing knowledge of how to interact with DataFrames.\n", - "\n", - "Notice also that our function returned a string containing the CSS attribute and value, separated by a colon just like in a `'.format(css))" + "# HTML(''.format(css))" ] }, { @@ -2093,7 +1631,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## CSS Classes and Ids\n", + "### CSS Classes and Ids\n", "\n", "The precise structure of the CSS `class` attached to each cells is as follows.\n", "\n", @@ -2121,50 +1659,140 @@ }, { "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
c1c2
i112
i234
\n", - "\n" - ] - } - ], + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "print(pd.DataFrame([[1,2],[3,4]], index=['i1', 'i2'], columns=['c1', 'c2']).style.render())" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### CSS Hierarchies\n", + "\n", + "The examples have shown that when CSS styles overlap, often the latter (in the HTML sense) takes precedence. So the following yield different results:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df4 = pd.DataFrame([['text']])\n", + "df4.style.applymap(lambda x: 'color:green;')\\\n", + " .applymap(lambda x: 'color:red;')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df4.style.applymap(lambda x: 'color:red;')\\\n", + " .applymap(lambda x: 'color:green;')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This is only true for CSS rules that are equivalent in hierarchy, or importance. You can read more about [CSS specificity here](https://www.w3schools.com/css/css_specificity.asp) but for our purposes it pays to summarise the key points:\n", + "\n", + "A CSS importance score for each HTML element is derived by starting at zero and adding:\n", + "\n", + " - 1000 for an inline style attribute\n", + " - 100 for each ID, \n", + " - 10 for each attribute, class or pseudo-class\n", + " - 1 for each element name or pseudo-element\n", + " \n", + "Let's use this to describe the action of the following configurations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df4.style.set_uuid('a_')\\\n", + " .set_table_styles([{'selector': 'td', 'props': 'color:red;'}])\\\n", + " .applymap(lambda x: 'color:green;')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This text is red because the generated selector `#T_a_ td` is worth 101 (ID plus element), whereas `#T_a_row0_col0` is only worth 100 (ID), so is considered inferior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df4.style.set_uuid('b_')\\\n", + " .set_table_styles([{'selector': 'td', 'props': 'color:red;'},\n", + " {'selector': '.cls-1', 'props': 'color:blue;'}])\\\n", + " .applymap(lambda x: 'color:green;')\\\n", + " .set_td_classes(pd.DataFrame([['cls-1']]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the above case the text is blue because the selector `#T_b_ .cls-1` is worth 110 (ID plus class), which takes precendence." + ] + }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "source": [ + "df4.style.set_uuid('c_')\\\n", + " .set_table_styles([{'selector': 'td', 'props': 'color:red;'},\n", + " {'selector': '.cls-1', 'props': 'color:blue;'},\n", + " {'selector': 'td.data', 'props': 'color:yellow;'}])\\\n", + " .applymap(lambda x: 'color:green;')\\\n", + " .set_td_classes(pd.DataFrame([['cls-1']]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now we have created another table style this time the selector `T_c_ td.data` (ID plus element plus class) gets bumped up to 111. \n", + "\n", + "If your style fails to be applied, and its really frustrating, try the `!important` trump card." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "df4.style.set_uuid('d_')\\\n", + " .set_table_styles([{'selector': 'td', 'props': 'color:red;'},\n", + " {'selector': '.cls-1', 'props': 'color:blue;'},\n", + " {'selector': 'td.data', 'props': 'color:yellow;'}])\\\n", + " .applymap(lambda x: 'color:green !important;')\\\n", + " .set_td_classes(pd.DataFrame([['cls-1']]))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally got that green text after all!" + ] } ], "metadata": { From bba157714ebe5efb0fd9fda83a210d83b49f0a27 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Tue, 9 Feb 2021 23:47:17 +0100 Subject: [PATCH 19/34] update --- doc/source/user_guide/style.ipynb | 380 ++++++++++++++---------------- 1 file changed, 182 insertions(+), 198 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 97ed4d833062d..5f0c73abc19a2 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -1010,7 +1010,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "4) **If every byte counts use string replacement**\n", + "4) **Don't use tooltips**\n", + "\n", + "Tooltips require `cell_ids` to work and they generate extra HTML elements for *every* data cell.\n", + "\n", + "5) **If every byte counts use string replacement**\n", "\n", "You can remove unnecessary HTML, or shorten the default class names with string replace functions..." ] @@ -1052,14 +1056,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Builtin styles" + "## Builtin Styles" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Finally, we expect certain styling functions to be common enough that we've included a few \"built-in\" to the `Styler`, so you don't have to write them yourself." + "We expect certain styling functions to be common enough that we've included a few \"built-in\" to the `Styler`, so you don't have to write them yourself." ] }, { @@ -1068,7 +1072,9 @@ "metadata": {}, "outputs": [], "source": [ - "df.style.highlight_null(null_color='red')" + "df2.iloc[0,2] = np.nan\n", + "df2.iloc[4,3] = np.nan\n", + "df2.loc[:4].style.highlight_null(null_color='red')" ] }, { @@ -1085,11 +1091,9 @@ "outputs": [], "source": [ "import seaborn as sns\n", - "\n", "cm = sns.light_palette(\"green\", as_cmap=True)\n", "\n", - "s = df.style.background_gradient(cmap=cm)\n", - "s" + "df2.style.background_gradient(cmap=cm)" ] }, { @@ -1106,7 +1110,7 @@ "outputs": [], "source": [ "# Uses the full color range\n", - "#df.loc[:4].style.background_gradient(cmap='viridis')" + "df2.loc[:4].style.background_gradient(cmap='viridis')" ] }, { @@ -1116,17 +1120,16 @@ "outputs": [], "source": [ "# Compress the color range\n", - "# (df.loc[:4]\n", - "# .style\n", - "# .background_gradient(cmap='viridis', low=.5, high=0)\n", - "# .highlight_null('red'))" + "df2.loc[:4].style\\\n", + " .background_gradient(cmap='viridis', low=.5, high=0)\\\n", + " .highlight_null('red')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "There's also `.highlight_min` and `.highlight_max`." + "There's also `.highlight_min` and `.highlight_max`, albeit the one-line custom function we created above is probably better since its more flexible on the styling. " ] }, { @@ -1135,14 +1138,14 @@ "metadata": {}, "outputs": [], "source": [ - "df.style.highlight_max(axis=0)" + "df2.loc[:4].style.highlight_max(axis=0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Use `Styler.set_properties` when the style doesn't actually depend on the values." + "Use `Styler.set_properties` when the style doesn't actually depend on the values. This is just a simple wrapper for `.applymap` where the function returns the same properties for all cells." ] }, { @@ -1151,7 +1154,7 @@ "metadata": {}, "outputs": [], "source": [ - "df.style.set_properties(**{'background-color': 'black',\n", + "df2.loc[:4].style.set_properties(**{'background-color': 'black',\n", " 'color': 'lawngreen',\n", " 'border-color': 'white'})" ] @@ -1176,7 +1179,7 @@ "metadata": {}, "outputs": [], "source": [ - "df.style.bar(subset=['A', 'B'], color='#d65f5f')" + "df2.style.bar(subset=['A', 'B'], color='#d65f5f')" ] }, { @@ -1194,7 +1197,7 @@ "metadata": {}, "outputs": [], "source": [ - "df.style.bar(subset=['A', 'B'], align='mid', color=['#d65f5f', '#5fba7d'])" + "df2.style.bar(subset=['A', 'B'], align='mid', color=['#d65f5f', '#5fba7d'])" ] }, { @@ -1207,9 +1210,12 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "nbsphinx": "hidden" + }, "outputs": [], "source": [ + "# Hide the construction of the display chart from the user\n", "import pandas as pd\n", "from IPython.display import HTML\n", "\n", @@ -1244,9 +1250,15 @@ " \n", "head+= \"\"\"\n", "
\"\"\"\n", - " \n", - "\n", + "
\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "HTML(head)" ] }, @@ -1297,33 +1309,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Missing values" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can control the default missing values representation for the entire table through `set_na_rep` method." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# (df.style\n", - "# .set_na_rep(\"FAIL\")\n", - "# .format(None, na_rep=\"PASS\", subset=[\"D\"])\n", - "# .highlight_null(\"yellow\"))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Limitations\n", + "## Limitations\n", "\n", "- DataFrame only `(use Series.to_frame().style)`\n", "- The index and columns must be unique\n", @@ -1353,10 +1339,10 @@ "metadata": {}, "outputs": [], "source": [ - "from IPython.html import widgets\n", + "from ipywidgets import widgets\n", "@widgets.interact\n", "def f(h_neg=(0, 359, 1), h_pos=(0, 359), s=(0., 99.9), l=(0., 99.9)):\n", - " return df.style.background_gradient(\n", + " return df2.style.background_gradient(\n", " cmap=sns.palettes.diverging_palette(h_neg=h_neg, h_pos=h_pos, s=s, l=l,\n", " as_cmap=True)\n", " )" @@ -1404,14 +1390,9 @@ "source": [ "## Export to Excel\n", "\n", - "*New in version 0.20.0*\n", - "\n", - "*Experimental: This is a new feature and still under development. We'll be adding features and possibly making breaking changes in future releases. We'd love to hear your feedback.*\n", - "\n", - "Some support is available for exporting styled `DataFrames` to Excel worksheets using the `OpenPyXL` or `XlsxWriter` engines. CSS2.2 properties handled include:\n", + "Some support (*since version 0.20.0*) is available for exporting styled `DataFrames` to Excel worksheets using the `OpenPyXL` or `XlsxWriter` engines. CSS2.2 properties handled include:\n", "\n", "- `background-color`\n", - "- `border-style`, `border-width`, `border-color` and their {`top`, `right`, `bottom`, `left` variants}\n", "- `color`\n", "- `font-family`\n", "- `font-style`\n", @@ -1422,11 +1403,14 @@ "- `white-space: nowrap`\n", "\n", "\n", + "- Currently broken: `border-style`, `border-width`, `border-color` and their {`top`, `right`, `bottom`, `left` variants}\n", + "\n", + "\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", "\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." + "Table level styles, and data cell CSS-classes are not included in the export to Excel: individual cells must have their properties mapped by the `Styler.apply` and/or `Styler.applymap` methods." ] }, { @@ -1435,10 +1419,10 @@ "metadata": {}, "outputs": [], "source": [ - "# df.style.\\\n", - "# applymap(color_negative_red).\\\n", - "# apply(highlight_max).\\\n", - "# to_excel('styled.xlsx', engine='openpyxl')" + "df2.style.\\\n", + " applymap(style_negative, props='color:red;').\\\n", + " highlight_max(axis=0).\\\n", + " to_excel('styled.xlsx', engine='openpyxl')" ] }, { @@ -1454,38 +1438,39 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Extensibility\n", - "\n", - "The core of pandas is, and will remain, its \"high-performance, easy-to-use data structures\".\n", - "With that in mind, we hope that `DataFrame.style` accomplishes two goals\n", - "\n", - "- Provide an API that is pleasing to use interactively and is \"good enough\" for many tasks\n", - "- Provide the foundations for dedicated libraries to build on\n", - "\n", - "If you build a great library on top of this, let us know and we'll [link](https://pandas.pydata.org/pandas-docs/stable/ecosystem.html) to it.\n", - "\n", - "### Subclassing\n", + "## More About CSS and HTML\n", "\n", - "If the default template doesn't quite suit your needs, you can subclass Styler and extend or override the template.\n", - "We'll show an example of extending the default template to insert a custom header before each table." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from jinja2 import Environment, ChoiceLoader, FileSystemLoader\n", - "from IPython.display import HTML\n", - "from pandas.io.formats.style import Styler" + "Cascading Style Sheet (CSS) language, which is designed to influence how a browser renders HTML elements, has its own peculiarities. It never reports errors: it just silently ignores them and doesn't render your objects how you intend so can sometimes be frustrating. Here is a very brief primer on how ``Styler`` creates HTML and interacts with CSS, with advice on common pitfalls to avoid." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We'll use the following template:" + "### CSS Classes and Ids\n", + "\n", + "The precise structure of the CSS `class` attached to each cells is as follows.\n", + "\n", + "- Cells with Index and Column names include `index_name` and `level` where `k` is its level in a MultiIndex\n", + "- Index label cells include\n", + " + `row_heading`\n", + " + `level` where `k` is the level in a MultiIndex\n", + " + `row` where `m` is the numeric position of the row\n", + "- Column label cells include\n", + " + `col_heading`\n", + " + `level` where `k` is the level in a MultiIndex\n", + " + `col` where `n` is the numeric position of the column\n", + "- Data cells include\n", + " + `data`\n", + " + `row`, where `m` is the numeric position of the cell.\n", + " + `col`, where `n` is the numeric position of the cell.\n", + "- Blank cells include `blank`\n", + "\n", + "The structure of the `id` is `T_uuid_level_row_col` where `level` is used only on headings, and headings will only have either `row` or `col` whichever is needed. By default 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 read more about the use of UUIDs in [Optimization](#Optimization).\n", + "\n", + "We can see example of the HTML by calling the [.render()][render] method.\n", + "\n", + "[render]: ../reference/api/pandas.io.formats.style.Styler.render.html#pandas.io.formats.style.Styler.render" ] }, { @@ -1494,16 +1479,16 @@ "metadata": {}, "outputs": [], "source": [ - "with open(\"templates/myhtml.tpl\") as f:\n", - " print(f.read())" + "print(pd.DataFrame([[1,2],[3,4]], index=['i1', 'i2'], columns=['c1', 'c2']).style.render())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now that we've created a template, we need to set up a subclass of ``Styler`` that\n", - "knows about it." + "### CSS Hierarchies\n", + "\n", + "The examples have shown that when CSS styles overlap, often the latter (in the HTML sense) takes precedence. So the following yield different results:" ] }, { @@ -1512,25 +1497,9 @@ "metadata": {}, "outputs": [], "source": [ - "class MyStyler(Styler):\n", - " env = Environment(\n", - " loader=ChoiceLoader([\n", - " FileSystemLoader(\"templates\"), # contains ours\n", - " Styler.loader, # the default\n", - " ])\n", - " )\n", - " template = env.get_template(\"myhtml.tpl\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Notice that we include the original loader in our environment's loader.\n", - "That's because we extend the original template, so the Jinja environment needs\n", - "to be able to find it.\n", - "\n", - "Now we can use that custom styler. It's `__init__` takes a DataFrame." + "df4 = pd.DataFrame([['text']])\n", + "df4.style.applymap(lambda x: 'color:green;')\\\n", + " .applymap(lambda x: 'color:red;')" ] }, { @@ -1539,14 +1508,24 @@ "metadata": {}, "outputs": [], "source": [ - "MyStyler(df)" + "df4.style.applymap(lambda x: 'color:red;')\\\n", + " .applymap(lambda x: 'color:green;')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Our custom template accepts a `table_title` keyword. We can provide the value in the `.render` method." + "This is only true for CSS rules that are equivalent in hierarchy, or importance. You can read more about [CSS specificity here](https://www.w3schools.com/css/css_specificity.asp) but for our purposes it pays to summarise the key points:\n", + "\n", + "A CSS importance score for each HTML element is derived by starting at zero and adding:\n", + "\n", + " - 1000 for an inline style attribute\n", + " - 100 for each ID, \n", + " - 10 for each attribute, class or pseudo-class\n", + " - 1 for each element name or pseudo-element\n", + " \n", + "Let's use this to describe the action of the following configurations" ] }, { @@ -1555,14 +1534,16 @@ "metadata": {}, "outputs": [], "source": [ - "HTML(MyStyler(df).render(table_title=\"Extending Example\"))" + "df4.style.set_uuid('a_')\\\n", + " .set_table_styles([{'selector': 'td', 'props': 'color:red;'}])\\\n", + " .applymap(lambda x: 'color:green;')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "For convenience, we provide the `Styler.from_custom_template` method that does the same as the custom subclass." + "This text is red because the generated selector `#T_a_ td` is worth 101 (ID plus element), whereas `#T_a_row0_col0` is only worth 100 (ID), so is considered inferior." ] }, { @@ -1571,15 +1552,18 @@ "metadata": {}, "outputs": [], "source": [ - "EasyStyler = Styler.from_custom_template(\"templates\", \"myhtml.tpl\")\n", - "EasyStyler(df)" + "df4.style.set_uuid('b_')\\\n", + " .set_table_styles([{'selector': 'td', 'props': 'color:red;'},\n", + " {'selector': '.cls-1', 'props': 'color:blue;'}])\\\n", + " .applymap(lambda x: 'color:green;')\\\n", + " .set_td_classes(pd.DataFrame([['cls-1']]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here's the template structure:" + "In the above case the text is blue because the selector `#T_b_ .cls-1` is worth 110 (ID plus class), which takes precendence." ] }, { @@ -1588,73 +1572,62 @@ "metadata": {}, "outputs": [], "source": [ - "with open(\"templates/template_structure.html\") as f:\n", - " structure = f.read()\n", - " \n", - "HTML(structure)" + "df4.style.set_uuid('c_')\\\n", + " .set_table_styles([{'selector': 'td', 'props': 'color:red;'},\n", + " {'selector': '.cls-1', 'props': 'color:blue;'},\n", + " {'selector': 'td.data', 'props': 'color:yellow;'}])\\\n", + " .applymap(lambda x: 'color:green;')\\\n", + " .set_td_classes(pd.DataFrame([['cls-1']]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "See the template in the [GitHub repo](https://github.com/pandas-dev/pandas) for more details." + "Now we have created another table style this time the selector `T_c_ td.data` (ID plus element plus class) gets bumped up to 111. \n", + "\n", + "If your style fails to be applied, and its really frustrating, try the `!important` trump card." ] }, { "cell_type": "code", "execution_count": null, - "metadata": { - "nbsphinx": "hidden" - }, + "metadata": {}, "outputs": [], "source": [ - "# # Hack to get the same style in the notebook as the\n", - "# # main site. This is hidden in the docs.\n", - "# from IPython.display import HTML\n", - "# with open(\"themes/nature_with_gtoc/static/nature.css_t\") as f:\n", - "# css = f.read()\n", - " \n", - "# HTML(''.format(css))" + "df4.style.set_uuid('d_')\\\n", + " .set_table_styles([{'selector': 'td', 'props': 'color:red;'},\n", + " {'selector': '.cls-1', 'props': 'color:blue;'},\n", + " {'selector': 'td.data', 'props': 'color:yellow;'}])\\\n", + " .applymap(lambda x: 'color:green !important;')\\\n", + " .set_td_classes(pd.DataFrame([['cls-1']]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## More About CSS and HTML\n", - "\n", - "Cascading Style Sheet (CSS) language, which is designed to influence how a browser renders HTML elements, has its own peculiarities. It never reports errors: it just silently ignores them and doesn't render your objects how you intend so can sometimes be frustrating. Here is a very brief primer on how ``Styler`` creates HTML and interacts with CSS, with advice on common pitfalls to avoid." + "Finally got that green text after all!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### CSS Classes and Ids\n", + "## Extensibility\n", "\n", - "The precise structure of the CSS `class` attached to each cells is as follows.\n", + "The core of pandas is, and will remain, its \"high-performance, easy-to-use data structures\".\n", + "With that in mind, we hope that `DataFrame.style` accomplishes two goals\n", "\n", - "- Cells with Index and Column names include `index_name` and `level` where `k` is its level in a MultiIndex\n", - "- Index label cells include\n", - " + `row_heading`\n", - " + `level` where `k` is the level in a MultiIndex\n", - " + `row` where `m` is the numeric position of the row\n", - "- Column label cells include\n", - " + `col_heading`\n", - " + `level` where `k` is the level in a MultiIndex\n", - " + `col` where `n` is the numeric position of the column\n", - "- Data cells include\n", - " + `data`\n", - " + `row`, where `m` is the numeric position of the cell.\n", - " + `col`, where `n` is the numeric position of the cell.\n", - "- Blank cells include `blank`\n", + "- Provide an API that is pleasing to use interactively and is \"good enough\" for many tasks\n", + "- Provide the foundations for dedicated libraries to build on\n", "\n", - "The structure of the `id` is `T_uuid_level_row_col` where `level` is used only on headings, and headings will only have either `row` or `col` whichever is needed. By default 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 read more about the use of UUIDs in [Optimization](#Optimization).\n", + "If you build a great library on top of this, let us know and we'll [link](https://pandas.pydata.org/pandas-docs/stable/ecosystem.html) to it.\n", "\n", - "We can see example of the HTML by calling the [.render()][render] method.\n", + "### Subclassing\n", "\n", - "[render]: ../reference/api/pandas.io.formats.style.Styler.render.html#pandas.io.formats.style.Styler.render" + "If the default template doesn't quite suit your needs, you can subclass Styler and extend or override the template.\n", + "We'll show an example of extending the default template to insert a custom header before each table." ] }, { @@ -1663,16 +1636,16 @@ "metadata": {}, "outputs": [], "source": [ - "print(pd.DataFrame([[1,2],[3,4]], index=['i1', 'i2'], columns=['c1', 'c2']).style.render())" + "from jinja2 import Environment, ChoiceLoader, FileSystemLoader\n", + "from IPython.display import HTML\n", + "from pandas.io.formats.style import Styler" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### CSS Hierarchies\n", - "\n", - "The examples have shown that when CSS styles overlap, often the latter (in the HTML sense) takes precedence. So the following yield different results:" + "We'll use the following template:" ] }, { @@ -1681,9 +1654,16 @@ "metadata": {}, "outputs": [], "source": [ - "df4 = pd.DataFrame([['text']])\n", - "df4.style.applymap(lambda x: 'color:green;')\\\n", - " .applymap(lambda x: 'color:red;')" + "with open(\"templates/myhtml.tpl\") as f:\n", + " print(f.read())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now that we've created a template, we need to set up a subclass of ``Styler`` that\n", + "knows about it." ] }, { @@ -1692,24 +1672,25 @@ "metadata": {}, "outputs": [], "source": [ - "df4.style.applymap(lambda x: 'color:red;')\\\n", - " .applymap(lambda x: 'color:green;')" + "class MyStyler(Styler):\n", + " env = Environment(\n", + " loader=ChoiceLoader([\n", + " FileSystemLoader(\"templates\"), # contains ours\n", + " Styler.loader, # the default\n", + " ])\n", + " )\n", + " template = env.get_template(\"myhtml.tpl\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This is only true for CSS rules that are equivalent in hierarchy, or importance. You can read more about [CSS specificity here](https://www.w3schools.com/css/css_specificity.asp) but for our purposes it pays to summarise the key points:\n", - "\n", - "A CSS importance score for each HTML element is derived by starting at zero and adding:\n", + "Notice that we include the original loader in our environment's loader.\n", + "That's because we extend the original template, so the Jinja environment needs\n", + "to be able to find it.\n", "\n", - " - 1000 for an inline style attribute\n", - " - 100 for each ID, \n", - " - 10 for each attribute, class or pseudo-class\n", - " - 1 for each element name or pseudo-element\n", - " \n", - "Let's use this to describe the action of the following configurations" + "Now we can use that custom styler. It's `__init__` takes a DataFrame." ] }, { @@ -1718,16 +1699,14 @@ "metadata": {}, "outputs": [], "source": [ - "df4.style.set_uuid('a_')\\\n", - " .set_table_styles([{'selector': 'td', 'props': 'color:red;'}])\\\n", - " .applymap(lambda x: 'color:green;')" + "MyStyler(df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "This text is red because the generated selector `#T_a_ td` is worth 101 (ID plus element), whereas `#T_a_row0_col0` is only worth 100 (ID), so is considered inferior." + "Our custom template accepts a `table_title` keyword. We can provide the value in the `.render` method." ] }, { @@ -1736,18 +1715,14 @@ "metadata": {}, "outputs": [], "source": [ - "df4.style.set_uuid('b_')\\\n", - " .set_table_styles([{'selector': 'td', 'props': 'color:red;'},\n", - " {'selector': '.cls-1', 'props': 'color:blue;'}])\\\n", - " .applymap(lambda x: 'color:green;')\\\n", - " .set_td_classes(pd.DataFrame([['cls-1']]))" + "HTML(MyStyler(df).render(table_title=\"Extending Example\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the above case the text is blue because the selector `#T_b_ .cls-1` is worth 110 (ID plus class), which takes precendence." + "For convenience, we provide the `Styler.from_custom_template` method that does the same as the custom subclass." ] }, { @@ -1756,21 +1731,15 @@ "metadata": {}, "outputs": [], "source": [ - "df4.style.set_uuid('c_')\\\n", - " .set_table_styles([{'selector': 'td', 'props': 'color:red;'},\n", - " {'selector': '.cls-1', 'props': 'color:blue;'},\n", - " {'selector': 'td.data', 'props': 'color:yellow;'}])\\\n", - " .applymap(lambda x: 'color:green;')\\\n", - " .set_td_classes(pd.DataFrame([['cls-1']]))" + "EasyStyler = Styler.from_custom_template(\"templates\", \"myhtml.tpl\")\n", + "EasyStyler(df)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Now we have created another table style this time the selector `T_c_ td.data` (ID plus element plus class) gets bumped up to 111. \n", - "\n", - "If your style fails to be applied, and its really frustrating, try the `!important` trump card." + "Here's the template structure:" ] }, { @@ -1779,19 +1748,34 @@ "metadata": {}, "outputs": [], "source": [ - "df4.style.set_uuid('d_')\\\n", - " .set_table_styles([{'selector': 'td', 'props': 'color:red;'},\n", - " {'selector': '.cls-1', 'props': 'color:blue;'},\n", - " {'selector': 'td.data', 'props': 'color:yellow;'}])\\\n", - " .applymap(lambda x: 'color:green !important;')\\\n", - " .set_td_classes(pd.DataFrame([['cls-1']]))" + "with open(\"templates/template_structure.html\") as f:\n", + " structure = f.read()\n", + " \n", + "HTML(structure)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Finally got that green text after all!" + "See the template in the [GitHub repo](https://github.com/pandas-dev/pandas) for more details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# # Hack to get the same style in the notebook as the\n", + "# # main site. This is hidden in the docs.\n", + "# from IPython.display import HTML\n", + "# with open(\"themes/nature_with_gtoc/static/nature.css_t\") as f:\n", + "# css = f.read()\n", + " \n", + "# HTML(''.format(css))" ] } ], From 205adfd968c2e5f2f2b5376b99ac00d852c8c27e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Wed, 10 Feb 2021 08:45:32 +0100 Subject: [PATCH 20/34] update --- doc/source/user_guide/style.ipynb | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/doc/source/user_guide/style.ipynb b/doc/source/user_guide/style.ipynb index 5f0c73abc19a2..3c86f8f62cb50 100644 --- a/doc/source/user_guide/style.ipynb +++ b/doc/source/user_guide/style.ipynb @@ -1282,9 +1282,8 @@ "metadata": {}, "outputs": [], "source": [ - "# df2 = -df\n", - "# style1 = df.style.applymap(color_negative_red)\n", - "# style1" + "style1 = df2.style.applymap(style_negative, props='color:red;')\\\n", + " .applymap(lambda v: 'opacity: 20%;' if (v < 0.3) and (v > -0.3) else None)" ] }, { @@ -1293,9 +1292,9 @@ "metadata": {}, "outputs": [], "source": [ - "# style2 = df2.style\n", - "# style2.use(style1.export())\n", - "# style2" + "style2 = df3.style\n", + "style2.use(style1.export())\n", + "style2" ] }, { @@ -1313,13 +1312,11 @@ "\n", "- DataFrame only `(use Series.to_frame().style)`\n", "- The index and columns must be unique\n", - "- No large repr, and performance isn't great; this is intended for summary DataFrames\n", + "- No large repr, and construction performance isn't great; although we have some [HTML optimizations](#Optimization)\n", "- You can only style the *values*, not the index or columns (except with `table_styles` above)\n", "- You can only apply styles, you can't insert new HTML entities\n", "\n", - "Some of these will be addressed in the future.\n", - "Performance can suffer when adding styles to each cell in a large DataFrame.\n", - "It is recommended to apply table or column based styles where possible to limit overall HTML length, as well as setting a shorter UUID to avoid unnecessary repeated data transmission. \n" + "Some of these might be addressed in the future. " ] }, { @@ -1699,7 +1696,7 @@ "metadata": {}, "outputs": [], "source": [ - "MyStyler(df)" + "MyStyler(df3)" ] }, { @@ -1715,7 +1712,7 @@ "metadata": {}, "outputs": [], "source": [ - "HTML(MyStyler(df).render(table_title=\"Extending Example\"))" + "HTML(MyStyler(df3).render(table_title=\"Extending Example\"))" ] }, { @@ -1732,7 +1729,7 @@ "outputs": [], "source": [ "EasyStyler = Styler.from_custom_template(\"templates\", \"myhtml.tpl\")\n", - "EasyStyler(df)" + "EasyStyler(df3)" ] }, { From 1cb876468e222d0d6fcee6f2c7afdd9b8c96c990 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Wed, 10 Feb 2021 09:32:55 +0100 Subject: [PATCH 21/34] update --- pandas/io/formats/style.py | 48 ++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 2b882207d9115..f7aac76ed65e5 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -222,8 +222,10 @@ def _default_display_func(self, x): def set_tooltips(self, ttips: DataFrame) -> Styler: """ - Add string based tooltips that will appear in the `Styler` HTML result. These - tooltips are applicable only to`` elements. + Sets the tooltips ``DataFrame`` on ``Styler`` that is used to generate on hover table tooltips. + + These string based tooltips are only applicable to ```` HTML elements, and cannot be used for + header rows or index column names. .. versionadded:: 1.3.0 @@ -268,8 +270,7 @@ def set_tooltips_class( properties: Optional[CSSProperties] = None, ) -> Styler: """ - Manually configure the name and/or properties of the class for - creating tooltips on hover. + Set the name and CSS-properties of the class for on hover tooltips. .. versionadded:: 1.3.0 @@ -286,7 +287,7 @@ def set_tooltips_class( Notes ----- - If arguments are `None` will not make any changes to the underlying ``Tooltips`` + If either argument is `None` will not make any changes to the underlying ``Tooltips`` existing values. The default properties for the tooltip CSS class are: @@ -634,14 +635,13 @@ def format(self, formatter, subset=None, na_rep: Optional[str] = None) -> Styler def set_td_classes(self, classes: DataFrame) -> Styler: """ - Add string based CSS class names to data cells that will appear within the - `Styler` HTML result. These classes are added within specified `` elements. + Set the ``DataFrame`` whose string values are added to the ``class`` attribute of ```` HTML elements. Parameters ---------- classes : DataFrame DataFrame containing strings that will be translated to CSS classes, - mapped by identical column and index values that must exist on the + mapped by identical column and index key values that must exist on the underlying `Styler` data. None, NaN values, and empty strings will be ignored and not affect the rendered HTML. @@ -993,7 +993,7 @@ def where( **kwargs, ) -> Styler: """ - Apply a function elementwise. + Apply CSS-styles based on conditional function elementwise. Updates the HTML representation with a style which is selected in accordance with the return value of a function. @@ -1029,7 +1029,7 @@ def where( def set_precision(self, precision: int) -> Styler: """ - Set the precision used to render. + Set the precision used for the display value of floats. Parameters ---------- @@ -1044,10 +1044,9 @@ def set_precision(self, precision: int) -> Styler: def set_table_attributes(self, attributes: str) -> Styler: """ - Set the table attributes. + Set the table attributes added to the ```` HTML element. - These are the items that show up in the opening ``
`` tag - in addition to automatic (by default) id. + These are items in addition to automatic (by default) ``id`` attribute. Parameters ---------- @@ -1106,7 +1105,7 @@ def use(self, styles: List[Tuple[Callable, Tuple, Dict]]) -> Styler: def set_uuid(self, uuid: str) -> Styler: """ - Set the uuid for a Styler. + Set the uuid applied to ``id`` attributes of HTML elements. Parameters ---------- @@ -1115,13 +1114,19 @@ def set_uuid(self, uuid: str) -> Styler: Returns ------- self : Styler + + Notes + _____ + Almost all HTML elements within the table, and including the ``
`` element + are assigned ``id`` attributes. The format is ``T_uuid_`` where ```` is + typically a more specific identifier, such as ``row1_col2``. """ self.uuid = uuid return self def set_caption(self, caption: str) -> Styler: """ - Set the caption on a Styler. + Set the text added to a ``
`` HTML element. Parameters ---------- @@ -1141,9 +1146,7 @@ def set_table_styles( overwrite: bool = True, ) -> Styler: """ - Set the table styles on a Styler. - - These are placed in a ``' - '' + '' + '
' ' ' ' ' ' ' ' ' - ' ' + ' ' ' ' '
0
1
1
' """ @@ -701,7 +723,7 @@ def set_td_classes(self, classes: DataFrame) -> Styler: def render(self, **kwargs) -> str: """ - Render the built up styles to HTML. + Render the ``Styler`` including all applied styles to HTML. Parameters ---------- @@ -804,7 +826,7 @@ def __deepcopy__(self, memo) -> Styler: def clear(self) -> None: """ - Reset the styler, removing any previously applied styles. + Reset the Styler, removing any previously applied styles. Returns None. """ @@ -888,10 +910,11 @@ def apply( Parameters ---------- func : function - ``func`` should take a Series or DataFrame (depending - on ``axis``), and return an object with the same shape. - Must return a DataFrame with identical index and - column labels or an ndarray with same shape as input when ``axis=None``. + ``func`` should take a Series if ``axis`` in [0,1] and return an object + of same length, also with identical index if the object is a Series. + ``func`` should take a DataFrame if ``axis`` is ``None`` and return either + an ndarray with the same shape or a DataFrame with identical columns and + index. .. versionchanged:: 1.3.0 @@ -909,13 +932,15 @@ def apply( ------- self : Styler + See Also + -------- + Styler.where: Apply CSS-styles based on a conditional function elementwise. + Styler.applymap: Apply a CSS-styling function elementwise. + Notes ----- - The output of ``func`` should be elements having CSS style as string or, + The elements of the output of ``func`` should be CSS styles as strings or, if nothing is to be applied to that element, an empty string or ``None``. - The output shape must match the input, i.e. if - ``x`` is the input row, column, or table (depending on ``axis``), - then ``func(x).shape == x.shape`` should be ``True``. This is similar to ``DataFrame.apply``, except that ``axis=None`` applies the function to the entire DataFrame at once, @@ -966,8 +991,8 @@ def applymap(self, func: Callable, subset=None, **kwargs) -> Styler: See Also -------- - Styler.where: Updates the HTML representation with a style which is - selected in accordance with the return value of a function. + Styler.where: Apply CSS-styles based on a conditional function elementwise. + Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise. Notes ----- @@ -995,7 +1020,7 @@ def where( **kwargs, ) -> Styler: """ - Apply CSS-styles based on conditional function elementwise. + Apply CSS-styles based on a conditional function elementwise. Updates the HTML representation with a style which is selected in accordance with the return value of a function. @@ -1020,7 +1045,15 @@ def where( See Also -------- - Styler.applymap: Updates the HTML representation with the result. + Styler.applymap: Apply a CSS-styling function elementwise. + Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise. + + Examples + -------- + >>> def cond(v): + ... return v > 1 and v != 4 + >>> df = pd.DataFrame([[1, 2], [3, 4]]) + >>> df.style.where(cond, value='color:red;', other='font-size:2em;') """ if other is None: other = "" @@ -1058,6 +1091,13 @@ def set_table_attributes(self, attributes: str) -> Styler: ------- self : Styler + See Also + -------- + Styler.set_table_styles: Set the table styles included within the ``' - '' + '' + '
' ' ' ' ' ' ' ' ' - ' ' + ' ' ' ' '
0
1
1
' """ @@ -751,7 +736,7 @@ def set_td_classes(self, classes: DataFrame) -> Styler: def render(self, **kwargs) -> str: """ - Render the ``Styler`` including all applied styles to HTML. + Render the built up styles to HTML. Parameters ---------- @@ -854,7 +839,7 @@ def __deepcopy__(self, memo) -> Styler: def clear(self) -> None: """ - Reset the Styler, removing any previously applied styles. + Reset the styler, removing any previously applied styles. Returns None. """ @@ -938,11 +923,10 @@ def apply( Parameters ---------- func : function - ``func`` should take a Series if ``axis`` in [0,1] and return an object - of same length, also with identical index if the object is a Series. - ``func`` should take a DataFrame if ``axis`` is ``None`` and return either - an ndarray with the same shape or a DataFrame with identical columns and - index. + ``func`` should take a Series or DataFrame (depending + on ``axis``), and return an object with the same shape. + Must return a DataFrame with identical index and + column labels or an ndarray with same shape as input when ``axis=None``. .. versionchanged:: 1.3.0 @@ -960,15 +944,13 @@ def apply( ------- self : Styler - See Also - -------- - Styler.where: Apply CSS-styles based on a conditional function elementwise. - Styler.applymap: Apply a CSS-styling function elementwise. - Notes ----- - The elements of the output of ``func`` should be CSS styles as strings or, + The output of ``func`` should be elements having CSS style as string or, if nothing is to be applied to that element, an empty string or ``None``. + The output shape must match the input, i.e. if + ``x`` is the input row, column, or table (depending on ``axis``), + then ``func(x).shape == x.shape`` should be ``True``. This is similar to ``DataFrame.apply``, except that ``axis=None`` applies the function to the entire DataFrame at once, @@ -1019,8 +1001,8 @@ def applymap(self, func: Callable, subset=None, **kwargs) -> Styler: See Also -------- - Styler.where: Apply CSS-styles based on a conditional function elementwise. - Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise. + Styler.where: Updates the HTML representation with a style which is + selected in accordance with the return value of a function. Notes ----- @@ -1048,7 +1030,7 @@ def where( **kwargs, ) -> Styler: """ - Apply CSS-styles based on a conditional function elementwise. + Apply a function elementwise. Updates the HTML representation with a style which is selected in accordance with the return value of a function. @@ -1073,15 +1055,7 @@ def where( See Also -------- - Styler.applymap: Apply a CSS-styling function elementwise. - Styler.apply: Apply a CSS-styling function column-wise, row-wise, or table-wise. - - Examples - -------- - >>> def cond(v): - ... return v > 1 and v != 4 - >>> df = pd.DataFrame([[1, 2], [3, 4]]) - >>> df.style.where(cond, value='color:red;', other='font-size:2em;') + Styler.applymap: Updates the HTML representation with the result. """ if other is None: other = "" @@ -1118,9 +1092,10 @@ def set_precision(self, precision: int) -> Styler: def set_table_attributes(self, attributes: str) -> Styler: """ - Set the table attributes added to the ```` HTML element. + Set the table attributes. - These are items in addition to automatic (by default) ``id`` attribute. + These are the items that show up in the opening ``
`` tag + in addition to automatic (by default) id. Parameters ---------- @@ -1130,13 +1105,6 @@ def set_table_attributes(self, attributes: str) -> Styler: ------- self : Styler - See Also - -------- - Styler.set_table_styles: Set the table styles included within the ``