Skip to content

BUG/ENH: Translate CSS border properties for Styler.to_excel #45312

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9543045
BUG/ENH: Improve border CSS parsing (GH42276)
tehunter Jan 9, 2022
d071893
Fix issue where width 'pt' is not recognized and fix border shorthand…
tehunter Jan 10, 2022
fb31af3
BUG: Xlsxwriter crashes on to_excel with only 'border-color' (GH30008…
tehunter Jan 10, 2022
9827ad9
DOC: Update documentation to resolve GH42267
tehunter Jan 10, 2022
67b9fff
Merge remote-tracking branch 'upstream/master' into tmp
tehunter Jan 11, 2022
aaac09a
CLN: Comply with PEP8 checks
tehunter Jan 11, 2022
f6af38a
CLN: Remove whitespace
tehunter Jan 11, 2022
9af2869
Fixes from pre-commit [automated commit]
tehunter Jan 11, 2022
13c3fa6
BUG: Fixed issue with invalid colors generating borders
tehunter Jan 11, 2022
3d067d0
Merged origin/style-border-css
tehunter Jan 11, 2022
18a6a41
Fixes from pre-commit [automated commit]
tehunter Jan 11, 2022
a5cd011
Merge remote-tracking branch 'upstream/master' into style-border-css
tehunter Jan 11, 2022
f3b1d13
Merge branch 'style-border-css' of https://github.com/tehunter/pandas…
tehunter Jan 11, 2022
905a6ca
Cleaned code and improved test (GH45312)
tehunter Jan 12, 2022
cce6457
Merge remote-tracking branch 'upstream/master' into style-border-css
tehunter Jan 12, 2022
7e6f5f1
Fixes from pre-commit [automated commit]
tehunter Jan 12, 2022
337b2f4
Clean border_expander
tehunter Jan 14, 2022
cd28198
Eliminate redundant test expansions
tehunter Jan 14, 2022
ed5d5ca
Merge branch 'style-border-css' of https://github.com/tehunter/pandas…
tehunter Jan 14, 2022
7caca16
Fixes from pre-commit [automated commit]
tehunter Jan 14, 2022
50f93a1
Updated docstrings and what's new entry
tehunter Jan 16, 2022
88590dc
Merge branch 'main' into style-border-css
jreback Jan 17, 2022
4785dc4
Update doc/source/whatsnew/v1.5.0.rst
tehunter Jan 17, 2022
93cfb92
Update pandas/tests/io/formats/test_css.py
tehunter Jan 17, 2022
dec6920
Resolved doc-build check
tehunter Jan 17, 2022
17dc457
Fixes from pre-commit [automated commit]
tehunter Jan 17, 2022
21bb3f6
Merge branch 'main' into style-border-css
attack68 Feb 8, 2022
15e8a5d
Merge remote-tracking branch 'upstream/main' into style-border-css
tehunter Feb 8, 2022
d9f2ed3
Merge branch 'style-border-css' of https://github.com/tehunter/pandas…
tehunter Feb 8, 2022
fa15e0a
Pre-commit cleanup
tehunter Feb 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion doc/source/user_guide/style.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1577,6 +1577,9 @@
"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` properties\n",
"- `border-width` properties\n",
"- `border-color` properties\n",
"- `color`\n",
"- `font-family`\n",
"- `font-style`\n",
Expand All @@ -1587,7 +1590,7 @@
"- `white-space: nowrap`\n",
"\n",
"\n",
"- Currently broken: `border-style`, `border-width`, `border-color` and their {`top`, `right`, `bottom`, `left` variants}\n",
"- Shorthand and side-specific border properties are supported (e.g. `border-style` and `border-left-style`) as well as the `border` shorthands for all sides (`border: 1px solid green`) or specified sides (`border-left: 1px solid green`). Using a `border` shorthand will override any border properties set before it (See [CSS Working Group](https://drafts.csswg.org/css-backgrounds/#border-shorthands) for more details)\n",
"\n",
"\n",
"- Only CSS2 named colors and hex colors of the form `#rgb` or `#rrggbb` are currently supported.\n",
Expand Down
54 changes: 54 additions & 0 deletions pandas/io/formats/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,47 @@ def expand(self, prop, value: str):
return expand


def _border_expander(side: str = ""):
if side != "":
side = f"-{side}"

def expand(self, prop, value: str):
tokens = value.split()
if len(tokens) == 0 or len(tokens) > 3:
raise ValueError(f'Too many tokens provided to "{prop}" (expected 1-3)')

# TODO: Can we use current color as initial value to comply with CSS standards?
border_declarations = {
f"border{side}-color": "black",
f"border{side}-style": "none",
f"border{side}-width": "medium",
}
for token in tokens:
token_key = "color"
if token in self.BORDER_STYLES:
token_key = "style"

for ratio in self.BORDER_WIDTH_RATIOS:
if ratio in token:
token_key = "width"
break

# TODO: Warn user if item entered more than once (e.g. "border: red green")
border_declarations[f"border{side}-{token_key}"] = token

# Per CSS, "border" will reset previous "border-*" definitions
yield from self.atomize(border_declarations.items())

return expand


class CSSResolver:
"""
A callable for parsing and resolving CSS to atomic properties.
"""

UNIT_RATIOS = {
"pt": ("pt", 1),
"rem": ("pt", 12),
"ex": ("em", 0.5),
# 'ch':
Expand Down Expand Up @@ -76,6 +111,19 @@ class CSSResolver:
}
)

