From e10cf8deb1684593e3d2a212ebf5b3295b2b180b Mon Sep 17 00:00:00 2001 From: Doug Blank Date: Sat, 20 Dec 2008 16:37:37 +0000 Subject: [PATCH] Added new Gramplet: AgeStats.py that creates textual distribution graphs where each row can be double-clicked to show matching groups svn: r11499 --- po/POTFILES.in | 1 + src/plugins/AgeStats.py | 203 ++++++++++++++++++++++++++++++++++++++++ src/plugins/Makefile.am | 1 + 3 files changed, 205 insertions(+) create mode 100644 src/plugins/AgeStats.py diff --git a/po/POTFILES.in b/po/POTFILES.in index e2f483a2d..305b26eb5 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -237,6 +237,7 @@ src/Selectors/__init__.py # plugins directory src/plugins/AgeOnDate.py +src/plugins/AgeStats.py src/plugins/all_events.py src/plugins/all_relations.py src/plugins/AncestorReport.py diff --git a/src/plugins/AgeStats.py b/src/plugins/AgeStats.py new file mode 100644 index 000000000..b38d6eb4d --- /dev/null +++ b/src/plugins/AgeStats.py @@ -0,0 +1,203 @@ +from DataViews import register, Gramplet + +class AgeStatsGramplet(Gramplet): + def on_load(self): + self.no_wrap() + tag = self.gui.buffer.create_tag("fixed") + tag.set_property("font", "Courier 8") + + def db_changed(self): + self.update() + + def main(self): + self.clear_text() + age_dict = {} + mother_dict = {} + father_dict = {} + age_handles = [[] for age in range(120)] + mother_handles = [[] for age in range(60)] + father_handles = [[] for age in range(60)] + text = "" + handles = self.dbstate.db.get_person_handles(sort_handles=False) + for h in handles: + yield True + p = self.dbstate.db.get_person_from_handle(h) + # if birth_date and death_date, compute age + birth_ref = p.get_birth_ref() + birth_date = None + if birth_ref: + birth_event = self.dbstate.db.get_event_from_handle(birth_ref.ref) + birth_date = birth_event.get_date_object() + death_ref = p.get_death_ref() + death_date = None + if death_ref: + death_event = self.dbstate.db.get_event_from_handle(death_ref.ref) + death_date = death_event.get_date_object() + if death_date and birth_date and birth_date.get_year() != 0: + age = death_date.get_year() - birth_date.get_year() + if age >= 0 and age < 120: + age_dict[age] = age_dict.get(age, 0) + 1 + age_handles[age].append(h) + #else: + # print "Age out of range: %d for %s" % (age, + # p.get_primary_name().get_first_name() + # + " " + p.get_primary_name().get_surname()) + # for each parent m/f: + family_list = p.get_parent_family_handle_list() + if len(family_list) > 0: + family = self.dbstate.db.get_family_from_handle(family_list[0]) # first is primary, I think + if family: + f_handle = family.get_father_handle() + m_handle = family.get_mother_handle() + # if they have a birth_date, compute difference each m/f + if f_handle: + f = self.dbstate.db.get_person_from_handle(f_handle) + bref = f.get_birth_ref() + if bref: + bevent = self.dbstate.db.get_event_from_handle(bref.ref) + bdate = bevent.get_date_object() + if bdate and birth_date and birth_date.get_year() != 0: + diff = birth_date.get_year() - bdate.get_year() + if diff >= 0 and diff < 60: + father_dict[diff] = father_dict.get(diff, 0) + 1 + father_handles[diff].append(f_handle) + #else: + # print "Father diff out of range: %d for %s" % (diff, + # p.get_primary_name().get_first_name() + # + " " + p.get_primary_name().get_surname()) + if m_handle: + m = self.dbstate.db.get_person_from_handle(m_handle) + bref = m.get_birth_ref() + if bref: + bevent = self.dbstate.db.get_event_from_handle(bref.ref) + bdate = bevent.get_date_object() + if bdate and birth_date and birth_date.get_year() != 0: + diff = birth_date.get_year() - bdate.get_year() + if diff >= 0 and diff < 60: + mother_dict[diff] = mother_dict.get(diff, 0) + 1 + mother_handles[diff].append(m_handle) + #else: + # print "Mother diff out of range: %d for %s" % (diff, + # p.get_primary_name().get_first_name() + # + " " + p.get_primary_name().get_surname()) + width = 60 + graph_width = width - 8 + self.create_bargraph(age_dict, age_handles, "Lifespan Age Distribution", "Age", graph_width, 5, 120) + self.create_bargraph(father_dict, father_handles, "Father - Child Age Diff Distribution", "Diff", graph_width, 5, 60) + self.create_bargraph(mother_dict, mother_handles, "Mother - Child Age Diff Distribution", "Diff", graph_width, 5, 60) + start, end = self.gui.buffer.get_bounds() + self.gui.buffer.apply_tag_by_name("fixed", start, end) + self.append_text("", scroll_to="begin") + + def ticks(self, width, start = 0, stop = 100, fill = " "): + """ Returns the tickmark numbers for a graph axis """ + count = int(width / 10.0) + retval = "%-3d" % start + space = int((width - count * 3) / float(count - 1)) + incr = (stop - start) / float(count - 1) + lastincr = 0 + for i in range(count - 2): + retval += " " * space + newincr = int(start + (i + 1) * incr) + if newincr != lastincr: + retval += "%3d" % newincr + else: + retval += " | " + lastincr = newincr + rest = width - len(retval) - 3 + 1 + retval += " " * rest + retval += "%3d" % int(stop) + return retval + + def format(self, text, width, align = "left", borders = "||", fill = " "): + """ Returns a formatted string for nice, fixed-font display """ + if align == "center": + text = text.center(width, fill) + elif align == "left": + text = (text + (fill * width))[:width] + elif align == "right": + text = ((fill * width) + text)[-width:] + if borders[0] != None: + text = borders[0] + text + if borders[1] != None: + text = text + borders[1] + return text + + def compute_stats(self, hash): + """ Returns the statistics of a dictionary of data """ + hashkeys = hash.keys() + hashkeys.sort() + count = sum(hash.values()) + sumval = sum([k * hash[k] for k in hash]) + minval = min(hashkeys) + maxval = max(hashkeys) + median = 0 + average = 0 + if count > 0: + current = 0 + for k in hashkeys: + if current + hash[k] > count/2: + median = k + break + current += hash[k] + average = sumval/float(count) + retval = "Statistics:\n" + retval += " Total : %d\n" % count + retval += " Minimum: %d\n" % minval + retval += " Average: %.2f\n" % average + retval += " Median : %d\n" % median + retval += " Maximum: %d\n" % maxval + return retval + + def make_handles_set(self, min, max, handles): + retval = [] + for i in range(min, max): + try: + retval.extend(handles[i]) + except: + pass + return retval + + def create_bargraph(self, hash, handles, title, column, graph_width, bin_size, max_val): + """ + Create a bargraph based on the data in hash. hash is a dict, like: + hash = {12: 4, 20: 6, 35: 13, 50: 5} + where the key is the age, and the value stored is the count. + """ + # first, binify: + bin = [0] * (max_val/bin_size) + for value in hash.keys(): + bin[value/bin_size] += hash[value] + text = "" + max_bin = float(max(bin)) + if max_bin != 0: + i = 0 + self.append_text("--------" + self.format("", graph_width, fill = "-", borders="++") + "\n") + self.append_text(column.center(8) + self.format(title, graph_width, align="center") + "\n") + self.append_text("--------" + self.format("", graph_width, fill = "-", borders="++") + "\n") + for bin in bin: + self.append_text((" %3d-%3d" % (i * 5, (i+1)* 5,))) + selected = self.make_handles_set(i * 5, (i+1) *5, handles) + self.link(self.format("X" * int(bin/max_bin * graph_width), graph_width), + 'PersonList', + selected, + tooltip=_("Double-click to see %d people" % len(selected))) + self.append_text("\n") + i += 1 + self.append_text("--------" + self.format("", graph_width, fill = "-", borders="++") + "\n") + self.append_text(" Counts " + self.ticks(graph_width, start = 0, stop = int(max_bin)) + "\n\n") + self.append_text(self.compute_stats(hash)) + self.append_text("\n") + +register(type="gramplet", + name = "Age Stats Gramplet", + tname = _("Age Stats Gramplet"), + height=300, + expand=True, + content = AgeStatsGramplet, + title=_("Age Stats"), + detached_width = 600, + detached_height = 450, + ) + + diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am index 82db0d422..96b2f3227 100644 --- a/src/plugins/Makefile.am +++ b/src/plugins/Makefile.am @@ -9,6 +9,7 @@ pkgdata_PYTHON = \ all_events.py\ all_relations.py\ AgeOnDate.py\ + AgeStats.py\ AncestorTree.py\ AncestorReport.py\ BookReport.py\