Skip to content

Commit 9c1eca2

Browse files
authored
Merge pull request #6 from Mathics3/handle-graphics
Start to handle matplotlib.pyplot for graphs
2 parents ba87ea7 + 1eacc47 commit 9c1eca2

File tree

4 files changed

+299
-49
lines changed

4 files changed

+299
-49
lines changed

mathicsscript/__main__.py

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pathlib import Path
88

99
from mathicsscript.termshell import TerminalShell
10+
from mathicsscript.format import format_output
1011

1112
from mathics.core.parser import LineFeeder, FileLineFeeder
1213
from mathics.core.definitions import Definitions
@@ -77,53 +78,6 @@ def load_settings(shell):
7778
return True
7879

7980

80-
def format_output(obj, expr, format=None):
81-
if format is None:
82-
format = obj.format
83-
84-
if isinstance(format, dict):
85-
return dict((k, obj.format_output(expr, f)) for k, f in format.items())
86-
87-
from mathics.core.expression import Expression, BoxError
88-
89-
expr_type = expr.get_head_name()
90-
if expr_type == "System`MathMLForm":
91-
format = "xml"
92-
leaves = expr.get_leaves()
93-
if len(leaves) == 1:
94-
expr = leaves[0]
95-
elif expr_type == "System`TeXForm":
96-
format = "tex"
97-
leaves = expr.get_leaves()
98-
if len(leaves) == 1:
99-
expr = leaves[0]
100-
elif expr_type == "System`Graphics":
101-
result = Expression("StandardForm", expr).format(obj, "System`MathMLForm")
102-
ml_str = result.leaves[0].leaves[0]
103-
# FIXME: not quite right. Need to parse out strings
104-
display_svg(str(ml_str))
105-
106-
if format == "text":
107-
result = expr.format(obj, "System`OutputForm")
108-
elif format == "xml":
109-
result = Expression("StandardForm", expr).format(obj, "System`MathMLForm")
110-
elif format == "tex":
111-
result = Expression("StandardForm", expr).format(obj, "System`TeXForm")
112-
else:
113-
raise ValueError
114-
115-
try:
116-
boxes = result.boxes_to_text(evaluation=obj)
117-
except BoxError:
118-
boxes = None
119-
if not hasattr(obj, "seen_box_error"):
120-
obj.seen_box_error = True
121-
obj.message(
122-
"General", "notboxes", Expression("FullForm", result).evaluate(obj)
123-
)
124-
return boxes
125-
126-
12781
Evaluation.format_output = format_output
12882

12983