BORDER_STYLES = [
"none",
"hidden",
"dotted",
"dashed",
"solid",
"double",
"groove",
"ridge",
"inset",
"outset",
]

SIDE_SHORTHANDS = {
1: [0, 0, 0, 0],
2: [0, 1, 0, 1],
Expand Down Expand Up @@ -255,6 +303,12 @@ def atomize(self, declarations):
for prop, value in expand(prop, value):
yield prop, value

expand_border = _border_expander()
expand_border_top = _border_expander("top")
expand_border_right = _border_expander("right")
expand_border_bottom = _border_expander("bottom")
expand_border_left = _border_expander("left")

expand_border_color = _side_expander("border-{:s}-color")
expand_border_style = _side_expander("border-{:s}-style")
expand_border_width = _side_expander("border-{:s}-width")
Expand Down
15 changes: 11 additions & 4 deletions pandas/io/formats/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,14 @@ def build_border(
"style": self._border_style(
props.get(f"border-{side}-style"),
props.get(f"border-{side}-width"),
self.color_to_excel(props.get(f"border-{side}-color")),
),
"color": self.color_to_excel(props.get(f"border-{side}-color")),
}
for side in ["top", "right", "bottom", "left"]
}

def _border_style(self, style: str | None, width: str | None):
def _border_style(self, style: str | None, width: str | None, color: str | None):
# convert styles and widths to openxml, one of:
# 'dashDot'
# 'dashDotDot'
Expand All @@ -256,14 +257,20 @@ def _border_style(self, style: str | None, width: str | None):
# 'slantDashDot'
# 'thick'
# 'thin'
if width is None and style is None:
if width is None and style is None and color is None:
# Return None will remove "border" from style dictionary
return None

if width is None and style is None:
# Return "none" will keep "border" in style dictionary
return "none"

if style == "none" or style == "hidden":
return None
return "none"

width_name = self._get_width_name(width)
if width_name is None:
return None
return "none"

if style in (None, "groove", "ridge", "inset", "outset", "solid"):
# not handled
Expand Down
71 changes: 71 additions & 0 deletions pandas/tests/io/formats/test_css.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,77 @@ def test_css_side_shorthands(shorthand, expansions):
assert_resolves(f"{shorthand}: 1pt 1pt 1pt 1pt 1pt", {})


@pytest.mark.parametrize(
"shorthand,sides",
[
("border-top", ["top"]),
("border-right", ["right"]),
(
"border-bottom",
["bottom"],
),
("border-left", ["left"]),
("border", ["top", "right", "bottom", "left"]),
],
)
def test_css_border_shorthands(shorthand, sides):
def create_border_dict(sides, color=None, style=None, width=None):
resolved = {}
for side in sides:
if color:
resolved[f"border-{side}-color"] = color
if style:
resolved[f"border-{side}-style"] = style
if width:
resolved[f"border-{side}-width"] = width
return resolved

assert_resolves(
f"{shorthand}: 1pt red solid", create_border_dict(sides, "red", "solid", "1pt")
)

assert_resolves(
f"{shorthand}: red 1pt solid", create_border_dict(sides, "red", "solid", "1pt")
)

assert_resolves(
f"{shorthand}: red solid 1pt", create_border_dict(sides, "red", "solid", "1pt")
)

assert_resolves(
f"{shorthand}: solid 1pt red", create_border_dict(sides, "red", "solid", "1pt")
)

assert_resolves(
f"{shorthand}: red solid",
create_border_dict(sides, "red", "solid", "1.500000pt"),
)

# Note: color=black is not CSS conforming
# (See https://drafts.csswg.org/css-backgrounds/#border-shorthands)
assert_resolves(
f"{shorthand}: 1pt solid", create_border_dict(sides, "black", "solid", "1pt")
)

assert_resolves(
f"{shorthand}: 1pt red", create_border_dict(sides, "red", "none", "1pt")
)

assert_resolves(
f"{shorthand}: red", create_border_dict(sides, "red", "none", "1.500000pt")
)

# Note: color=black is not CSS conforming
assert_resolves(
f"{shorthand}: 1pt", create_border_dict(sides, "black", "none", "1pt")
)

# Note: color=black is not CSS conforming
assert_resolves(
f"{shorthand}: solid", create_border_dict(sides, "black", "solid", "1.500000pt")
)


@pytest.mark.parametrize(
"style,inherited,equiv",
[
Expand Down
7 changes: 6 additions & 1 deletion pandas/tests/io/formats/test_to_excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,10 @@
"border-top-style: solid; border-top-color: #06c",
{"border": {"top": {"style": "medium", "color": "0066CC"}}},
),
(
"border-top-color: blue",
{"border": {"top": {"color": "0000FF", "style": "none"}}},
),
# ALIGNMENT
# - horizontal
("text-align: center", {"alignment": {"horizontal": "center"}}),
Expand Down Expand Up @@ -288,7 +292,8 @@ def test_css_to_excel_good_colors(input_color, output_color):
expected["font"] = {"color": output_color}

expected["border"] = {
k: {"color": output_color} for k in ("top", "right", "bottom", "left")
k: {"color": output_color, "style": "none"}
for k in ("top", "right", "bottom", "left")
}

with tm.assert_produces_warning(None):
Expand Down