Contact
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/smc_sagews/smc_sagews/graphics.py
Views: 286
1
###############################################################################
2
#
3
# CoCalc: Collaborative Calculation
4
#
5
# Copyright (C) 2016, Sagemath Inc.
6
#
7
# This program is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License as published by
9
# the Free Software Foundation, either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# This program is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#
20
###############################################################################
21
22
from __future__ import absolute_import
23
24
import json, math
25
from . import sage_salvus
26
27
from uuid import uuid4
28
29
30
def uuid():
31
return str(uuid4())
32
33
34
def json_float(t):
35
if t is None:
36
return t
37
t = float(t)
38
# Neither of nan or inf get JSON'd in a way that works properly, for some reason. I don't understand why.
39
if math.isnan(t) or math.isinf(t):
40
return None
41
else:
42
return t
43
44
45
#######################################################
46
# Three.js based plotting
47
#######################################################
48
49
import sage.plot.plot3d.index_face_set
50
import sage.plot.plot3d.shapes
51
import sage.plot.plot3d.base
52
import sage.plot.plot3d.shapes2
53
from sage.structure.element import Element
54
55
56
def jsonable(x):
57
if isinstance(x, Element):
58
return json_float(x)
59
elif isinstance(x, (list, tuple)):
60
return [jsonable(y) for y in x]
61
return x
62
63
64
def graphics3d_to_jsonable(p):
65
obj_list = []
66
67
def parse_obj(obj):
68
material_name = ''
69
faces = []
70
for item in obj.split("\n"):
71
tmp = str(item.strip())
72
if not tmp:
73
continue
74
k = tmp.split()
75
if k[0] == "usemtl": # material name
76
material_name = k[1]
77
elif k[0] == 'f': # face
78
v = [int(a) for a in k[1:]]
79
faces.append(v)
80
# other types are parse elsewhere in a different pass.
81
82
return [{"material_name": material_name, "faces": faces}]
83
84
def parse_texture(p):
85
texture_dict = []
86
textures = p.texture_set()
87
for item in range(0, len(textures)):
88
texture_pop = textures.pop()
89
string = str(texture_pop)
90
item = string.split("(")[1]
91
name = item.split(",")[0]
92
color = texture_pop.color
93
tmp_dict = {"name": name, "color": color}
94
texture_dict.append(tmp_dict)
95
return texture_dict
96
97
def get_color(name, texture_set):
98
for item in range(0, len(texture_set)):
99
if (texture_set[item]["name"] == name):
100
color = texture_set[item]["color"]
101
color_list = [color[0], color[1], color[2]]
102
break
103
else:
104
color_list = []
105
return color_list
106
107
def parse_mtl(p):
108
mtl = p.mtl_str()
109
all_material = []
110
for item in mtl.split("\n"):
111
if "newmtl" in item:
112
tmp = str(item.strip())
113
tmp_list = []
114
try:
115
texture_set = parse_texture(p)
116
color = get_color(name, texture_set)
117
except (ValueError, UnboundLocalError):
118
pass
119
try:
120
tmp_list = {
121
"name": name,
122
"ambient": ambient,
123
"specular": specular,
124
"diffuse": diffuse,
125
"illum": illum_list[0],
126
"shininess": shininess_list[0],
127
"opacity": opacity_diffuse[3],
128
"color": color
129
}
130
all_material.append(tmp_list)
131
except (ValueError, UnboundLocalError):
132
pass
133
134
ambient = []
135
specular = []
136
diffuse = []
137
illum_list = []
138
shininess_list = []
139
opacity_diffuse = []
140
tmp_list = []
141
name = tmp.split()[1]
142
143
if "Ka" in item:
144
tmp = str(item.strip())
145
for t in tmp.split():
146
try:
147
ambient.append(json_float(t))
148
except ValueError:
149
pass
150
151
if "Ks" in item:
152
tmp = str(item.strip())
153
for t in tmp.split():
154
try:
155
specular.append(json_float(t))
156
except ValueError:
157
pass
158
159
if "Kd" in item:
160
tmp = str(item.strip())
161
for t in tmp.split():
162
try:
163
diffuse.append(json_float(t))
164
except ValueError:
165
pass
166
167
if "illum" in item:
168
tmp = str(item.strip())
169
for t in tmp.split():
170
try:
171
illum_list.append(json_float(t))
172
except ValueError:
173
pass
174
175
if "Ns" in item:
176
tmp = str(item.strip())
177
for t in tmp.split():
178
try:
179
shininess_list.append(json_float(t))
180
except ValueError:
181
pass
182
183
if "d" in item:
184
tmp = str(item.strip())
185
for t in tmp.split():
186
try:
187
opacity_diffuse.append(json_float(t))
188
except ValueError:
189
pass
190
191
try:
192
color = list(p.all[0].texture.color.rgb())
193
except (ValueError, AttributeError):
194
pass
195
196
try:
197
texture_set = parse_texture(p)
198
color = get_color(name, texture_set)
199
except (ValueError, AttributeError):
200
color = []
201
#pass
202
203
tmp_list = {
204
"name": name,
205
"ambient": ambient,
206
"specular": specular,
207
"diffuse": diffuse,
208
"illum": illum_list[0],
209
"shininess": shininess_list[0],
210
"opacity": opacity_diffuse[3],
211
"color": color
212
}
213
all_material.append(tmp_list)
214
215
return all_material
216
217
#####################################
218
# Conversion functions
219
#####################################
220
221
def convert_index_face_set(p, T, extra_kwds):
222
if T is not None:
223
p = p.transform(T=T)
224
face_geometry = parse_obj(p.obj())
225
if hasattr(p, 'has_local_colors') and p.has_local_colors():
226
convert_index_face_set_with_colors(p, T, extra_kwds)
227
return
228
material = parse_mtl(p)
229
vertex_geometry = []
230
obj = p.obj()
231
for item in obj.split("\n"):
232
if "v" in item:
233
tmp = str(item.strip())
234
for t in tmp.split():
235
try:
236
vertex_geometry.append(json_float(t))
237
except ValueError:
238
pass
239
myobj = {
240
"face_geometry": face_geometry,
241
"type": 'index_face_set',
242
"vertex_geometry": vertex_geometry,
243
"material": material,
244
"has_local_colors": 0
245
}
246
for e in ['wireframe', 'mesh']:
247
if p._extra_kwds is not None:
248
v = p._extra_kwds.get(e, None)
249
if v is not None:
250
myobj[e] = jsonable(v)
251
obj_list.append(myobj)
252
253
def convert_index_face_set_with_colors(p, T, extra_kwds):
254
face_geometry = [{
255
"material_name":
256
p.texture.id,
257
"faces": [[int(v) + 1 for v in f[0]] + [f[1]]
258
for f in p.index_faces_with_colors()]
259
}]
260
material = parse_mtl(p)
261
vertex_geometry = [json_float(t) for v in p.vertices() for t in v]
262
myobj = {
263
"face_geometry": face_geometry,
264
"type": 'index_face_set',
265
"vertex_geometry": vertex_geometry,
266
"material": material,
267
"has_local_colors": 1
268
}
269
for e in ['wireframe', 'mesh']:
270
if p._extra_kwds is not None:
271
v = p._extra_kwds.get(e, None)
272
if v is not None:
273
myobj[e] = jsonable(v)
274
obj_list.append(myobj)
275
276
def convert_text3d(p, T, extra_kwds):
277
obj_list.append({
278
"type":
279
"text",
280
"text":
281
p.string,
282
"pos": [0, 0, 0] if T is None else T([0, 0, 0]),
283
"color":
284
"#" + p.get_texture().hex_rgb(),
285
'fontface':
286
str(extra_kwds.get('fontface', 'Arial')),
287
'constant_size':
288
bool(extra_kwds.get('constant_size', True)),
289
'fontsize':
290
int(extra_kwds.get('fontsize', 12))
291
})
292
293
def convert_line(p, T, extra_kwds):
294
obj_list.append({
295
"type":
296
"line",
297
"points":
298
jsonable(p.points if T is None else
299
[T.transform_point(point) for point in p.points]),
300
"thickness":
301
jsonable(p.thickness),
302
"color":
303
"#" + p.get_texture().hex_rgb(),
304
"arrow_head":
305
bool(p.arrow_head)
306
})
307
308
def convert_point(p, T, extra_kwds):
309
obj_list.append({
310
"type": "point",
311
"loc": p.loc if T is None else T(p.loc),
312
"size": json_float(p.size),
313
"color": "#" + p.get_texture().hex_rgb()
314
})
315
316
def convert_combination(p, T, extra_kwds):
317
for x in p.all:
318
handler(x)(x, T, p._extra_kwds)
319
320
def convert_transform_group(p, T, extra_kwds):
321
if T is not None:
322
T = T * p.get_transformation()
323
else:
324
T = p.get_transformation()
325
for x in p.all:
326
handler(x)(x, T, p._extra_kwds)
327
328
def nothing(p, T, extra_kwds):
329
pass
330
331
def handler(p):
332
if isinstance(p, sage.plot.plot3d.index_face_set.IndexFaceSet):
333
return convert_index_face_set
334
elif isinstance(p, sage.plot.plot3d.shapes.Text):
335
return convert_text3d
336
elif isinstance(p, sage.plot.plot3d.base.TransformGroup):
337
return convert_transform_group
338
elif isinstance(p, sage.plot.plot3d.base.Graphics3dGroup):
339
return convert_combination
340
elif isinstance(p, sage.plot.plot3d.shapes2.Line):
341
return convert_line
342
elif isinstance(p, sage.plot.plot3d.shapes2.Point):
343
return convert_point
344
elif isinstance(p, sage.plot.plot3d.base.PrimitiveObject):
345
return convert_index_face_set
346
elif isinstance(p, sage.plot.plot3d.base.Graphics3d):
347
# this is an empty scene
348
return nothing
349
else:
350
raise NotImplementedError("unhandled type ", type(p))
351
352
# start it going -- this modifies obj_list
353
handler(p)(p, None, None)
354
355
# now obj_list is full of the objects
356
return obj_list
357
358
359
###
360
# Interactive 2d Graphics
361
###
362
363
import os, matplotlib.figure
364
365
366
class InteractiveGraphics(object):
367
def __init__(self, g, **events):
368
self._g = g
369
self._events = events
370
371
def figure(self, **kwds):
372
if isinstance(self._g, matplotlib.figure.Figure):
373
return self._g
374
375
options = dict()
376
options.update(self._g.SHOW_OPTIONS)
377
options.update(self._g._extra_kwds)
378
options.update(kwds)
379
options.pop('dpi')
380
options.pop('transparent')
381
options.pop('fig_tight')
382
fig = self._g.matplotlib(**options)
383
384
from matplotlib.backends.backend_agg import FigureCanvasAgg
385
canvas = FigureCanvasAgg(fig)
386
fig.set_canvas(canvas)
387
fig.tight_layout(
388
) # critical, since sage does this -- if not, coords all wrong
389
return fig
390
391
def save(self, filename, **kwds):
392
if isinstance(self._g, matplotlib.figure.Figure):
393
self._g.savefig(filename)
394
else:
395
# When fig_tight=True (the default), the margins are very slightly different.
396
# I don't know how to properly account for this yet (or even if it is possible),
397
# since it only happens at figsize time -- do "a=plot(sin); a.save??".
398
# So for interactive graphics, we just set this to false no matter what.
399
kwds['fig_tight'] = False
400
self._g.save(filename, **kwds)
401
402
def show(self, **kwds):
403
fig = self.figure(**kwds)
404
ax = fig.axes[0]
405
# upper left data coordinates
406
xmin, ymax = ax.transData.inverted().transform(
407
fig.transFigure.transform((0, 1)))
408
# lower right data coordinates
409
xmax, ymin = ax.transData.inverted().transform(
410
fig.transFigure.transform((1, 0)))
411
412
id = '_a' + uuid().replace('-', '')
413
414
def to_data_coords(p):
415
# 0<=x,y<=1
416
return ((xmax - xmin) * p[0] + xmin,
417
(ymax - ymin) * (1 - p[1]) + ymin)
418
419
if kwds.get('svg', False):
420
filename = '%s.svg' % id
421
del kwds['svg']
422
else:
423
filename = '%s.png' % id
424
425
fig.savefig(filename)
426
427
def f(event, p):
428
self._events[event](to_data_coords(p))
429
430
sage_salvus.salvus.namespace[id] = f
431
x = {}
432
for ev in list(self._events.keys()):
433
x[ev] = id
434
435
sage_salvus.salvus.file(filename, show=True, events=x)
436
os.unlink(filename)
437
438
def __del__(self):
439
for ev in self._events:
440
u = self._id + ev
441
if u in sage_salvus.salvus.namespace:
442
del sage_salvus.salvus.namespace[u]
443
444
445
###
446
# D3-based interactive 2d Graphics
447
###
448
449
450
###
451
# The following is a modified version of graph_plot_js.py from the Sage library, which was
452
# written by Nathann Cohen in 2013.
453
###
454
def graph_to_d3_jsonable(G,
455
vertex_labels=True,
456
edge_labels=False,
457
vertex_partition=[],
458
edge_partition=[],
459
force_spring_layout=False,
460
charge=-120,
461
link_distance=50,
462
link_strength=1,
463
gravity=.04,
464
vertex_size=7,
465
edge_thickness=2,
466
width=None,
467
height=None,
468
**ignored):
469
r"""
470
Display a graph in CoCalc using the D3 visualization library.
471
472
INPUT:
473
474
- ``G`` -- the graph
475
476
- ``vertex_labels`` (boolean) -- Whether to display vertex labels (set to
477
``True`` by default).
478
479
- ``edge_labels`` (boolean) -- Whether to display edge labels (set to
480
``False`` by default).
481
482
- ``vertex_partition`` -- a list of lists representing a partition of the
483
vertex set. Vertices are then colored in the graph according to the
484
partition. Set to ``[]`` by default.
485
486
- ``edge_partition`` -- same as ``vertex_partition``, with edges
487
instead. Set to ``[]`` by default.
488
489
- ``force_spring_layout`` -- whether to take sage's position into account if
490
there is one (see :meth:`~sage.graphs.generic_graph.GenericGraph.` and
491
:meth:`~sage.graphs.generic_graph.GenericGraph.`), or to compute a spring
492
layout. Set to ``False`` by default.
493
494
- ``vertex_size`` -- The size of a vertex' circle. Set to `7` by default.
495
496
- ``edge_thickness`` -- Thickness of an edge. Set to ``2`` by default.
497
498
- ``charge`` -- the vertices' charge. Defines how they repulse each
499
other. See `<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more
500
information. Set to ``-120`` by default.
501
502
- ``link_distance`` -- See
503
`<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more
504
information. Set to ``30`` by default.
505
506
- ``link_strength`` -- See
507
`<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more
508
information. Set to ``1.5`` by default.
509
510
- ``gravity`` -- See
511
`<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more
512
information. Set to ``0.04`` by default.
513
514
515
EXAMPLES::
516
517
show(graphs.RandomTree(50), d3=True)
518
519
show(graphs.PetersenGraph(), d3=True, vertex_partition=g.coloring())
520
521
show(graphs.DodecahedralGraph(), d3=True, force_spring_layout=True)
522
523
show(graphs.DodecahedralGraph(), d3=True)
524
525
g = digraphs.DeBruijn(2,2)
526
g.allow_multiple_edges(True)
527
g.add_edge("10","10","a")
528
g.add_edge("10","10","b")
529
g.add_edge("10","10","c")
530
g.add_edge("10","10","d")
531
g.add_edge("01","11","1")
532
show(g, d3=True, vertex_labels=True,edge_labels=True,
533
link_distance=200,gravity=.05,charge=-500,
534
edge_partition=[[("11","12","2"),("21","21","a")]],
535
edge_thickness=4)
536
537
"""
538
directed = G.is_directed()
539
multiple_edges = G.has_multiple_edges()
540
541
# Associated an integer to each vertex
542
v_to_id = {v: i for i, v in enumerate(G.vertices())}
543
544
# Vertex colors
545
color = {i: len(vertex_partition) for i in range(G.order())}
546
for i, l in enumerate(vertex_partition):
547
for v in l:
548
color[v_to_id[v]] = i
549
550
# Vertex list
551
nodes = []
552
for v in G.vertices():
553
nodes.append({"name": str(v), "group": str(color[v_to_id[v]])})
554
555
# Edge colors.
556
edge_color_default = "#aaa"
557
from sage.plot.colors import rainbow
558
color_list = rainbow(len(edge_partition))
559
edge_color = {}
560
for i, l in enumerate(edge_partition):
561
for e in l:
562
u, v, label = e if len(e) == 3 else e + (None, )
563
edge_color[u, v, label] = color_list[i]
564
if not directed:
565
edge_color[v, u, label] = color_list[i]
566
567
# Edge list
568
edges = []
569
seen = {} # How many times has this edge been seen ?
570
571
for u, v, l in G.edges():
572
573
# Edge color
574
color = edge_color.get((u, v, l), edge_color_default)
575
576
# Computes the curve of the edge
577
curve = 0
578
579
# Loop ?
580
if u == v:
581
seen[u, v] = seen.get((u, v), 0) + 1
582
curve = seen[u, v] * 10 + 10
583
584
# For directed graphs, one also has to take into accounts
585
# edges in the opposite direction
586
elif directed:
587
if G.has_edge(v, u):
588
seen[u, v] = seen.get((u, v), 0) + 1
589
curve = seen[u, v] * 15
590
else:
591
if multiple_edges and len(G.edge_label(u, v)) != 1:
592
# Multiple edges. The first one has curve 15, then
593
# -15, then 30, then -30, ...
594
seen[u, v] = seen.get((u, v), 0) + 1
595
curve = (1 if seen[u, v] % 2 else -1) * (seen[u, v] //
596
2) * 15
597
598
elif not directed and multiple_edges:
599
# Same formula as above for multiple edges
600
if len(G.edge_label(u, v)) != 1:
601
seen[u, v] = seen.get((u, v), 0) + 1
602
curve = (1 if seen[u, v] % 2 else -1) * (seen[u, v] // 2) * 15
603
604
# Adding the edge to the list
605
edges.append({
606
"source": v_to_id[u],
607
"target": v_to_id[v],
608
"strength": 0,
609
"color": color,
610
"curve": curve,
611
"name": str(l) if edge_labels else ""
612
})
613
614
loops = [e for e in edges if e["source"] == e["target"]]
615
edges = [e for e in edges if e["source"] != e["target"]]
616
617
# Defines the vertices' layout if possible
618
Gpos = G.get_pos()
619
pos = []
620
if Gpos is not None and force_spring_layout is False:
621
charge = 0
622
link_strength = 0
623
gravity = 0
624
625
for v in G.vertices():
626
x, y = Gpos[v]
627
pos.append([json_float(x), json_float(-y)])
628
629
return {
630
"nodes": nodes,
631
"links": edges,
632
"loops": loops,
633
"pos": pos,
634
"directed": G.is_directed(),
635
"charge": int(charge),
636
"link_distance": int(link_distance),
637
"link_strength": int(link_strength),
638
"gravity": float(gravity),
639
"vertex_labels": bool(vertex_labels),
640
"edge_labels": bool(edge_labels),
641
"vertex_size": int(vertex_size),
642
"edge_thickness": int(edge_thickness),
643
"width": json_float(width),
644
"height": json_float(height)
645
}
646
647