Path: blob/master/src/smc_sagews/smc_sagews/graphics.py
Views: 286
###############################################################################1#2# CoCalc: Collaborative Calculation3#4# Copyright (C) 2016, Sagemath Inc.5#6# This program is free software: you can redistribute it and/or modify7# it under the terms of the GNU General Public License as published by8# the Free Software Foundation, either version 3 of the License, or9# (at your option) any later version.10#11# This program is distributed in the hope that it will be useful,12# but WITHOUT ANY WARRANTY; without even the implied warranty of13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the14# GNU General Public License for more details.15#16# You should have received a copy of the GNU General Public License17# along with this program. If not, see <http://www.gnu.org/licenses/>.18#19###############################################################################2021from __future__ import absolute_import2223import json, math24from . import sage_salvus2526from uuid import uuid4272829def uuid():30return str(uuid4())313233def json_float(t):34if t is None:35return t36t = float(t)37# Neither of nan or inf get JSON'd in a way that works properly, for some reason. I don't understand why.38if math.isnan(t) or math.isinf(t):39return None40else:41return t424344#######################################################45# Three.js based plotting46#######################################################4748import sage.plot.plot3d.index_face_set49import sage.plot.plot3d.shapes50import sage.plot.plot3d.base51import sage.plot.plot3d.shapes252from sage.structure.element import Element535455def jsonable(x):56if isinstance(x, Element):57return json_float(x)58elif isinstance(x, (list, tuple)):59return [jsonable(y) for y in x]60return x616263def graphics3d_to_jsonable(p):64obj_list = []6566def parse_obj(obj):67material_name = ''68faces = []69for item in obj.split("\n"):70tmp = str(item.strip())71if not tmp:72continue73k = tmp.split()74if k[0] == "usemtl": # material name75material_name = k[1]76elif k[0] == 'f': # face77v = [int(a) for a in k[1:]]78faces.append(v)79# other types are parse elsewhere in a different pass.8081return [{"material_name": material_name, "faces": faces}]8283def parse_texture(p):84texture_dict = []85textures = p.texture_set()86for item in range(0, len(textures)):87texture_pop = textures.pop()88string = str(texture_pop)89item = string.split("(")[1]90name = item.split(",")[0]91color = texture_pop.color92tmp_dict = {"name": name, "color": color}93texture_dict.append(tmp_dict)94return texture_dict9596def get_color(name, texture_set):97for item in range(0, len(texture_set)):98if (texture_set[item]["name"] == name):99color = texture_set[item]["color"]100color_list = [color[0], color[1], color[2]]101break102else:103color_list = []104return color_list105106def parse_mtl(p):107mtl = p.mtl_str()108all_material = []109for item in mtl.split("\n"):110if "newmtl" in item:111tmp = str(item.strip())112tmp_list = []113try:114texture_set = parse_texture(p)115color = get_color(name, texture_set)116except (ValueError, UnboundLocalError):117pass118try:119tmp_list = {120"name": name,121"ambient": ambient,122"specular": specular,123"diffuse": diffuse,124"illum": illum_list[0],125"shininess": shininess_list[0],126"opacity": opacity_diffuse[3],127"color": color128}129all_material.append(tmp_list)130except (ValueError, UnboundLocalError):131pass132133ambient = []134specular = []135diffuse = []136illum_list = []137shininess_list = []138opacity_diffuse = []139tmp_list = []140name = tmp.split()[1]141142if "Ka" in item:143tmp = str(item.strip())144for t in tmp.split():145try:146ambient.append(json_float(t))147except ValueError:148pass149150if "Ks" in item:151tmp = str(item.strip())152for t in tmp.split():153try:154specular.append(json_float(t))155except ValueError:156pass157158if "Kd" in item:159tmp = str(item.strip())160for t in tmp.split():161try:162diffuse.append(json_float(t))163except ValueError:164pass165166if "illum" in item:167tmp = str(item.strip())168for t in tmp.split():169try:170illum_list.append(json_float(t))171except ValueError:172pass173174if "Ns" in item:175tmp = str(item.strip())176for t in tmp.split():177try:178shininess_list.append(json_float(t))179except ValueError:180pass181182if "d" in item:183tmp = str(item.strip())184for t in tmp.split():185try:186opacity_diffuse.append(json_float(t))187except ValueError:188pass189190try:191color = list(p.all[0].texture.color.rgb())192except (ValueError, AttributeError):193pass194195try:196texture_set = parse_texture(p)197color = get_color(name, texture_set)198except (ValueError, AttributeError):199color = []200#pass201202tmp_list = {203"name": name,204"ambient": ambient,205"specular": specular,206"diffuse": diffuse,207"illum": illum_list[0],208"shininess": shininess_list[0],209"opacity": opacity_diffuse[3],210"color": color211}212all_material.append(tmp_list)213214return all_material215216#####################################217# Conversion functions218#####################################219220def convert_index_face_set(p, T, extra_kwds):221if T is not None:222p = p.transform(T=T)223face_geometry = parse_obj(p.obj())224if hasattr(p, 'has_local_colors') and p.has_local_colors():225convert_index_face_set_with_colors(p, T, extra_kwds)226return227material = parse_mtl(p)228vertex_geometry = []229obj = p.obj()230for item in obj.split("\n"):231if "v" in item:232tmp = str(item.strip())233for t in tmp.split():234try:235vertex_geometry.append(json_float(t))236except ValueError:237pass238myobj = {239"face_geometry": face_geometry,240"type": 'index_face_set',241"vertex_geometry": vertex_geometry,242"material": material,243"has_local_colors": 0244}245for e in ['wireframe', 'mesh']:246if p._extra_kwds is not None:247v = p._extra_kwds.get(e, None)248if v is not None:249myobj[e] = jsonable(v)250obj_list.append(myobj)251252def convert_index_face_set_with_colors(p, T, extra_kwds):253face_geometry = [{254"material_name":255p.texture.id,256"faces": [[int(v) + 1 for v in f[0]] + [f[1]]257for f in p.index_faces_with_colors()]258}]259material = parse_mtl(p)260vertex_geometry = [json_float(t) for v in p.vertices() for t in v]261myobj = {262"face_geometry": face_geometry,263"type": 'index_face_set',264"vertex_geometry": vertex_geometry,265"material": material,266"has_local_colors": 1267}268for e in ['wireframe', 'mesh']:269if p._extra_kwds is not None:270v = p._extra_kwds.get(e, None)271if v is not None:272myobj[e] = jsonable(v)273obj_list.append(myobj)274275def convert_text3d(p, T, extra_kwds):276obj_list.append({277"type":278"text",279"text":280p.string,281"pos": [0, 0, 0] if T is None else T([0, 0, 0]),282"color":283"#" + p.get_texture().hex_rgb(),284'fontface':285str(extra_kwds.get('fontface', 'Arial')),286'constant_size':287bool(extra_kwds.get('constant_size', True)),288'fontsize':289int(extra_kwds.get('fontsize', 12))290})291292def convert_line(p, T, extra_kwds):293obj_list.append({294"type":295"line",296"points":297jsonable(p.points if T is None else298[T.transform_point(point) for point in p.points]),299"thickness":300jsonable(p.thickness),301"color":302"#" + p.get_texture().hex_rgb(),303"arrow_head":304bool(p.arrow_head)305})306307def convert_point(p, T, extra_kwds):308obj_list.append({309"type": "point",310"loc": p.loc if T is None else T(p.loc),311"size": json_float(p.size),312"color": "#" + p.get_texture().hex_rgb()313})314315def convert_combination(p, T, extra_kwds):316for x in p.all:317handler(x)(x, T, p._extra_kwds)318319def convert_transform_group(p, T, extra_kwds):320if T is not None:321T = T * p.get_transformation()322else:323T = p.get_transformation()324for x in p.all:325handler(x)(x, T, p._extra_kwds)326327def nothing(p, T, extra_kwds):328pass329330def handler(p):331if isinstance(p, sage.plot.plot3d.index_face_set.IndexFaceSet):332return convert_index_face_set333elif isinstance(p, sage.plot.plot3d.shapes.Text):334return convert_text3d335elif isinstance(p, sage.plot.plot3d.base.TransformGroup):336return convert_transform_group337elif isinstance(p, sage.plot.plot3d.base.Graphics3dGroup):338return convert_combination339elif isinstance(p, sage.plot.plot3d.shapes2.Line):340return convert_line341elif isinstance(p, sage.plot.plot3d.shapes2.Point):342return convert_point343elif isinstance(p, sage.plot.plot3d.base.PrimitiveObject):344return convert_index_face_set345elif isinstance(p, sage.plot.plot3d.base.Graphics3d):346# this is an empty scene347return nothing348else:349raise NotImplementedError("unhandled type ", type(p))350351# start it going -- this modifies obj_list352handler(p)(p, None, None)353354# now obj_list is full of the objects355return obj_list356357358###359# Interactive 2d Graphics360###361362import os, matplotlib.figure363364365class InteractiveGraphics(object):366def __init__(self, g, **events):367self._g = g368self._events = events369370def figure(self, **kwds):371if isinstance(self._g, matplotlib.figure.Figure):372return self._g373374options = dict()375options.update(self._g.SHOW_OPTIONS)376options.update(self._g._extra_kwds)377options.update(kwds)378options.pop('dpi')379options.pop('transparent')380options.pop('fig_tight')381fig = self._g.matplotlib(**options)382383from matplotlib.backends.backend_agg import FigureCanvasAgg384canvas = FigureCanvasAgg(fig)385fig.set_canvas(canvas)386fig.tight_layout(387) # critical, since sage does this -- if not, coords all wrong388return fig389390def save(self, filename, **kwds):391if isinstance(self._g, matplotlib.figure.Figure):392self._g.savefig(filename)393else:394# When fig_tight=True (the default), the margins are very slightly different.395# I don't know how to properly account for this yet (or even if it is possible),396# since it only happens at figsize time -- do "a=plot(sin); a.save??".397# So for interactive graphics, we just set this to false no matter what.398kwds['fig_tight'] = False399self._g.save(filename, **kwds)400401def show(self, **kwds):402fig = self.figure(**kwds)403ax = fig.axes[0]404# upper left data coordinates405xmin, ymax = ax.transData.inverted().transform(406fig.transFigure.transform((0, 1)))407# lower right data coordinates408xmax, ymin = ax.transData.inverted().transform(409fig.transFigure.transform((1, 0)))410411id = '_a' + uuid().replace('-', '')412413def to_data_coords(p):414# 0<=x,y<=1415return ((xmax - xmin) * p[0] + xmin,416(ymax - ymin) * (1 - p[1]) + ymin)417418if kwds.get('svg', False):419filename = '%s.svg' % id420del kwds['svg']421else:422filename = '%s.png' % id423424fig.savefig(filename)425426def f(event, p):427self._events[event](to_data_coords(p))428429sage_salvus.salvus.namespace[id] = f430x = {}431for ev in list(self._events.keys()):432x[ev] = id433434sage_salvus.salvus.file(filename, show=True, events=x)435os.unlink(filename)436437def __del__(self):438for ev in self._events:439u = self._id + ev440if u in sage_salvus.salvus.namespace:441del sage_salvus.salvus.namespace[u]442443444###445# D3-based interactive 2d Graphics446###447448449###450# The following is a modified version of graph_plot_js.py from the Sage library, which was451# written by Nathann Cohen in 2013.452###453def graph_to_d3_jsonable(G,454vertex_labels=True,455edge_labels=False,456vertex_partition=[],457edge_partition=[],458force_spring_layout=False,459charge=-120,460link_distance=50,461link_strength=1,462gravity=.04,463vertex_size=7,464edge_thickness=2,465width=None,466height=None,467**ignored):468r"""469Display a graph in CoCalc using the D3 visualization library.470471INPUT:472473- ``G`` -- the graph474475- ``vertex_labels`` (boolean) -- Whether to display vertex labels (set to476``True`` by default).477478- ``edge_labels`` (boolean) -- Whether to display edge labels (set to479``False`` by default).480481- ``vertex_partition`` -- a list of lists representing a partition of the482vertex set. Vertices are then colored in the graph according to the483partition. Set to ``[]`` by default.484485- ``edge_partition`` -- same as ``vertex_partition``, with edges486instead. Set to ``[]`` by default.487488- ``force_spring_layout`` -- whether to take sage's position into account if489there is one (see :meth:`~sage.graphs.generic_graph.GenericGraph.` and490:meth:`~sage.graphs.generic_graph.GenericGraph.`), or to compute a spring491layout. Set to ``False`` by default.492493- ``vertex_size`` -- The size of a vertex' circle. Set to `7` by default.494495- ``edge_thickness`` -- Thickness of an edge. Set to ``2`` by default.496497- ``charge`` -- the vertices' charge. Defines how they repulse each498other. See `<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more499information. Set to ``-120`` by default.500501- ``link_distance`` -- See502`<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more503information. Set to ``30`` by default.504505- ``link_strength`` -- See506`<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more507information. Set to ``1.5`` by default.508509- ``gravity`` -- See510`<https://github.com/mbostock/d3/wiki/Force-Layout>`_ for more511information. Set to ``0.04`` by default.512513514EXAMPLES::515516show(graphs.RandomTree(50), d3=True)517518show(graphs.PetersenGraph(), d3=True, vertex_partition=g.coloring())519520show(graphs.DodecahedralGraph(), d3=True, force_spring_layout=True)521522show(graphs.DodecahedralGraph(), d3=True)523524g = digraphs.DeBruijn(2,2)525g.allow_multiple_edges(True)526g.add_edge("10","10","a")527g.add_edge("10","10","b")528g.add_edge("10","10","c")529g.add_edge("10","10","d")530g.add_edge("01","11","1")531show(g, d3=True, vertex_labels=True,edge_labels=True,532link_distance=200,gravity=.05,charge=-500,533edge_partition=[[("11","12","2"),("21","21","a")]],534edge_thickness=4)535536"""537directed = G.is_directed()538multiple_edges = G.has_multiple_edges()539540# Associated an integer to each vertex541v_to_id = {v: i for i, v in enumerate(G.vertices())}542543# Vertex colors544color = {i: len(vertex_partition) for i in range(G.order())}545for i, l in enumerate(vertex_partition):546for v in l:547color[v_to_id[v]] = i548549# Vertex list550nodes = []551for v in G.vertices():552nodes.append({"name": str(v), "group": str(color[v_to_id[v]])})553554# Edge colors.555edge_color_default = "#aaa"556from sage.plot.colors import rainbow557color_list = rainbow(len(edge_partition))558edge_color = {}559for i, l in enumerate(edge_partition):560for e in l:561u, v, label = e if len(e) == 3 else e + (None, )562edge_color[u, v, label] = color_list[i]563if not directed:564edge_color[v, u, label] = color_list[i]565566# Edge list567edges = []568seen = {} # How many times has this edge been seen ?569570for u, v, l in G.edges():571572# Edge color573color = edge_color.get((u, v, l), edge_color_default)574575# Computes the curve of the edge576curve = 0577578# Loop ?579if u == v:580seen[u, v] = seen.get((u, v), 0) + 1581curve = seen[u, v] * 10 + 10582583# For directed graphs, one also has to take into accounts584# edges in the opposite direction585elif directed:586if G.has_edge(v, u):587seen[u, v] = seen.get((u, v), 0) + 1588curve = seen[u, v] * 15589else:590if multiple_edges and len(G.edge_label(u, v)) != 1:591# Multiple edges. The first one has curve 15, then592# -15, then 30, then -30, ...593seen[u, v] = seen.get((u, v), 0) + 1594curve = (1 if seen[u, v] % 2 else -1) * (seen[u, v] //5952) * 15596597elif not directed and multiple_edges:598# Same formula as above for multiple edges599if len(G.edge_label(u, v)) != 1:600seen[u, v] = seen.get((u, v), 0) + 1601curve = (1 if seen[u, v] % 2 else -1) * (seen[u, v] // 2) * 15602603# Adding the edge to the list604edges.append({605"source": v_to_id[u],606"target": v_to_id[v],607"strength": 0,608"color": color,609"curve": curve,610"name": str(l) if edge_labels else ""611})612613loops = [e for e in edges if e["source"] == e["target"]]614edges = [e for e in edges if e["source"] != e["target"]]615616# Defines the vertices' layout if possible617Gpos = G.get_pos()618pos = []619if Gpos is not None and force_spring_layout is False:620charge = 0621link_strength = 0622gravity = 0623624for v in G.vertices():625x, y = Gpos[v]626pos.append([json_float(x), json_float(-y)])627628return {629"nodes": nodes,630"links": edges,631"loops": loops,632"pos": pos,633"directed": G.is_directed(),634"charge": int(charge),635"link_distance": int(link_distance),636"link_strength": int(link_strength),637"gravity": float(gravity),638"vertex_labels": bool(vertex_labels),639"edge_labels": bool(edge_labels),640"vertex_size": int(vertex_size),641"edge_thickness": int(edge_thickness),642"width": json_float(width),643"height": json_float(height)644}645646647