diff --git a/lmfdb/ecnf/isog_class.py b/lmfdb/ecnf/isog_class.py
index 291b75f1c5..22848307d2 100644
--- a/lmfdb/ecnf/isog_class.py
+++ b/lmfdb/ecnf/isog_class.py
@@ -1,6 +1,6 @@
from flask import url_for
from lmfdb import db
-from lmfdb.utils import encode_plot, names_and_urls, web_latex
+from lmfdb.utils import make_graph, setup_isogeny_graph, names_and_urls, web_latex
from lmfdb.logger import make_logger
from lmfdb.ecnf.WebEllipticCurve import web_ainvs, FIELD
from lmfdb.number_fields.web_number_field import field_pretty, nf_display_knowl
@@ -85,9 +85,19 @@ def make_class(self):
# Create isogeny graph:
self.graph = make_graph(self.isogeny_matrix)
- P = self.graph.plot(edge_labels=True)
- self.graph_img = encode_plot(P, transparent=True)
- self.graph_link = '' % self.graph_img
+ self.graph_data, self.graph_link, self.graph_layouts, self.graph_default_layout = setup_isogeny_graph(self.graph)
+ # Attach curve URLs and labels to nodes
+ for el in self.graph_data:
+ if el['group'] == 'nodes':
+ idx = int(el['data']['id']) - 1 # 1-indexed labels
+ if 0 <= idx < len(self.db_curves):
+ c = self.db_curves[idx]
+ el['data']['url'] = curve_url(c)
+ el['data']['label'] = c['iso_label'] + str(c['number'])
+ ts = c.get('torsion_structure', [])
+ el['data']['torsion'] = ' x '.join('Z/%dZ' % t for t in ts) if ts else 'Trivial'
+ if c.get('cm'):
+ el['data']['cm'] = str(c['cm'])
self.isogeny_matrix_str = latex(Matrix(self.isogeny_matrix))
self.field = FIELD(self.field_label)
@@ -152,10 +162,11 @@ def make_class(self):
self.friends += [('L-function not available', "")]
self.properties = [('Base field', self.field_name),
- ('Label', self.class_label)]
- if self.class_size > 1:
- self.properties.append((None, self.graph_link))
- self.properties.append(('Conductor', '%s' % self.conductor_label))
+ ('Label', self.class_label),
+ ('Number of curves', str(self.class_size)),
+ ('Graph', ''),
+ (None, self.graph_link),
+ ('Conductor', '%s' % self.conductor_label)]
if self.rk != '?':
self.properties += [('Rank', '%s' % self.rk)]
@@ -171,88 +182,6 @@ def make_class(self):
('isogeny class %s' % self.short_label, self.urls['class'])]
-def make_graph(M):
- """
- Code extracted from Sage's elliptic curve isogeny class (reshaped
- in the case maxdegree==12)
- """
- from sage.schemes.elliptic_curves.ell_curve_isogeny import fill_isogeny_matrix, unfill_isogeny_matrix
- from sage.graphs.graph import Graph
- n = M.nrows() # = M.ncols()
- G = Graph(unfill_isogeny_matrix(M), format='weighted_adjacency_matrix')
- MM = fill_isogeny_matrix(M)
- # The maximum degree classifies the shape of the isogeny
- # graph, though the number of vertices is often enough.
- # This only holds over Q, so this code will need to change
- # once other isogeny classes are implemented.
- if n == 1:
- # one vertex
- pass
- elif n == 2:
- # one edge, two vertices. We align horizontally and put
- # the lower number on the left vertex.
- G.set_pos(pos={0: [-0.5, 0], 1: [0.5, 0]})
- else:
- maxdegree = max(max(MM))
- if n == 3:
- # o--o--o
- centervert = [i for i in range(3) if max(MM.row(i)) < maxdegree][0]
- other = [i for i in range(3) if i != centervert]
- G.set_pos(pos={centervert: [0, 0], other[0]: [-1, 0], other[1]: [1, 0]})
- elif maxdegree == 4:
- # o--o<8
- centervert = [i for i in range(4) if max(MM.row(i)) < maxdegree][0]
- other = [i for i in range(4) if i != centervert]
- G.set_pos(pos={centervert: [0, 0], other[0]: [0, 1], other[1]: [-0.8660254, -0.5], other[2]: [0.8660254, -0.5]})
- elif maxdegree == 27 and n == 4:
- # o--o--o--o
- centers = [i for i in range(4) if list(MM.row(i)).count(3) == 2]
- left = [j for j in range(4) if MM[centers[0], j] == 3 and j not in centers][0]
- right = [j for j in range(4) if MM[centers[1], j] == 3 and j not in centers][0]
- G.set_pos(pos={left: [-1.5, 0], centers[0]: [-0.5, 0], centers[1]: [0.5, 0], right: [1.5, 0]})
- elif n == 4:
- # square
- opp = [i for i in range(1, 4) if not MM[0, i].is_prime()][0]
- other = [i for i in range(1, 4) if i != opp]
- G.set_pos(pos={0: [1, 1], other[0]: [-1, 1], opp: [-1, -1], other[1]: [1, -1]})
- elif maxdegree == 8:
- # 8>o--o<8
- centers = [i for i in range(6) if list(MM.row(i)).count(2) == 3]
- left = [j for j in range(6) if MM[centers[0], j] == 2 and j not in centers]
- right = [j for j in range(6) if MM[centers[1], j] == 2 and j not in centers]
- G.set_pos(pos={centers[0]: [-0.5, 0], left[0]: [-1, 0.8660254], left[1]: [-1, -0.8660254], centers[1]: [0.5, 0], right[0]: [1, 0.8660254], right[1]: [1, -0.8660254]})
- elif maxdegree == 18 and n == 6:
- # two squares joined on an edge
- centers = [i for i in range(6) if list(MM.row(i)).count(3) == 2]
- top = [j for j in range(6) if MM[centers[0], j] == 3]
- bl = [j for j in range(6) if MM[top[0], j] == 2][0]
- br = [j for j in range(6) if MM[top[1], j] == 2][0]
- G.set_pos(pos={centers[0]: [0, 0.5], centers[1]: [0, -0.5], top[0]: [-1, 0.5], top[1]: [1, 0.5], bl: [-1, -0.5], br: [1, -0.5]})
- elif maxdegree == 16 and n == 8:
- # tree from bottom, 3 regular except for the leaves.
- centers = [i for i in range(8) if list(MM.row(i)).count(2) == 3]
- center = [i for i in centers if len([j for j in centers if MM[i, j] == 2]) == 2][0]
- centers.remove(center)
- bottom = [j for j in range(8) if MM[center, j] == 2 and j not in centers][0]
- left = [j for j in range(8) if MM[centers[0], j] == 2 and j != center]
- right = [j for j in range(8) if MM[centers[1], j] == 2 and j != center]
- G.set_pos(pos={center: [0, 0], bottom: [0, -1], centers[0]: [-0.8660254, 0.5], centers[1]: [0.8660254, 0.5], left[0]: [-0.8660254, 1.5], right[0]: [0.8660254, 1.5], left[1]: [-1.7320508, 0], right[1]: [1.7320508, 0]})
- elif maxdegree == 12:
- # tent
- centers = [i for i in range(8) if list(MM.row(i)).count(2) == 3]
- left = [j for j in range(8) if MM[centers[0], j] == 2]
- right = []
- for i in range(3):
- right.append([j for j in range(8) if MM[centers[1], j] == 2 and MM[left[i], j] == 3][0])
- G.set_pos(pos={centers[0]: [-0.3, 0], centers[1]: [0.3, 0],
- left[0]: [-0.14, 0.15], right[0]: [0.14, 0.15],
- left[1]: [-0.14, -0.15], right[1]: [0.14, -0.15],
- left[2]: [-0.14, -0.3], right[2]: [0.14, -0.3]})
-
- G.relabel(list(range(1, n + 1)))
- return G
-
-
def make_iso_matrix(clist): # clist is a list of ECNFs
Elist = [E.E for E in clist]
cl = Elist[0].isogeny_class()
diff --git a/lmfdb/ecnf/templates/ecnf-isoclass.html b/lmfdb/ecnf/templates/ecnf-isoclass.html
index 3c634bd423..35e40e3b6f 100644
--- a/lmfdb/ecnf/templates/ecnf-isoclass.html
+++ b/lmfdb/ecnf/templates/ecnf-isoclass.html
@@ -66,9 +66,13 @@
+
+
+{% set graph_layouts = cl.graph_layouts %}
+{% include "cytoscape_scripts.html" %}
+
{% else %}
Not available.
{% endif %}
diff --git a/lmfdb/elliptic_curves/isog_class.py b/lmfdb/elliptic_curves/isog_class.py
index a7d08dd61b..c7365c4852 100644
--- a/lmfdb/elliptic_curves/isog_class.py
+++ b/lmfdb/elliptic_curves/isog_class.py
@@ -1,5 +1,5 @@
from flask import url_for
-from lmfdb.utils import encode_plot, prop_int_pretty, raw_typeset, integer_squarefree_part, list_to_factored_poly_otherorder
+from lmfdb.utils import make_graph, setup_isogeny_graph, prop_int_pretty, raw_typeset, integer_squarefree_part, list_to_factored_poly_otherorder
from lmfdb.elliptic_curves import ec_logger
from lmfdb.elliptic_curves.web_ec import split_lmfdb_label, split_cremona_label, OPTIMALITY_BOUND, CREMONA_BOUND
from lmfdb.number_fields.web_number_field import field_pretty
@@ -127,9 +127,20 @@ def perm(i): return next(c for c in self.curves if c['Cnumber'] == i+1)['lmfdb_n
# Create isogeny graph with appropriate vertex labels:
self.graph = make_graph(M, [c['short_label'] for c in self.curves])
- P = self.graph.plot(edge_labels=True, vertex_size=1000)
- self.graph_img = encode_plot(P, transparent=True)
- self.graph_link = '' % self.graph_img
+ self.graph_data, self.graph_link, self.graph_layouts, self.graph_default_layout = setup_isogeny_graph(self.graph)
+ # Attach curve metadata to nodes for tooltip display
+ curve_by_label = {c['short_label']: c for c in self.curves}
+ for el in self.graph_data:
+ if el['group'] == 'nodes':
+ label = el['data']['label']
+ c = curve_by_label.get(label)
+ if c:
+ el['data']['url'] = c['curve_url_lmfdb']
+ el['data']['torsion'] = ' x '.join('Z/%dZ' % t for t in c['torsion_structure']) if c['torsion_structure'] else 'Trivial'
+ el['data']['degree'] = c['degree']
+ el['data']['faltings_height'] = str(c['FH'])
+ el['data']['optimal'] = bool(c.get('optimal'))
+ el['data']['j_inv'] = str(c['j_inv'])
self.newform = raw_typeset(PowerSeriesRing(QQ, 'q')(classdata['anlist'], 20, check=True))
self.newform_label = ".".join([str(self.conductor), str(2), 'a', self.iso_label])
@@ -170,8 +181,7 @@ def perm(i): return next(c for c in self.curves if c['Cnumber'] == i+1)['lmfdb_n
('CM', '%s' % self.CMfield),
('Rank', prop_int_pretty(self.rank))
]
- if ncurves > 1:
- self.properties += [('Graph', ''),(None, self.graph_link)]
+ self.properties += [('Graph', ''), (None, self.graph_link)]
self.downloads = [('q-expansion to text', url_for(".download_EC_qexp", label=self.lmfdb_iso, limit=1000)),
('All stored data to text', url_for(".download_EC_all", label=self.lmfdb_iso)),
@@ -207,86 +217,3 @@ def perm(i): return next(c for c in self.curves if c['Cnumber'] == i+1)['lmfdb_n
else:
self.has_lfunction = False
-def make_graph(M, vertex_labels=None):
- """
- Code extracted from Sage's elliptic curve isogeny class (reshaped
- in the case maxdegree==12)
- """
- from sage.schemes.elliptic_curves.ell_curve_isogeny import fill_isogeny_matrix, unfill_isogeny_matrix
- from sage.graphs.graph import Graph
- n = M.nrows() # = M.ncols()
- G = Graph(unfill_isogeny_matrix(M), format='weighted_adjacency_matrix')
- MM = fill_isogeny_matrix(M)
- # The maximum degree classifies the shape of the isogeny
- # graph, though the number of vertices is often enough.
- # This only holds over Q, so this code will need to change
- # once other isogeny classes are implemented.
- if n == 1:
- # one vertex
- pass
- elif n == 2:
- # one edge, two vertices. We align horizontally and put
- # the lower number on the left vertex.
- G.set_pos(pos={0:[-0.5,0],1:[0.5,0]})
- else:
- maxdegree = max(max(MM))
- if n == 3:
- # o--o--o
- centervert = [i for i in range(3) if max(MM.row(i)) < maxdegree][0]
- other = [i for i in range(3) if i != centervert]
- G.set_pos(pos={centervert:[0,0],other[0]:[-1,0],other[1]:[1,0]})
- elif maxdegree == 4:
- # o--o<8
- centervert = [i for i in range(4) if max(MM.row(i)) < maxdegree][0]
- other = [i for i in range(4) if i != centervert]
- G.set_pos(pos={centervert:[0,0],other[0]:[0,1],other[1]:[-0.8660254,-0.5],other[2]:[0.8660254,-0.5]})
- elif maxdegree == 27:
- # o--o--o--o
- centers = [i for i in range(4) if list(MM.row(i)).count(3) == 2]
- left = [j for j in range(4) if MM[centers[0],j] == 3 and j not in centers][0]
- right = [j for j in range(4) if MM[centers[1],j] == 3 and j not in centers][0]
- G.set_pos(pos={left:[-1.5,0],centers[0]:[-0.5,0],centers[1]:[0.5,0],right:[1.5,0]})
- elif n == 4:
- # square
- opp = [i for i in range(1,4) if not MM[0,i].is_prime()][0]
- other = [i for i in range(1,4) if i != opp]
- G.set_pos(pos={0:[1,1],other[0]:[-1,1],opp:[-1,-1],other[1]:[1,-1]})
- elif maxdegree == 8:
- # 8>o--o<8
- centers = [i for i in range(6) if list(MM.row(i)).count(2) == 3]
- left = [j for j in range(6) if MM[centers[0],j] == 2 and j not in centers]
- right = [j for j in range(6) if MM[centers[1],j] == 2 and j not in centers]
- G.set_pos(pos={centers[0]:[-0.5,0],left[0]:[-1,0.8660254],left[1]:[-1,-0.8660254],centers[1]:[0.5,0],right[0]:[1,0.8660254],right[1]:[1,-0.8660254]})
- elif maxdegree == 18:
- # two squares joined on an edge
- centers = [i for i in range(6) if list(MM.row(i)).count(3) == 2]
- top = [j for j in range(6) if MM[centers[0],j] == 3]
- bl = [j for j in range(6) if MM[top[0],j] == 2][0]
- br = [j for j in range(6) if MM[top[1],j] == 2][0]
- G.set_pos(pos={centers[0]:[0,0.5],centers[1]:[0,-0.5],top[0]:[-1,0.5],top[1]:[1,0.5],bl:[-1,-0.5],br:[1,-0.5]})
- elif maxdegree == 16:
- # tree from bottom, 3 regular except for the leaves.
- centers = [i for i in range(8) if list(MM.row(i)).count(2) == 3]
- center = [i for i in centers if len([j for j in centers if MM[i,j] == 2]) == 2][0]
- centers.remove(center)
- bottom = [j for j in range(8) if MM[center,j] == 2 and j not in centers][0]
- left = [j for j in range(8) if MM[centers[0],j] == 2 and j != center]
- right = [j for j in range(8) if MM[centers[1],j] == 2 and j != center]
- G.set_pos(pos={center:[0,0],bottom:[0,-1],centers[0]:[-0.8660254,0.5],centers[1]:[0.8660254,0.5],left[0]:[-0.8660254,1.5],right[0]:[0.8660254,1.5],left[1]:[-1.7320508,0],right[1]:[1.7320508,0]})
- elif maxdegree == 12:
- # tent
- centers = [i for i in range(8) if list(MM.row(i)).count(2) == 3]
- left = [j for j in range(8) if MM[centers[0],j] == 2]
- right = []
- for i in range(3):
- right.append([j for j in range(8) if MM[centers[1],j] == 2 and MM[left[i],j] == 3][0])
- G.set_pos(pos={centers[0]:[-0.3,0],centers[1]:[0.3,0],
- left[0]:[-0.14,0.15], right[0]:[0.14,0.15],
- left[1]:[-0.14,-0.15],right[1]:[0.14,-0.15],
- left[2]:[-0.14,-0.3],right[2]:[0.14,-0.3]})
-
- if vertex_labels:
- G.relabel(vertex_labels)
- else:
- G.relabel(list(range(1, n + 1)))
- return G
diff --git a/lmfdb/elliptic_curves/templates/ec-isoclass.html b/lmfdb/elliptic_curves/templates/ec-isoclass.html
index 7665e75fe0..5a28c4d049 100644
--- a/lmfdb/elliptic_curves/templates/ec-isoclass.html
+++ b/lmfdb/elliptic_curves/templates/ec-isoclass.html
@@ -107,7 +107,7 @@
});
-{% if info.class_size>1 %}
+{% if info.class_size > 1 %}