Skip to content

Commit 9a3e610

Browse files
authored
Merge pull request #841 from igraph/feat/align-layout
2 parents 99d5d36 + 183e6a4 commit 9a3e610

File tree

10 files changed

+133
-3
lines changed

10 files changed

+133
-3
lines changed

src/_igraph/graphobject.c

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8188,14 +8188,15 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject *
81888188
return NULL;
81898189
}
81908190

8191-
if (dim == 2)
8191+
if (dim == 2) {
81928192
ret = igraph_layout_kamada_kawai
81938193
(&self->g, &m, use_seed, maxiter, epsilon, kkconst,
81948194
weights, /*bounds*/ minx, maxx, miny, maxy);
8195-
else
8195+
} else {
81968196
ret = igraph_layout_kamada_kawai_3d
81978197
(&self->g, &m, use_seed, maxiter, epsilon, kkconst,
81988198
weights, /*bounds*/ minx, maxx, miny, maxy, minz, maxz);
8199+
}
81998200

82008201
DESTROY_VECTORS;
82018202

@@ -8207,6 +8208,19 @@ PyObject *igraphmodule_Graph_layout_kamada_kawai(igraphmodule_GraphObject *
82078208
return NULL;
82088209
}
82098210

8211+
/* Align layout, but only if no bounding box was specified. */
8212+
if (minx == NULL && maxx == NULL &&
8213+
miny == NULL && maxy == NULL &&
8214+
minz == NULL && maxz == NULL &&
8215+
igraph_vcount(&self->g) <= 1000) {
8216+
ret = igraph_layout_align(&self->g, &m);
8217+
if (ret) {
8218+
igraph_matrix_destroy(&m);
8219+
igraphmodule_handle_igraph_error();
8220+
return NULL;
8221+
}
8222+
}
8223+
82108224
result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT);
82118225
igraph_matrix_destroy(&m);
82128226
return (PyObject *) result_o;
@@ -8298,6 +8312,16 @@ PyObject* igraphmodule_Graph_layout_davidson_harel(igraphmodule_GraphObject *sel
82988312
return NULL;
82998313
}
83008314

8315+
/* Align layout */
8316+
if (igraph_vcount(&self->g)) {
8317+
retval = igraph_layout_align(&self->g, &m);
8318+
if (retval) {
8319+
igraph_matrix_destroy(&m);
8320+
igraphmodule_handle_igraph_error();
8321+
return NULL;
8322+
}
8323+
}
8324+
83018325
result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT);
83028326
igraph_matrix_destroy(&m);
83038327
return (PyObject *) result_o;
@@ -8520,6 +8544,19 @@ PyObject
85208544
return NULL;
85218545
}
85228546

8547+
/* Align layout, but only if no bounding box was specified. */
8548+
if (minx == NULL && maxx == NULL &&
8549+
miny == NULL && maxy == NULL &&
8550+
minz == NULL && maxz == NULL &&
8551+
igraph_vcount(&self->g) <= 1000) {
8552+
ret = igraph_layout_align(&self->g, &m);
8553+
if (ret) {
8554+
igraph_matrix_destroy(&m);
8555+
igraphmodule_handle_igraph_error();
8556+
return NULL;
8557+
}
8558+
}
8559+
85238560
#undef DESTROY_VECTORS
85248561

85258562
result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT);
@@ -8574,6 +8611,15 @@ PyObject *igraphmodule_Graph_layout_graphopt(igraphmodule_GraphObject *self,
85748611
return NULL;
85758612
}
85768613

8614+
/* Align layout */
8615+
if (igraph_vcount(&self->g) <= 1000) {
8616+
if (igraph_layout_align(&self->g, &m)) {
8617+
igraph_matrix_destroy(&m);
8618+
igraphmodule_handle_igraph_error();
8619+
return NULL;
8620+
}
8621+
}
8622+
85778623
result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT);
85788624
igraph_matrix_destroy(&m);
85798625
return (PyObject *) result_o;
@@ -8632,6 +8678,15 @@ PyObject *igraphmodule_Graph_layout_lgl(igraphmodule_GraphObject * self,
86328678
return NULL;
86338679
}
86348680

8681+
/* Align layout */
8682+
if (igraph_vcount(&self->g) <= 1000) {
8683+
if (igraph_layout_align(&self->g, &m)) {
8684+
igraph_matrix_destroy(&m);
8685+
igraphmodule_handle_igraph_error();
8686+
return NULL;
8687+
}
8688+
}
8689+
86358690
result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT);
86368691
igraph_matrix_destroy(&m);
86378692
return (PyObject *) result_o;
@@ -8697,6 +8752,15 @@ PyObject *igraphmodule_Graph_layout_mds(igraphmodule_GraphObject * self,
86978752
igraph_matrix_destroy(dist); free(dist);
86988753
}
86998754

8755+
/* Align layout */
8756+
if (igraph_vcount(&self->g) <= 1000) {
8757+
if (igraph_layout_align(&self->g, &m)) {
8758+
igraph_matrix_destroy(&m);
8759+
igraphmodule_handle_igraph_error();
8760+
return NULL;
8761+
}
8762+
}
8763+
87008764
result_o = igraphmodule_matrix_t_to_PyList(&m, IGRAPHMODULE_TYPE_FLOAT);
87018765
igraph_matrix_destroy(&m);
87028766
return (PyObject *) result_o;

