diff --git a/README.md b/README.md
index 7378192..7c8b24c 100644
--- a/README.md
+++ b/README.md
@@ -62,7 +62,7 @@ In [11]: print(planarity.ascii(edgelist))
```python
-In [12]: planarity.draw(edgelist, outfileName='K5-minus-edge.png')
+In [12]: planarity.draw(edgelist, outfileName='K5-minus-edge.png', vertex_bordercolor='black')
```

@@ -93,4 +93,4 @@ license](https://github.com/graph-algorithms/planarity/blob/master/LICENSE.txt).
Planarity includes the Edge Addition Planarity Suite, which is
Copyright (c) 1997-2026, John M. Boyer.
The BSD-3-Clause license for the Edge Additional Planarity Suite
- included in Planarity appears [here](https://github.com/graph-algorithms/planarity/blob/master/planarity/c/LICENSE.TXT).
\ No newline at end of file
+ included in Planarity appears [here](https://github.com/graph-algorithms/planarity/blob/master/planarity/c/LICENSE.TXT).
diff --git a/examples/classic/draw.py b/examples/classic/draw.py
index 0d920ad..2765dfa 100644
--- a/examples/classic/draw.py
+++ b/examples/classic/draw.py
@@ -34,13 +34,13 @@
print(planar_rendition)
# Output Matplotlib rendering
- P2.draw(outfileName='K5-minus-edge.png')
+ P2.draw(outfileName='K5-minus-edge.png', vertex_bordercolor='black')
# Verify fig_kwargs (#87): generate 300dpi and 600dpi variants
P3 = planarity.PGraph(edgelist)
- P3.draw(outfileName='K5-minus-edge-300dpi.png', dpi=300)
+ P3.draw(outfileName='K5-minus-edge-300dpi.png', dpi=300, vertex_bordercolor='black')
P4 = planarity.PGraph(edgelist)
- P4.draw(outfileName='K5-minus-edge-600dpi.png', dpi=600)
+ P4.draw(outfileName='K5-minus-edge-600dpi.png', dpi=600, vertex_bordercolor='black')
else:
print("The graph cannot be drawn because it is non-planar.")
diff --git a/examples/classic/draw_planar_colors.py b/examples/classic/draw_planar_colors.py
new file mode 100644
index 0000000..a815d4d
--- /dev/null
+++ b/examples/classic/draw_planar_colors.py
@@ -0,0 +1,24 @@
+"""Run from the desired output directory to generate seven color examples."""
+
+import planarity
+
+
+edgelist = [('a', 'c'), ('a', 'd'), ('a', 'e'), ('b', 'c'), ('b', 'd'),
+ ('b', 'e'), ('c', 'd'), ('c', 'e'), ('d', 'e')]
+
+colors = {
+ 'facecolor': 'lightyellow',
+ 'transparent': True,
+ 'vertex_facecolor': 'lightgreen',
+ 'vertex_bordercolor': 'black',
+ 'vertex_label_facecolor': 'lavender',
+ 'vertex_label_bordercolor': 'red',
+ 'edge_linecolor': 'darkorange',
+}
+
+for property_name, value in colors.items():
+ planarity.draw(
+ edgelist,
+ outfileName=f'K5-minus-edge_{property_name}.png',
+ **{property_name: value},
+ )
diff --git a/examples/classic/output/K5-minus-edge.png b/examples/classic/output/K5-minus-edge.png
index 27922e4..87ad4a5 100644
Binary files a/examples/classic/output/K5-minus-edge.png and b/examples/classic/output/K5-minus-edge.png differ
diff --git a/planarity/classic/planarity.pyx b/planarity/classic/planarity.pyx
index bc6b5cd..eff7644 100644
--- a/planarity/classic/planarity.pyx
+++ b/planarity/classic/planarity.pyx
@@ -499,11 +499,21 @@ cdef class PGraph:
outfileName (:obj:`str`): File to which to output a Matplotlib
rendering of the planar graph. If not given, then the caller can
call :external+matplotlib:py:func:`matplotlib.pyplot.savefig`.
- **kwargs: Optional figure-level parameters to apply to the
- current figure after it is cleared. Supported keys are
- ``figsize`` and ``dpi``. If none are given, Matplotlib's
- default figure size and DPI are used, matching prior
- behavior.
+ **kwargs: Optional drawing parameters. ``figsize`` and ``dpi``
+ control the figure size and resolution. ``facecolor`` sets
+ the figure background (default ``'#ffffff'``).
+ ``vertex_facecolor`` and ``vertex_bordercolor`` set vertex
+ rectangle colors; omitted values use Matplotlib's patch
+ defaults. ``vertex_label_facecolor`` and
+ ``vertex_label_bordercolor`` set label rectangle colors
+ (defaults ``'white'`` and ``'black'``). ``edge_linecolor``
+ sets edge colors (default Matplotlib line color). All color
+ values accept Matplotlib color specifications.
+ ``transparent`` (default ``False``) makes the figure and
+ axes backgrounds transparent when saving to ``outfileName``;
+ it does not remove matching colors from vertices or labels.
+ When saving the figure later yourself, pass ``transparent``
+ to :external+matplotlib:py:func:`matplotlib.pyplot.savefig`.
Raises:
ValueError: if an unsupported keyword argument is given.
@@ -531,18 +541,24 @@ cdef class PGraph:
plt.clf()
fig = plt.gcf()
- valid_fig_kwargs = ('figsize', 'dpi')
+ valid_draw_kwargs = (
+ 'figsize', 'dpi', 'facecolor', 'transparent', 'vertex_facecolor',
+ 'vertex_bordercolor', 'vertex_label_facecolor',
+ 'vertex_label_bordercolor', 'edge_linecolor',
+ )
for key, value in kwargs.items():
- if key not in valid_fig_kwargs:
+ if key not in valid_draw_kwargs:
raise ValueError(
f"planarity: '{key}' is not a supported draw() keyword "
- f"argument. Supported options are: {valid_fig_kwargs}."
+ f"argument. Supported options are: {valid_draw_kwargs}."
)
if key == 'figsize':
fig.set_size_inches(value)
elif key == 'dpi':
fig.set_dpi(value)
+ fig.set_facecolor(kwargs.get('facecolor', '#ffffff'))
+
self.embed_drawplanar()
if self._embedding_workflow_status != cplanarity.OK:
@@ -582,9 +598,14 @@ cdef class PGraph:
ye = drawplanar_edge_info['edge_end']
ys.extend([yb, ye])
xs.append(x)
- plt.vlines([x], [yb], [ye])
+ plt.vlines([x], [yb], [ye], colors=kwargs.get('edge_linecolor'), zorder=1)
- p = PatchCollection(patches)
+ p = PatchCollection(
+ patches,
+ facecolors=kwargs.get('vertex_facecolor'),
+ edgecolors=kwargs.get('vertex_bordercolor'),
+ zorder = 2,
+ )
ax = plt.gca()
ax.add_collection(p)
plt.axis('equal')
@@ -614,8 +635,8 @@ cdef class PGraph:
verticalalignment='center',
bbox = dict(
boxstyle='round',
- ec=(0.0, 0.0, 0.0),
- fc=(1.0, 1.0, 1.0),
+ ec=kwargs.get('vertex_label_bordercolor', 'black'),
+ fc=kwargs.get('vertex_label_facecolor', 'white'),
)
)
@@ -634,7 +655,11 @@ cdef class PGraph:
).transformed(inv)
if outfileName:
- plt.savefig(outfileName, dpi=fig.dpi)
+ transparent = kwargs.get('transparent', False)
+ plt.savefig(
+ outfileName, dpi=fig.dpi, transparent=transparent,
+ facecolor='none' if transparent else fig.get_facecolor(),
+ )
def write(
self, str path='stdout', int writeMode=cplanarity.WRITE_ADJLIST
diff --git a/planarity/classic/planarity_functions.py b/planarity/classic/planarity_functions.py
index 47312de..7d46abc 100644
--- a/planarity/classic/planarity_functions.py
+++ b/planarity/classic/planarity_functions.py
@@ -120,9 +120,12 @@ def draw(graph, labels=True, outfileName=None, **kwargs):
outfileName (:obj:`str`): File to which to output a Matplotlib
rendering of the planar graph. If not given, then the caller can
call :external+matplotlib:py:func:`matplotlib.pyplot.savefig`.
- **kwargs: Optional figure-level parameters forwarded to
+ **kwargs: Optional drawing parameters forwarded to
:py:meth:`~planarity.classic.planarity.PGraph.draw`. Supported
- keys are ``figsize`` and ``dpi``.
+ keys are ``figsize``, ``dpi``, ``facecolor``, ``transparent``,
+ ``vertex_facecolor``, ``vertex_bordercolor``,
+ ``vertex_label_facecolor``, ``vertex_label_bordercolor`` and
+ ``edge_linecolor``. See the method documentation for defaults.
Raises:
ValueError: if the given graph is already a
diff --git a/tests/test_draw_colors.py b/tests/test_draw_colors.py
new file mode 100644
index 0000000..f19bffc
--- /dev/null
+++ b/tests/test_draw_colors.py
@@ -0,0 +1,116 @@
+"""Exercise color controls on real artists and rendered PNGs."""
+
+import matplotlib
+
+matplotlib.use('Agg')
+
+import matplotlib.pyplot as plt
+from matplotlib.collections import LineCollection, PatchCollection
+from matplotlib.colors import to_rgba
+from PIL import Image
+import pytest
+
+import planarity
+
+
+EDGES = [('a', 'c'), ('a', 'd'), ('a', 'e'), ('b', 'c'), ('b', 'd'),
+ ('b', 'e'), ('c', 'd'), ('c', 'e'), ('d', 'e')]
+
+
+@pytest.fixture(autouse=True)
+def isolated_figures():
+ with matplotlib.rc_context():
+ yield
+ plt.close('all')
+
+
+@pytest.fixture(params=['function', 'method'])
+def draw(request):
+ if request.param == 'function':
+ return lambda **kwargs: planarity.draw(EDGES, **kwargs)
+ return lambda **kwargs: planarity.PGraph(EDGES).draw(**kwargs)
+
+
+@pytest.mark.parametrize('option,color', [
+ ('facecolor', '#abcdef'),
+ ('vertex_facecolor', 'gold'),
+ ('vertex_bordercolor', (0.3, 0.2, 0.8)),
+ ('vertex_label_facecolor', 'lightgreen'),
+ ('vertex_label_bordercolor', '#aabbcc'),
+ ('edge_linecolor', 'magenta'),
+])
+def test_individual_colors_reach_artists(draw, option, color):
+ draw(**{option: color})
+ ax = plt.gca()
+ if option == 'facecolor':
+ actual = [plt.gcf().get_facecolor()]
+ elif option.startswith('vertex_label_'):
+ method = ('get_facecolor' if option.endswith('facecolor')
+ else 'get_edgecolor')
+ actual = [getattr(text.get_bbox_patch(), method)() for text in ax.texts]
+ assert len(actual) == 5
+ elif option.startswith('vertex_'):
+ vertices = next(c for c in ax.collections if isinstance(c, PatchCollection))
+ actual = (vertices.get_facecolors() if option.endswith('facecolor')
+ else vertices.get_edgecolors())
+ else:
+ actual = [c.get_colors()[0] for c in ax.collections
+ if isinstance(c, LineCollection)]
+ assert len(actual) == 9
+ assert len(actual) > 0
+ for rgba in actual:
+ assert tuple(rgba) == pytest.approx(to_rgba(color))
+
+
+@pytest.mark.parametrize('transparent', [False, True])
+def test_png_background_and_transparency(draw, tmp_path, transparent):
+ output = tmp_path / 'colors.png'
+ # A user's savefig style must not override an explicit draw() facecolor.
+ matplotlib.rcParams['savefig.facecolor'] = 'red'
+ draw(outfileName=str(output), facecolor='#abcdef', transparent=transparent,
+ figsize=(4, 3), dpi=100)
+ with Image.open(output) as image:
+ assert image.size == (400, 300)
+ rgba = image.convert('RGBA')
+ if transparent:
+ assert rgba.getpixel((0, 0))[3] == 0
+ assert rgba.getextrema()[3][1] == 255 # Graph remains visible.
+ else:
+ assert rgba.getpixel((0, 0)) == (171, 205, 239, 255)
+ assert plt.gcf().get_facecolor() == pytest.approx(to_rgba('#abcdef'))
+
+
+def test_color_controls_combine_without_labels(draw, tmp_path):
+ draw(labels=False, outfileName=str(tmp_path / 'no-labels.png'),
+ facecolor='white', transparent=True, vertex_facecolor='yellow',
+ vertex_bordercolor='black', vertex_label_facecolor='pink',
+ vertex_label_bordercolor='green', edge_linecolor='red')
+ assert not plt.gca().texts
+ vertices = next(c for c in plt.gca().collections if isinstance(c, PatchCollection))
+ assert tuple(vertices.get_facecolors()[0]) == pytest.approx(to_rgba('yellow'))
+ assert tuple(vertices.get_edgecolors()[0]) == pytest.approx(to_rgba('black'))
+
+
+def test_default_background_resets_after_custom_draw(draw):
+ draw(facecolor='red')
+ draw()
+ assert plt.gcf().get_facecolor() == pytest.approx(to_rgba('#ffffff'))
+ for text in plt.gca().texts:
+ assert text.get_bbox_patch().get_facecolor() == to_rgba('white')
+ assert text.get_bbox_patch().get_edgecolor() == to_rgba('black')
+
+
+def test_existing_figure_options(draw):
+ draw(figsize=(4, 3), dpi=120)
+ assert tuple(plt.gcf().get_size_inches()) == pytest.approx((4, 3))
+ assert plt.gcf().dpi == 120
+
+
+def test_unknown_keyword_still_rejected(draw):
+ with pytest.raises(ValueError, match='not a supported draw'):
+ draw(vertex_colour='red')
+
+
+def test_invalid_color_rejected(draw):
+ with pytest.raises(ValueError, match='not-a-color'):
+ draw(vertex_facecolor='not-a-color')