Improved Age Stats gramplet
* New Histogram widget * Extra bucket for ages above maximum * Automatic scaling to available width
This commit is contained in:
parent
72f7944814
commit
00fa6d472a
@ -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 *
|
||||
|
340
gramps/gui/widgets/histogram.py
Normal file
340
gramps/gui/widgets/histogram.py
Normal 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)
|
@ -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:
|
||||
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(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())
|
||||
age_handles[age].append(person.handle)
|
||||
|
||||
# for each parent m/f:
|
||||
family_list = p.get_parent_family_handle_list()
|
||||
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
|
||||
|
||||
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 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 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 == p.handle] # get first, if more than one
|
||||
if ref.ref == person.handle]
|
||||
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())
|
||||
mother = None
|
||||
father = None
|
||||
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")
|
||||
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 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.values())
|
||||
sumval = sum(k * hash[k] for k in hash)
|
||||
minval = min(hashkeys)
|
||||
maxval = max(hashkeys)
|
||||
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])
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user