├── LouisXIVfamily.png ├── LouisXIVfamily.txt ├── README.md └── familytreemaker.py /LouisXIVfamily.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlkirschner/familytreemaker/master/LouisXIVfamily.png -------------------------------------------------------------------------------- /LouisXIVfamily.txt: -------------------------------------------------------------------------------- 1 | # This file is an example to show how to format a family. 2 | 3 | # Two lines represent an union: 4 | Louis XIV (M, birthday=1638-09-05, deathday=1715-09-01) 5 | Marie-Thérèse d'Autriche (F) 6 | # Indented lines after the union represent children 7 | Louis de France (id=Louis1661, M, birthday=1661-11-01, deathday=1711-04-14) 8 | Marie-Thérèse\nde France (F, surname=la Petite Madame, birthday=1667, deathday=1672) 9 | Philippe-Charles\nde France (M, surname=Duc d'Anjou, birthday=1668-08-05) 10 | 11 | # Another union (2 parents + 3 children), father is one the previous union's children: 12 | Louis de France (id=Louis1661) 13 | Marie Anne\nChristine\nde Bavière (F) 14 | Louis de France (id=Louis1682, M, birthday=1682, deathday=1712-02-19, surname=duc de Bourgogne) 15 | Philippe (M, birthday=1683, deathday=1746, surname=roi d'Espagne\nsous le nom de\nPhilippe V) 16 | Charles (M, birthday=1686-07-31) 17 | 18 | # When several persons have the same name, ids can be used to differentiate them. 19 | Louis de France (id=Louis1682) 20 | Marie-Adélaïde\nde Savoie (F, deathday=1712-02-12) 21 | Louis XV (M, id=LouisXV, birthday=1710-02-15, deathday=1774-05-10) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | familytreemaker 2 | =============== 3 | 4 | This program creates family tree graphs from simple text files. 5 | 6 | The input file format is very simple, you describe persons of your family line 7 | by line, children just have to follow parents in the file. Persons can be 8 | repeated as long as they keep the same name or id. An example is given in the 9 | file `LouisXIVfamily.txt`. 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | Simply clone the repo. 16 | 17 | This script outputs a graph descriptor in DOT format. To make the image 18 | containing the graph, you will need a graph drawer such as [GraphViz] [1]. 19 | 20 | [1]: http://www.graphviz.org/ "GraphViz" 21 | 22 | Usage 23 | ----- 24 | 25 | The sample family descriptor `LouisXIVfamily.txt` is here to show you the 26 | usage. Simply run: 27 | ``` 28 | $ ./familytreemaker.py -a 'Louis XIV' LouisXIVfamily.txt | dot -Tpng -o LouisXIVfamily.png 29 | ``` 30 | It will generate the tree from the infos in `LouisXIVfamily.txt`, starting from 31 | *Louis XIV* and saving the image in `LouisXIVfamily.png`. 32 | 33 | You can see the result: 34 | 35 | ![result: LouisXIVfamily.png](/LouisXIVfamily.png) 36 | -------------------------------------------------------------------------------- /familytreemaker.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # Copyright (C) 2013 Adrien Vergé 4 | 5 | """familytreemaker 6 | 7 | This program creates family tree graphs from simple text files. 8 | 9 | The input file format is very simple, you describe persons of your family line 10 | by line, children just have to follow parents in the file. Persons can be 11 | repeated as long as they keep the same name or id. An example is given in the 12 | file LouisXIVfamily.txt. 13 | 14 | This script outputs a graph descriptor in DOT format. To make the image 15 | containing the graph, you will need a graph drawer such as GraphViz. 16 | 17 | For instance: 18 | 19 | $ ./familytreemaker.py -a 'Louis XIV' LouisXIVfamily.txt | \ 20 | dot -Tpng -o LouisXIVfamily.png 21 | 22 | will generate the tree from the infos in LouisXIVfamily.txt, starting from 23 | Louis XIV and saving the image in LouisXIVfamily.png. 24 | 25 | """ 26 | 27 | __author__ = "Adrien Vergé" 28 | __copyright__ = "Copyright 2013, Adrien Vergé" 29 | __license__ = "GPL" 30 | __version__ = "1.0" 31 | 32 | import argparse 33 | import random 34 | import re 35 | import sys 36 | 37 | class Person: 38 | """This class represents a person. 39 | 40 | Characteristics: 41 | - name real name of the person 42 | - id unique ID to be distinguished in a dictionnary 43 | - attr attributes (e.g. gender, birth date...) 44 | - households list of households this person belongs to 45 | - follow_kids boolean to tell the algorithm to display this person's 46 | descendent or not 47 | 48 | """ 49 | 50 | def __init__(self, desc): 51 | self.attr = {} 52 | self.parents = [] 53 | self.households = [] 54 | 55 | desc = desc.strip() 56 | if '(' in desc and ')' in desc: 57 | self.name, attr = desc[0:-1].split('(') 58 | self.name = self.name.strip() 59 | attr = map(lambda x: x.strip(), attr.split(',')) 60 | for a in attr: 61 | if '=' in a: 62 | k, v = a.split('=') 63 | self.attr[k] = v 64 | else: 65 | self.attr[a] = True 66 | else: 67 | self.name = desc 68 | 69 | if 'id' in self.attr: 70 | self.id = self.attr['id'] 71 | else: 72 | self.id = re.sub('[^0-9A-Za-z]', '', self.name) 73 | if 'unique' in self.attr: 74 | self.id += str(random.randint(100, 999)) 75 | 76 | self.follow_kids = True 77 | 78 | def __str__(self): 79 | return self.name 80 | 81 | def dump(self): 82 | return 'Person: %s (%s)\n' % (self.name, str(self.attr)) + \ 83 | ' %d households' % len(self.households) 84 | 85 | def graphviz(self): 86 | label = self.name 87 | if 'surname' in self.attr: 88 | label += '\\n« ' + str(self.attr['surname']) + '»' 89 | if 'birthday' in self.attr: 90 | label += '\\n' + str(self.attr['birthday']) 91 | if 'deathday' in self.attr: 92 | label += ' † ' + str(self.attr['deathday']) 93 | elif 'deathday' in self.attr: 94 | label += '\\n† ' + str(self.attr['deathday']) 95 | if 'notes' in self.attr: 96 | label += '\\n' + str(self.attr['notes']) 97 | opts = ['label="' + label + '"'] 98 | opts.append('style=filled') 99 | opts.append('fillcolor=' + ('F' in self.attr and 'bisque' or 100 | ('M' in self.attr and 'azure2' or 'white'))) 101 | return self.id + '[' + ','.join(opts) + ']' 102 | 103 | class Household: 104 | """This class represents a household, i.e. a union of two person. 105 | 106 | Those two persons are listed in 'parents'. If they have children, they are 107 | listed in 'kids'. 108 | 109 | """ 110 | 111 | def __init__(self): 112 | self.parents = [] 113 | self.kids = [] 114 | self.id = 0 115 | 116 | def __str__(self): 117 | return 'Family:\n' + \ 118 | '\tparents = ' + ', '.join(map(str, self.parents)) + '\n' \ 119 | '\tchildren = ' + ', '.join(map(str, self.kids)) 120 | 121 | def isempty(self): 122 | if len(self.parents) == 0 and len(self.kids) == 0: 123 | return True 124 | return False 125 | 126 | class Family: 127 | """Represents the whole family. 128 | 129 | 'everybody' contains all persons, indexed by their unique id 130 | 'households' is the list of all unions (with or without children) 131 | 132 | """ 133 | 134 | everybody = {} 135 | households = [] 136 | 137 | invisible = '[shape=circle,label="",height=0.01,width=0.01]'; 138 | 139 | def add_person(self, string): 140 | """Adds a person to self.everybody, or update his/her info if this 141 | person already exists. 142 | 143 | """ 144 | p = Person(string) 145 | key = p.id 146 | 147 | if key in self.everybody: 148 | self.everybody[key].attr.update(p.attr) 149 | else: 150 | self.everybody[key] = p 151 | 152 | return self.everybody[key] 153 | 154 | def add_household(self, h): 155 | """Adds a union (household) to self.households, and updates the 156 | family members infos about this union. 157 | 158 | """ 159 | if len(h.parents) != 2: 160 | print('error: number of parents != 2') 161 | return 162 | 163 | h.id = len(self.households) 164 | self.households.append(h) 165 | 166 | for p in h.parents: 167 | if not h in p.households: 168 | p.households.append(h) 169 | 170 | def find_person(self, name): 171 | """Tries to find a person matching the 'name' argument. 172 | 173 | """ 174 | # First, search in ids 175 | if name in self.everybody: 176 | return self.everybody[name] 177 | # Ancestor not found in 'id', maybe it's in the 'name' field? 178 | for p in self.everybody.values(): 179 | if p.name == name: 180 | return p 181 | return None 182 | 183 | def populate(self, f): 184 | """Reads the input file line by line, to find persons and unions. 185 | 186 | """ 187 | h = Household() 188 | while True: 189 | line = f.readline() 190 | if line == '': # end of file 191 | if not h.isempty(): 192 | self.add_household(h) 193 | break 194 | line = line.rstrip() 195 | if line == '': 196 | if not h.isempty(): 197 | self.add_household(h) 198 | h = Household() 199 | elif line[0] == '#': 200 | continue 201 | else: 202 | if line[0] == '\t': 203 | p = self.add_person(line[1:]) 204 | p.parents = h.parents 205 | h.kids.append(p) 206 | else: 207 | p = self.add_person(line) 208 | h.parents.append(p) 209 | 210 | def find_first_ancestor(self): 211 | """Returns the first ancestor found. 212 | 213 | A person is considered an ancestor if he/she has no parents. 214 | 215 | This function is not very good, because we can have many persons with 216 | no parents, it will always return the first found. A better practice 217 | would be to return the one with the highest number of descendant. 218 | 219 | """ 220 | for p in self.everybody.values(): 221 | if len(p.parents) == 0: 222 | return p 223 | 224 | def next_generation(self, gen): 225 | """Takes the generation N in argument, returns the generation N+1. 226 | 227 | Generations are represented as a list of persons. 228 | 229 | """ 230 | next_gen = [] 231 | 232 | for p in gen: 233 | if not p.follow_kids: 234 | continue 235 | for h in p.households: 236 | next_gen.extend(h.kids) 237 | # append mari/femme 238 | 239 | return next_gen 240 | 241 | def get_spouse(household, person): 242 | """Returns the spouse or husband of a person in a union. 243 | 244 | """ 245 | return household.parents[0] == person \ 246 | and household.parents[1] or household.parents[0] 247 | 248 | def display_generation(self, gen): 249 | """Outputs an entire generation in DOT format. 250 | 251 | """ 252 | # Display persons 253 | print('\t{ rank=same;') 254 | 255 | prev = None 256 | for p in gen: 257 | l = len(p.households) 258 | 259 | if prev: 260 | if l <= 1: 261 | print('\t\t%s -> %s [style=invis];' % (prev, p.id)) 262 | else: 263 | print('\t\t%s -> %s [style=invis];' 264 | % (prev, Family.get_spouse(p.households[0], p).id)) 265 | 266 | if l == 0: 267 | prev = p.id 268 | continue 269 | elif len(p.households) > 2: 270 | raise Exception('Person "' + p.name + '" has more than 2 ' + 271 | 'spouses/husbands: drawing this is not ' + 272 | 'implemented') 273 | 274 | # Display those on the left (if any) 275 | for i in range(0, int(l/2)): 276 | h = p.households[i] 277 | spouse = Family.get_spouse(h, p) 278 | print('\t\t%s -> h%d -> %s;' % (spouse.id, h.id, p.id)) 279 | print('\t\th%d%s;' % (h.id, Family.invisible)) 280 | 281 | # Display those on the right (at least one) 282 | for i in range(int(l/2), l): 283 | h = p.households[i] 284 | spouse = Family.get_spouse(h, p) 285 | print('\t\t%s -> h%d -> %s;' % (p.id, h.id, spouse.id)) 286 | print('\t\th%d%s;' % (h.id, Family.invisible)) 287 | prev = spouse.id 288 | print('\t}') 289 | 290 | # Display lines below households 291 | print('\t{ rank=same;') 292 | prev = None 293 | for p in gen: 294 | for h in p.households: 295 | if len(h.kids) == 0: 296 | continue 297 | if prev: 298 | print('\t\t%s -> h%d_0 [style=invis];' % (prev, h.id)) 299 | l = len(h.kids) 300 | if l % 2 == 0: 301 | # We need to add a node to keep symmetry 302 | l += 1 303 | print('\t\t' + ' -> '.join(map(lambda x: 'h%d_%d' % (h.id, x), range(l))) + ';') 304 | for i in range(l): 305 | print('\t\th%d_%d%s;' % (h.id, i, Family.invisible)) 306 | prev = 'h%d_%d' % (h.id, i) 307 | print('\t}') 308 | 309 | for p in gen: 310 | for h in p.households: 311 | if len(h.kids) > 0: 312 | print('\t\th%d -> h%d_%d;' 313 | % (h.id, h.id, int(len(h.kids)/2))) 314 | i = 0 315 | for c in h.kids: 316 | print('\t\th%d_%d -> %s;' 317 | % (h.id, i, c.id)) 318 | i += 1 319 | if i == len(h.kids)/2: 320 | i += 1 321 | 322 | def output_descending_tree(self, ancestor): 323 | """Outputs the whole descending family tree from a given ancestor, 324 | in DOT format. 325 | 326 | """ 327 | # Find the first households 328 | gen = [ancestor] 329 | 330 | print('digraph {\n' + \ 331 | '\tnode [shape=box];\n' + \ 332 | '\tedge [dir=none];\n') 333 | 334 | for p in self.everybody.values(): 335 | print('\t' + p.graphviz() + ';') 336 | print('') 337 | 338 | while gen: 339 | self.display_generation(gen) 340 | gen = self.next_generation(gen) 341 | 342 | print('}') 343 | 344 | def main(): 345 | """Entry point of the program when called as a script. 346 | 347 | """ 348 | # Parse command line options 349 | parser = argparse.ArgumentParser(description= 350 | 'Generates a family tree graph from a simple text file') 351 | parser.add_argument('-a', dest='ancestor', 352 | help='make the family tree from an ancestor (if '+ 353 | 'omitted, the program will try to find an ancestor)') 354 | parser.add_argument('input', metavar='INPUTFILE', 355 | help='the formatted text file representing the family') 356 | args = parser.parse_args() 357 | 358 | # Create the family 359 | family = Family() 360 | 361 | # Populate the family 362 | f = open(args.input, 'r', encoding='utf-8') 363 | family.populate(f) 364 | f.close() 365 | 366 | # Find the ancestor from whom the tree is built 367 | if args.ancestor: 368 | ancestor = family.find_person(args.ancestor) 369 | if not ancestor: 370 | raise Exception('Cannot find person "' + args.ancestor + '"') 371 | else: 372 | ancestor = family.find_first_ancestor() 373 | 374 | # Output the graph descriptor, in DOT format 375 | family.output_descending_tree(ancestor) 376 | 377 | if __name__ == '__main__': 378 | main() 379 | --------------------------------------------------------------------------------