Skip to content
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

Graph Plotting updates #38823

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@
"Cython"
],
"editor.formatOnType": true,
"esbonio.sphinx.confDir": ""
"esbonio.sphinx.confDir": "",
"flake8.args": [
"--select=E111,E21,E221,E222,E225,E227,E228,E25,E271,E303,E305,E306,E401,E502,E701,E702,E703,E71,E72,W291,W293,W391,W605"
]
}
217 changes: 187 additions & 30 deletions src/sage/graphs/graph_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@
'a dictionary keyed by vertices and associating to each vertex '
'a label string, or a function taking as input a vertex and returning '
'a label string.',
'vertex_label_shift':
'If layout is circular and we have vertex labels, will shift vertices '
'away from center of circle in coordinate fashion `(x, y)`.',
'vertex_color':
'Default color for vertices not listed '
'in vertex_colors dictionary.',
Expand All @@ -197,10 +200,19 @@
'Whether or not to draw edge labels.',
'edge_style':
'The linestyle of the edges. It should be '
'one of "solid", "dashed", "dotted", dashdot", '
'one of "solid", "dashed", "dotted", "dashdot", '
'or "-", "--", ":", "-.", respectively. ',
'edge_styles':
'A dictionary specifying edge styles: '
'each key is an edge or a label (all same) and value is the linestyle '
'of the edge. It should be one of "solid", "dashed", "dotted", '
'"dashdot", or "-", "--", ":", "-.", respectively.',
'edge_thickness':
'The thickness of the edges.',
'edge_thicknesses':
'A dictionary specifying edge thicknesses: '
'each key is an edge or a label (all same) and thickness of the '
'corresponding edge.',
'edge_color':
'The default color for edges not listed in edge_colors.',
'edge_colors':
Expand All @@ -215,12 +227,16 @@
'cell in a different color; vertex_colors takes precedence.',
'loop_size':
'The radius of the smallest loop.',
'arrowsize':
'Size of arrows.',
'dist':
'The distance between multiedges.',
'max_dist':
'The max distance range to allow multiedges.',
'talk':
'Whether to display the vertices in talk mode (larger and white).',
'label_fontsize':
'font size of all labels',
'graph_border':
'Whether or not to draw a frame around the graph.',
'edge_labels_background':
Expand All @@ -238,9 +254,12 @@
DEFAULT_PLOT_OPTIONS = {
'vertex_size' : 200,
'vertex_labels' : True,
'vertex_label_shift' : None,
'layout' : None,
'edge_style' : 'solid',
'edge_styles' : None,
'edge_thickness' : 1,
'edge_thicknesses' : None,
'edge_color' : 'black',
'edge_colors' : None,
'edge_labels' : False,
Expand All @@ -253,6 +272,7 @@
'partition' : None,
'dist' : .075,
'max_dist' : 1.5,
'label_fontsize' : 12,
'loop_size' : .075,
'edge_labels_background' : 'white'}

Expand Down Expand Up @@ -568,9 +588,26 @@
return vlabels.get(x, "")
else:
vfun = vlabels

# TODO: allow text options
self._plot_components['vertex_labels'] = [text(vfun(v), self._pos[v], color='black', zorder=8)
for v in self._nodelist]
if self._options['layout'] == 'circular' and self._options['vertex_label_shift'] is not None:
def pos_shift(v, shift):
return (v[0] + (v[0] * shift[0])/100, v[1] + (v[1] * shift[1])/100)
self._plot_components['vertex_labels'] = [
text(
vfun(v),
pos_shift(self._pos[v], self._options['vertex_label_shift']),
fontsize=self._options['label_fontsize'],
color='black',
zorder=8
)
for v in self._nodelist
]
else:
self._plot_components['vertex_labels'] = [
text(vfun(v), self._pos[v], color='black', zorder=8, fontsize=self._options['label_fontsize'])
for v in self._nodelist
]