@@ -376,7 +330,9 @@ def main(
376330

377331
if full_form != 0:
378332
print(fmt(query))
379-
result = evaluation.evaluate(query, timeout=settings.TIMEOUT)
333+
result = evaluation.evaluate(
334+
query, timeout=settings.TIMEOUT, format="unformatted"
335+
)
380336
if result is not None:
381337
shell.print_result(result, output_style)
382338
except (KeyboardInterrupt):

mathicsscript/format.py

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
"""
2+
Format Mathics objects
3+
"""
4+
5+
import random
6+
import networkx as nx
7+
8+
9+
def format_output(obj, expr, format=None):
10+
if format is None:
11+
format = obj.format
12+
13+
if isinstance(format, dict):
14+
return dict((k, obj.format_output(expr, f)) for k, f in format.items())
15+
16+
from mathics.core.expression import Expression, BoxError
17+
18+
expr_type = expr.get_head_name()
19+
if expr_type == "System`MathMLForm":
20+
format = "xml"
21+
leaves = expr.get_leaves()
22+
if len(leaves) == 1:
23+
expr = leaves[0]
24+
elif expr_type == "System`TeXForm":
25+
format = "tex"
26+
leaves = expr.get_leaves()
27+
if len(leaves) == 1:
28+
expr = leaves[0]
29+
elif expr_type == "System`Graphics":
30+
result = Expression("StandardForm", expr).format(obj, "System`MathMLForm")
31+
ml_str = result.leaves[0].leaves[0]
32+
# FIXME: not quite right. Need to parse out strings
33+
display_svg(str(ml_str))
34+
35+
if format == "text":
36+
result = expr.format(obj, "System`OutputForm")
37+
elif format == "xml":
38+
result = Expression("StandardForm", expr).format(obj, "System`MathMLForm")
39+
elif format == "tex":
40+
result = Expression("StandardForm", expr).format(obj, "System`TeXForm")
41+
elif format == "unformatted":
42+
if str(expr) == "-Graph-":
43+
return format_graph(expr.G)
44+
else:
45+
result = expr.format(obj, "System`OutputForm")
46+
else:
47+
raise ValueError
48+
49+
try:
50+
boxes = result.boxes_to_text(evaluation=obj)
51+
except BoxError:
52+
boxes = None
53+
if not hasattr(obj, "seen_box_error"):
54+
obj.seen_box_error = True
55+
obj.message(
56+
"General", "notboxes", Expression("FullForm", result).evaluate(obj)
57+
)
58+
return boxes
59+
60+
61+
def hierarchy_pos(
62+
G, root=None, width=1.0, vert_gap=0.2, vert_loc=0, leaf_vs_root_factor=0.5
63+
):
64+
65+
"""From EoN (Epidemics on Networks): a fast, flexible Python package
66+
for simulation, analytic approximation, and analysis of epidemics
67+
on networks
68+
https://joss.theoj.org/papers/10.21105/joss.01731
69+
70+
If the graph is a tree this will return the positions to plot this in a
71+
hierarchical layout.
72+
73+
Based on Joel's answer at https://stackoverflow.com/a/29597209/2966723,
74+
but with some modifications.
75+
76+
We include this because it may be useful for plotting transmission trees,
77+
and there is currently no networkx equivalent (though it may be coming soon).
78+
79+
There are two basic approaches we think of to allocate the horizontal
80+
location of a node.
81+
82+
- Top down: we allocate horizontal space to a node. Then its ``k``
83+
descendants split up that horizontal space equally. This tends to result
84+
in overlapping nodes when some have many descendants.
85+
- Bottom up: we allocate horizontal space to each leaf node. A node at a
86+
higher level gets the entire space allocated to its descendant leaves.
87+
Based on this, leaf nodes at higher levels get the same space as leaf
88+
nodes very deep in the tree.
89+
90+
We use use both of these approaches simultaneously with ``leaf_vs_root_factor``
91+
determining how much of the horizontal space is based on the bottom up
92+
or top down approaches. ``0`` gives pure bottom up, while 1 gives pure top
93+
down.
94+
95+
96+
:Arguments:
97+
98+
**G** the graph (must be a tree)
99+
100+
**root** the root node of the tree
101+
- if the tree is directed and this is not given, the root will be found and used
102+
- if the tree is directed and this is given, then the positions will be
103+
just for the descendants of this node.
104+
- if the tree is undirected and not given, then a random choice will be used.
105+
106+
**width** horizontal space allocated for this branch - avoids overlap with other branches
107+
108+
**vert_gap** gap between levels of hierarchy
109+
110+
**vert_loc** vertical location of root
111+
112+
**leaf_vs_root_factor**
113+
114+
xcenter: horizontal location of root
115+
116+
"""
117+
if not nx.is_tree(G):
118+
raise TypeError("cannot use hierarchy_pos on a graph that is not a tree")
119+
120+
if root is None:
121+
if isinstance(G, nx.DiGraph):
122+
root = next(
123+
iter(nx.topological_sort(G))
124+
) # allows back compatibility with nx version 1.11
125+
else:
126+
root = random.choice(list(G.nodes))
127+
128+
def _hierarchy_pos(
129+
G,
130+
root,
131+
leftmost,
132+
width,
133+
leafdx=0.2,
134+
vert_gap=0.2,
135+
vert_loc=0,
136+
xcenter=0.5,
137+
rootpos=None,
138+
leafpos=None,
139+
parent=None,
140+
):
141+
"""
142+
see hierarchy_pos docstring for most arguments
143+
144+
pos: a dict saying where all nodes go if they have been assigned
145+
parent: parent of this branch. - only affects it if non-directed
146+
147+
"""
148+
149+
if rootpos is None:
150+
rootpos = {root: (xcenter, vert_loc)}
151+
else:
152+
rootpos[root] = (xcenter, vert_loc)
153+
if leafpos is None:
154+
leafpos = {}
155+
children = list(G.neighbors(root))
156+
leaf_count = 0
157+
if not isinstance(G, nx.DiGraph) and parent is not None:
158+
children.remove(parent)
159+
if len(children) != 0:
160+
rootdx = width / len(children)
161+
nextx = xcenter - width / 2 - rootdx / 2
162+
for child in children:
163+
nextx += rootdx
164+
rootpos, leafpos, newleaves = _hierarchy_pos(
165+
G,
166+
child,
167+
leftmost + leaf_count * leafdx,
168+
width=rootdx,
169+
leafdx=leafdx,
170+
vert_gap=vert_gap,
171+
vert_loc=vert_loc - vert_gap,
172+
xcenter=nextx,
173+
rootpos=rootpos,
174+
leafpos=leafpos,
175+
parent=root,
176+
)
177+
leaf_count += newleaves
178+
179+
leftmostchild = min((x for x, y in [leafpos[child] for child in children]))
180+
rightmostchild = max((x for x, y in [leafpos[child] for child in children]))
181+
leafpos[root] = ((leftmostchild + rightmostchild) / 2, vert_loc)
182+
else:
183+
leaf_count = 1
184+
leafpos[root] = (leftmost, vert_loc)
185+
# pos[root] = (leftmost + (leaf_count-1)*dx/2., vert_loc)
186+
# print(leaf_count)
187+
return rootpos, leafpos, leaf_count
188+
189+
xcenter = width / 2.0
190+
if isinstance(G, nx.DiGraph):
191+
leafcount = len(
192+
[node for node in nx.descendants(G, root) if G.out_degree(node) == 0]
193+
)
194+
elif isinstance(G, nx.Graph):
195+
leafcount = len(
196+
[
197+
node
198+
for node in nx.node_connected_component(G, root)
199+
if G.degree(node) == 1 and node != root
200+
]
201+
)
202+
rootpos, leafpos, leaf_count = _hierarchy_pos(
203+
G,
204+
root,
205+
0,
206+
width,
207+
leafdx=width * 1.0 / leafcount,
208+
vert_gap=vert_gap,
209+
vert_loc=vert_loc,
210+
xcenter=xcenter,
211+
)
212+
pos = {}
213+
for node in rootpos:
214+
pos[node] = (
215+
leaf_vs_root_factor * leafpos[node][0]
216+
+ (1 - leaf_vs_root_factor) * rootpos[node][0],
217+
leafpos[node][1],
218+
)
219+
# pos = {node:(leaf_vs_root_factor*x1+(1-leaf_vs_root_factor)*x2, y1) for ((x1,y1), (x2,y2)) in (leafpos[node], rootpos[node]) for node in rootpos}
220+
xmax = max(x for x, y in pos.values())
221+
y_list = {}
222+
for node in pos:
223+
x, y = pos[node] = (pos[node][0] * width / xmax, pos[node][1])
224+
y_list[y] = y_list.get(y, set([]))
225+
y_list[y].add(x)
226+
227+
min_sep = xmax
228+
for y in y_list.keys():
229+
x_list = sorted(y_list[y])
230+
n = len(x_list) - 1
231+
if n <= 0:
232+
continue
233+
min_sep = min([x_list[i + 1] - x_list[i] for i in range(n)] + [min_sep])
234+
return pos, min_sep
235+
236+
237+
node_size = 300 # this is networkx's default size
238+
239+
240+
def tree_layout(G):
241+
global node_size
242+
root = G.root if hasattr(G, "root") else None
243+
pos, min_sep = hierarchy_pos(G, root=root)
244+
node_size = min_sep * 2000
245+
return pos
246+
247+
248+
NETWORKX_LAYOUTS = {
249+
"circular": nx.circular_layout,
250+
"multipartite": nx.multipartite_layout,
251+
"planar": nx.planar_layout,
252+
"random": nx.random_layout,
253+
"shell": nx.shell_layout,
254+
"spectral": nx.spectral_layout,
255+
"spring": nx.spring_layout,
256+
"tree": tree_layout,
257+
}
258+
259+
260+
def format_graph(G):
261+
"""
262+
Format a Graph
263+
"""
264+
# FIXME handle graphviz as well
265+
import matplotlib.pyplot as plt
266+
267+
global node_size
268+
node_size = 300 # This is networkx's default
269+
270+
graph_layout = G.graph_layout if hasattr(G, "graph_layout") else None
271+
vertex_labeling = G.vertex_labeling if hasattr(G, "vertex_labeling") else False
272+
if vertex_labeling:
273+
vertex_labeling = vertex_labeling.to_python() or False
274+
275+
if hasattr(G, "title") and G.title.get_string_value():
276+
fig, ax = plt.subplots() # Create a figure and an axes
277+
ax.set_title(G.title.get_string_value())
278+
279+
if graph_layout:
280+
if not isinstance(graph_layout, str):
281+
graph_layout = graph_layout.get_string_value()
282+
layout_fn = NETWORKX_LAYOUTS.get(graph_layout, None)
283+
else:
284+
layout_fn = None
285+
286+
if layout_fn:
287+
nx.draw(G, pos=layout_fn(G), with_labels=vertex_labeling, node_size=node_size)
288+
else:
289+
nx.draw_shell(G, with_labels=vertex_labeling, node_size=node_size)
290+
plt.show()
291+
return None

mathicsscript/termshell.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ def read_line(self, prompt):
226226
def print_result(self, result, output_style=""):
227227
if result is not None and result.result is not None:
228228
out_str = str(result.result)
229+
if output_style == "//Graph":
230+
out_str = "*Graph*"
229231
if self.terminal_formatter: # pygmentize
230232
show_pygments_tokens = self.definitions.get_ownvalue(
231233
"Settings`$PygmentsShowTokens"
@@ -243,7 +245,7 @@ def print_result(self, result, output_style=""):
243245
print(list(lex(out_str, mma_lexer)))
244246
out_str = highlight(out_str, mma_lexer, self.terminal_formatter)
245247
output = self.to_output(out_str)
246-
print(self.get_out_prompt(output_style) + output + "\n")
248+
print(self.get_out_prompt("") + output + "\n")
247249

248250
def rl_read_line(self, prompt):
249251
# Wrap ANSI color sequences in \001 and \002, so readline

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def read(*rnames):
6969
"click",
7070
"colorama",
7171
"columnize",
72+
"networkx",
7273
"pygments",
7374
"term-background >= 1.0.1",
7475
],

0 commit comments

Comments
 (0)