diff --git a/include/common/graph_attributes.h b/include/common/graph_attributes.h index 6494cde..ec0bf03 100644 --- a/include/common/graph_attributes.h +++ b/include/common/graph_attributes.h @@ -18,6 +18,8 @@ namespace graph_layout { constexpr std::string_view ATTR_KEY_HEAD_LABEL = "headlabel"; constexpr std::string_view ATTR_KEY_LABEL_DISTANCE = "labeldistance"; constexpr std::string_view ATTR_KEY_SPLINES = "splines"; + constexpr std::string_view ATTR_KEY_ARROW_HEAD = "arrowhead"; + constexpr std::string_view ATTR_KEY_ARROW_TAIL = "arrowtail"; class Attribute { public: @@ -69,6 +71,15 @@ namespace graph_layout { static const std::string SPLINE; }; + class AttributeArrowShape : public Attribute { + public: + using Attribute::Attribute; + + static const std::string NONE; + static const std::string NORMAL; + static const std::string EMPTY; + }; + class Attributes { public: Attributes() = default; @@ -103,6 +114,11 @@ namespace graph_layout { void setEdgeHeadLabel(int edgeId, const std::string& label); void setEdgeLabelDistance(int edgeId, double scale); + void setEdgeDefaultArrowHead(const std::string& value); + void setEdgeDefaultArrowTail(const std::string& value); + void setEdgeArrowHead(int edgeId, const std::string& value); + void setEdgeArrowTail(int edgeId, const std::string& value); + private: static const std::string MONOSPACE_FONT_FAMILY; diff --git a/pyproject.toml b/pyproject.toml index a459273..c834540 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "sp-graph-layout" -version = "1.2.0" +version = "1.3.0" description = "A library for visualizing directed graphs." requires-python = ">=3.8" readme = "python/README.md" diff --git a/python/bindings.cpp b/python/bindings.cpp index 5252e54..b2a5163 100644 --- a/python/bindings.cpp +++ b/python/bindings.cpp @@ -59,6 +59,12 @@ PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) { .def_property_readonly_static("RECT", [](py::object) { return AttributeShape::RECT; }) .def_property_readonly_static("RECORD", [](py::object) { return AttributeShape::RECORD; }) ; + py::class_(m, "AttributeArrowShape") + .def(py::init<>()) + .def_property_readonly_static("NONE", [](py::object) { return AttributeArrowShape::NONE; }) + .def_property_readonly_static("NORMAL", [](py::object) { return AttributeArrowShape::NORMAL; }) + .def_property_readonly_static("EMPTY", [](py::object) { return AttributeArrowShape::EMPTY; }) + ; py::class_(m, "Attributes") .def(py::init<>()) .def("set_rank_dir", &Attributes::setRankDir) @@ -69,6 +75,10 @@ PYBIND11_MODULE(_core, m, py::mod_gil_not_used()) { .def("set_vertex_default_shape", &Attributes::setVertexDefaultShape) .def("set_vertex_default_monospace", &Attributes::setVertexDefaultMonospace) .def("set_edge_default_monospace", &Attributes::setEdgeDefaultMonospace) + .def("set_edge_default_arrow_head", &Attributes::setEdgeDefaultArrowHead, py::arg("value")) + .def("set_edge_default_arrow_tail", &Attributes::setEdgeDefaultArrowTail, py::arg("value")) + .def("set_edge_arrow_head", &Attributes::setEdgeArrowHead, py::arg("edge_id"), py::arg("value")) + .def("set_edge_arrow_tail", &Attributes::setEdgeArrowTail, py::arg("edge_id"), py::arg("value")) ; py::class_(m, "DirectedGraphHierarchicalLayout") .def(py::init<>()) diff --git a/python/wrapper/__init__.py b/python/wrapper/__init__.py index da764a3..d13145f 100644 --- a/python/wrapper/__init__.py +++ b/python/wrapper/__init__.py @@ -1,5 +1,6 @@ from ._core import ( Attribute, + AttributeArrowShape, AttributeRankDir, Attributes, AttributeShape, @@ -21,6 +22,7 @@ "VertexPositioningMethod", "SPDirectedGraph", "Attribute", + "AttributeArrowShape", "AttributeRankDir", "AttributeShape", "Attributes", diff --git a/src/common/graph_attributes.cpp b/src/common/graph_attributes.cpp index 5e1824d..0cd93f2 100644 --- a/src/common/graph_attributes.cpp +++ b/src/common/graph_attributes.cpp @@ -22,6 +22,10 @@ const string AttributeShape::RECORD = "record"; const string AttributeSplines::LINE = "line"; const string AttributeSplines::SPLINE = "spline"; +const string AttributeArrowShape::NONE = "none"; +const string AttributeArrowShape::NORMAL = "normal"; +const string AttributeArrowShape::EMPTY = "empty"; + const string Attributes::MONOSPACE_FONT_FAMILY = R"(ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace)"; unordered_map Attributes::DEFAULT_GRAPH_ATTRIBUTE_VALUES = { @@ -42,6 +46,8 @@ unordered_map Attributes::DEFAULT_EDGE_ATTRIBUTE_VALUES = { {ATTR_KEY_HEAD_LABEL, ""}, {ATTR_KEY_LABEL_DISTANCE, "2.0"}, {ATTR_KEY_SPLINES, "line"}, + {ATTR_KEY_ARROW_HEAD, AttributeArrowShape::NORMAL}, + {ATTR_KEY_ARROW_TAIL, AttributeArrowShape::NONE}, }; Attribute::Attribute() = default; @@ -171,3 +177,19 @@ void Attributes::setEdgeHeadLabel(const int edgeId, const string& label) { void Attributes::setEdgeLabelDistance(const int edgeId, const double scale) { setEdgeAttributes(edgeId, ATTR_KEY_LABEL_DISTANCE, scale); } + +void Attributes::setEdgeDefaultArrowHead(const string& value) { + _edgeDefaultAttributes[ATTR_KEY_ARROW_HEAD] = value; +} + +void Attributes::setEdgeDefaultArrowTail(const string& value) { + _edgeDefaultAttributes[ATTR_KEY_ARROW_TAIL] = value; +} + +void Attributes::setEdgeArrowHead(const int edgeId, const string& value) { + setEdgeAttributes(edgeId, ATTR_KEY_ARROW_HEAD, value); +} + +void Attributes::setEdgeArrowTail(const int edgeId, const string& value) { + setEdgeAttributes(edgeId, ATTR_KEY_ARROW_TAIL, value); +} diff --git a/src/directed/cross_minimization.cpp b/src/directed/cross_minimization.cpp index f1efc02..b3500c6 100644 --- a/src/directed/cross_minimization.cpp +++ b/src/directed/cross_minimization.cpp @@ -273,12 +273,16 @@ void CrossMinimization::reduceNumCrossWithBaryCenterHeuristic(SPDirectedGraph& g for (const auto& edge : _graph.getInEdges(u)) { weight += positions.find(edge.u)->second; } - weight /= _graph.getInDegrees()[u]; + if (const auto degree = _graph.getInDegrees()[u]; degree > 0) { + weight /= degree; + } } else { for (const auto& edge : _graph.getOutEdges(u)) { weight += positions.find(edge.v)->second; } - weight /= _graph.getOutDegrees()[u]; + if (const auto degree = _graph.getOutDegrees()[u]; degree > 0) { + weight /= degree; + } } return weight; }; @@ -299,7 +303,6 @@ void CrossMinimization::reduceNumCrossWithMedianHeuristic(SPDirectedGraph& graph for (const auto& edge : _graph.getInEdges(u)) { adjPositions.emplace_back(positions.find(edge.u)->second); } - ranges::sort(adjPositions); } else { for (const auto& edge : _graph.getOutEdges(u)) { adjPositions.emplace_back(positions.find(edge.v)->second); diff --git a/src/directed/vertex_positioning.cpp b/src/directed/vertex_positioning.cpp index 9b0e62f..d3349fb 100644 --- a/src/directed/vertex_positioning.cpp +++ b/src/directed/vertex_positioning.cpp @@ -47,12 +47,16 @@ void VertexPositioning::sortIncidentEdges(SPDirectedGraph& graph, SPLayering& la auto& outEdgeIds = graph.getOutEdgeIdsRef(); for (int i = 0; i < numLayers; i++) { for (const auto u : layering.orders[i]) { - ranges::sort(inEdgeIds[u], [&](const int a, const int b) { - return layering.positions[i - 1][graph.getEdge(a).u] < layering.positions[i - 1][graph.getEdge(b).u]; - }); - ranges::sort(outEdgeIds[u], [&](const int a, const int b) { - return layering.positions[i + 1][graph.getEdge(a).v] < layering.positions[i + 1][graph.getEdge(b).v]; - }); + if (i > 0) { + ranges::sort(inEdgeIds[u], [&](const int a, const int b) { + return layering.positions[i - 1][graph.getEdge(a).u] < layering.positions[i - 1][graph.getEdge(b).u]; + }); + } + if (i + 1 < numLayers) { + ranges::sort(outEdgeIds[u], [&](const int a, const int b) { + return layering.positions[i + 1][graph.getEdge(a).v] < layering.positions[i + 1][graph.getEdge(b).v]; + }); + } } } } diff --git a/src/graph_layout.cpp b/src/graph_layout.cpp index f0509b4..4aa7a08 100644 --- a/src/graph_layout.cpp +++ b/src/graph_layout.cpp @@ -192,7 +192,8 @@ string DirectedGraphHierarchicalLayout::render() const { e->setFont(_attributes.edgeAttributes(edge.id, ATTR_KEY_FONT_NAME), stod(_attributes.edgeAttributes(edge.id, ATTR_KEY_FONT_SIZE))); e->setLabelDistance(stod(_attributes.edgeAttributes(edge.id, ATTR_KEY_LABEL_DISTANCE))); e->setMargin(2); - e->setArrowHead(); + e->setArrowHead(_attributes.edgeAttributes(edge.id, ATTR_KEY_ARROW_HEAD)); + e->setArrowTail(_attributes.edgeAttributes(edge.id, ATTR_KEY_ARROW_TAIL)); if (edge.u != edge.v) { if (outEdges[edge.v].contains(edge.u)) { // There is a reverse edge @@ -240,7 +241,8 @@ string DirectedGraphHierarchicalLayout::render() const { e->setFont(_attributes.edgeAttributes(edgeId, ATTR_KEY_FONT_NAME), stod(_attributes.edgeAttributes(edgeId, ATTR_KEY_FONT_SIZE))); e->setLabelDistance(stod(_attributes.edgeAttributes(edgeId, ATTR_KEY_LABEL_DISTANCE))); e->setSplines(_attributes.edgeAttributes(edgeId, ATTR_KEY_SPLINES)); - e->setArrowHead(); + e->setArrowHead(_attributes.edgeAttributes(edgeId, ATTR_KEY_ARROW_HEAD)); + e->setArrowTail(_attributes.edgeAttributes(edgeId, ATTR_KEY_ARROW_TAIL)); e->setMargin(2); for (int i = 0; i + 1 < static_cast(edgeIds.size()); ++i) { const auto& edge = _graph->getEdge(edgeIds[i]); diff --git a/wasm/graph_layout_wasm.cpp b/wasm/graph_layout_wasm.cpp index d8501fc..80caee4 100644 --- a/wasm/graph_layout_wasm.cpp +++ b/wasm/graph_layout_wasm.cpp @@ -53,6 +53,12 @@ EMSCRIPTEN_BINDINGS(GraphLayoutWASM) { .class_property("RECT", &AttributeShape::RECT) .class_property("RECORD", &AttributeShape::RECORD) ; + class_>("AttributeArrowShape") + .constructor<>() + .class_property("NONE", &AttributeArrowShape::NONE) + .class_property("NORMAL", &AttributeArrowShape::NORMAL) + .class_property("EMPTY", &AttributeArrowShape::EMPTY) + ; class_("Attributes") .constructor<>() .function("setRankDir", &Attributes::setRankDir) @@ -63,6 +69,10 @@ EMSCRIPTEN_BINDINGS(GraphLayoutWASM) { .function("setVertexDefaultShape", &Attributes::setVertexDefaultShape) .function("setVertexDefaultMonospace", &Attributes::setVertexDefaultMonospace) .function("setEdgeDefaultMonospace", &Attributes::setEdgeDefaultMonospace) + .function("setEdgeDefaultArrowHead", &Attributes::setEdgeDefaultArrowHead) + .function("setEdgeDefaultArrowTail", &Attributes::setEdgeDefaultArrowTail) + .function("setEdgeArrowHead", &Attributes::setEdgeArrowHead) + .function("setEdgeArrowTail", &Attributes::setEdgeArrowTail) ; class_("DirectedGraphHierarchicalLayout") .constructor<>() diff --git a/wasm/index.d.ts b/wasm/index.d.ts index 108d383..03e203c 100644 --- a/wasm/index.d.ts +++ b/wasm/index.d.ts @@ -69,6 +69,13 @@ declare module 'sp-graph-layout' { static readonly RECORD: string } + export class AttributeArrowShape extends Attribute { + constructor() + static readonly NONE: string + static readonly NORMAL: string + static readonly EMPTY: string + } + export class Attributes { constructor() setRankDir(rankDir: string): void @@ -76,6 +83,13 @@ declare module 'sp-graph-layout' { setEdgeTailLabel(edgeId: number, label: string): void setEdgeHeadLabel(edgeId: number, label: string): void setEdgeLabelDistance(edgeId: number, scale: number): void + setVertexDefaultShape(shape: string): void + setVertexDefaultMonospace(): void + setEdgeDefaultMonospace(): void + setEdgeDefaultArrowHead(value: string): void + setEdgeDefaultArrowTail(value: string): void + setEdgeArrowHead(edgeId: number, value: string): void + setEdgeArrowTail(edgeId: number, value: string): void } export class DirectedGraphHierarchicalLayout { diff --git a/wasm/index.js b/wasm/index.js index 6c87fe0..c045ffa 100644 --- a/wasm/index.js +++ b/wasm/index.js @@ -21,6 +21,7 @@ const SPDirectedGraph = GraphLayoutWASM.SPDirectedGraph; const Attribute = GraphLayoutWASM.Attribute; const AttributeRankDir = GraphLayoutWASM.AttributeRankDir; const AttributeShape = GraphLayoutWASM.AttributeShape; +const AttributeArrowShape = GraphLayoutWASM.AttributeArrowShape; const Attributes = GraphLayoutWASM.Attributes; const DirectedGraphHierarchicalLayout = GraphLayoutWASM.DirectedGraphHierarchicalLayout; @@ -41,6 +42,7 @@ export { Attribute, AttributeRankDir, AttributeShape, + AttributeArrowShape, Attributes, DirectedGraphHierarchicalLayout, }; \ No newline at end of file diff --git a/wasm/package.json b/wasm/package.json index 6f622c3..928f1e1 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -1,6 +1,6 @@ { "name": "sp-graph-layout", - "version": "1.2.0", + "version": "1.3.0", "type": "module", "main": "index.js", "types": "index.d.ts",