Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ In [11]: print(planarity.ascii(edgelist))
</pre>

```python
In [12]: planarity.draw(edgelist, outfileName='K5-minus-edge.png')
In [12]: planarity.draw(edgelist, outfileName='K5-minus-edge.png', vertex_bordercolor='black')
```
![K_5-minus-edge](/examples/classic/output/K5-minus-edge.png
"Matplotlib render of $K_5$-minus-edge")
Expand Down Expand Up @@ -93,4 +93,4 @@ license](https://github.com/graph-algorithms/planarity/blob/master/LICENSE.txt).
&nbsp;&nbsp;&nbsp;&nbsp;Planarity includes the Edge Addition Planarity Suite, which is<br/>
&nbsp;&nbsp;&nbsp;&nbsp;Copyright (c) 1997-2026, John M. Boyer.<br/>
&nbsp;&nbsp;&nbsp;&nbsp;The BSD-3-Clause license for the Edge Additional Planarity Suite<br/>
&nbsp;&nbsp;&nbsp;&nbsp;included in Planarity appears [here](https://github.com/graph-algorithms/planarity/blob/master/planarity/c/LICENSE.TXT).
&nbsp;&nbsp;&nbsp;&nbsp;included in Planarity appears [here](https://github.com/graph-algorithms/planarity/blob/master/planarity/c/LICENSE.TXT).
6 changes: 3 additions & 3 deletions examples/classic/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
24 changes: 24 additions & 0 deletions examples/classic/draw_planar_colors.py
Original file line number Diff line number Diff line change
@@ -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},
)
Binary file modified examples/classic/output/K5-minus-edge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
51 changes: 38 additions & 13 deletions planarity/classic/planarity.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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'),
)
)

Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions planarity/classic/planarity_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
116 changes: 116 additions & 0 deletions tests/test_draw_colors.py
Original file line number Diff line number Diff line change
@@ -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')