diff --git a/src/_igraph/graphobject.c b/src/_igraph/graphobject.c index bacd006d6..6393de1c1 100644 --- a/src/_igraph/graphobject.c +++ b/src/_igraph/graphobject.c @@ -8188,14 +8188,15 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * return NULL; } - if (dim == 2) + if (dim == 2) { ret = igraph_layout_kamada_kawai (&self->g, &m, use_seed, maxiter, epsilon, kkconst, weights, /*bounds*/ minx, maxx, miny, maxy); - else + } else { ret = igraph_layout_kamada_kawai_3d (&self->g, &m, use_seed, maxiter, epsilon, kkconst, weights, /*bounds*/ minx, maxx, miny, maxy, minz, maxz); + } DESTROY_VECTORS; @@ -8207,6 +8208,19 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject * return NULL; } + /* Align layout, but only if no bounding box was specified. */ + if (minx == NULL && maxx == NULL && + miny == NULL && maxy == NULL && + minz == NULL && maxz == NULL && + igraph_vcount(&self->g) <= 1000) { + ret = igraph_layout_align(&self->g, &m); + if (ret) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8298,6 +8312,16 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel return NULL; } + /* Align layout */ + if (igraph_vcount(&self->g)) { + retval = igraph_layout_align(&self->g, &m); + if (retval) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8520,6 +8544,19 @@ PyObject return NULL; } + /* Align layout, but only if no bounding box was specified. */ + if (minx == NULL && maxx == NULL && + miny == NULL && maxy == NULL && + minz == NULL && maxz == NULL && + igraph_vcount(&self->g) <= 1000) { + ret = igraph_layout_align(&self->g, &m); + if (ret) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + #undef DESTROY_VECTORS result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); @@ -8574,6 +8611,15 @@ PyObject *igraphmodule_Graph_layout_graphopt(igraphmodule_GraphObject *self, return NULL; } + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8632,6 +8678,15 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self, return NULL; } + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; @@ -8697,6 +8752,15 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self, igraph_matrix_destroy(dist); free(dist); } + /* Align layout */ + if (igraph_vcount(&self->g) <= 1000) { + if (igraph_layout_align(&self->g, &m)) { + igraph_matrix_destroy(&m); + igraphmodule_handle_igraph_error(); + return NULL; + } + } + result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT); igraph_matrix_destroy(&m); return (PyObject *) result_o; diff --git a/src/_igraph/igraphmodule.c b/src/_igraph/igraphmodule.c index 4bac0f081..0af15fed4 100644 --- a/src/_igraph/igraphmodule.c +++ b/src/_igraph/igraphmodule.c @@ -227,6 +227,38 @@ PyObject* igraphmodule_set_status_handler(PyObject* self, PyObject* o) { Py_RETURN_NONE; } +PyObject* igraphmodule_align_layout(PyObject* self, PyObject* args, PyObject* kwds) { + static char* kwlist[] = {"graph", "layout", NULL}; + PyObject *graph_o, *layout_o; + PyObject *res; + igraph_t *graph; + igraph_matrix_t layout; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &graph_o, &layout_o)) { + return NULL; + } + + if (igraphmodule_PyObject_to_igraph_t(graph_o, &graph)) { + return NULL; + } + + if (igraphmodule_PyObject_to_matrix_t(layout_o, &layout, "layout")) { + return NULL; + } + + if (igraph_layout_align(graph, &layout)) { + igraphmodule_handle_igraph_error(); + igraph_matrix_destroy(&layout); + return NULL; + } + + res = igraphmodule_matrix_t_to_PyList(&layout, IGRAPHMODULE_TYPE_FLOAT); + + igraph_matrix_destroy(&layout); + + return res; +} + PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwds) { static char* kwlist[] = {"vs", "coords", NULL}; PyObject *vs, *o, *o1 = 0, *o2 = 0, *o1_float, *o2_float, *coords = Py_False; @@ -790,6 +822,10 @@ static PyMethodDef igraphmodule_methods[] = METH_VARARGS | METH_KEYWORDS, "_power_law_fit(data, xmin=-1, force_continuous=False, p_precision=0.01)\n--\n\n" }, + {"_align_layout", (PyCFunction)igraphmodule_align_layout, + METH_VARARGS | METH_KEYWORDS, + "_align_layout(graph, layout)\n--\n\n" + }, {"convex_hull", (PyCFunction)igraphmodule_convex_hull, METH_VARARGS | METH_KEYWORDS, "convex_hull(vs, coords=False)\n--\n\n" diff --git a/src/igraph/__init__.py b/src/igraph/__init__.py index 16f41d6ed..65399e8ed 100644 --- a/src/igraph/__init__.py +++ b/src/igraph/__init__.py @@ -227,6 +227,7 @@ from igraph.io.images import _write_graph_to_svg from igraph.layout import ( Layout, + align_layout, _layout, _layout_auto, _layout_sugiyama, diff --git a/src/igraph/layout.py b/src/igraph/layout.py index f5b0a9181..712323f7a 100644 --- a/src/igraph/layout.py +++ b/src/igraph/layout.py @@ -16,6 +16,7 @@ __all__ = ( "Layout", + "align_layout", "_layout", "_layout_auto", "_layout_sugiyama", @@ -534,6 +535,27 @@ def _layout(graph, layout=None, *args, **kwds): return layout +def align_layout(graph, layout): + """Aligns a graph layout with the coordinate axes + + This function centers a vertex layout on the coordinate system origin and + rotates the layout to achieve a visually pleasing alignment with the coordinate + axes. Doing this is particularly useful with force-directed layouts such as + L{Graph.layout_fruchterman_reingold}. Layouts in arbitrary dimensional spaces + are supported. + + @param graph: the graph that the layout is associated with. + @param layout: the L{Layout} object containing the vertex coordinates + to align. + @return: a new aligned L{Layout} object. + """ + from igraph._igraph import _align_layout + + if not isinstance(layout, Layout): + layout = Layout(layout) + + return Layout(_align_layout(graph, layout.coords)) + def _layout_auto(graph, *args, **kwds): """Chooses and runs a suitable layout function based on simple topological properties of the graph. diff --git a/tests/drawing/cairo/baseline_images/clustering_directed.png b/tests/drawing/cairo/baseline_images/clustering_directed.png index 712f1b95f..282f3fb21 100644 Binary files a/tests/drawing/cairo/baseline_images/clustering_directed.png and b/tests/drawing/cairo/baseline_images/clustering_directed.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_basic.png b/tests/drawing/cairo/baseline_images/graph_basic.png index 2eee93d86..cfd394e49 100644 Binary files a/tests/drawing/cairo/baseline_images/graph_basic.png and b/tests/drawing/cairo/baseline_images/graph_basic.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_directed.png b/tests/drawing/cairo/baseline_images/graph_directed.png index 83d6e3764..b94e82169 100644 Binary files a/tests/drawing/cairo/baseline_images/graph_directed.png and b/tests/drawing/cairo/baseline_images/graph_directed.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png b/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png index 83d6e3764..b94e82169 100644 Binary files a/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png and b/tests/drawing/cairo/baseline_images/graph_mark_groups_directed.png differ diff --git a/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png b/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png index c5372745c..0e437f288 100644 Binary files a/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png and b/tests/drawing/cairo/baseline_images/graph_mark_groups_squares_directed.png differ diff --git a/tests/test_layouts.py b/tests/test_layouts.py index ac32e0210..67dad6ca9 100644 --- a/tests/test_layouts.py +++ b/tests/test_layouts.py @@ -1,6 +1,6 @@ import unittest from math import hypot -from igraph import Graph, Layout, BoundingBox, InternalError +from igraph import Graph, Layout, BoundingBox, InternalError, align_layout from igraph import umap_compute_weights @@ -452,6 +452,13 @@ def testDRL(self): lo = g.layout("drl") self.assertTrue(isinstance(lo, Layout)) + def testAlign(self): + g = Graph.Ring(3, circular=False) + lo = Layout([[1,1], [2,2], [3,3]]) + lo = align_layout(g, lo) + self.assertTrue(isinstance(lo, Layout)) + self.assertTrue(all(abs(lo[i][1]) < 1e-10 for i in range(3))) + def suite(): layout_suite = unittest.defaultTestLoader.loadTestsFromTestCase(LayoutTests)