Skip to content

Commit 15283d1

Browse files
Unify color export and handle rgba colors
1 parent b25cbdd commit 15283d1

3 files changed

Lines changed: 72 additions & 39 deletions

File tree

‎plotly/matplotlylib/mpltools.py‎

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
from _plotly_utils.colors import hex_to_rgb
1414

15+
from plotly.matplotlylib.mplexporter.utils import export_color
16+
1517

1618
def check_bar_match(old_bar, new_bar):
1719
"""Check if two bars belong in the same collection (bar chart).
@@ -106,26 +108,6 @@ def convert_symbol(mpl_symbol):
106108
return "circle" # default
107109

108110

109-
def merge_color_and_opacity(color, opacity):
110-
"""
111-
Merge hex color with an alpha (opacity) to get an rgba tuple.
112-
113-
:param (str|unicode) color: A hex color string.
114-
:param (float|int) opacity: A value [0, 1] for the 'a' in 'rgba'.
115-
:return: (int, int, int, float) The rgba color and alpha tuple.
116-
117-
"""
118-
if color is None: # None can be used as a placeholder, just bail.
119-
return None
120-
121-
rgb_tup = hex_to_rgb(color)
122-
if opacity is None:
123-
return "rgb{}".format(rgb_tup)
124-
125-
rgba_tup = rgb_tup + (opacity,)
126-
return "rgba{}".format(rgba_tup)
127-
128-
129111
def convert_va(mpl_va):
130112
"""Convert mpl vertical alignment word to equivalent HTML word.
131113
@@ -516,19 +498,29 @@ def prep_ticks(ax, index, ax_type, props):
516498
return axis_dict
517499

518500

519-
def _export_color(color):
501+
def _export_color(color, opacity=None):
520502
"""Export a matplotlib color for use as a plotly color.
521503
522504
matplotlib uses "none" for fully transparent colors, which plotly does not
523-
accept, so transparent colors are exported as transparent black.
524-
Colors already exported by the mplexporter (hex or rgba strings) are
525-
passed through unchanged.
505+
accept, so transparent colors are exported as transparent black. Colors
506+
already exported by the mplexporter (hex or rgba strings) keep the alpha
507+
they carry unless an explicit opacity overrides it.
526508
"""
527509
if color is None:
528510
return None
529-
if isinstance(color, str):
530-
return color if color != "none" else "rgba(0,0,0,0)"
531-
return [_export_color(c) for c in color]
511+
if isinstance(color, (list, tuple)) and all(isinstance(c, str) for c in color):
512+
return [_export_color(c, opacity) for c in color]
513+
if not isinstance(color, str):
514+
color = export_color(color)
515+
if color == "none":
516+
return "rgba(0,0,0,0)"
517+
if color.startswith("#") and opacity is not None:
518+
return "rgba{0}".format(hex_to_rgb(color) + (opacity,))
519+
if color.startswith("rgb") and opacity not in (None, 1):
520+
rgb = color[color.index("(") + 1 : color.rindex(")")].split(",")
521+
r, g, b = (int(round(float(component))) for component in rgb[:3])
522+
return "rgba({0}, {1}, {2}, {3})".format(r, g, b, opacity)
523+
return color
532524

533525

534526
def prep_xy_axis(ax, props, x_bounds, y_bounds):

‎plotly/matplotlylib/renderer.py‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -388,13 +388,9 @@ def draw_marked_line(self, **props):
388388
self.msg += "... with just markers\n"
389389
mode = "markers"
390390
if props["linestyle"]:
391-
if props["linestyle"]["color"] == "none":
392-
# a fully transparent line; plotly rejects "none" as a color
393-
color = "rgba(0,0,0,0)"
394-
else:
395-
color = mpltools.merge_color_and_opacity(
396-
props["linestyle"]["color"], props["linestyle"]["alpha"]
397-
)
391+
color = _export_color(
392+
props["linestyle"]["color"], props["linestyle"]["alpha"]
393+
)
398394