def set_edges(self, **edge_options):
"""
Expand Down Expand Up @@ -709,15 +746,24 @@
if self._options['edge_labels_background'] == "transparent":
self._options['edge_labels_background'] = "None"

# Handle base edge options: thickness, linestyle
# Whether a key is an edge or not:
# None => edge_x is not set
# True => keys are edges
# False => keys are labels
style_key_edges = None
thickness_key_edges = None
if isinstance(self._options['edge_styles'], dict):
for k in self._options['edge_styles']:
style_key_edges = k in self._graph.edges()
break
if isinstance(self._options['edge_thicknesses'], dict):
for k in self._options['edge_thicknesses']:
thickness_key_edges = k in self._graph.edges()
break

eoptions = {}
if 'edge_style' in self._options:
from sage.plot.misc import get_matplotlib_linestyle
eoptions['linestyle'] = get_matplotlib_linestyle(
self._options['edge_style'],
return_type='long')
if 'edge_thickness' in self._options:
eoptions['thickness'] = self._options['edge_thickness']
if 'arrowsize' in self._options:
eoptions['arrowsize'] = self._options['arrowsize']

# Set labels param to add labels on the fly
labels = False
Expand Down Expand Up @@ -812,12 +858,20 @@
# Now add all the loops at this vertex, varying their size
for lab, col, _ in local_labels:
x, y = self._pos[a][0], self._pos[a][1] - loop_size
c = circle((x, y), loop_size, rgbcolor=col, **eoptions)

estyle = self._options['edge_style']
ethickness = self._options['edge_thickness']
if style_key_edges is not None and ((style_key_edges and (x, y) in self._options['edge_styles']) or (not style_key_edges and lab in self._options['edge_styles'])):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please improve the alignment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by the alignment? Do you mean for the if statement? Should I break it down so it's something like:

if (style_key_edges is not None
    and ((style_key_edges and (x, y) in self._options['edge_styles'])
        or (not style_key_edges and lab in self._options['edge_styles']))):

Or are you thinking something else?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this is much easier to read this way.

estyle = style_key_edges and self._options['edge_styles'][(x, y)] or self._options['edge_styles'][lab]
if thickness_key_edges is not None and ((thickness_key_edges and (x, y) in self._options['edge_thicknesses']) or (not thickness_key_edges and lab in self._options['edge_thicknesses'])):
ethickness = thickness_key_edges and self._options['edge_thicknesses'][(x, y)] or self._options['edge_thicknesses'][lab]

c = circle((x, y), loop_size, rgbcolor=col, linestyle=estyle, thickness=ethickness)
self._plot_components['edges'].append(c)
if labels:
bg = self._options['edge_labels_background']
y -= loop_size # place label at bottom of loop
t = text(lab, (x, y), background_color=bg)
t = text(lab, (x, y), background_color=bg, fontsize=self._options['label_fontsize'])
self._plot_components['edge_labels'].append(t)
loop_size += loop_size_increment
elif len(edges_to_draw[a, b]) > 1:
Expand Down Expand Up @@ -883,80 +937,115 @@
distance = float(max_dist) / len_local_labels
for i in range(len_local_labels // 2):
k = (i + 1.0) * distance
estyle = self._options['edge_style']
ethickness = self._options['edge_thickness']

if self._arcdigraph:
vr = self._vertex_radius
ph = self._polar_hack_for_multidigraph
odd_start = ph(p1, odd_xy(k), vr)[0]
odd_end = ph(odd_xy(k), p2, vr)[1]
even_start = ph(p1, even_xy(k), vr)[0]
even_end = ph(even_xy(k), p2, vr)[1]

self._plot_components['edges'].append(
arrow(path=[[odd_start, odd_xy(k), odd_end]],
head=local_labels[2 * i][2], zorder=1,
rgbcolor=local_labels[2 * i][1],
**eoptions))
linestyle=estyle,
width=ethickness,
**eoptions
))
self._plot_components['edges'].append(
arrow(path=[[even_start, even_xy(k), even_end]],
head=local_labels[2 * i + 1][2], zorder=1,
rgbcolor=local_labels[2 * i + 1][1],
**eoptions))
linestyle=estyle,
width=ethickness,
**eoptions
))
else:
self._plot_components['edges'].append(
bezier_path([[p1, odd_xy(k), p2]], zorder=1,
rgbcolor=local_labels[2 * i][1],
**eoptions))
linestyle=estyle,
thickness=ethickness
))
self._plot_components['edges'].append(
bezier_path([[p1, even_xy(k), p2]], zorder=1,
rgbcolor=local_labels[2 * i + 1][1],
**eoptions))
linestyle=estyle,
thickness=ethickness
))
if labels:
j = k / 2.0
bg = self._options['edge_labels_background']
self._plot_components['edge_labels'].append(
text(local_labels[2 * i][0], odd_xy(j),
background_color=bg))
background_color=bg, fontsize=self._options['label_fontsize']))
self._plot_components['edge_labels'].append(
text(local_labels[2 * i + 1][0], even_xy(j),
background_color=bg))
background_color=bg, fontsize=self._options['label_fontsize']))
if len_local_labels % 2:
# draw line for last odd
edges_to_draw[a, b] = [local_labels[-1]]

is_directed = self._graph.is_directed()
for a, b in edges_to_draw:
elabel = edges_to_draw[a, b][0][0]
ecolor = edges_to_draw[a, b][0][1]
ehead = edges_to_draw[a, b][0][2]
e = (a, b, elabel)

estyle = self._options['edge_style']
ethickness = self._options['edge_thickness']
if style_key_edges is not None and ((style_key_edges and e in self._options['edge_styles']) or (not style_key_edges and elabel in self._options['edge_styles'])):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

improve alignment

estyle = style_key_edges and self._options['edge_styles'][e] or self._options['edge_styles'][elabel]
if thickness_key_edges is not None and ((thickness_key_edges and e in self._options['edge_thicknesses']) or (not thickness_key_edges and elabel in self._options['edge_thicknesses'])):
ethickness = thickness_key_edges and self._options['edge_thicknesses'][e] or self._options['edge_thicknesses'][elabel]

if self._arcdigraph:
ph = self._polar_hack_for_multidigraph
C, D = ph(self._pos[a], self._pos[b], self._vertex_radius)
self._plot_components['edges'].append(
arrow(C, D,
rgbcolor=edges_to_draw[a, b][0][1],
head=edges_to_draw[a, b][0][2],
**eoptions))
rgbcolor=ecolor,
head=ehead,
linestyle=estyle,
width=ethickness,
**eoptions
))
if labels:
bg = self._options['edge_labels_background']
self._plot_components['edge_labels'].append(
text(str(edges_to_draw[a, b][0][0]),
text(str(elabel),
[(C[0] + D[0]) / 2., (C[1] + D[1]) / 2.],
background_color=bg))
background_color=bg,
fontsize=self._options['label_fontsize']))
elif is_directed:
self._plot_components['edges'].append(
arrow(self._pos[a], self._pos[b],
rgbcolor=edges_to_draw[a, b][0][1],
rgbcolor=ecolor,
arrowshorten=self._arrowshorten,
head=edges_to_draw[a, b][0][2],
**eoptions))
head=ehead,
linestyle=estyle,
width=ethickness,
**eoptions
))
else:
self._plot_components['edges'].append(
line([self._pos[a], self._pos[b]],
rgbcolor=edges_to_draw[a, b][0][1],
**eoptions))
rgbcolor=ecolor,
linestyle=estyle,
thickness=ethickness))
if labels and not self._arcdigraph:
bg = self._options['edge_labels_background']
self._plot_components['edge_labels'].append(
text(str(edges_to_draw[a, b][0][0]),
[(self._pos[a][0] + self._pos[b][0]) / 2.,
(self._pos[a][1] + self._pos[b][1]) / 2.],
background_color=bg))
background_color=bg,
fontsize=self._options['label_fontsize']))

def _polar_hack_for_multidigraph(self, A, B, VR):
"""
Expand Down Expand Up @@ -1147,6 +1236,31 @@
for u, v, l in D.edges(sort=True):
D.set_edge_label(u, v, f'({u},{v})')
sphinx_plot(D.graphplot(edge_labels=True, layout='circular'))

