# # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2008 Douglas S. Blank # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # $Id$ # """ This Gramplet shows textual distributions of age breakdowns of various types. """ import locale from collections import defaultdict from gen.plug import Gramplet from gen.ggettext import gettext as _ import gen.lib class AgeStatsGramplet(Gramplet): def init(self): self.max_age = 120 self.max_mother_diff = 60 self.max_father_diff = 60 self.chart_width = 60 def post_init(self): self.disconnect("active-changed") def build_options(self): from gen.plug.menu import NumberOption self.add_option(NumberOption(_("Max age"), self.max_age, 1, 150)) self.add_option(NumberOption(_("Max age of Mother at birth"), self.max_mother_diff, 1, 150)) self.add_option(NumberOption(_("Max age of Father at birth"), self.max_father_diff, 1, 150)) self.add_option(NumberOption(_("Chart width"), self.chart_width, 1, 150)) def save_options(self): self.max_age = int(self.get_option(_("Max age")).get_value()) self.max_mother_diff = int(self.get_option(_("Max age of Mother at birth")).get_value()) self.max_father_diff = int(self.get_option(_("Max age of Father at birth")).get_value()) self.chart_width = int(self.get_option(_("Chart width")).get_value()) def on_load(self): self.no_wrap() tag = self.gui.buffer.create_tag("fixed") tag.set_property("font", "Courier 8") # FIXME: something wrong saving ordered data list?! # if len(self.gui.data) > 0: # self.max_age = int(self.gui.data[0]) # if len(self.gui.data) > 1: # self.max_mother_diff = int(self.gui.data[1]) # if len(self.gui.data) > 2: # self.max_father_diff = int(self.gui.data[2]) # if len(self.gui.data) > 3: # self.chart_width = int(self.gui.data[3]) # def on_save(self): # self.gui.data = [self.max_age, self.max_mother_diff, self.max_father_diff, self.chart_width] def db_changed(self): self.update() def main(self): self.clear_text() age_dict = defaultdict(int) mother_dict = defaultdict(int) father_dict = defaultdict(int) age_handles = [[]] * self.max_age mother_handles = [[]] * self.max_mother_diff father_handles = [[]] * self.max_father_diff text = "" count = 0 for p in self.dbstate.db.iter_people(): if count % 300 == 0: yield True # 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 < self.max_age: age_dict[age] += 1 age_handles[age].append(p.handle) #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() for family_handle in family_list: family = self.dbstate.db.get_family_from_handle(family_handle) if family: childrel = [(ref.get_mother_relation(), ref.get_father_relation()) for ref in family.get_child_ref_list() if ref.ref == p.handle] # get first, if more than one if childrel[0][0] == gen.lib.ChildRefType.BIRTH: m_handle = family.get_mother_handle() else: m_handle = None if childrel[0][1] == gen.lib.ChildRefType.BIRTH: f_handle = family.get_father_handle() else: f_handle = None # 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 < self.max_father_diff: father_dict[diff] += 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 < self.max_mother_diff: mother_dict[diff] += 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()) count += 1 width = self.chart_width graph_width = width - 8 self.create_bargraph(age_dict, age_handles, _("Lifespan Age Distribution"), _("Age"), graph_width, 5, self.max_age) self.create_bargraph(father_dict, father_handles, _("Father - Child Age Diff Distribution"), _("Diff"), graph_width, 5, self.max_father_diff) self.create_bargraph(mother_dict, mother_handles, _("Mother - Child Age Diff Distribution"), _("Diff"), graph_width, 5, self.max_mother_diff) 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] is not None: text = borders[0] + text if borders[1] is not None: text = text + borders[1] return text def compute_stats(self, hash): """ Returns the statistics of a dictionary of data """ #print "compute_stats", hash hashkeys = sorted(hash) count = sum(hash.itervalues()) 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") + locale.format_string(": %.1f\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: #print "create_bargraph", hash bin = [0] * (max_val/bin_size) for value, hash_value in hash.iteritems(): 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-4, fill = "-", borders="++") + "-----\n") self.append_text(column.center(8) + self.format(title, graph_width-4, align="center") + " % " + "\n") self.append_text("--------" + self.format("", graph_width-4, 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-4)), graph_width-4), 'PersonList', selected, tooltip=_("Double-click to see %d people") % len(selected)) procent = (float(len(selected)) / (float(sum(hash.itervalues())))*100) self.append_text(locale.format("%#5.2f", procent)) self.append_text("\n") i += 1 self.append_text("--------" + self.format("", graph_width-4, fill = "-", borders="++") + "-----\n") self.append_text(" % " + self.ticks(graph_width-4, start = 0, stop = int(max_bin/(float(sum(hash.itervalues())))*100)) + "\n\n") self.append_text(self.compute_stats(hash))