399395
if props["coordinates"] == "data":
400396
line = go.scatter.Line(
@@ -722,7 +718,8 @@ def draw_text(self, **props):
722718
yanchor=yanchor,
723719
showarrow=False, # change this later?
724720
font=go.layout.annotation.Font(
725-
color=props["style"]["color"], size=props["style"]["fontsize"]
721+
color=_export_color(props["style"]["color"]),
722+
size=props["style"]["fontsize"],
726723
),
727724
)
728725
self.plotly_fig["layout"]["annotations"] += (annotation,)
@@ -762,7 +759,8 @@ def draw_title(self, **props):
762759
annotation = go.layout.Annotation(
763760
text=props["text"],
764761
font=go.layout.annotation.Font(
765-
color=props["style"]["color"], size=props["style"]["fontsize"]
762+
color=_export_color(props["style"]["color"]),
763+
size=props["style"]["fontsize"],
766764
),
767765
xref="paper",
768766
yref="paper",
@@ -777,7 +775,8 @@ def draw_title(self, **props):
777775
self.msg += " Only one subplot found, adding as a plotly title\n"
778776
self.plotly_fig["layout"]["title"] = props["text"]
779777
title_font = dict(
780-
size=props["style"]["fontsize"], color=props["style"]["color"]
778+
size=props["style"]["fontsize"],
779+
color=_export_color(props["style"]["color"]),
781780
)
782781
self.plotly_fig["layout"]["title_font"] = title_font
783782

@@ -808,7 +807,8 @@ def draw_xlabel(self, **props):
808807
axis_key = "xaxis{0}".format(self.axis_ct)
809808
self.plotly_fig["layout"][axis_key]["title"] = str(props["text"])
810809
title_font = dict(
811-
size=props["style"]["fontsize"], color=props["style"]["color"]
810+
size=props["style"]["fontsize"],
811+
color=_export_color(props["style"]["color"]),
812812
)
813813
self.plotly_fig["layout"][axis_key]["title_font"] = title_font
814814

@@ -839,7 +839,8 @@ def draw_ylabel(self, **props):
839839
axis_key = "yaxis{0}".format(self.axis_ct)
840840
self.plotly_fig["layout"][axis_key]["title"] = props["text"]
841841
title_font = dict(
842-
size=props["style"]["fontsize"], color=props["style"]["color"]
842+
size=props["style"]["fontsize"],
843+
color=_export_color(props["style"]["color"]),
843844
)
844845
self.plotly_fig["layout"][axis_key]["title_font"] = title_font
845846

‎plotly/matplotlylib/tests/test_renderer.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,46 @@ def test_line_with_none_color_converts():
129129
assert plotly_fig.data[0].line.color == "rgba(0,0,0,0)"
130130

131131

132+
def test_line_with_rgba_color_converts():
133+
"""Line colors that carry their own alpha (rgba tuple or 8-digit hex)
134+
export as rgba strings."""
135+
fig, ax = plt.subplots()
136+
ax.plot([0, 1], [0, 1], color=(1.0, 0.0, 0.0, 0.5))
137+
ax.plot([0, 1], [1, 0], color="#0000FF80")
138+
139+
plotly_fig = tls.mpl_to_plotly(fig)
140+
141+
assert plotly_fig.data[0].line.color == "rgba(255, 0, 0, 0.5)"
142+
assert plotly_fig.data[1].line.color == "rgba(0, 0, 255, 0.5019607843137255)"
143+
144+
145+
def test_line_rgba_color_with_separate_alpha_converts():
146+
"""An explicit alpha overrides the alpha carried by the line color."""
147+
fig, ax = plt.subplots()
148+
ax.plot([0, 1], [0, 1], color=(1.0, 0.0, 0.0, 0.2), alpha=0.5)
149+
150+
plotly_fig = tls.mpl_to_plotly(fig)
151+
152+
assert plotly_fig.data[0].line.color == "rgba(255, 0, 0, 0.5)"
153+
154+
155+
def test_transparent_text_colors_export():
156+
"""Text, title, and axis labels with color 'none' export transparent
157+
fonts."""
158+
fig, ax = plt.subplots()
159+
ax.text(0.5, 0.5, "text", color="none")
160+
ax.set_title("title", color="none")
161+
ax.set_xlabel("xlabel", color="none")
162+
ax.set_ylabel("ylabel", color="none")
163+
164+
plotly_fig = tls.mpl_to_plotly(fig)
165+
166+
assert plotly_fig.layout.annotations[0].font.color == "rgba(0,0,0,0)"
167+
assert plotly_fig.layout.title.font.color == "rgba(0,0,0,0)"
168+
assert plotly_fig.layout.xaxis.title.font.color == "rgba(0,0,0,0)"
169+
assert plotly_fig.layout.yaxis.title.font.color == "rgba(0,0,0,0)"
170+
171+
132172
def test_eventplot_segments_render():
133173
fig, ax = plt.subplots()
134174
ax.eventplot([np.random.randn(20) for _ in range(5)])

0 commit comments

Comments
 (0)