For graphs with ``circular`` layouts, one may shift the vertex labels by
specifying coordinates to shift by::

sage: D = DiGraph({
....: 0: [1, 10, 19], 1: [8, 2], 2: [3, 6], 3: [19, 4],
....: 4: [17, 5], 5: [6, 15], 6: [7], 7: [8, 14], 8: [9],
....: 9: [10, 13], 10: [11], 11: [12, 18], 12: [16, 13],
....: 13: [14], 14: [15], 15: [16], 16: [17], 17: [18],
....: 18: [19], 19: []})
sage: for u, v, l in D.edges(sort=True):
....: D.set_edge_label(u, v, f'({u},{v})')
sage: D.graphplot(edge_labels=True, layout='circular', vertex_label_shift=(15,10)).show()

.. PLOT::

D = DiGraph({
0: [1, 10, 19], 1: [8, 2], 2: [3, 6], 3: [19, 4],
4: [17, 5], 5: [6, 15], 6: [7], 7: [8, 14], 8: [9],
9: [10, 13], 10: [11], 11: [12, 18], 12: [16, 13],
13: [14], 14: [15], 15: [16], 16: [17], 17: [18],
18: [19], 19: []})
for u, v, l in D.edges(sort=True):
D.set_edge_label(u, v, f'({u},{v})')
sphinx_plot(D.graphplot(edge_labels=True, layout='circular', vertex_label_shift=(15,10)))