src/_igraph/igraphmodule.c

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,38 @@ PyObject* igraphmodule_set_status_handler(PyObject* self, PyObject* o) {
227227
Py_RETURN_NONE;
228228
}
229229

230+
PyObject* igraphmodule_align_layout(PyObject* self, PyObject* args, PyObject* kwds) {
231+
static char* kwlist[] = {"graph", "layout", NULL};
232+
PyObject *graph_o, *layout_o;
233+
PyObject *res;
234+
igraph_t *graph;
235+
igraph_matrix_t layout;
236+
237+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO", kwlist, &graph_o, &layout_o)) {
238+
return NULL;
239+
}
240+
241+
if (igraphmodule_PyObject_to_igraph_t(graph_o, &graph)) {
242+
return NULL;
243+
}
244+
245+
if (igraphmodule_PyObject_to_matrix_t(layout_o, &layout, "layout")) {
246+
return NULL;
247+
}
248+
249+
if (igraph_layout_align(graph, &layout)) {
250+
igraphmodule_handle_igraph_error();
251+
igraph_matrix_destroy(&layout);
252+
return NULL;
253+
}
254+
255+
res = igraphmodule_matrix_t_to_PyList(&layout, IGRAPHMODULE_TYPE_FLOAT);
256+
257+
igraph_matrix_destroy(&layout);
258+
259+
return res;
260+
}
261+
230262
PyObject* igraphmodule_convex_hull(PyObject* self, PyObject* args, PyObject* kwds) {
231263
static char* kwlist[] = {"vs", "coords", NULL};
232264
PyObject *vs, *o, *o1 = 0, *o2 = 0, *o1_float, *o2_float, *coords = Py_False;
@@ -790,6 +822,10 @@ static PyMethodDef igraphmodule_methods[] =
790822
METH_VARARGS | METH_KEYWORDS,
791823
"_power_law_fit(data, xmin=-1, force_continuous=False, p_precision=0.01)\n--\n\n"
792824
},
825+
{"_align_layout", (PyCFunction)igraphmodule_align_layout,
826+
METH_VARARGS | METH_KEYWORDS,
827+
"_align_layout(graph, layout)\n--\n\n"
828+
},
793829
{"convex_hull", (PyCFunction)igraphmodule_convex_hull,
794830
METH_VARARGS | METH_KEYWORDS,
795831
"convex_hull(vs, coords=False)\n--\n\n"

src/igraph/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@
227227
from igraph.io.images import _write_graph_to_svg
228228
from igraph.layout import (
229229
Layout,
230+
align_layout,
230231
_layout,
231232
_layout_auto,
232233
_layout_sugiyama,

src/igraph/layout.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
__all__ = (
1818
"Layout",
19+
"align_layout",
1920
"_layout",
2021
"_layout_auto",
2122
"_layout_sugiyama",
@@ -534,6 +535,27 @@ def _layout(graph, layout=None, *args, **kwds):
534535
return layout
535536

536537

538+
def align_layout(graph, layout):
539+
"""Aligns a graph layout with the coordinate axes
540+
541+
This function centers a vertex layout on the coordinate system origin and
542+
rotates the layout to achieve a visually pleasing alignment with the coordinate
543+
axes. Doing this is particularly useful with force-directed layouts such as
544+
L{Graph.layout_fruchterman_reingold}. Layouts in arbitrary dimensional spaces
545+
are supported.
546+
547+
@param graph: the graph that the layout is associated with.
548+
@param layout: the L{Layout} object containing the vertex coordinates
549+
to align.
550+
@return: a new aligned L{Layout} object.
551+
"""
552+
from igraph._igraph import _align_layout
553+
554+
if not isinstance(layout, Layout):
555+
layout = Layout(layout)
556+
557+
return Layout(_align_layout(graph, layout.coords))
558+
537559
def _layout_auto(graph, *args, **kwds):
538560
"""Chooses and runs a suitable layout function based on simple
539561
topological properties of the graph.
877 Bytes
Loading
660 Bytes
Loading
667 Bytes
Loading
667 Bytes
Loading
975 Bytes
Loading

tests/test_layouts.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22
from math import hypot
3-
from igraph import Graph, Layout, BoundingBox, InternalError
3+
from igraph import Graph, Layout, BoundingBox, InternalError, align_layout
44
from igraph import umap_compute_weights
55

66

@@ -452,6 +452,13 @@ def testDRL(self):
452452
lo = g.layout("drl")
453453
self.assertTrue(isinstance(lo, Layout))
454454

455+
def testAlign(self):
456+
g = Graph.Ring(3, circular=False)
457+
lo = Layout([[1,1], [2,2], [3,3]])
458+
lo = align_layout(g, lo)
459+
self.assertTrue(isinstance(lo, Layout))
460+
self.assertTrue(all(abs(lo[i][1]) < 1e-10 for i in range(3)))
461+
455462

456463
def suite():
457464
layout_suite = unittest.defaultTestLoader.loadTestsFromTestCase(LayoutTests)

0 commit comments

Comments
 (0)