def draw_small_multiples(nb_col, TPs, SM, lay): """ Create a graph containing a number of thumbnails per line. @type nb_col: Int @param nb_col: Number of thumbnails by line @type TPs: List @param TPs: Time points name @type SM: Tulip graph @param SM: Small Multiples subgraph @type lay: tlp.LayoutProperty @param lay: Coord of nodes @note : disable (better v2) """ bb_tp = tlp.computeBoundingBox(SM) x = 0 y = 0 count = 0 for gr in TPs: x = x + bb_tp.width() + 2000 tp = SM.getSubGraph(gr) if count >= nb_col: x = bb_tp.width() + 2000 y = y - bb_tp.height() - 2000 count = 0 for n in tp.getNodes(): lay[n] = lay[n] + tlp.Vec3f(x, y, 0) for e in tp.getEdges(): Ltmp = [] for el in lay[e]: h = el + tlp.Vec3f(x, y, 0) Ltmp.append(h) lay[e] = Ltmp count += 1
def constructGrid(smallMultGraph, columns): """ Function to create the grid of the small multiple graph, and displace each small multiple according to the layout of the grid. To create the grid, the bounding box of the small multiple graph is computed. Hence we can get the width and the height of a graph. Those two parameters will be used to move each subgraph of the hierarchy in the small multiple graph according to their id order. The translattion will be function of the product of the column by the width for the x axis. And the product of the line by the negative form of the height for the y axis.For example, the subgraph tp1 will be placed in the coordinate (0,0) of the grid and then doesn't need to be moved. But for tp2, this graph will be settled in the coordinate (1,1) of the grid and then be moved along the x axis by : 1 * width. This part of the algorithm is taken care by the function "drawSmallMultiple". Args: smallMultGraph (tlp.Graph) : the small multiple graph columns (integer) : the number of columns for the grid Returns: None """ layout = smallMultGraph.getLayoutProperty("viewLayout") bBox = tlp.computeBoundingBox(smallMultGraph) numberSub = smallMultGraph.numberOfSubGraphs() idSubgraph = 0 line = 0 while idSubgraph < numberSub: for column in range(columns): if (idSubgraph < numberSub): idSubgraph += 1 subGraph = smallMultGraph.getSubGraph( "tp{}".format(idSubgraph)) drawSmallMultiple(subGraph, line, column, bBox, layout) else: break line += 1
def draw_small_multiples_v2(nb_col, TPs, SM, lay): """ Create a graph containing a number of thumbnails per line. Optimized algorithm to save around 5 seconds. @type nb_col: Int @param nb_col: Number of thumbnails by line @type TPs: List @param TPs: Time points name @type SM: Tulip graph @param SM: Small Multiples subgraph @type lay: tlp.LayoutProperty @param lay: Coord of nodes """ bb_tp = tlp.computeBoundingBox(SM) x = 0 y = 0 count = 0 for gr in TPs: x = x + bb_tp.width() + 2000 tp = SM.getSubGraph(gr) if count >= nb_col: x = bb_tp.width() + 2000 y = y - bb_tp.height() - 2000 count = 0 count = count + 1 lay.translate(tlp.Vec3f(x, y, 0), tp)
def placeSmallMultiples(smallMultiplesTree, nbCol): bb = tlp.computeBoundingBox(smallMultiplesTree) #Multiply by 2 to have a shift between all the graphs xmax = bb[1][0] * 2 ymax = bb[1][1] * 2 #Shifts all the graphs according to the number of their associated timepoint, the number of columns choosen and the bounding box for sg in smallMultiplesTree.getSubGraphs(): smallLayout = sg.getLayoutProperty("viewLayout") sgNames = sg.getName().split(" ") sgName = sgNames[0] nbSG = int(sgName[2:len(sgName)]) for i in range(0, nbCol + 1): if (nbSG <= nbCol * (i + 1) and nbSG > nbCol * i): for node in sg.getNodes(): newPos = tlp.Coord( smallLayout[node][0] + xmax * (nbSG - nbCol * i), smallLayout[node][1] - ymax * i, smallLayout[node][2]) smallLayout.setNodeValue(node, newPos) for edge in sg.getEdges(): newEdgePos = [] for pos in smallLayout[edge]: newPos = tlp.Coord(pos[0] + xmax * (nbSG - nbCol * i), pos[1] - ymax * i, pos[2]) newEdgePos.append(newPos) smallLayout.setEdgeValue(edge, newEdgePos)
def dfs(g, cur_height): node = { 'id': g.getId(), 'geometry': None, 'diameter': 0, 'desc_metanodes': {}, 'parent_metanode': None, 'leaf_nodes': {}, 'level': cur_height } # For finding whether a node is in a subgraph for leaf in g.getNodes(): node['leaf_nodes'][leaf.id] = True # For finding whether two meta-nodes are on the same path of the node hierarchy for s in g.getDescendantGraphs(): node['desc_metanodes'][s.getId()] = True height['a'] = max(height['a'], cur_height + 1) for s in g.getSubGraphs(): dfs(s, cur_height + 1) metanodes[s.getId()]['parent_metanode'] = g.getId() # Add the field parent_metanode to the leaf node for leaf in g.getNodes(): tmp = leaf_nodes[leaf.id] if tmp['parent_metanode'] is None: tmp['parent_metanode'] = g.getId() # Compute convex hull of this sub-graph in post-order if bounding_shape == 'convex_hull': if g.numberOfSubGraphs() == 0: # compute a convex hull of its leaf nodes coords = tlp.computeConvexHull(g) node['geometry'] = Polygon([(c.x(), c.y()) for c in coords]) else: # union the convex hull of its sub-graphs node['geometry'] = MultiPolygon([ metanodes[s.getId()]['geometry'] for s in g.getSubGraphs() ]).convex_hull # Note that we don't compute the real diameter for a polygon, but instead, only use the diagonal of # the axis aligned bounding box to approximate the diameter, which is cheap to compute bbox = node['geometry'].bounds node['diameter'] = Point(bbox[0], bbox[1]).distance(Point(bbox[2], bbox[3])) elif bounding_shape == 'circle': center, fur = tlp.computeBoundingRadius(g) radius = center.dist(fur) node['geometry'] = Point(center.x(), center.y()).buffer( radius, cap_style=CAP_STYLE.round) node['diameter'] = 2 * radius else: bbox = tlp.computeBoundingBox(g) node['geometry'] = Polygon([(c.x(), c.y()) for c in bbox]) node['diameter'] = bbox[0].dist(bbox[1]) metanodes[g.getId()] = node
def draw_small_multiples_v2(nb_col, TPs, SM, lay): #TODO dire qu'on a gagné du temps par rapport à l'autre v1 bb_tp = tlp.computeBoundingBox(SM) x = 0 y = 0 count = 0 for gr in TPs: x = x + bb_tp.width() + 2000 tp = SM.getSubGraph(gr) #lay = tp.getLayoutProperty("viewlayout") if count >= nb_col: x = bb_tp.width() + 2000 y = y - bb_tp.height() - 2000 count = 0 count = count + 1 lay.translate(tlp.Vec3f(x, y, 0), tp)
def positionSmallMultiples(g, smallG, columnNumber): layout = g.getLayoutProperty("viewLayout") graphBoundingBox = tlp.computeBoundingBox(g) gHeight = graphBoundingBox.height() gWidth = graphBoundingBox.width() n = 1 y = 0 line = 0 for smallImg in smallG.getSubGraphs(): if n == columnNumber + 1: line += 1 y = -gHeight * line * 1.5 n = 1 x = gWidth * n * 1.5 newCenter = tlp.Vec3f(x, y, 0) layout.center(newCenter, smallImg) n += 1
def subgraph_grid(multiple_graph, nbcolumn): """ Align all the subgraph of a graph, in a grid. Author: Pierre Jacquet Modfied by Eliot Ragueneau Args: multiple_graph (tlp.Graph): A parent graph nbcolumn (int): number of column in the grid """ # get one subgraph's bounding box bounding_box = tlp.computeBoundingBox(multiple_graph.getNthSubGraph(1)) size_x = 1.5 * abs(bounding_box[1][0] - bounding_box[0][0]) size_y = 1.5 * abs(bounding_box[1][1] - bounding_box[0][1]) # Multiplied by 1.5 to have an separation between graphs number_of_visited_subgraph = 0 offset_x = 0 offset_y = 0 layout = multiple_graph.getLayoutProperty("viewLayout") for sub_graph in multiple_graph.getSubGraphs(): number_of_visited_subgraph += 1 for node in sub_graph.getNodes(): layout[node] += tlp.Vec3f(offset_x, -offset_y, 0) # Move the node by offsets values for edge in sub_graph.getEdges(): control_points = layout[edge] new_control_points = [] for vector in control_points: new_control_points.append( tuple(map(sum, zip(vector, (offset_x, -offset_y, 0))))) layout[edge] = new_control_points # Calculate the new offset value offset_x = number_of_visited_subgraph % nbcolumn * size_x offset_y = (number_of_visited_subgraph // nbcolumn) * size_y
def draw_small_multiples(nb_col, TPs, SM, lay): """ """ bb_tp = tlp.computeBoundingBox(SM) x = 0 y = 0 count = 0 for gr in TPs: x = x + bb_tp.width() + 2000 tp = SM.getSubGraph(gr) if count >= nb_col: x = bb_tp.width() + 2000 y = y - bb_tp.height() - 2000 count = 0 for n in tp.getNodes(): lay[n] = lay[n] + tlp.Vec3f(x, y, 0) for e in tp.getEdges(): Ltmp = [] for el in lay[e]: h = el + tlp.Vec3f(x, y, 0) Ltmp.append(h) lay[e] = Ltmp count += 1
def convert(input_path, leaf_only=False, bounding_shape='convex_hull'): graph = tlp.loadGraph(input_path) # Retrieve nodes and edges from graph and construct Shapely geometries view_layout = graph.getLayoutProperty('viewLayout') view_size = graph.getSizeProperty('viewSize') leaf_nodes = [ { 'id': n.id, 'parent_metanode': None, # fulfill later 'geometry': Point(view_layout[n].x(), view_layout[n].y()).buffer(view_size[n][0] / 2.0, cap_style=CAP_STYLE.round), 'diameter': view_size[n][0] } for n in graph.nodes() ] node_mapping = {} for n in leaf_nodes: node_mapping[n['id']] = n edges = [] check_dup = {} for e in graph.getEdges(): src, tgt = graph.ends(e) if src.id > tgt.id: tmp = src src = tgt tgt = tmp edge_id = '{}-{}'.format(src.id, tgt.id) # remove duplicate and self-connecting edges if edge_id not in check_dup and src.id != tgt.id: edges.append({ 'id': e.id, 'ends': (src.id, tgt.id), 'geometry': chop_segment(node_mapping[src.id]['geometry'], node_mapping[tgt.id]['geometry']) }) check_dup[edge_id] = True bbox = tlp.computeBoundingBox(graph) root = graph.getId() height = {'a': 1} metanodes = {} # Construct a simple node (graph) hierarchy data structure from the tulip graph and count levels # This is the same level counting method in the Bourqui multi-level force layout paper. def dfs(g, cur_height): node = { 'id': g.getId(), 'geometry': None, 'diameter': 0, 'desc_metanodes': {}, 'parent_metanode': None, 'leaf_nodes': {}, 'level': cur_height } # For finding whether a node is in a subgraph for leaf in g.getNodes(): node['leaf_nodes'][leaf.id] = True # For finding whether two meta-nodes are on the same path of the node hierarchy for s in g.getDescendantGraphs(): node['desc_metanodes'][s.getId()] = True height['a'] = max(height['a'], cur_height + 1) for s in g.getSubGraphs(): dfs(s, cur_height + 1) metanodes[s.getId()]['parent_metanode'] = g.getId() # Add the field parent_metanode to the leaf node for leaf in g.getNodes(): tmp = leaf_nodes[leaf.id] if tmp['parent_metanode'] is None: tmp['parent_metanode'] = g.getId() # Compute convex hull of this sub-graph in post-order if bounding_shape == 'convex_hull': if g.numberOfSubGraphs() == 0: # compute a convex hull of its leaf nodes coords = tlp.computeConvexHull(g) node['geometry'] = Polygon([(c.x(), c.y()) for c in coords]) else: # union the convex hull of its sub-graphs node['geometry'] = MultiPolygon([ metanodes[s.getId()]['geometry'] for s in g.getSubGraphs() ]).convex_hull # Note that we don't compute the real diameter for a polygon, but instead, only use the diagonal of # the axis aligned bounding box to approximate the diameter, which is cheap to compute bbox = node['geometry'].bounds node['diameter'] = Point(bbox[0], bbox[1]).distance(Point(bbox[2], bbox[3])) elif bounding_shape == 'circle': center, fur = tlp.computeBoundingRadius(g) radius = center.dist(fur) node['geometry'] = Point(center.x(), center.y()).buffer( radius, cap_style=CAP_STYLE.round) node['diameter'] = 2 * radius else: bbox = tlp.computeBoundingBox(g) node['geometry'] = Polygon([(c.x(), c.y()) for c in bbox]) node['diameter'] = bbox[0].dist(bbox[1]) metanodes[g.getId()] = node if not leaf_only: dfs(graph, root) # Output json file at the same directory with same filename but "json" extension output_path = re.sub(r'\.tlp$', '.json', input_path) # Use the mapping function from shapely to serialize the geometry objects for n in leaf_nodes: n['geometry'] = mapping(n['geometry']) for e in edges: e['geometry'] = mapping(e['geometry']) for _, n in metanodes.items(): n['geometry'] = mapping(n['geometry']) json_data = { 'leaf_nodes': leaf_nodes, 'edges': edges, 'height': height['a'], 'root': root, 'metanodes': metanodes, 'bounding_box': [[bbox[0].x(), bbox[0].y()], [bbox[1].x(), bbox[1].y()]] } json.dump(json_data, open(output_path, 'w')) print('Converted to ', output_path, ' #nodes:', len(leaf_nodes), ' #edges: ', len(edges), ' height: ', height['a'])