This example shows off the coloring of edges::

Expand Down Expand Up @@ -1237,6 +1351,17 @@
P = g.graphplot(pos=pos, layout='spring', iterations=0).plot()
sphinx_plot(P)

::

sage: D = graphs.CubeGraph(3)
sage: D.graphplot(layout='planar').plot()

Check failure on line 1357 in src/sage/graphs/graph_plot.py

View workflow job for this annotation

GitHub Actions / test-new

Failed example:

Failed example:: Got: Graphics object consisting of 21 graphics primitives
Launched png viewer for Graphics object consisting of 21 graphics primitives

.. PLOT::

D = graphs.CubeGraph(3)
sphinx_plot(D.graphplot(layout='planar'))

::

sage: G = Graph()
Expand Down Expand Up @@ -1345,6 +1470,18 @@
D = DiGraph({0:[1,2,3], 2:[1,4], 3:[0]})
sphinx_plot(D.graphplot())

::

sage: D = DiGraph({0:[1,2,3], 2:[1,4], 3:[0]})
sage: D.graphplot(label_fontsize=20).show()

Check failure on line 1476 in src/sage/graphs/graph_plot.py

View workflow job for this annotation

GitHub Actions / test-new

Failed example:

Failed example:: Got:
Graphics object consisting of 8 graphics primitives

.. PLOT::

D = DiGraph({0:[1,2,3], 2:[1,4], 3:[0]})
sphinx_plot(D.graphplot(label_fontsize=20))


::

sage: D = DiGraph(multiedges=True, sparse=True)
Expand Down Expand Up @@ -1395,6 +1532,26 @@
....: ).plot()
Graphics object consisting of 22 graphics primitives

The ``edge_styles`` option may be provided if you need only certain edges
to have certain styles::

sage: GP.set_edges(edge_styles={'a':'dashed', 'g':'dotted'})

Check failure on line 1538 in src/sage/graphs/graph_plot.py

View workflow job for this annotation

GitHub Actions / test-new

Failed example:

Failed example:: Exception raised: Traceback (most recent call last): File "/sage/src/sage/doctest/forker.py", line 714, in _run self.compile_and_execute(example, compiler, test.globs) File "/sage/src/sage/doctest/forker.py", line 1144, in compile_and_execute exec(compiled, globs) File "<doctest sage.graphs.graph_plot.GraphPlot.plot[78]>", line 1, in <module> GP.set_edges(edge_styles={'a':'dashed', 'g':'dotted'}) NameError: name 'GP' is not defined
sage: GP.plot()

Check failure on line 1539 in src/sage/graphs/graph_plot.py

View workflow job for this annotation

GitHub Actions / test-new

Failed example:

Failed example:: Exception raised: Traceback (most recent call last): File "/sage/src/sage/doctest/forker.py", line 714, in _run self.compile_and_execute(example, compiler, test.globs) File "/sage/src/sage/doctest/forker.py", line 1144, in compile_and_execute exec(compiled, globs) File "<doctest sage.graphs.graph_plot.GraphPlot.plot[79]>", line 1, in <module> GP.plot() NameError: name 'GP' is not defined
Graphics object consisting of 22 graphics primitives

.. PLOT::

g = Graph(loops=True, multiedges=True, sparse=True)
g.add_edges([(0, 0, 'a'), (0, 0, 'b'), (0, 1, 'c'),
(0, 1, 'd'), (0, 1, 'e'), (0, 1, 'f'),
(0, 1, 'f'), (2, 1, 'g'), (2, 2, 'h')])
GP = g.graphplot(vertex_size=100, edge_labels=True,
color_by_label=True, edge_style='dashed')
GP.set_edges(edge_style='solid')
GP.set_edges(edge_color='black')
GP.set_edges(edge_styles={'a':'dashed', 'g':'dotted'})
sphinx_plot(GP)

TESTS:

Make sure that show options work with plot also::
Expand Down
Loading