From 9c8aedb7b02e6569c81c5726a92e5c6887d1037e Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 7 Feb 2021 19:28:17 +0100 Subject: [PATCH 01/17] stage 1 --- pandas/io/formats/style.py | 36 +-- pandas/tests/io/formats/test_style.py | 415 +++++++++++++------------- 2 files changed, 230 insertions(+), 221 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 6eac9ba87c73d..1c1f4a7df10a7 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -157,7 +157,7 @@ def __init__( na_rep: Optional[str] = None, uuid_len: int = 5, ): - self.ctx: DefaultDict[Tuple[int, int], List[str]] = defaultdict(list) + self.ctx: DefaultDict[Tuple[int, int], CSSSequence] = defaultdict(list) self._todo: List[Tuple[Callable, Tuple, Dict]] = [] if not isinstance(data, (pd.Series, pd.DataFrame)): @@ -528,12 +528,13 @@ def format_attr(pair): props = [] if self.cell_ids or (r, c) in ctx: row_dict["id"] = "_".join(cs[1:]) - for x in ctx[r, c]: - # have to handle empty styles like [''] - if x.count(":"): - props.append(tuple(x.split(":"))) - else: - props.append(("", "")) + props.extend(ctx[r, c]) + # for x in ctx[r, c]: + # # have to handle empty styles like [''] + # if x.count(":"): + # props.append(tuple(x.split(":"))) + # else: + # props.append(("", "")) # add custom classes from cell context cs.extend(cell_context.get("data", {}).get(r, {}).get(c, [])) @@ -770,20 +771,19 @@ def _update_ctx(self, attrs: DataFrame) -> None: Whitespace shouldn't matter and the final trailing ';' shouldn't matter. """ - coli = {k: i for i, k in enumerate(self.columns)} - rowi = {k: i for i, k in enumerate(self.index)} - for jj in range(len(attrs.columns)): - cn = attrs.columns[jj] - j = coli[cn] + data_col_idx = {k: i for i, k in enumerate(self.columns)} + data_row_idx = {k: i for i, k in enumerate(self.index)} + for cn in attrs.columns: for rn, c in attrs[[cn]].itertuples(): if not c: continue - c = c.rstrip(";") - if not c: - continue - i = rowi[rn] - for pair in c.split(";"): - self.ctx[(i, j)].append(pair) + css_tuples = _maybe_convert_css_to_tuples(c) + self.ctx[(data_row_idx[rn], data_col_idx[cn])].extend(css_tuples) + # c = c.rstrip(";") + # if not c: + # continue + # for pair in c.split(";"): + # self.ctx[(data_row_idx[rn], data_col_idx[cn])].append(pair) def _copy(self, deepcopy: bool = False) -> Styler: styler = Styler( diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index d2c5b5b9d0b2c..f4162e1e6bce1 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -40,6 +40,11 @@ def h(x, foo="bar"): ), ] + self.bar_props = [("width", "10em"), ("height", "80%")] + + def bar_grad(): + return self.bar_props + [] + def test_init_non_pandas(self): msg = "``data`` must be a Series or DataFrame" with pytest.raises(TypeError, match=msg): @@ -61,24 +66,15 @@ def test_repr_html_mathjax(self): def test_update_ctx(self): self.styler._update_ctx(self.attrs) - expected = {(0, 0): ["color: red"], (1, 0): ["color: blue"]} + expected = {(0, 0): [("color", "red")], (1, 0): [("color", "blue")]} assert self.styler.ctx == expected - def test_update_ctx_flatten_multi(self): - attrs = DataFrame({"A": ["color: red; foo: bar", "color: blue; foo: baz"]}) + def test_update_ctx_flatten_multi_and_trailing_semi(self): + attrs = DataFrame({"A": ["color: red; foo: bar", "color:blue ; foo: baz;"]}) self.styler._update_ctx(attrs) expected = { - (0, 0): ["color: red", " foo: bar"], - (1, 0): ["color: blue", " foo: baz"], - } - assert self.styler.ctx == expected - - def test_update_ctx_flatten_multi_traliing_semi(self): - attrs = DataFrame({"A": ["color: red; foo: bar;", "color: blue; foo: baz;"]}) - self.styler._update_ctx(attrs) - expected = { - (0, 0): ["color: red", " foo: bar"], - (1, 0): ["color: blue", " foo: baz"], + (0, 0): [("color", "red"), ("foo", "bar")], + (1, 0): [("color", "blue"), ("foo", "baz")], } assert self.styler.ctx == expected @@ -141,7 +137,7 @@ def test_multiple_render(self): s.render() # do 2 renders to ensure css styles not duplicated assert ( '" in s.render() + "color: red;\n }" in s.render() ) def test_render_empty_dfs(self): @@ -167,7 +163,7 @@ def test_set_properties(self): df = DataFrame({"A": [0, 1]}) result = df.style.set_properties(color="white", size="10px")._compute().ctx # order is deterministic - v = ["color: white", "size: 10px"] + v = [("color", "white"), ("size", "10px")] expected = {(0, 0): v, (1, 0): v} assert result.keys() == expected.keys() for v1, v2 in zip(result.values(), expected.values()): @@ -180,7 +176,7 @@ def test_set_properties_subset(self): ._compute() .ctx ) - expected = {(0, 0): ["color: white"]} + expected = {(0, 0): [("color", "white")]} assert result == expected def test_empty_index_name_doesnt_display(self): @@ -313,19 +309,19 @@ def test_apply_axis(self): assert len(result.ctx) == 0 result._compute() expected = { - (0, 0): ["val: 1"], - (0, 1): ["val: 1"], - (1, 0): ["val: 1"], - (1, 1): ["val: 1"], + (0, 0): [("val", "1")], + (0, 1): [("val", "1")], + (1, 0): [("val", "1")], + (1, 1): [("val", "1")], } assert result.ctx == expected result = df.style.apply(f, axis=0) expected = { - (0, 0): ["val: 0"], - (0, 1): ["val: 1"], - (1, 0): ["val: 0"], - (1, 1): ["val: 1"], + (0, 0): [("val", "0")], + (0, 1): [("val", "1")], + (1, 0): [("val", "0")], + (1, 1): [("val", "1")], } result._compute() assert result.ctx == expected @@ -333,53 +329,52 @@ def test_apply_axis(self): result._compute() assert result.ctx == expected - def test_apply_subset(self): - axes = [0, 1] - slices = [ + @pytest.mark.parametrize( + "slice_", + [ pd.IndexSlice[:], pd.IndexSlice[:, ["A"]], pd.IndexSlice[[1], :], pd.IndexSlice[[1], ["A"]], pd.IndexSlice[:2, ["A", "B"]], - ] - for ax in axes: - for slice_ in slices: - result = ( - self.df.style.apply(self.h, axis=ax, subset=slice_, foo="baz") - ._compute() - .ctx - ) - expected = { - (r, c): ["color: baz"] - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns) - if row in self.df.loc[slice_].index - and col in self.df.loc[slice_].columns - } - assert result == expected - - def test_applymap_subset(self): - def f(x): - return "foo: bar" + ], + ) + @pytest.mark.parametrize("axis", [0, 1]) + def test_apply_subset(self, slice_, axis): + result = ( + self.df.style.apply(self.h, axis=axis, subset=slice_, foo="baz") + ._compute() + .ctx + ) + expected = { + (r, c): [("color", "baz")] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns) + if row in self.df.loc[slice_].index and col in self.df.loc[slice_].columns + } + assert result == expected - slices = [ + @pytest.mark.parametrize( + "slice_", + [ pd.IndexSlice[:], pd.IndexSlice[:, ["A"]], pd.IndexSlice[[1], :], pd.IndexSlice[[1], ["A"]], pd.IndexSlice[:2, ["A", "B"]], - ] - - for slice_ in slices: - result = self.df.style.applymap(f, subset=slice_)._compute().ctx - expected = { - (r, c): ["foo: bar"] - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns) - if row in self.df.loc[slice_].index - and col in self.df.loc[slice_].columns - } - assert result == expected + ], + ) + def test_applymap_subset(self, slice_): + result = ( + self.df.style.applymap(lambda x: "color:baz;", subset=slice_)._compute().ctx + ) + expected = { + (r, c): [("color", "baz")] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns) + if row in self.df.loc[slice_].index and col in self.df.loc[slice_].columns + } + assert result == expected @pytest.mark.parametrize( "slice_", @@ -430,14 +425,24 @@ def f(x): result = self.df.style.where(f, style1)._compute().ctx expected = { - (r, c): [style1] + (r, c): [("foo", "bar")] for r, row in enumerate(self.df.index) for c, col in enumerate(self.df.columns) if f(self.df.loc[row, col]) } assert result == expected - def test_where_subset(self): + @pytest.mark.parametrize( + "slice_", + [ + pd.IndexSlice[:], + pd.IndexSlice[:, ["A"]], + pd.IndexSlice[[1], :], + pd.IndexSlice[[1], ["A"]], + pd.IndexSlice[:2, ["A", "B"]], + ], + ) + def test_where_subset(self, slice_): # GH 17474 def f(x): return x > 0.5 @@ -445,26 +450,14 @@ def f(x): style1 = "foo: bar" style2 = "baz: foo" - slices = [ - pd.IndexSlice[:], - pd.IndexSlice[:, ["A"]], - pd.IndexSlice[[1], :], - pd.IndexSlice[[1], ["A"]], - pd.IndexSlice[:2, ["A", "B"]], - ] - - for slice_ in slices: - result = ( - self.df.style.where(f, style1, style2, subset=slice_)._compute().ctx - ) - expected = { - (r, c): [style1 if f(self.df.loc[row, col]) else style2] - for r, row in enumerate(self.df.index) - for c, col in enumerate(self.df.columns) - if row in self.df.loc[slice_].index - and col in self.df.loc[slice_].columns - } - assert result == expected + result = self.df.style.where(f, style1, style2, subset=slice_)._compute().ctx + expected = { + (r, c): [("foo", "bar") if f(self.df.loc[row, col]) else ("baz", "foo")] + for r, row in enumerate(self.df.index) + for c, col in enumerate(self.df.columns) + if row in self.df.loc[slice_].index and col in self.df.loc[slice_].columns + } + assert result == expected def test_where_subset_compare_with_applymap(self): # GH 17474 @@ -495,11 +488,11 @@ def g(x): def test_empty(self): df = DataFrame({"A": [1, 0]}) s = df.style - s.ctx = {(0, 0): ["color: red"], (1, 0): [""]} + s.ctx = {(0, 0): ["color: red"], (1, 0): [("", "")]} result = s._translate()["cellstyle"] expected = [ - {"props": [("color", " red")], "selectors": ["row0_col0"]}, + {"props": [("color", "red")], "selectors": ["row0_col0"]}, {"props": [("", "")], "selectors": ["row1_col0"]}, ] assert result == expected @@ -843,10 +836,9 @@ def test_bar_align_mid_axis_none(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", + (0, 0): self.bar_props, + (1, 0): self.bar_props + + [ "background: linear-gradient(90deg, " "transparent 33.3%, #d65f5f 33.3%, " "#d65f5f 50.0%, transparent 50.0%)", @@ -871,24 +863,21 @@ def test_bar_align_mid_vmin(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-6)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", + (0, 0): self.bar_props, + (1, 0): self.bar_props + + [ "background: linear-gradient(90deg, " "transparent 60.0%, #d65f5f 60.0%, " "#d65f5f 70.0%, transparent 70.0%)", ], - (0, 1): [ - "width: 10em", - " height: 80%", + (0, 1): self.bar_props + + [ "background: linear-gradient(90deg, " "transparent 40.0%, #d65f5f 40.0%, " "#d65f5f 60.0%, transparent 60.0%)", ], - (1, 1): [ - "width: 10em", - " height: 80%", + (1, 1): self.bar_props + + [ "background: linear-gradient(90deg, " "transparent 60.0%, #d65f5f 60.0%, " "#d65f5f 100.0%, transparent 100.0%)", @@ -900,23 +889,20 @@ def test_bar_align_mid_vmax(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmax=8)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", + (0, 0): self.bar_props, + (1, 0): self.bar_props + + [ "background: linear-gradient(90deg, " "transparent 20.0%, #d65f5f 20.0%, " "#d65f5f 30.0%, transparent 30.0%)", ], - (0, 1): [ - "width: 10em", - " height: 80%", + (0, 1): self.bar_props + + [ "background: linear-gradient(90deg," "#d65f5f 20.0%, transparent 20.0%)", ], - (1, 1): [ - "width: 10em", - " height: 80%", + (1, 1): self.bar_props + + [ "background: linear-gradient(90deg, " "transparent 20.0%, #d65f5f 20.0%, " "#d65f5f 60.0%, transparent 60.0%)", @@ -928,27 +914,33 @@ def test_bar_align_mid_vmin_vmax_wide(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-3, vmax=7)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 30.0%, #d65f5f 30.0%, " - "#d65f5f 40.0%, transparent 40.0%)", + (0, 0): self.bar_props, + (1, 0): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 30.0%, #d65f5f 30.0%, " + "#d65f5f 40.0%, transparent 40.0%)", + ), ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 10.0%, #d65f5f 10.0%, " - "#d65f5f 30.0%, transparent 30.0%)", + (0, 1): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 10.0%, #d65f5f 10.0%, " + "#d65f5f 30.0%, transparent 30.0%)", + ), ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 30.0%, #d65f5f 30.0%, " - "#d65f5f 70.0%, transparent 70.0%)", + (1, 1): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 30.0%, #d65f5f 30.0%, " + "#d65f5f 70.0%, transparent 70.0%)", + ), ], } assert result == expected @@ -957,26 +949,31 @@ def test_bar_align_mid_vmin_vmax_clipping(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-1, vmax=3)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", + (0, 0): self.bar_props, + (1, 0): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 25.0%, #d65f5f 25.0%, " + "#d65f5f 50.0%, transparent 50.0%)", + ), ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 25.0%, transparent 25.0%)", + (0, 1): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg," "#d65f5f 25.0%, transparent 25.0%)", + ) ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 100.0%, transparent 100.0%)", + (1, 1): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 25.0%, #d65f5f 25.0%, " + "#d65f5f 100.0%, transparent 100.0%)", + ), ], } assert result == expected @@ -985,25 +982,30 @@ def test_bar_align_mid_nans(self): df = DataFrame({"A": [1, None], "B": [-1, 3]}) result = df.style.bar(align="mid", axis=None)._compute().ctx expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", + (0, 0): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 25.0%, #d65f5f 25.0%, " + "#d65f5f 50.0%, transparent 50.0%)", + ), ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 25.0%, transparent 25.0%)", + (0, 1): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg," "#d65f5f 25.0%, transparent 25.0%)", + ), ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 100.0%, transparent 100.0%)", + (1, 1): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 25.0%, #d65f5f 25.0%, " + "#d65f5f 100.0%, transparent 100.0%)", + ), ], } assert result == expected @@ -1012,26 +1014,32 @@ def test_bar_align_zero_nans(self): df = DataFrame({"A": [1, None], "B": [-1, 2]}) result = df.style.bar(align="zero", axis=None)._compute().ctx expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 75.0%, transparent 75.0%)", + (0, 0): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 50.0%, #d65f5f 50.0%, " + "#d65f5f 75.0%, transparent 75.0%)", + ), ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", + (0, 1): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 25.0%, #d65f5f 25.0%, " + "#d65f5f 50.0%, transparent 50.0%)", + ), ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 100.0%, transparent 100.0%)", + (1, 1): self.bar_props + + [ + ( + "background", + "linear-gradient(90deg, " + "transparent 50.0%, #d65f5f 50.0%, " + "#d65f5f 100.0%, transparent 100.0%)", + ), ], } assert result == expected @@ -1115,7 +1123,7 @@ def test_format_with_bad_na_rep(self): def test_highlight_null(self, null_color="red"): df = DataFrame({"A": [0, np.nan]}) result = df.style.highlight_null()._compute().ctx - expected = {(1, 0): ["background-color: red"]} + expected = {(1, 0): [("background-color", "red")]} assert result == expected def test_highlight_null_subset(self): @@ -1128,8 +1136,8 @@ def test_highlight_null_subset(self): .ctx ) expected = { - (1, 0): ["background-color: red"], - (1, 1): ["background-color: green"], + (1, 0): [("background-color", "red")], + (1, 1): [("background-color", "green")], } assert result == expected @@ -1228,7 +1236,7 @@ def f(x): ) result = DataFrame([[1, 2], [3, 4]]).style.apply(f, axis=None)._compute().ctx - assert result[(1, 1)] == ["color: red"] + assert result[(1, 1)] == [("color", "red")] def test_trim(self): result = self.df.style.render() # trim=True @@ -1239,6 +1247,7 @@ def test_trim(self): def test_highlight_max(self): df = DataFrame([[1, 2], [3, 4]], columns=["A", "B"]) + css_seq = [("background-color", "yellow")] # max(df) = min(-df) for max_ in [True, False]: if max_: @@ -1247,35 +1256,35 @@ def test_highlight_max(self): df = -df attr = "highlight_min" result = getattr(df.style, attr)()._compute().ctx - assert result[(1, 1)] == ["background-color: yellow"] + assert result[(1, 1)] == css_seq result = getattr(df.style, attr)(color="green")._compute().ctx - assert result[(1, 1)] == ["background-color: green"] + assert result[(1, 1)] == [("background-color", "green")] result = getattr(df.style, attr)(subset="A")._compute().ctx - assert result[(1, 0)] == ["background-color: yellow"] + assert result[(1, 0)] == css_seq result = getattr(df.style, attr)(axis=0)._compute().ctx expected = { - (1, 0): ["background-color: yellow"], - (1, 1): ["background-color: yellow"], + (1, 0): css_seq, + (1, 1): css_seq, } assert result == expected result = getattr(df.style, attr)(axis=1)._compute().ctx expected = { - (0, 1): ["background-color: yellow"], - (1, 1): ["background-color: yellow"], + (0, 1): css_seq, + (1, 1): css_seq, } assert result == expected # separate since we can't negate the strs df["C"] = ["a", "b"] result = df.style.highlight_max()._compute().ctx - expected = {(1, 1): ["background-color: yellow"]} + expected = {(1, 1): css_seq} result = df.style.highlight_min()._compute().ctx - expected = {(0, 0): ["background-color: yellow"]} + expected = {(0, 0): css_seq} def test_export(self): f = lambda x: "color: red" if x > 0 else "color: blue" @@ -1939,7 +1948,7 @@ def test_background_gradient(self): for c_map in [None, "YlOrRd"]: result = df.style.background_gradient(cmap=c_map)._compute().ctx - assert all("#" in x[0] for x in result.values()) + assert all("#" in x[0][1] for x in result.values()) assert result[(0, 0)] == result[(0, 1)] assert result[(1, 0)] == result[(1, 1)] @@ -1947,7 +1956,7 @@ def test_background_gradient(self): df.style.background_gradient(subset=pd.IndexSlice[1, "A"])._compute().ctx ) - assert result[(1, 0)] == ["background-color: #fff7fb", "color: #000000"] + assert result[(1, 0)] == [("background-color", "#fff7fb"), ("color", "#000000")] @pytest.mark.parametrize( "c_map,expected", @@ -1955,15 +1964,15 @@ def test_background_gradient(self): ( None, { - (0, 0): ["background-color: #440154", "color: #f1f1f1"], - (1, 0): ["background-color: #fde725", "color: #000000"], + (0, 0): [("background-color", "#440154"), ("color", "#f1f1f1")], + (1, 0): [("background-color", "#fde725"), ("color", "#000000")], }, ), ( "YlOrRd", { - (0, 0): ["background-color: #ffffcc", "color: #000000"], - (1, 0): ["background-color: #800026", "color: #f1f1f1"], + (0, 0): [("background-color", "#ffffcc"), ("color", "#000000")], + (1, 0): [("background-color", "#800026"), ("color", "#f1f1f1")], }, ), ], @@ -1986,9 +1995,9 @@ def test_text_color_threshold_raises(self, text_color_threshold): def test_background_gradient_axis(self): df = DataFrame([[1, 2], [2, 4]], columns=["A", "B"]) - low = ["background-color: #f7fbff", "color: #000000"] - high = ["background-color: #08306b", "color: #f1f1f1"] - mid = ["background-color: #abd0e6", "color: #000000"] + low = [("background-color", "#f7fbff"), ("color", "#000000")] + high = [("background-color", "#08306b"), ("color", "#f1f1f1")] + mid = [("background-color", "#abd0e6"), ("color", "#000000")] result = df.style.background_gradient(cmap="Blues", axis=0)._compute().ctx assert result[(0, 0)] == low assert result[(0, 1)] == low From 25685072adb3e926ba6efb85a5f3a30de03303fb Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 7 Feb 2021 22:20:20 +0100 Subject: [PATCH 02/17] stage 2 --- pandas/tests/io/formats/test_style.py | 259 +++++++++++--------------- 1 file changed, 113 insertions(+), 146 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index f4162e1e6bce1..a666cf519fd6a 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -40,10 +40,20 @@ def h(x, foo="bar"): ), ] - self.bar_props = [("width", "10em"), ("height", "80%")] + def bar_grad(a=None, b=None, c=None, d=None): + ret = [("width", "10em"), ("height", "80%")] + if all(x is None for x in [a, b, c, d]): + return ret + return ret + [ + ( + "background", + f"linear-gradient(90deg,{','.join(x for x in [a, b, c, d] if x)})", + ) + ] + + self.bar_grad = bar_grad - def bar_grad(): - return self.bar_props + [] + self.bar_props = [("width", "10em"), ("height", "80%")] def test_init_non_pandas(self): msg = "``data`` must be a Series or DataFrame" @@ -863,25 +873,25 @@ def test_bar_align_mid_vmin(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-6)._compute().ctx expected = { - (0, 0): self.bar_props, - (1, 0): self.bar_props - + [ - "background: linear-gradient(90deg, " - "transparent 60.0%, #d65f5f 60.0%, " - "#d65f5f 70.0%, transparent 70.0%)", - ], - (0, 1): self.bar_props - + [ - "background: linear-gradient(90deg, " - "transparent 40.0%, #d65f5f 40.0%, " - "#d65f5f 60.0%, transparent 60.0%)", - ], - (1, 1): self.bar_props - + [ - "background: linear-gradient(90deg, " - "transparent 60.0%, #d65f5f 60.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): self.bar_grad(), + (1, 0): self.bar_grad( + " transparent 60.0%", + " #d65f5f 60.0%", + " #d65f5f 70.0%", + " transparent 70.0%", + ), + (0, 1): self.bar_grad( + " transparent 40.0%", + " #d65f5f 40.0%", + " #d65f5f 60.0%", + " transparent 60.0%", + ), + (1, 1): self.bar_grad( + " transparent 60.0%", + " #d65f5f 60.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -889,24 +899,23 @@ def test_bar_align_mid_vmax(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmax=8)._compute().ctx expected = { - (0, 0): self.bar_props, - (1, 0): self.bar_props - + [ - "background: linear-gradient(90deg, " - "transparent 20.0%, #d65f5f 20.0%, " - "#d65f5f 30.0%, transparent 30.0%)", - ], - (0, 1): self.bar_props - + [ - "background: linear-gradient(90deg," - "#d65f5f 20.0%, transparent 20.0%)", - ], - (1, 1): self.bar_props - + [ - "background: linear-gradient(90deg, " - "transparent 20.0%, #d65f5f 20.0%, " - "#d65f5f 60.0%, transparent 60.0%)", - ], + (0, 0): self.bar_grad(), + (1, 0): self.bar_grad( + " transparent 20.0%", + " #d65f5f 20.0%", + " #d65f5f 30.0%", + " transparent 30.0%", + ), + (0, 1): self.bar_grad( + "#d65f5f 20.0%", + " transparent 20.0%", + ), + (1, 1): self.bar_grad( + " transparent 20.0%", + " #d65f5f 20.0%", + " #d65f5f 60.0%", + " transparent 60.0%", + ), } assert result == expected @@ -914,34 +923,25 @@ def test_bar_align_mid_vmin_vmax_wide(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-3, vmax=7)._compute().ctx expected = { - (0, 0): self.bar_props, - (1, 0): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 30.0%, #d65f5f 30.0%, " - "#d65f5f 40.0%, transparent 40.0%)", - ), - ], - (0, 1): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 10.0%, #d65f5f 10.0%, " - "#d65f5f 30.0%, transparent 30.0%)", - ), - ], - (1, 1): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 30.0%, #d65f5f 30.0%, " - "#d65f5f 70.0%, transparent 70.0%)", - ), - ], + (0, 0): self.bar_grad(), + (1, 0): self.bar_grad( + " transparent 30.0%", + " #d65f5f 30.0%", + " #d65f5f 40.0%", + " transparent 40.0%", + ), + (0, 1): self.bar_grad( + " transparent 10.0%", + " #d65f5f 10.0%", + " #d65f5f 30.0%", + " transparent 30.0%", + ), + (1, 1): self.bar_grad( + " transparent 30.0%", + " #d65f5f 30.0%", + " #d65f5f 70.0%", + " transparent 70.0%", + ), } assert result == expected @@ -949,32 +949,20 @@ def test_bar_align_mid_vmin_vmax_clipping(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-1, vmax=3)._compute().ctx expected = { - (0, 0): self.bar_props, - (1, 0): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", - ), - ], - (0, 1): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg," "#d65f5f 25.0%, transparent 25.0%)", - ) - ], - (1, 1): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ), - ], + (0, 0): self.bar_grad(), + (1, 0): self.bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (0, 1): self.bar_grad("#d65f5f 25.0%", " transparent 25.0%"), + (1, 1): self.bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -982,31 +970,19 @@ def test_bar_align_mid_nans(self): df = DataFrame({"A": [1, None], "B": [-1, 3]}) result = df.style.bar(align="mid", axis=None)._compute().ctx expected = { - (0, 0): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", - ), - ], - (0, 1): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg," "#d65f5f 25.0%, transparent 25.0%)", - ), - ], - (1, 1): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ), - ], + (0, 0): self.bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (0, 1): self.bar_grad("#d65f5f 25.0%", " transparent 25.0%"), + (1, 1): self.bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -1014,33 +990,24 @@ def test_bar_align_zero_nans(self): df = DataFrame({"A": [1, None], "B": [-1, 2]}) result = df.style.bar(align="zero", axis=None)._compute().ctx expected = { - (0, 0): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 75.0%, transparent 75.0%)", - ), - ], - (0, 1): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", - ), - ], - (1, 1): self.bar_props - + [ - ( - "background", - "linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ), - ], + (0, 0): self.bar_grad( + " transparent 50.0%", + " #d65f5f 50.0%", + " #d65f5f 75.0%", + " transparent 75.0%", + ), + (0, 1): self.bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (1, 1): self.bar_grad( + " transparent 50.0%", + " #d65f5f 50.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected From e628b980ee7270d8522e5c34386eaca7075f3103 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 7 Feb 2021 22:27:37 +0100 Subject: [PATCH 03/17] stage 3 --- pandas/tests/io/formats/test_style.py | 78 +++++++++++++-------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index a666cf519fd6a..d7caccf7057a2 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -817,28 +817,25 @@ def test_bar_align_zero_axis_none(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="zero", axis=None)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 62.5%, transparent 62.5%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 25.0%, #d65f5f 25.0%, " - "#d65f5f 50.0%, transparent 50.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 50.0%, #d65f5f 50.0%, " - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): self.bar_grad(), + (1, 0): self.bar_grad( + " transparent 50.0%", + " #d65f5f 50.0%", + " #d65f5f 62.5%", + " transparent 62.5%", + ), + (0, 1): self.bar_grad( + " transparent 25.0%", + " #d65f5f 25.0%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (1, 1): self.bar_grad( + " transparent 50.0%", + " #d65f5f 50.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -846,26 +843,23 @@ def test_bar_align_mid_axis_none(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None)._compute().ctx expected = { - (0, 0): self.bar_props, - (1, 0): self.bar_props - + [ - "background: linear-gradient(90deg, " - "transparent 33.3%, #d65f5f 33.3%, " - "#d65f5f 50.0%, transparent 50.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 33.3%, transparent 33.3%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 33.3%, #d65f5f 33.3%, " - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): self.bar_grad(), + (1, 0): self.bar_grad( + " transparent 33.3%", + " #d65f5f 33.3%", + " #d65f5f 50.0%", + " transparent 50.0%", + ), + (0, 1): self.bar_grad( + "#d65f5f 33.3%", + " transparent 33.3%", + ), + (1, 1): self.bar_grad( + " transparent 33.3%", + " #d65f5f 33.3%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected From cbbf5f33c7c6bdd30d6fbfffc5330134422f5336 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 7 Feb 2021 23:05:58 +0100 Subject: [PATCH 04/17] stage 4 --- pandas/tests/io/formats/test_style.py | 335 +++++++++----------------- 1 file changed, 114 insertions(+), 221 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index d7caccf7057a2..b9398aa010d3c 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -498,7 +498,7 @@ def g(x): def test_empty(self): df = DataFrame({"A": [1, 0]}) s = df.style - s.ctx = {(0, 0): ["color: red"], (1, 0): [("", "")]} + s.ctx = {(0, 0): [("color", "red")], (1, 0): [("", "")]} result = s._translate()["cellstyle"] expected = [ @@ -510,11 +510,11 @@ def test_empty(self): def test_duplicate(self): df = DataFrame({"A": [1, 0]}) s = df.style - s.ctx = {(0, 0): ["color: red"], (1, 0): ["color: red"]} + s.ctx = {(0, 0): [("color", "red")], (1, 0): [("color", "red")]} result = s._translate()["cellstyle"] expected = [ - {"props": [("color", " red")], "selectors": ["row0_col0", "row1_col0"]} + {"props": [("color", "red")], "selectors": ["row0_col0", "row1_col0"]} ] assert result == expected @@ -522,35 +522,17 @@ def test_bar_align_left(self): df = DataFrame({"A": [0, 1, 2]}) result = df.style.bar()._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(" - "90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(" - "90deg,#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): self.bar_grad(), + (1, 0): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (2, 0): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), } assert result == expected result = df.style.bar(color="red", width=50)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,red 25.0%, transparent 25.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,red 50.0%, transparent 50.0%)", - ], + (0, 0): self.bar_grad(), + (1, 0): self.bar_grad("red 25.0%", " transparent 25.0%"), + (2, 0): self.bar_grad("red 50.0%", " transparent 50.0%"), } assert result == expected @@ -565,118 +547,54 @@ def test_bar_align_left_0points(self): df = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) result = df.style.bar()._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (0, 1): ["width: 10em", " height: 80%"], - (0, 2): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (1, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], - (2, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], - (2, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], + (0, 0): self.bar_grad(), + (0, 1): self.bar_grad(), + (0, 2): self.bar_grad(), + (1, 0): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (1, 1): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (1, 2): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (2, 0): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 1): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 2): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), } assert result == expected result = df.style.bar(axis=1)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%, transparent 50.0%)", - ], - (0, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], - (1, 0): ["width: 10em", " height: 80%"], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%" - ", transparent 50.0%)", - ], - (1, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], - (2, 0): ["width: 10em", " height: 80%"], - (2, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 50.0%" - ", transparent 50.0%)", - ], - (2, 2): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg,#d65f5f 100.0%" - ", transparent 100.0%)", - ], + (0, 0): self.bar_grad(), + (0, 1): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (0, 2): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (1, 0): self.bar_grad(), + (1, 1): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (1, 2): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 0): self.bar_grad(), + (2, 1): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (2, 2): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), } assert result == expected def test_bar_align_mid_pos_and_neg(self): df = DataFrame({"A": [-10, 0, 20, 90]}) - result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx - expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 10.0%, transparent 10.0%)", - ], - (1, 0): ["width: 10em", " height: 80%"], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 10.0%, #5fba7d 10.0%" - ", #5fba7d 30.0%, transparent 30.0%)", - ], - (3, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 10.0%, " - "#5fba7d 10.0%, #5fba7d 100.0%, " - "transparent 100.0%)", - ], + (0, 0): self.bar_grad( + "#d65f5f 10.0%", + " transparent 10.0%", + ), + (1, 0): self.bar_grad(), + (2, 0): self.bar_grad( + " transparent 10.0%", + " #5fba7d 10.0%", + " #5fba7d 30.0%", + " transparent 30.0%", + ), + (3, 0): self.bar_grad( + " transparent 10.0%", + " #5fba7d 10.0%", + " #5fba7d 100.0%", + " transparent 100.0%", + ), } - assert result == expected def test_bar_align_mid_all_pos(self): @@ -685,30 +603,22 @@ def test_bar_align_mid_all_pos(self): result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#5fba7d 10.0%, transparent 10.0%)", - ], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#5fba7d 20.0%, transparent 20.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#5fba7d 50.0%, transparent 50.0%)", - ], - (3, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#5fba7d 100.0%, transparent 100.0%)", - ], + (0, 0): self.bar_grad( + "#5fba7d 10.0%", + " transparent 10.0%", + ), + (1, 0): self.bar_grad( + "#5fba7d 20.0%", + " transparent 20.0%", + ), + (2, 0): self.bar_grad( + "#5fba7d 50.0%", + " transparent 50.0%", + ), + (3, 0): self.bar_grad( + "#5fba7d 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -719,36 +629,28 @@ def test_bar_align_mid_all_neg(self): result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 100.0%, transparent 100.0%)", - ], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 40.0%, " - "#d65f5f 40.0%, #d65f5f 100.0%, " - "transparent 100.0%)", - ], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 70.0%, " - "#d65f5f 70.0%, #d65f5f 100.0%, " - "transparent 100.0%)", - ], - (3, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 80.0%, " - "#d65f5f 80.0%, #d65f5f 100.0%, " - "transparent 100.0%)", - ], + (0, 0): self.bar_grad( + "#d65f5f 100.0%", + " transparent 100.0%", + ), + (1, 0): self.bar_grad( + " transparent 40.0%", + " #d65f5f 40.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), + (2, 0): self.bar_grad( + " transparent 70.0%", + " #d65f5f 70.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), + (3, 0): self.bar_grad( + " transparent 80.0%", + " #d65f5f 80.0%", + " #d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected @@ -762,28 +664,25 @@ def test_bar_align_zero_pos_and_neg(self): .ctx ) expected = { - (0, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 40.0%, #d65f5f 40.0%, " - "#d65f5f 45.0%, transparent 45.0%)", - ], - (1, 0): ["width: 10em", " height: 80%"], - (2, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 45.0%, #5fba7d 45.0%, " - "#5fba7d 55.0%, transparent 55.0%)", - ], - (3, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg, " - "transparent 45.0%, #5fba7d 45.0%, " - "#5fba7d 90.0%, transparent 90.0%)", - ], + (0, 0): self.bar_grad( + " transparent 40.0%", + " #d65f5f 40.0%", + " #d65f5f 45.0%", + " transparent 45.0%", + ), + (1, 0): self.bar_grad(), + (2, 0): self.bar_grad( + " transparent 45.0%", + " #5fba7d 45.0%", + " #5fba7d 55.0%", + " transparent 55.0%", + ), + (3, 0): self.bar_grad( + " transparent 45.0%", + " #5fba7d 45.0%", + " #5fba7d 90.0%", + " transparent 90.0%", + ), } assert result == expected @@ -791,25 +690,19 @@ def test_bar_align_left_axis_none(self): df = DataFrame({"A": [0, 1], "B": [2, 4]}) result = df.style.bar(axis=None)._compute().ctx expected = { - (0, 0): ["width: 10em", " height: 80%"], - (1, 0): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 25.0%, transparent 25.0%)", - ], - (0, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 50.0%, transparent 50.0%)", - ], - (1, 1): [ - "width: 10em", - " height: 80%", - "background: linear-gradient(90deg," - "#d65f5f 100.0%, transparent 100.0%)", - ], + (0, 0): self.bar_grad(), + (1, 0): self.bar_grad( + "#d65f5f 25.0%", + " transparent 25.0%", + ), + (0, 1): self.bar_grad( + "#d65f5f 50.0%", + " transparent 50.0%", + ), + (1, 1): self.bar_grad( + "#d65f5f 100.0%", + " transparent 100.0%", + ), } assert result == expected From 3b441361faf4d5271864381b319439d1010e6b6f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 7 Feb 2021 23:23:31 +0100 Subject: [PATCH 05/17] improve --- pandas/io/formats/style.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 1c1f4a7df10a7..5363e9acb9a19 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -529,12 +529,6 @@ def format_attr(pair): if self.cell_ids or (r, c) in ctx: row_dict["id"] = "_".join(cs[1:]) props.extend(ctx[r, c]) - # for x in ctx[r, c]: - # # have to handle empty styles like [''] - # if x.count(":"): - # props.append(tuple(x.split(":"))) - # else: - # props.append(("", "")) # add custom classes from cell context cs.extend(cell_context.get("data", {}).get(r, {}).get(c, [])) @@ -771,19 +765,14 @@ def _update_ctx(self, attrs: DataFrame) -> None: Whitespace shouldn't matter and the final trailing ';' shouldn't matter. """ - data_col_idx = {k: i for i, k in enumerate(self.columns)} - data_row_idx = {k: i for i, k in enumerate(self.index)} for cn in attrs.columns: for rn, c in attrs[[cn]].itertuples(): if not c: continue css_tuples = _maybe_convert_css_to_tuples(c) - self.ctx[(data_row_idx[rn], data_col_idx[cn])].extend(css_tuples) - # c = c.rstrip(";") - # if not c: - # continue - # for pair in c.split(";"): - # self.ctx[(data_row_idx[rn], data_col_idx[cn])].append(pair) + self.ctx[(self.index.get_loc(rn), self.columns.get_loc(cn))].extend( + css_tuples + ) def _copy(self, deepcopy: bool = False) -> Styler: styler = Styler( From 260a272be8dab91beb0b700d3a138547015550d7 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 7 Feb 2021 23:25:22 +0100 Subject: [PATCH 06/17] improve --- pandas/tests/io/formats/test_style.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index b9398aa010d3c..07db94dbdf605 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -53,8 +53,6 @@ def bar_grad(a=None, b=None, c=None, d=None): self.bar_grad = bar_grad - self.bar_props = [("width", "10em"), ("height", "80%")] - def test_init_non_pandas(self): msg = "``data`` must be a Series or DataFrame" with pytest.raises(TypeError, match=msg): From e9017dc41564574253ba0e41b3e8d846d0f971b0 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 7 Feb 2021 23:41:57 +0100 Subject: [PATCH 07/17] typing --- pandas/io/formats/style.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 5363e9acb9a19..100d48a03a77c 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -42,8 +42,8 @@ from pandas.core.indexing import maybe_numeric_slice, non_reducing_slice jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.") -CSSSequence = Sequence[Tuple[str, Union[str, int, float]]] -CSSProperties = Union[str, CSSSequence] +CSSList = List[Tuple[str, Union[str, int, float]]] +CSSProperties = Union[str, CSSList] CSSStyles = List[Dict[str, CSSProperties]] try: @@ -157,7 +157,7 @@ def __init__( na_rep: Optional[str] = None, uuid_len: int = 5, ): - self.ctx: DefaultDict[Tuple[int, int], CSSSequence] = defaultdict(list) + self.ctx: DefaultDict[Tuple[int, int], CSSList] = defaultdict(list) self._todo: List[Tuple[Callable, Tuple, Dict]] = [] if not isinstance(data, (pd.Series, pd.DataFrame)): @@ -769,10 +769,9 @@ def _update_ctx(self, attrs: DataFrame) -> None: for rn, c in attrs[[cn]].itertuples(): if not c: continue - css_tuples = _maybe_convert_css_to_tuples(c) - self.ctx[(self.index.get_loc(rn), self.columns.get_loc(cn))].extend( - css_tuples - ) + css_seq = _maybe_convert_css_to_tuples(c) + i, j = self.index.get_loc(rn), self.columns.get_loc(cn) + self.ctx[(i, j)].extend(css_seq) def _copy(self, deepcopy: bool = False) -> Styler: styler = Styler( @@ -2045,7 +2044,7 @@ def _maybe_wrap_formatter( raise TypeError(msg) -def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSSequence: +def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList: """ Convert css-string to sequence of tuples format if needed. 'color:red; border:1px solid black;' -> [('color', 'red'), From 7d6a4abbe02ac7418556fcc38a1cce0fd29c66cc Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 7 Feb 2021 23:45:05 +0100 Subject: [PATCH 08/17] docs --- 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 100d48a03a77c..b9ac61f067180 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -754,9 +754,9 @@ def render(self, **kwargs) -> str: def _update_ctx(self, attrs: DataFrame) -> None: """ - Update the state of the Styler. + Update the state of the Styler for data cells. - Collects a mapping of {index_label: [': ']}. + Collects a mapping of {index_label: [('', ''), ..]}. Parameters ---------- From 951171257325f9516e5cb59ec3eed3a65acadf41 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Mon, 8 Feb 2021 00:45:04 +0100 Subject: [PATCH 09/17] test update --- pandas/tests/io/formats/test_style.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index 8922b5b7e7555..bbb55c37f3fd2 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -145,7 +145,7 @@ def test_multiple_render(self): s.render() # do 2 renders to ensure css styles not duplicated assert ( '" in s.render() + " color: red;\n}\n" in s.render() ) def test_render_empty_dfs(self): From cbed55ceed944d8670d6c55717e7a234da59ad3d Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 14 Feb 2021 12:53:13 +0100 Subject: [PATCH 10/17] typing of cellstyle_map --- pandas/io/formats/style.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 79b6c78763e77..bf6015d8a19e2 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -42,7 +42,9 @@ from pandas.core.indexing import maybe_numeric_slice, non_reducing_slice jinja2 = import_optional_dependency("jinja2", extra="DataFrame.style requires jinja2.") -CSSList = List[Tuple[str, Union[str, int, float]]] + +CSSPair = Tuple[str, Union[str, int, float]] +CSSList = List[CSSPair] CSSProperties = Union[str, CSSList] CSSStyles = List[Dict[str, CSSProperties]] @@ -404,7 +406,8 @@ def _translate(self): clabels = [[x] for x in clabels] clabels = list(zip(*clabels)) - cellstyle_map = defaultdict(list) + cellstyle_map: DefaultDict[Tuple[CSSPair, ...], List[str]] = defaultdict(list) + head = [] for r in range(n_clvls): @@ -514,7 +517,7 @@ def _translate(self): } # only add an id if the cell has a style - props = [] + props: CSSList = [] if self.cell_ids or (r, c) in ctx: row_dict["id"] = "_".join(cs[1:]) props.extend(ctx[r, c]) @@ -527,7 +530,7 @@ def _translate(self): cellstyle_map[tuple(props)].append(f"row{r}_col{c}") body.append(row_es) - cellstyle = [ + cellstyle: List[Dict[str, Union[CSSList, List[str]]]] = [ {"props": list(props), "selectors": selectors} for props, selectors in cellstyle_map.items() ] From 8039b7a968230c2ced6c6a31efa46bf84c1af215 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 14 Feb 2021 13:46:02 +0100 Subject: [PATCH 11/17] remove redundant code --- pandas/io/formats/style.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index bf6015d8a19e2..b54bc19ddff5b 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -527,7 +527,8 @@ def _translate(self): row_dict["class"] = " ".join(cs) row_es.append(row_dict) - cellstyle_map[tuple(props)].append(f"row{r}_col{c}") + if props: # (), [] won't be in cellstyle_map, cellstyle respectively + cellstyle_map[tuple(props)].append(f"row{r}_col{c}") body.append(row_es) cellstyle: List[Dict[str, Union[CSSList, List[str]]]] = [ @@ -736,11 +737,6 @@ def render(self, **kwargs) -> str: self._compute() # TODO: namespace all the pandas keys d = self._translate() - # filter out empty styles, every cell will have a class - # but the list of props may just be [['', '']]. - # so we have the nested anys below - trimmed = [x for x in d["cellstyle"] if any(any(y) for y in x["props"])] - d["cellstyle"] = trimmed d.update(kwargs) return self.template.render(**d) From c797a736107290e6e26a68962ec367484c740de9 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 14 Feb 2021 19:44:37 +0100 Subject: [PATCH 12/17] update to_excel with new ctx_obj --- pandas/io/formats/excel.py | 5 ++++- pandas/io/formats/style.py | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index b027d8139f24b..8fda39f688379 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -35,6 +35,7 @@ from pandas.io.formats.css import CSSResolver, CSSWarning from pandas.io.formats.format import get_level_lengths from pandas.io.formats.printing import pprint_thing +from pandas.io.formats.style import convert_tuples_to_css class ExcelCell: @@ -760,7 +761,9 @@ def _generate_body(self, coloffset: int) -> Iterable[ExcelCell]: series = self.df.iloc[:, colidx] for i, val in enumerate(series): if styles is not None: - xlstyle = self.style_converter(";".join(styles[i, colidx])) + xlstyle = self.style_converter( + convert_tuples_to_css(styles[i, colidx]) + ) yield ExcelCell(self.rowcounter + i, colidx + coloffset, val, xlstyle) def get_formatted_cells(self) -> Iterable[ExcelCell]: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index b54bc19ddff5b..677d6bf2127b1 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2075,3 +2075,11 @@ def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList: f"for example 'attr: val;'. '{style}' was given." ) return style + + +def convert_tuples_to_css(styles: CSSList) -> str: + """ + Convert CSSList type to string. Used by Excel converter. + [('color', 'red'), ('border', '1px solid red')] -> 'color:red;border:1px solid red' + """ + return ";".join([x[0] + ":" + str(x[1]) for x in styles]) From de43f815990607343ccd897848200933c307c19f Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Sun, 14 Feb 2021 20:12:06 +0100 Subject: [PATCH 13/17] update to_excel with new ctx_obj --- pandas/io/formats/excel.py | 6 ++---- pandas/io/formats/style.py | 8 -------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index 8fda39f688379..ef7a7eb1ec429 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -35,7 +35,6 @@ from pandas.io.formats.css import CSSResolver, CSSWarning from pandas.io.formats.format import get_level_lengths from pandas.io.formats.printing import pprint_thing -from pandas.io.formats.style import convert_tuples_to_css class ExcelCell: @@ -761,9 +760,8 @@ def _generate_body(self, coloffset: int) -> Iterable[ExcelCell]: series = self.df.iloc[:, colidx] for i, val in enumerate(series): if styles is not None: - xlstyle = self.style_converter( - convert_tuples_to_css(styles[i, colidx]) - ) + css = ";".join([x[0] + ":" + str(x[1]) for x in styles[i, colidx]]) + xlstyle = self.style_converter(css) yield ExcelCell(self.rowcounter + i, colidx + coloffset, val, xlstyle) def get_formatted_cells(self) -> Iterable[ExcelCell]: diff --git a/pandas/io/formats/style.py b/pandas/io/formats/style.py index 677d6bf2127b1..b54bc19ddff5b 100644 --- a/pandas/io/formats/style.py +++ b/pandas/io/formats/style.py @@ -2075,11 +2075,3 @@ def _maybe_convert_css_to_tuples(style: CSSProperties) -> CSSList: f"for example 'attr: val;'. '{style}' was given." ) return style - - -def convert_tuples_to_css(styles: CSSList) -> str: - """ - Convert CSSList type to string. Used by Excel converter. - [('color', 'red'), ('border', '1px solid red')] -> 'color:red;border:1px solid red' - """ - return ";".join([x[0] + ":" + str(x[1]) for x in styles]) From 7b876764866d236929d3146f07434d0e49c49edf Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (MBP)" Date: Sun, 14 Feb 2021 23:37:02 +0100 Subject: [PATCH 14/17] fix old bug --- doc/source/whatsnew/v0.20.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.20.0.rst b/doc/source/whatsnew/v0.20.0.rst index 5dac3a26424a8..ad8a23882e1e8 100644 --- a/doc/source/whatsnew/v0.20.0.rst +++ b/doc/source/whatsnew/v0.20.0.rst @@ -374,7 +374,7 @@ For example, after running the following, ``styled.xlsx`` renders as below: df.iloc[0, 2] = np.nan df styled = (df.style - .applymap(lambda val: 'color: %s' % 'red' if val < 0 else 'black') + .applymap(lambda val: 'color:red;' if val < 0 else 'color:black;') .highlight_max()) styled.to_excel('styled.xlsx', engine='openpyxl') From 1d703c5b4c2a8893881ab2c3d3e612d8c76b07ef Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Tue, 16 Feb 2021 08:42:24 +0100 Subject: [PATCH 15/17] req changes --- pandas/io/formats/excel.py | 2 +- pandas/tests/io/formats/test_style.py | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pandas/io/formats/excel.py b/pandas/io/formats/excel.py index ef7a7eb1ec429..6aed081700a96 100644 --- a/pandas/io/formats/excel.py +++ b/pandas/io/formats/excel.py @@ -760,7 +760,7 @@ def _generate_body(self, coloffset: int) -> Iterable[ExcelCell]: series = self.df.iloc[:, colidx] for i, val in enumerate(series): if styles is not None: - css = ";".join([x[0] + ":" + str(x[1]) for x in styles[i, colidx]]) + css = ";".join([a + ":" + str(v) for (a, v) in styles[i, colidx]]) xlstyle = self.style_converter(css) yield ExcelCell(self.rowcounter + i, colidx + coloffset, val, xlstyle) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index c232c7e32c7ae..cc6ab05236de4 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -40,19 +40,6 @@ def h(x, foo="bar"): ), ] - def bar_grad(a=None, b=None, c=None, d=None): - ret = [("width", "10em"), ("height", "80%")] - if all(x is None for x in [a, b, c, d]): - return ret - return ret + [ - ( - "background", - f"linear-gradient(90deg,{','.join(x for x in [a, b, c, d] if x)})", - ) - ] - - self.bar_grad = bar_grad - def test_init_non_pandas(self): msg = "``data`` must be a Series or DataFrame" with pytest.raises(TypeError, match=msg): @@ -516,6 +503,19 @@ def test_duplicate(self): ] assert result == expected + @staticmethod + def bar_grad(a=None, b=None, c=None, d=None): + """Used in multiple tests to simplify formatting of expected result""" + ret = [("width", "10em"), ("height", "80%")] + if all(x is None for x in [a, b, c, d]): + return ret + return ret + [ + ( + "background", + f"linear-gradient(90deg,{','.join(x for x in [a, b, c, d] if x)})", + ) + ] + def test_bar_align_left(self): df = DataFrame({"A": [0, 1, 2]}) result = df.style.bar()._compute().ctx From 67f7e883229d0592016fe3e4288b6c30d5414e36 Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Feb 2021 08:17:07 +0100 Subject: [PATCH 16/17] no staticmethod --- pandas/tests/io/formats/test_style.py | 174 +++++++++++++------------- 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/pandas/tests/io/formats/test_style.py b/pandas/tests/io/formats/test_style.py index cc6ab05236de4..d7d001e5b861a 100644 --- a/pandas/tests/io/formats/test_style.py +++ b/pandas/tests/io/formats/test_style.py @@ -19,6 +19,19 @@ ) +def bar_grad(a=None, b=None, c=None, d=None): + """Used in multiple tests to simplify formatting of expected result""" + ret = [("width", "10em"), ("height", "80%")] + if all(x is None for x in [a, b, c, d]): + return ret + return ret + [ + ( + "background", + f"linear-gradient(90deg,{','.join(x for x in [a, b, c, d] if x)})", + ) + ] + + class TestStyler: def setup_method(self, method): np.random.seed(24) @@ -503,34 +516,21 @@ def test_duplicate(self): ] assert result == expected - @staticmethod - def bar_grad(a=None, b=None, c=None, d=None): - """Used in multiple tests to simplify formatting of expected result""" - ret = [("width", "10em"), ("height", "80%")] - if all(x is None for x in [a, b, c, d]): - return ret - return ret + [ - ( - "background", - f"linear-gradient(90deg,{','.join(x for x in [a, b, c, d] if x)})", - ) - ] - def test_bar_align_left(self): df = DataFrame({"A": [0, 1, 2]}) result = df.style.bar()._compute().ctx expected = { - (0, 0): self.bar_grad(), - (1, 0): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (2, 0): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (0, 0): bar_grad(), + (1, 0): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (2, 0): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), } assert result == expected result = df.style.bar(color="red", width=50)._compute().ctx expected = { - (0, 0): self.bar_grad(), - (1, 0): self.bar_grad("red 25.0%", " transparent 25.0%"), - (2, 0): self.bar_grad("red 50.0%", " transparent 50.0%"), + (0, 0): bar_grad(), + (1, 0): bar_grad("red 25.0%", " transparent 25.0%"), + (2, 0): bar_grad("red 50.0%", " transparent 50.0%"), } assert result == expected @@ -545,29 +545,29 @@ def test_bar_align_left_0points(self): df = DataFrame([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) result = df.style.bar()._compute().ctx expected = { - (0, 0): self.bar_grad(), - (0, 1): self.bar_grad(), - (0, 2): self.bar_grad(), - (1, 0): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (1, 1): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (1, 2): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (2, 0): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), - (2, 1): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), - (2, 2): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (0, 0): bar_grad(), + (0, 1): bar_grad(), + (0, 2): bar_grad(), + (1, 0): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (1, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (1, 2): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (2, 0): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 1): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), } assert result == expected result = df.style.bar(axis=1)._compute().ctx expected = { - (0, 0): self.bar_grad(), - (0, 1): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (0, 2): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), - (1, 0): self.bar_grad(), - (1, 1): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (1, 2): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), - (2, 0): self.bar_grad(), - (2, 1): self.bar_grad("#d65f5f 50.0%", " transparent 50.0%"), - (2, 2): self.bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (0, 0): bar_grad(), + (0, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (0, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (1, 0): bar_grad(), + (1, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (1, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), + (2, 0): bar_grad(), + (2, 1): bar_grad("#d65f5f 50.0%", " transparent 50.0%"), + (2, 2): bar_grad("#d65f5f 100.0%", " transparent 100.0%"), } assert result == expected @@ -575,18 +575,18 @@ def test_bar_align_mid_pos_and_neg(self): df = DataFrame({"A": [-10, 0, 20, 90]}) result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx expected = { - (0, 0): self.bar_grad( + (0, 0): bar_grad( "#d65f5f 10.0%", " transparent 10.0%", ), - (1, 0): self.bar_grad(), - (2, 0): self.bar_grad( + (1, 0): bar_grad(), + (2, 0): bar_grad( " transparent 10.0%", " #5fba7d 10.0%", " #5fba7d 30.0%", " transparent 30.0%", ), - (3, 0): self.bar_grad( + (3, 0): bar_grad( " transparent 10.0%", " #5fba7d 10.0%", " #5fba7d 100.0%", @@ -601,19 +601,19 @@ def test_bar_align_mid_all_pos(self): result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx expected = { - (0, 0): self.bar_grad( + (0, 0): bar_grad( "#5fba7d 10.0%", " transparent 10.0%", ), - (1, 0): self.bar_grad( + (1, 0): bar_grad( "#5fba7d 20.0%", " transparent 20.0%", ), - (2, 0): self.bar_grad( + (2, 0): bar_grad( "#5fba7d 50.0%", " transparent 50.0%", ), - (3, 0): self.bar_grad( + (3, 0): bar_grad( "#5fba7d 100.0%", " transparent 100.0%", ), @@ -627,23 +627,23 @@ def test_bar_align_mid_all_neg(self): result = df.style.bar(align="mid", color=["#d65f5f", "#5fba7d"])._compute().ctx expected = { - (0, 0): self.bar_grad( + (0, 0): bar_grad( "#d65f5f 100.0%", " transparent 100.0%", ), - (1, 0): self.bar_grad( + (1, 0): bar_grad( " transparent 40.0%", " #d65f5f 40.0%", " #d65f5f 100.0%", " transparent 100.0%", ), - (2, 0): self.bar_grad( + (2, 0): bar_grad( " transparent 70.0%", " #d65f5f 70.0%", " #d65f5f 100.0%", " transparent 100.0%", ), - (3, 0): self.bar_grad( + (3, 0): bar_grad( " transparent 80.0%", " #d65f5f 80.0%", " #d65f5f 100.0%", @@ -662,20 +662,20 @@ def test_bar_align_zero_pos_and_neg(self): .ctx ) expected = { - (0, 0): self.bar_grad( + (0, 0): bar_grad( " transparent 40.0%", " #d65f5f 40.0%", " #d65f5f 45.0%", " transparent 45.0%", ), - (1, 0): self.bar_grad(), - (2, 0): self.bar_grad( + (1, 0): bar_grad(), + (2, 0): bar_grad( " transparent 45.0%", " #5fba7d 45.0%", " #5fba7d 55.0%", " transparent 55.0%", ), - (3, 0): self.bar_grad( + (3, 0): bar_grad( " transparent 45.0%", " #5fba7d 45.0%", " #5fba7d 90.0%", @@ -688,16 +688,16 @@ def test_bar_align_left_axis_none(self): df = DataFrame({"A": [0, 1], "B": [2, 4]}) result = df.style.bar(axis=None)._compute().ctx expected = { - (0, 0): self.bar_grad(), - (1, 0): self.bar_grad( + (0, 0): bar_grad(), + (1, 0): bar_grad( "#d65f5f 25.0%", " transparent 25.0%", ), - (0, 1): self.bar_grad( + (0, 1): bar_grad( "#d65f5f 50.0%", " transparent 50.0%", ), - (1, 1): self.bar_grad( + (1, 1): bar_grad( "#d65f5f 100.0%", " transparent 100.0%", ), @@ -708,20 +708,20 @@ def test_bar_align_zero_axis_none(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="zero", axis=None)._compute().ctx expected = { - (0, 0): self.bar_grad(), - (1, 0): self.bar_grad( + (0, 0): bar_grad(), + (1, 0): bar_grad( " transparent 50.0%", " #d65f5f 50.0%", " #d65f5f 62.5%", " transparent 62.5%", ), - (0, 1): self.bar_grad( + (0, 1): bar_grad( " transparent 25.0%", " #d65f5f 25.0%", " #d65f5f 50.0%", " transparent 50.0%", ), - (1, 1): self.bar_grad( + (1, 1): bar_grad( " transparent 50.0%", " #d65f5f 50.0%", " #d65f5f 100.0%", @@ -734,18 +734,18 @@ def test_bar_align_mid_axis_none(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None)._compute().ctx expected = { - (0, 0): self.bar_grad(), - (1, 0): self.bar_grad( + (0, 0): bar_grad(), + (1, 0): bar_grad( " transparent 33.3%", " #d65f5f 33.3%", " #d65f5f 50.0%", " transparent 50.0%", ), - (0, 1): self.bar_grad( + (0, 1): bar_grad( "#d65f5f 33.3%", " transparent 33.3%", ), - (1, 1): self.bar_grad( + (1, 1): bar_grad( " transparent 33.3%", " #d65f5f 33.3%", " #d65f5f 100.0%", @@ -758,20 +758,20 @@ def test_bar_align_mid_vmin(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-6)._compute().ctx expected = { - (0, 0): self.bar_grad(), - (1, 0): self.bar_grad( + (0, 0): bar_grad(), + (1, 0): bar_grad( " transparent 60.0%", " #d65f5f 60.0%", " #d65f5f 70.0%", " transparent 70.0%", ), - (0, 1): self.bar_grad( + (0, 1): bar_grad( " transparent 40.0%", " #d65f5f 40.0%", " #d65f5f 60.0%", " transparent 60.0%", ), - (1, 1): self.bar_grad( + (1, 1): bar_grad( " transparent 60.0%", " #d65f5f 60.0%", " #d65f5f 100.0%", @@ -784,18 +784,18 @@ def test_bar_align_mid_vmax(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmax=8)._compute().ctx expected = { - (0, 0): self.bar_grad(), - (1, 0): self.bar_grad( + (0, 0): bar_grad(), + (1, 0): bar_grad( " transparent 20.0%", " #d65f5f 20.0%", " #d65f5f 30.0%", " transparent 30.0%", ), - (0, 1): self.bar_grad( + (0, 1): bar_grad( "#d65f5f 20.0%", " transparent 20.0%", ), - (1, 1): self.bar_grad( + (1, 1): bar_grad( " transparent 20.0%", " #d65f5f 20.0%", " #d65f5f 60.0%", @@ -808,20 +808,20 @@ def test_bar_align_mid_vmin_vmax_wide(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-3, vmax=7)._compute().ctx expected = { - (0, 0): self.bar_grad(), - (1, 0): self.bar_grad( + (0, 0): bar_grad(), + (1, 0): bar_grad( " transparent 30.0%", " #d65f5f 30.0%", " #d65f5f 40.0%", " transparent 40.0%", ), - (0, 1): self.bar_grad( + (0, 1): bar_grad( " transparent 10.0%", " #d65f5f 10.0%", " #d65f5f 30.0%", " transparent 30.0%", ), - (1, 1): self.bar_grad( + (1, 1): bar_grad( " transparent 30.0%", " #d65f5f 30.0%", " #d65f5f 70.0%", @@ -834,15 +834,15 @@ def test_bar_align_mid_vmin_vmax_clipping(self): df = DataFrame({"A": [0, 1], "B": [-2, 4]}) result = df.style.bar(align="mid", axis=None, vmin=-1, vmax=3)._compute().ctx expected = { - (0, 0): self.bar_grad(), - (1, 0): self.bar_grad( + (0, 0): bar_grad(), + (1, 0): bar_grad( " transparent 25.0%", " #d65f5f 25.0%", " #d65f5f 50.0%", " transparent 50.0%", ), - (0, 1): self.bar_grad("#d65f5f 25.0%", " transparent 25.0%"), - (1, 1): self.bar_grad( + (0, 1): bar_grad("#d65f5f 25.0%", " transparent 25.0%"), + (1, 1): bar_grad( " transparent 25.0%", " #d65f5f 25.0%", " #d65f5f 100.0%", @@ -855,14 +855,14 @@ def test_bar_align_mid_nans(self): df = DataFrame({"A": [1, None], "B": [-1, 3]}) result = df.style.bar(align="mid", axis=None)._compute().ctx expected = { - (0, 0): self.bar_grad( + (0, 0): bar_grad( " transparent 25.0%", " #d65f5f 25.0%", " #d65f5f 50.0%", " transparent 50.0%", ), - (0, 1): self.bar_grad("#d65f5f 25.0%", " transparent 25.0%"), - (1, 1): self.bar_grad( + (0, 1): bar_grad("#d65f5f 25.0%", " transparent 25.0%"), + (1, 1): bar_grad( " transparent 25.0%", " #d65f5f 25.0%", " #d65f5f 100.0%", @@ -875,19 +875,19 @@ def test_bar_align_zero_nans(self): df = DataFrame({"A": [1, None], "B": [-1, 2]}) result = df.style.bar(align="zero", axis=None)._compute().ctx expected = { - (0, 0): self.bar_grad( + (0, 0): bar_grad( " transparent 50.0%", " #d65f5f 50.0%", " #d65f5f 75.0%", " transparent 75.0%", ), - (0, 1): self.bar_grad( + (0, 1): bar_grad( " transparent 25.0%", " #d65f5f 25.0%", " #d65f5f 50.0%", " transparent 50.0%", ), - (1, 1): self.bar_grad( + (1, 1): bar_grad( " transparent 50.0%", " #d65f5f 50.0%", " #d65f5f 100.0%", From 2de18e18564af108b0a93f01daa7aebd6016744a Mon Sep 17 00:00:00 2001 From: "JHM Darbyshire (iMac)" Date: Wed, 17 Feb 2021 08:22:36 +0100 Subject: [PATCH 17/17] whats new --- doc/source/whatsnew/v1.3.0.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v1.3.0.rst b/doc/source/whatsnew/v1.3.0.rst index 799bc88ffff4e..c9b59bb2de0ec 100644 --- a/doc/source/whatsnew/v1.3.0.rst +++ b/doc/source/whatsnew/v1.3.0.rst @@ -55,7 +55,8 @@ Other enhancements - :meth:`DataFrame.plot.scatter` can now accept a categorical column as the argument to ``c`` (:issue:`12380`, :issue:`31357`) - :meth:`.Styler.set_tooltips` allows on hover tooltips to be added to styled HTML dataframes (:issue:`35643`, :issue:`21266`, :issue:`39317`) - :meth:`.Styler.set_tooltips_class` and :meth:`.Styler.set_table_styles` amended to optionally allow certain css-string input arguments (:issue:`39564`) -- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None``. (:issue:`39359`) +- :meth:`.Styler.apply` now more consistently accepts ndarray function returns, i.e. in all cases for ``axis`` is ``0, 1 or None`` (:issue:`39359`) +- :meth:`.Styler.apply` and :meth:`.Styler.applymap` now raise errors if wrong format CSS is passed on render (:issue:`39660`) - :meth:`Series.loc.__getitem__` and :meth:`Series.loc.__setitem__` with :class:`MultiIndex` now raising helpful error message when indexer has too many dimensions (:issue:`35349`) - :meth:`pandas.read_stata` and :class:`StataReader` support reading data from compressed files. - Add support for parsing ``ISO 8601``-like timestamps with negative signs to :meth:`pandas.Timedelta` (:issue:`37172`)