dyng
DynamicGraphLayout
fruchterman_reingold.h
1 /*
2  Copyright 2020 František Bráblík
3 
4  Licensed under the Apache License, Version 2.0 (the "License");
5  you may not use this file except in compliance with the License.
6  You may obtain a copy of the License at
7 
8  http://www.apache.org/licenses/LICENSE-2.0
9 
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15 */
16 #pragma once
17 
18 #include "optimization_grid.h"
19 #include "cooling.h"
20 
21 #include <random>
22 #include <cmath>
23 #include <unordered_map>
24 
25 namespace dyng {
26 
38 template<typename InitialLayout>
40 
41 using disp_map = std::unordered_map<node_id, coords>;
42 
43 public:
53  template<typename Graph>
54  void operator()(Graph& graph, float canvas_width, float canvas_height) {
55  // if no nodes, then graph must be empty
56  if (graph.nodes().empty()) {
57  return;
58  }
59  m_initial_layouter(graph, canvas_width, canvas_height);
60  layout_pass(canvas_width, canvas_height, graph, m_first_cooling);
61  layout_pass(canvas_width, canvas_height, graph, m_second_cooling);
62  }
63 
65  const InitialLayout& initial_layout() const { return m_initial_layouter; }
66 
68  InitialLayout& initial_layout() { return m_initial_layouter; }
69 
71  float relative_unit(float width, float height) const {
72  return length(width, height) * UnitCoeff;
73  }
74 
77  m_first_cooling = std::move(c);
78  }
79 
82  m_second_cooling = std::move(c);
83  }
84 
86 
89  void set_k_coeff(float coeff) {
90  m_k_coeff = coeff;
91  }
92 
94 
99  void set_border_force_coeff(float coeff) {
100  m_border_force = coeff;
101  }
102 
104 
112  void use_global_repulsion(bool value) {
113  m_use_global_repulsion = value;
114  }
115 
127  template<typename Graph>
128  void iteration(Graph& graph, float width, float height, float temperature) {
129  float area = width * height;
130  float k = m_k_coeff * std::sqrt(area / static_cast<float>(graph.nodes().size()));
131  temperature = temperature * relative_unit(width, height);
132 
133  std::vector<coords> displacements(graph.nodes().size());
134  reset_and_border(graph, width, height, k, displacements);
135  repulsive_forces(graph, width, height, k, temperature, displacements);
136  attractive_forces(graph, k, displacements);
137  displacement(graph, width, height, temperature, displacements);
138  }
139 
140 private:
141  static constexpr float SmallOffset = 0.001f;
142  static constexpr float UnitCoeff = 0.68;
143 
144  float m_border_force = 0.6;
145  float m_k_coeff = 0.6;
146  bool m_use_global_repulsion = false; // use local repulsion limited to the radius of 2k
147 
148  cooling m_first_cooling{ 500, 0.8, [](float t){ return t * 0.9893; } };
149  cooling m_second_cooling{ 500, 0.05, [](float t){ return t * 0.993; } };
150 
151  InitialLayout m_initial_layouter;
152 
153  template<typename Graph>
154  void reset_and_border(
155  Graph& graph
156  , float width
157  , float height
158  , float k
159  , std::vector<coords>& disp) const {
160  // reset displacement + border repulsion
161  for (unsigned i = 0; i < graph.nodes().size(); ++i) {
162  auto& pos = graph.nodes()[i].pos();
163  disp[i].x = border_displacement(k, width, pos.x);
164  disp[i].y = border_displacement(k, height, pos.y);
165  }
166  }
167 
168  template<typename Graph>
169  void repulsive_forces(
170  Graph& graph
171  , float width
172  , float height
173  , float k
174  , float t
175  , std::vector<coords>& disp) const {
176  std::mt19937 rand_gen(0); // to allow random displacement when necessary
177  std::uniform_real_distribution<float> rand_angle(0.0f, 3.14159f * 2.0f);
178 
179  // calculate repulsive forces
180  for_each_pair_of_nodes(graph, width, height, k, [&](unsigned i, unsigned j){
181  auto& node_i = graph.nodes()[i];
182  auto& node_j = graph.nodes()[j];
183  float diff_x = node_j.pos().x - node_i.pos().x;
184  float diff_y = node_j.pos().y - node_i.pos().y;
185  float dst = length(diff_x, diff_y);
186  if (dst == 0) {
187  // this rarely happens, but when it does it needs displacement
188  // otherwise resulting layout can be terrible
189  float angle = rand_angle(rand_gen);
190  float r = t * 0.5;
191  disp[i].x -= std::cos(angle) * r;
192  disp[i].y -= std::sin(angle) * r;
193  disp[j].x += std::cos(angle) * r;
194  disp[j].y += std::sin(angle) * r;
195  } else if (m_use_global_repulsion || dst < k * 2.0f) {
196  float rep_force = (1.0f / dst) * (k * k / dst);
197  disp[i].x -= diff_x * rep_force;
198  disp[i].y -= diff_y * rep_force;
199  disp[j].x += diff_x * rep_force;
200  disp[j].y += diff_y * rep_force;
201  }
202  });
203  }
204 
205  // calls func exactly once for each pair of nodes
206  template <typename Graph, typename Function>
207  void for_each_pair_of_nodes(
208  Graph& graph
209  , float width
210  , float height
211  , float k
212  , Function func) const {
213  if (m_use_global_repulsion) {
214  for (unsigned i = 0; i < graph.nodes().size(); ++i) {
215  for (unsigned j = 0; j < i; ++j) {
216  func(i, j);
217  }
218  }
219  } else {
220  // setup the grid
221  detail::optimization_grid m_grid(width, height, k);
222  for (unsigned i = 0; i < graph.nodes().size(); ++i) {
223  m_grid.add(graph.nodes()[i].pos(), i);
224  }
225  // iterate through all pairs that are close together
226  for (unsigned i = 0; i < graph.nodes().size(); ++i) {
227  auto& node_i = graph.nodes()[i];
228  m_grid.for_each_around(node_i.pos(), [&](unsigned j){
229  if (j < i) {
230  func(i, j);
231  }
232  });
233  }
234  }
235  }
236 
237  template<typename Graph>
238  void attractive_forces(Graph& graph, float k, std::vector<coords>& disp) const {
239  for (const auto& e : graph.edges()) {
240  unsigned index_one = graph.node_index(e.one_id());
241  unsigned index_two = graph.node_index(e.two_id());
242  auto& node_one = graph.nodes()[index_one];
243  auto& node_two = graph.nodes()[index_two];
244  float diff_x = node_two.pos().x - node_one.pos().x;
245  float diff_y = node_two.pos().y - node_one.pos().y;
246  float dst = length(diff_x, diff_y);
247  if (dst != 0.0f) {
248  auto& disp_one = disp[index_one];
249  auto& disp_two = disp[index_two];
250  float attr_force = (1.0f / dst) * (dst * dst / k);
251  disp_one.x += diff_x * attr_force;
252  disp_one.y += diff_y * attr_force;
253  disp_two.x -= diff_x * attr_force;
254  disp_two.y -= diff_y * attr_force;
255  }
256  }
257  }
258 
259  template<typename Graph>
260  void displacement(
261  Graph& graph
262  , float width
263  , float height
264  , float t
265  , const std::vector<coords>& disp) const {
266  for (unsigned i = 0; i < graph.nodes().size(); ++i) {
267  auto& node = graph.nodes()[i];
268  const auto& current_disp = disp[i];
269  float disp_len = length(current_disp.x, current_disp.y);
270  if (disp_len != 0) {
271  float result_disp = std::min(disp_len, t) / disp_len;
272  node.pos().x += result_disp * current_disp.x;
273  node.pos().y += result_disp * current_disp.y;
274  }
275  auto clamp = [](float size, float coord){
276  return std::min(size, std::max(-size, coord));
277  };
278  node.pos().x = clamp(width * 0.5f, node.pos().x);
279  node.pos().y = clamp(height * 0.5f, node.pos().y);
280  }
281  }
282 
283  float border_displacement(float k, float coord, float size) const {
284  auto displace = [&](float coord, float size){
285  return (k * k * m_border_force) / (std::fabs(size * 0.5f - coord)
286  + std::fabs(size * SmallOffset));
287  };
288  return displace(coord, -size)
289  - displace(coord, size);
290  }
291 
292  template<typename Graph>
293  void layout_pass(
294  float width
295  , float height
296  , Graph& graph
297  , const cooling& c) {
298  float t = c.start_temperature;
299  for (unsigned r = 0; r < c.iterations; ++r) {
300  iteration(graph, width, height, t);
301  t = c.anneal(t);
302  }
303  }
304 
305  float pow2(float one) const {
306  return one * one;
307  }
308 
309  // calculates the length of hypotenuse
310  float length(float a, float b) const {
311  return std::sqrt(pow2(a) + pow2(b));
312  }
313 };
314 
315 } // namespace dyng
dyng::fruchterman_reingold::set_k_coeff
void set_k_coeff(float coeff)
Sets the coefficient for the parameter k representing average edge length.
Definition: fruchterman_reingold.h:89
dyng::fruchterman_reingold::initial_layout
InitialLayout & initial_layout()
Returns the object that crates initial placement.
Definition: fruchterman_reingold.h:68
dyng::fruchterman_reingold::iteration
void iteration(Graph &graph, float width, float height, float temperature)
Definition: fruchterman_reingold.h:128
dyng::fruchterman_reingold::initial_layout
const InitialLayout & initial_layout() const
Returns the object that crates initial placement.
Definition: fruchterman_reingold.h:65
dyng::fruchterman_reingold::set_border_force_coeff
void set_border_force_coeff(float coeff)
Sets the coefficient for border force calculations.
Definition: fruchterman_reingold.h:99
dyng::graph::nodes
const std::vector< NodeType > & nodes() const
Returns const reference to the vector of all nodes in the graph.
Definition: graph.h:96
dyng::fruchterman_reingold::set_second_cooling
void set_second_cooling(cooling c)
Sets the cooling strategy for the second algorithm pass.
Definition: fruchterman_reingold.h:81
dyng::fruchterman_reingold::use_global_repulsion
void use_global_repulsion(bool value)
Switches whether or not repulsive force should be calculated between all nodes.
Definition: fruchterman_reingold.h:112
dyng::fruchterman_reingold::operator()
void operator()(Graph &graph, float canvas_width, float canvas_height)
Definition: fruchterman_reingold.h:54
dyng::fruchterman_reingold
Definition: fruchterman_reingold.h:39
dyng::fruchterman_reingold::relative_unit
float relative_unit(float width, float height) const
Returns the relative unit that is used with temperature calculations.
Definition: fruchterman_reingold.h:71
dyng::cooling
Structure representing a cooling strategy.
Definition: cooling.h:27
dyng::fruchterman_reingold::set_first_cooling
void set_first_cooling(cooling c)
Sets the cooling strategy for the first algorithm pass.
Definition: fruchterman_reingold.h:76
dyng::graph
Templated class for representing a static graph and its layout.
Definition: graph.h:40