Skip to content

Commit 6c624b9

Browse files
Group consecutive same-style line collection paths into single traces
1 parent 5769d95 commit 6c624b9

2 files changed

Lines changed: 105 additions & 29 deletions

File tree

‎plotly/matplotlylib/renderer.py‎

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -632,15 +632,35 @@ def draw_path_collection(self, **props):
632632

633633
def _draw_line_collection(self, props):
634634
"""Draw a path collection without face colors (e.g. contour lines)
635-
as plain lines."""
635+
as plain lines, grouping consecutive same-style paths into single traces."""
636636
edgecolors = mpltools.convert_rgba_array(props["styles"]["edgecolor"])
637637
linewidths = mpltools.convert_linewidth_array(props["styles"]["linewidth"])
638638
linestyles = props["styles"].get("linestyle")
639639

640+
current_style = None
641+
grouped_x = []
642+
grouped_y = []
643+
644+
def flush():
645+
if current_style is not None and grouped_x:
646+
ec, lw, d = current_style
647+
self.plotly_fig.add_trace(
648+
go.Scatter(
649+
x=grouped_x,
650+
y=grouped_y,
651+
mode="lines",
652+
showlegend=False,
653+
line=go.scatter.Line(
654+
color=_export_color(ec),
655+
width=lw,
656+
dash=d,
657+
),
658+
xaxis="x{0}".format(self.axis_ct),
659+
yaxis="y{0}".format(self.axis_ct),
660+
)
661+
)
662+
640663
for i, (verts, codes) in enumerate(props["paths"]):
641-
edgecolor = _per_path(edgecolors, i, "rgba(0,0,0,0)")
642-
linewidth = _per_path(linewidths, i, 0)
643-
dash = _convert_collection_dash(_per_path(linestyles, i, None))
644664
# a path may contain several disjoint lines (e.g. contour lines
645665
# of the same level); separate disjoint subpaths with None so
646666
# plotly does not connect them.
@@ -666,8 +686,8 @@ def _draw_line_collection(self, props):
666686
vi += step
667687
if current:
668688
subpaths.append((current, closed))
669-
x_combined = []
670-
y_combined = []
689+
path_x = []
690+
path_y = []
671691
for sub, closed in subpaths:
672692
if len(sub) < 2:
673693
continue
@@ -677,27 +697,31 @@ def _draw_line_collection(self, props):
677697
sub = sub + [sub[0]]
678698
sub_x = self._convert_x_dates([v[0] for v in sub])
679699
sub_y = [v[1] for v in sub]
680-
if x_combined:
681-
x_combined.append(None)
682-
y_combined.append(None)
683-
x_combined.extend(sub_x)
684-
y_combined.extend(sub_y)
685-
if x_combined:
686-
self.plotly_fig.add_trace(
687-
go.Scatter(
688-
x=x_combined,
689-
y=y_combined,
690-
mode="lines",
691-
showlegend=False,
692-
line=go.scatter.Line(
693-
color=_export_color(edgecolor),
694-
width=linewidth,
695-
dash=dash,
696-
),
697-
xaxis="x{0}".format(self.axis_ct),
698-
yaxis="y{0}".format(self.axis_ct),
699-
)
700-
)
700+
if path_x:
701+
path_x.append(None)
702+
path_y.append(None)
703+
path_x.extend(sub_x)
704+
path_y.extend(sub_y)
705+
if not path_x:
706+
continue
707+
708+
edgecolor = _per_path(edgecolors, i, "rgba(0,0,0,0)")
709+
linewidth = _per_path(linewidths, i, 0)
710+
dash = _convert_collection_dash(_per_path(linestyles, i, None))
711+
style = (edgecolor, linewidth, dash)
712+
713+
if style != current_style:
714+
flush()
715+
current_style = style
716+
grouped_x = list(path_x)
717+
grouped_y = list(path_y)
718+
else:
719+
grouped_x.append(None)
720+
grouped_y.append(None)
721+
grouped_x.extend(path_x)
722+
grouped_y.extend(path_y)
723+
724+
flush()
701725

702726
def _draw_filled_path_collection(self, props):
703727
"""Draw a path collection (e.g. violin plot bodies) as filled polygons."""

‎plotly/matplotlylib/tests/test_renderer.py‎

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,9 @@ def test_eventplot_segments_render():
434434
fig, ax = plt.subplots()
435435
ax.eventplot([np.random.randn(20) for _ in range(5)])
436436
plotly_fig = tls.mpl_to_plotly(fig)
437-
assert len(plotly_fig.data) == 100
437+
# Each of the 5 event rows is a line collection whose 20 segments are grouped
438+
assert len(plotly_fig.data) == 5
439+
assert all(t.x.count(None) == 19 for t in plotly_fig.data)
438440

439441

440442
def test_stackplot_areas_render():
@@ -486,7 +488,11 @@ def test_stem_plot_renders():
486488
fig, ax = plt.subplots()
487489
ax.stem(x, np.sin(x))
488490
plotly_fig = tls.mpl_to_plotly(fig)
489-
assert len(plotly_fig.data) >= 20
491+
# The 20 vertical stem lines are grouped into a single line trace with 19 None separators
492+
stem_lines = [
493+
t for t in plotly_fig.data if t.mode == "lines" and t.x.count(None) == 19
494+
]
495+
assert len(stem_lines) == 1
490496

491497

492498
def test_contour_lines_convert():
@@ -885,3 +891,49 @@ def test_contour_lines_not_in_legend():
885891
contour_traces = [t for t in plotly_fig.data if t.name != "Line"]
886892
assert len(contour_traces) >= 1
887893
assert all(t.showlegend is False for t in contour_traces)
894+
895+
896+
def test_consecutive_same_style_lines_grouped_into_one_trace():
897+
"""Consecutive paths with identical styles are grouped into a single trace."""
898+
x = np.linspace(-3, 3, 30)
899+
X, Y = np.meshgrid(x, x)
900+
fig, ax = plt.subplots()
901+
# 4 contour levels, all black, solid, width 1.5
902+
ax.contour(
903+
X,
904+
Y,
905+
X**2 + Y**2,
906+
levels=[1, 2, 3, 4],
907+
colors="k",
908+
linestyles="solid",
909+
linewidths=1.5,
910+
)
911+
plotly_fig = tls.mpl_to_plotly(fig)
912+
913+
# All 4 levels share the same style, so they should be grouped into 1 trace
914+
assert len(plotly_fig.data) == 1
915+
trace = plotly_fig.data[0]
916+
# The trace combines the distinct levels separated by None
917+
assert trace.x.count(None) >= 3
918+
919+
920+
def test_mixed_style_lines_group_consecutive_matches():
921+
"""Paths are grouped by consecutive matching style (color, width, dash)."""
922+
x = np.linspace(-3, 3, 30)
923+
X, Y = np.meshgrid(x, x)
924+
fig, ax = plt.subplots()
925+
# 4 levels: 2 negative (dashed by default in mpl), 2 positive (solid by default)
926+
ax.contour(
927+
X,
928+
Y,
929+
np.sin(X) * np.cos(Y),
930+
levels=[-0.5, -0.25, 0.25, 0.5],
931+
colors="k",
932+
linewidths=1.5,
933+
)
934+
plotly_fig = tls.mpl_to_plotly(fig)
935+
936+
# 2 dashed levels grouped into 1 trace, 2 solid levels grouped into 1 trace -> 2 traces total
937+
assert len(plotly_fig.data) == 2
938+
assert plotly_fig.data[0].line.dash != "solid"
939+
assert plotly_fig.data[1].line.dash == "solid"

0 commit comments

Comments
 (0)