From 00fa6d472a1c1f8323b318934073a77cd12c449e Mon Sep 17 00:00:00 2001 From: Nick Hall Date: Sun, 20 Oct 2019 19:54:16 +0100 Subject: [PATCH] Improved Age Stats gramplet * New Histogram widget * Extra bucket for ages above maximum * Automatic scaling to available width --- gramps/gui/widgets/__init__.py | 1 + gramps/gui/widgets/histogram.py | 340 +++++++++++++++++++++++ gramps/plugins/gramplet/agestats.py | 404 ++++++++++++++-------------- po/POTFILES.skip | 1 + 4 files changed, 541 insertions(+), 205 deletions(-) create mode 100644 gramps/gui/widgets/histogram.py diff --git a/gramps/gui/widgets/__init__.py b/gramps/gui/widgets/__init__.py index b12eec59b..0fc61dd28 100644 --- a/gramps/gui/widgets/__init__.py +++ b/gramps/gui/widgets/__init__.py @@ -26,6 +26,7 @@ from .basicentry import * from .buttons import * from .dateentry import * from .expandcollapsearrow import * +from .histogram import * from .labels import * from .linkbox import * from .photo import * diff --git a/gramps/gui/widgets/histogram.py b/gramps/gui/widgets/histogram.py new file mode 100644 index 000000000..e44f57ec0 --- /dev/null +++ b/gramps/gui/widgets/histogram.py @@ -0,0 +1,340 @@ +# +# Gramps - a GTK+/GNOME based genealogy program +# +# Copyright (C) 2019 Nick Hall +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# +""" +Provides a simple histogram widget for use in gramplets. +""" + +#------------------------------------------------------------------------- +# +# Python modules +# +#------------------------------------------------------------------------- +import math + +#------------------------------------------------------------------------- +# +# Gtk modules +# +#------------------------------------------------------------------------- +from gi.repository import Gtk +from gi.repository import Gdk +from gi.repository import GObject +from gi.repository import Pango, PangoCairo + +#------------------------------------------------------------------------- +# +# Gramps modules +# +#------------------------------------------------------------------------- + +from ...gen.const import GRAMPS_LOCALE as glocale + +class Histogram(Gtk.DrawingArea): + """ + A simple histogram widget for use in gramplets. + """ + + __gsignals__ = {'clicked': (GObject.SignalFlags.RUN_FIRST, None, (int,))} + + def __init__(self): + Gtk.DrawingArea.__init__(self) + + self.add_events(Gdk.EventMask.POINTER_MOTION_MASK | + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.BUTTON_RELEASE_MASK) + self.connect('motion-notify-event', self.on_pointer_motion) + self.connect('button-press-event', self.on_button_press) + + self.title = '' + self.bucket_axis = '' + self.value_axis = '' + self.grid_lines = True + self.data = [] + self.labels = [] + self.tooltip = '' + self.highlight = None + self.__bars = None + self.__active = -1 + + def set_title(self, title): + """ + Set the main chart title. + @param title: The main chart title. + @type title: str + """ + self.title = title + + def set_bucket_axis(self, bucket_axis): + """ + Set the bucket axis label. + @param bucket_axis: The bucket axis label. + @type bucket_axis: str + """ + self.bucket_axis = bucket_axis + + def set_value_axis(self, value_axis): + """ + Set the value axis label. + @param bucket_axis: The value axis label. + @type bucket_axis: str + """ + self.value_axis = value_axis + + def set_grid_lines(self, grid_lines): + """ + Specify if grid lines should be displayed. + @param grid_lines: True if grid lines should be displayed. + @type grid_lines: bool + """ + self.grid_lines = grid_lines + + def set_values(self, data): + """ + Set the chart values. + @param data: A list of values, one for each bucket. + @type data: list + """ + self.data = data + + def set_labels(self, labels): + """ + Set the labels on the bucket axis. + @param labels: A list of labels, one for each bucket. + @type labels: list + """ + self.labels = labels + + def set_tooltip(self, tooltip): + """ + Set the tooltip to display on bars. If the string contains a "%d" + substitution variable it will be replaced with the value that the + bar represents. + @param labels: A tooltip. + @type labels: str + """ + self.tooltip = tooltip + + def set_highlight(self, highlight): + """ + Specify the bars to hightlight. + @param labels: A list of bucket numbers. + @type labels: list + """ + self.highlight = highlight + + def do_draw(self, cr): + """ + A custom draw method for this widget. + @param cr: A cairo context. + @type cr: cairo.Context + """ + allocation = self.get_allocation() + context = self.get_style_context() + fg_color = context.get_color(context.get_state()) + cr.set_source_rgba(*fg_color) + + # Title + layout = self.create_pango_layout(self.title) + width, height = layout.get_pixel_size() + offset = height + 5 + + # Labels + label_width = 0 + for i in range(len(self.labels)): + layout = self.create_pango_layout(self.labels[i]) + width, height = layout.get_pixel_size() + if width > label_width: + label_width = width + cr.move_to(0, i*height + offset) + PangoCairo.show_layout(cr, layout) + + layout = self.create_pango_layout(self.bucket_axis) + width, height = layout.get_pixel_size() + if width > label_width: + label_width = width + label_width += 5 + cr.move_to((label_width - width) / 2, 0) + PangoCairo.show_layout(cr, layout) + + # Values + percent_width = 0 + total = sum(self.data) + for i in range(len(self.data)): + if total > 0: + percent = glocale.format('%.2f', self.data[i] / total * 100) + else: + percent = '' + layout = self.create_pango_layout(percent) + width, height = layout.get_pixel_size() + if width > percent_width: + percent_width = width + cr.move_to(allocation.width-width, i*height + offset) + PangoCairo.show_layout(cr, layout) + + layout = self.create_pango_layout(self.value_axis) + width, height = layout.get_pixel_size() + if width > percent_width: + percent_width = width + percent_width += 5 + cr.move_to(allocation.width - (percent_width + width) / 2, 0) + PangoCairo.show_layout(cr, layout) + + chart_width = allocation.width - label_width - percent_width + spacing = 2 + + # Title + layout = self.create_pango_layout(self.title) + layout.set_ellipsize(Pango.EllipsizeMode.END) + layout.set_width((chart_width - 10) * Pango.SCALE) + cr.move_to(label_width + 5, 0) + PangoCairo.show_layout(cr, layout) + + # Border + cr.move_to(0, offset) + cr.line_to(allocation.width, offset) + cr.stroke() + + bottom = len(self.data) * height + (2 * spacing) + offset + cr.move_to(0, bottom) + cr.line_to(allocation.width, bottom) + cr.stroke() + + cr.move_to(label_width, 0) + cr.line_to(label_width, bottom) + cr.stroke() + + cr.move_to(allocation.width - percent_width, 0) + cr.line_to(allocation.width - percent_width, bottom) + cr.stroke() + + # Ticks and grid lines + tick_step, maximum = self.__get_tick_step(chart_width) + count = 0 + while count <= maximum: + # draw tick + tick_pos = label_width + chart_width * count / maximum + cr.move_to(tick_pos, bottom) + cr.line_to(tick_pos, bottom + 5) + cr.stroke() + # draw grid line + if self.grid_lines: + cr.set_dash([1, 2]) + cr.move_to(tick_pos, bottom) + cr.line_to(tick_pos, (2 * spacing) + offset) + cr.stroke() + cr.set_dash([]) + layout = self.create_pango_layout('%d' % count) + width, height = layout.get_pixel_size() + cr.move_to(tick_pos - (width / 2), bottom + 5) + PangoCairo.show_layout(cr, layout) + count += tick_step + + # Bars + cr.set_line_width(1) + bar_size = height - (2 * spacing) + self.__bars = [] + for i in range(len(self.labels)): + cr.rectangle(label_width, + i * height + (2 * spacing) + offset, + chart_width * self.data[i] / maximum, + bar_size) + self.__bars.append([label_width, + i * height + (2 * spacing) + offset, + chart_width * self.data[i] / maximum, + bar_size]) + if i in self.highlight: + if self.__active == i: + cr.set_source_rgba(1, 0.7, 0, 1) + else: + cr.set_source_rgba(1, 0.5, 0, 1) + else: + if self.__active == i: + cr.set_source_rgba(0.7, 0.7, 1, 1) + else: + cr.set_source_rgba(0.5, 0.5, 1, 1) + + cr.fill_preserve() + cr.set_source_rgba(*fg_color) + cr.stroke() + + self.set_size_request(-1, bottom + height + 5) + + def __get_tick_step(self, chart_width): + """ + A method used to calculate the value axis scale and label spacing. + @param chart_width: The chart size in pixels. + @type chart_width: int + """ + max_data = max(self.data) + if max_data == 0: + return 1, 1 + digits = int(math.log10(max_data)) + 1 + ticks = chart_width / (digits * 10 * 3) + approx_step = max_data / ticks + if approx_step < 1: + approx_step = 1 + multiplier = 10 ** int(math.log10(approx_step)) + intervals = [1, 2, 5, 10] + for interval in intervals: + if interval >= approx_step / multiplier: + break + step = interval * multiplier + max_value = (int(max_data / step) + 1) * step + return step, max_value + + def on_pointer_motion(self, _dummy, event): + """ + Called when the pointer is moved. + @param _dummy: This widget. Unused. + @type _dummy: Gtk.Widget + @param event: An event. + @type event: Gdk.Event + """ + if self.__bars is None: + return False + active = -1 + for i, bar in enumerate(self.__bars): + if (event.x > bar[0] and event.x < bar[0] + bar[2] and + event.y > bar[1] and event.y < bar[1] + bar[3]): + active = i + if self.__active != active: + self.__active = active + self.queue_draw() + if active == -1: + self.set_tooltip_text('') + else: + if '%d' in self.tooltip: + self.set_tooltip_text(self.tooltip % self.data[active]) + else: + self.set_tooltip_text(self.tooltip) + return False + + def on_button_press(self, _dummy, event): + """ + Called when a mouse button is clicked. + @param _dummy: This widget. Unused. + @type _dummy: Gtk.Widget + @param event: An event. + @type event: Gdk.Event + """ + if (event.button == 1 and + event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS and + self.__active != -1): + self.emit('clicked', self.__active) diff --git a/gramps/plugins/gramplet/agestats.py b/gramps/plugins/gramplet/agestats.py index 01665153b..977bc76e3 100644 --- a/gramps/plugins/gramplet/agestats.py +++ b/gramps/plugins/gramplet/agestats.py @@ -2,6 +2,7 @@ # Gramps - a GTK+/GNOME based genealogy program # # Copyright (C) 2008 Douglas S. Blank +# Copyright (C) 2019 Nick Hall # # 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 @@ -21,7 +22,7 @@ """ Age Stats Gramplet -This Gramplet shows textual distributions of age breakdowns of various types. +This Gramplet shows distributions of age breakdowns of various types. """ #------------------------------------------------------------------------- # @@ -30,23 +31,51 @@ This Gramplet shows textual distributions of age breakdowns of various types. #------------------------------------------------------------------------- from collections import defaultdict +#------------------------------------------------------------------------- +# +# Gtk modules +# +#------------------------------------------------------------------------- +from gi.repository import Gtk + #------------------------------------------------------------------------ # # Gramps modules # #------------------------------------------------------------------------ from gramps.gen.plug import Gramplet -from gramps.gen.lib import ChildRefType +from gramps.gen.lib import Date, ChildRefType +from gramps.gui.widgets import Histogram +from gramps.gui.plug.quick import run_quick_report_by_name from gramps.gen.const import GRAMPS_LOCALE as glocale _ = glocale.translation.gettext class AgeStatsGramplet(Gramplet): def init(self): + self.gui.WIDGET = self.build_gui() + self.gui.get_container_widget().remove(self.gui.textview) + self.gui.get_container_widget().add(self.gui.WIDGET) + self.gui.WIDGET.show() + self.max_age = 110 - self.max_mother_diff = 40 - self.max_father_diff = 60 - self.chart_width = 60 + self.max_mother_diff = 55 + self.max_father_diff = 70 + + def db_changed(self): + self.connect(self.dbstate.db, 'person-add', self.update) + self.connect(self.dbstate.db, 'person-delete', self.update) + self.connect(self.dbstate.db, 'person-update', self.update) + self.connect(self.dbstate.db, 'event-update', self.update) + + def build_gui(self): + self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + self.vbox.set_margin_start(6) + self.vbox.set_margin_end(6) + self.vbox.set_margin_top(6) + self.vbox.set_margin_bottom(6) + self.vbox.set_spacing(12) + return self.vbox def build_options(self): from gramps.gen.plug.menu import NumberOption @@ -56,247 +85,212 @@ class AgeStatsGramplet(Gramplet): self.max_mother_diff, 5, 150, 5)) self.add_option(NumberOption(_("Max age of Father at birth"), self.max_father_diff, 5, 150, 5)) - self.add_option(NumberOption(_("Chart width"), - self.chart_width, 45, 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") - if len(self.gui.data) == 4: + if len(self.gui.data) == 3: self.max_age = int(self.gui.data[0]) self.max_mother_diff = int(self.gui.data[1]) self.max_father_diff = int(self.gui.data[2]) - self.chart_width = int(self.gui.data[3]) def save_update_options(self, widget=None): 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()) self.gui.data = [self.max_age, self.max_mother_diff, - self.max_father_diff, - self.chart_width] - self.update() - - def db_changed(self): + self.max_father_diff] self.update() def main(self): - self.clear_text() + for widget in self.vbox.get_children(): + self.vbox.remove(widget) + if not self.dbstate.is_open(): + return age_dict = defaultdict(int) mother_dict = defaultdict(int) father_dict = defaultdict(int) - age_handles = [[] for i in range(self.max_age)] - mother_handles = [[] for i in range(self.max_mother_diff)] - father_handles = [[] for i in range(self.max_father_diff)] + age_handles = defaultdict(list) + mother_handles = defaultdict(list) + father_handles = defaultdict(list) text = "" count = 0 - for p in self.dbstate.db.iter_people(): + for person 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] == ChildRefType.BIRTH: - m_handle = family.get_mother_handle() - else: - m_handle = None - if childrel[0][1] == 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()) + birth_date = self.get_date('BIRTH', person) + death_date = self.get_date('DEATH', person) + if birth_date: + if death_date: + age = (death_date - birth_date).tuple()[0] + if age >= 0: + age_dict[age] += 1 + age_handles[age].append(person.handle) + + # for each parent m/f: + mother, father = self.get_birth_parents(person) + if mother: + bdate = self.get_date('BIRTH', mother) + if bdate: + diff = (birth_date - bdate).tuple()[0] + if diff >= 0: + mother_dict[diff] += 1 + mother_handles[diff].append(mother.handle) + if father: + bdate = self.get_date('BIRTH', father) + if bdate: + diff = (birth_date - bdate).tuple()[0] + if diff >= 0: + father_dict[diff] += 1 + father_handles[diff].append(father.handle) + 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 + self.create_histogram(age_dict, age_handles, + _("Lifespan Age Distribution"), + _("Age"), 5, self.max_age) + self.create_histogram(father_dict, father_handles, + _("Father - Child Age Diff Distribution"), + _("Diff"), 5, self.max_father_diff) + self.create_histogram(mother_dict, mother_handles, + _("Mother - Child Age Diff Distribution"), + _("Diff"), 5, self.max_mother_diff) - 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 get_date(self, event_type, person): + """ + Find the birth or death date of a given person. + """ + if event_type == 'BIRTH': + ref = person.get_birth_ref() + else: + ref = person.get_death_ref() + if ref: + event = self.dbstate.db.get_event_from_handle(ref.ref) + date = event.get_date_object() + if date.is_valid(): + return date + return None - def compute_stats(self, hash): - """ Returns the statistics of a dictionary of data """ - #print "compute_stats", hash - hashkeys = sorted(hash) - count = sum(hash.values()) - sumval = sum(k * hash[k] for k in hash) - minval = min(hashkeys) - maxval = max(hashkeys) + def get_birth_parents(self, person): + """ + Find the biological parents of a given person. + """ + m_handle = None + f_handle = None + family_list = person.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 == person.handle] + if childrel[0][0] == ChildRefType.BIRTH: + m_handle = family.get_mother_handle() + if childrel[0][1] == ChildRefType.BIRTH: + f_handle = family.get_father_handle() + mother = None + father = None + if m_handle: + mother = self.dbstate.db.get_person_from_handle(m_handle) + if f_handle: + father = self.dbstate.db.get_person_from_handle(f_handle) + return mother, father + + def compute_stats(self, data): + """ + Create a table of statistics based on a dictionary of data. + """ + keys = sorted(data) + count = sum(data.values()) + sumval = sum(k * data[k] for k in data) + minval = min(keys) + maxval = max(keys) median = 0 average = 0 if count > 0: current = 0 - for k in hashkeys: - if current + hash[k] > count/2: + for k in keys: + if current + data[k] > count/2: median = k break - current += hash[k] + current += data[k] average = sumval/float(count) - retval = _("Statistics") + ":\n" - retval += " " + _("Total") + ": %d\n" % count - retval += " " + _("Minimum") + ": %d\n" % minval - retval += " " + _("Average") + glocale.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 + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + label = Gtk.Label(label=_("Statistics") + ":") + label.set_halign(Gtk.Align.START) + vbox.pack_start(label, False, False, 0) + grid = Gtk.Grid() + grid.set_margin_start(12) + grid.set_column_spacing(12) + rows = [[_("Total"), "%d" % count], + [_("Minimum"), "%d" % minval], + [_("Average"), glocale.format_string("%.1f", average)], + [_("Median"), "%d" % median], + [_("Maximum"), "%d" % maxval]] + for row, value in enumerate(rows): + label1 = Gtk.Label(label=value[0] + ":") + label1.set_halign(Gtk.Align.START) + grid.attach(label1, 0, row, 1, 1) + label2 = Gtk.Label(label=value[1]) + label2.set_halign(Gtk.Align.END) + grid.attach(label2, 1, row, 1, 1) + vbox.pack_start(grid, False, False, 0) + vbox.show_all() - def create_bargraph(self, hash, handles, title, column, graph_width, bin_size, max_val): + return vbox + + def create_histogram(self, data, handles, title, column, interval, 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} + Create a histogram based on a dictionary of data, like: + data = {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] * int(max_val/bin_size) - for value, hash_value in hash.items(): - bin[int(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.values())))*100) - self.append_text(glocale.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.values())))*100)) + - "\n\n") - self.append_text(self.compute_stats(hash)) + if len(data) == 0: + return + + buckets = [0] * (int(max_val/interval) + 1) + handle_data = defaultdict(list) + for value, count in data.items(): + if value > max_val: + buckets[int(max_val/interval)] += count + handle_data[int(max_val/interval)].extend(handles[value]) + else: + buckets[int(value/interval)] += count + handle_data[int(value/interval)].extend(handles[value]) + + labels = [] + for i in range(int(max_val/interval)): + labels.append("%d-%d" % (i * interval, (i+1)* interval,)) + labels.append("%d+" % ((i+1)* interval,)) + + hist = Histogram() + hist.set_title(title) + hist.set_bucket_axis(column) + hist.set_value_axis('%') + hist.set_values(buckets) + hist.set_labels(labels) + hist.set_tooltip(_('Double-click to see %d people')) + hist.set_highlight([len(labels) - 1]) + hist.connect('clicked', self.on_bar_clicked, handle_data) + hist.show() + self.vbox.pack_start(hist, True, True, 0) + + stats = self.compute_stats(data) + self.vbox.pack_start(stats, False, False, 0) + + def on_bar_clicked(self, _dummy, value, handle_data): + """ + Called when a histogram bar is double-clicked. + """ + run_quick_report_by_name(self.gui.dbstate, + self.gui.uistate, + 'filterbyname', + 'list of people', + handles=handle_data[value]) diff --git a/po/POTFILES.skip b/po/POTFILES.skip index acd9d6a69..4f267660f 100644 --- a/po/POTFILES.skip +++ b/po/POTFILES.skip @@ -435,6 +435,7 @@ gramps/gui/widgets/dateentry.py gramps/gui/widgets/fanchart2way.py gramps/gui/widgets/fanchartdesc.py gramps/gui/widgets/grabbers.py +gramps/gui/widgets/histogram.py gramps/gui/widgets/interactivesearchbox.py gramps/gui/widgets/linkbox.py gramps/gui/widgets/menuitem.py