Improved Age Stats gramplet

* New Histogram widget
* Extra bucket for ages above maximum
* Automatic scaling to available width
This commit is contained in:
Nick Hall 2019-10-20 19:54:16 +01:00
parent 72f7944814
commit 00fa6d472a
4 changed files with 541 additions and 205 deletions

View File

@ -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 *

View File

@ -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)

View File

@ -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])

View File

@ -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