gramps/src/plugins/drawreport/StatisticsChart.py

1108 lines
41 KiB
Python
Raw Normal View History

#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2003-2006 Donald N. Allingham
# Copyright (C) 2004-2005 Eero Tamminen
# Copyright (C) 2007-2008 Brian G. Matherly
# Copyright (C) 2008 Peter Landgren
# Copyright (C) 2010 Jakim Friant
#
# 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$
"""Reports/Graphical Reports/Statistics Report"""
#------------------------------------------------------------------------
#
# python modules
#
#------------------------------------------------------------------------
import time
from gen.ggettext import sgettext as _
#------------------------------------------------------------------------
#
# GRAMPS modules
#
#------------------------------------------------------------------------
# Person and relation types
from gen.lib import Person, FamilyRelType, EventType, EventRoleType
from gen.lib.date import Date
# gender and report type names
from gen.plug.docgen import (FontStyle, ParagraphStyle, GraphicsStyle,
FONT_SANS_SERIF, FONT_SERIF,
PARA_ALIGN_CENTER, PARA_ALIGN_LEFT)
from gen.plug.menu import BooleanOption, NumberOption, EnumeratedListOption, \
FilterOption, PersonOption
from gen.plug.report import Report
from gen.plug.report import utils as ReportUtils
from gui.plug.report import MenuReportOptions
import DateHandler
from gui.utils import ProgressMeter
#------------------------------------------------------------------------
#
# Private Functions
#
#------------------------------------------------------------------------
def draw_wedge(doc, style, centerx, centery, radius, start_angle,
end_angle, short_radius=0):
from math import pi, cos, sin
while end_angle < start_angle:
end_angle += 360
p = []
degreestoradians = pi / 180.0
radiansdelta = degreestoradians / 2
sangle = start_angle * degreestoradians
eangle = end_angle * degreestoradians
while eangle < sangle:
eangle = eangle + 2 * pi
angle = sangle
if short_radius == 0:
if (end_angle - start_angle) != 360:
p.append((centerx, centery))
else:
origx = (centerx + cos(angle) * short_radius)
origy = (centery + sin(angle) * short_radius)
p.append((origx, origy))
while angle < eangle:
x = centerx + cos(angle) * radius
y = centery + sin(angle) * radius
p.append((x, y))
angle = angle + radiansdelta
x = centerx + cos(eangle) * radius
y = centery + sin(eangle) * radius
p.append((x, y))
if short_radius:
x = centerx + cos(eangle) * short_radius
y = centery + sin(eangle) * short_radius
p.append((x, y))
angle = eangle
while angle >= sangle:
x = centerx + cos(angle) * short_radius
y = centery + sin(angle) * short_radius
p.append((x, y))
angle -= radiansdelta
doc.draw_path(style, p)
delta = (eangle - sangle) / 2.0
rad = short_radius + (radius - short_radius) / 2.0
return ( (centerx + cos(sangle + delta) * rad),
(centery + sin(sangle + delta) * rad))
def draw_pie_chart(doc, center_x, center_y, radius, data, start=0):
"""
Draws a pie chart in the specified document. The data passed is plotted as
a pie chart. The data should consist of the actual data. Percentages of
each slice are determined by the routine.
@param doc: Document to which the pie chart should be added
@type doc: BaseDoc derived class
@param center_x: x coordinate in centimeters where the center of the pie
chart should be. 0 is the left hand edge of the document.
@type center_x: float
@param center_y: y coordinate in centimeters where the center of the pie
chart should be. 0 is the top edge of the document
@type center_y: float
@param radius: radius of the pie chart. The pie charts width and height
will be twice this value.
@type radius: float
@param data: List of tuples containing the data to be plotted. The values
are (graphics_format, value), where graphics_format is a BaseDoc
GraphicsStyle, and value is a floating point number. Any other items in
the tuple are ignored. This allows you to share the same data list with
the L{draw_legend} function.
@type data: list
@param start: starting point in degrees, where the default of 0 indicates
a start point extending from the center to right in a horizontal line.
@type start: float
"""
total = 0.0
for item in data:
total += item[1]
for item in data:
incr = 360.0*(item[1]/total)
draw_wedge(doc, item[0], center_x, center_y, radius, start, start + incr)
start += incr
def draw_legend(doc, start_x, start_y, data, title, label_style):
"""
Draws a legend for a graph in the specified document. The data passed is
used to define the legend. First item style is used for the optional
Legend title.
@param doc: Document to which the legend chart should be added
@type doc: BaseDoc derived class
@param start_x: x coordinate in centimeters where the left hand corner
of the legend is placed. 0 is the left hand edge of the document.
@type start_x: float
@param start_y: y coordinate in centimeters where the top of the legend
should be. 0 is the top edge of the document
@type start_y: float
@param data: List of tuples containing the data to be used to create the
legend. In order to be compatible with the graph plots, the first and
third values of the tuple used. The format is (graphics_format, value,
legend_description).
@type data: list
"""
style_sheet = doc.get_style_sheet()
if title:
gstyle = style_sheet.get_draw_style(label_style)
pstyle_name = gstyle.get_paragraph_style()
pstyle = style_sheet.get_paragraph_style(pstyle_name)
size = ReportUtils.pt2cm(pstyle.get_font().get_size())
doc.draw_text(label_style, title, start_x + (3*size), start_y - (size*0.25))
start_y += size * 1.3
for (format, size, legend) in data:
gstyle = style_sheet.get_draw_style(format)
pstyle_name = gstyle.get_paragraph_style()
pstyle = style_sheet.get_paragraph_style(pstyle_name)
size = ReportUtils.pt2cm(pstyle.get_font().get_size())
doc.draw_box(format, "", start_x, start_y, (2*size), size)
doc.draw_text(label_style, legend, start_x + (3*size), start_y - (size*0.25))
start_y += size * 1.3
_t = time.localtime(time.time())
_TODAY = DateHandler.parser.parse("%04d-%02d-%02d" % _t[:3])
def estimate_age(db, person, end_handle=None, start_handle=None, today=_TODAY):
"""
Estimates the age of a person based off the birth and death
dates of the person. A tuple containing the estimated upper
and lower bounds of the person's age is returned. If either
the birth or death date is missing, a (-1, -1) is returned.
@param db: GRAMPS database to which the Person object belongs
@type db: DbBase
@param person: Person object to calculate the age of
@type person: Person
@param end_handle: Determines the event handle that determines
the upper limit of the age. If None, the death event is used
@type end_handle: str
@param start_handle: Determines the event handle that determines
the lower limit of the event. If None, the birth event is
used
@type start_handle: str
@returns: tuple containing the lower and upper bounds of the
person's age, or (-1, -1) if it could not be determined.
@rtype: tuple
"""
bhandle = None
if start_handle:
bhandle = start_handle
else:
bref = person.get_birth_ref()
if bref:
bhandle = bref.get_reference_handle()
dhandle = None
if end_handle:
dhandle = end_handle
else:
dref = person.get_death_ref()
if dref:
dhandle = dref.get_reference_handle()
# if either of the events is not defined, return an error message
if not bhandle:
return (-1, -1)
bdata = db.get_event_from_handle(bhandle).get_date_object()
if dhandle:
ddata = db.get_event_from_handle(dhandle).get_date_object()
else:
if today is not None:
ddata = today
else:
return (-1, -1)
# if the date is not valid, return an error message
if not bdata.get_valid() or not ddata.get_valid():
return (-1, -1)
# if a year is not valid, return an error message
if not bdata.get_year_valid() or not ddata.get_year_valid():
return (-1, -1)
bstart = bdata.get_start_date()
bstop = bdata.get_stop_date()
dstart = ddata.get_start_date()
dstop = ddata.get_stop_date()
def _calc_diff(low, high):
if (low[1], low[0]) > (high[1], high[0]):
return high[2] - low[2] - 1
else:
return high[2] - low[2]
if bstop == dstop == Date.EMPTY:
lower = _calc_diff(bstart, dstart)
age = (lower, lower)
elif bstop == Date.EMPTY:
lower = _calc_diff(bstart, dstart)
upper = _calc_diff(bstart, dstop)
age = (lower, upper)
elif dstop == Date.EMPTY:
lower = _calc_diff(bstop, dstart)
upper = _calc_diff(bstart, dstart)
age = (lower, upper)
else:
lower = _calc_diff(bstop, dstart)
upper = _calc_diff(bstart, dstop)
age = (lower, upper)
return age
#------------------------------------------------------------------------
#
# Global options and their names
#
#------------------------------------------------------------------------
class _options:
# sort type identifiers
SORT_VALUE = 0
SORT_KEY = 1
sorts = [
(SORT_VALUE, "Item count", _("Item count")),
(SORT_KEY, "Item name", _("Item name"))
]
genders = [
(Person.UNKNOWN, "Both", _("Both")),
(Person.MALE, "Men", _("Men")),
(Person.FEMALE, "Women", _("Women"))
]
#------------------------------------------------------------------------
#
# Data extraction methods from the database
#
#------------------------------------------------------------------------
class Extract(object):
def __init__(self):
"""Methods for extracting statistical data from the database"""
# key, non-localized name, localized name, type method, data method
self.extractors = {
'data_title': ("Title", _("person|Title"),
self.get_person, self.get_title),
2005-12-06 12:08:09 +05:30
'data_sname': ("Surname", _("Surname"),
self.get_person, self.get_surname),
2005-12-06 12:08:09 +05:30
'data_fname': ("Forename", _("Forename"),
self.get_person, self.get_forename),
2005-12-06 12:08:09 +05:30
'data_gender': ("Gender", _("Gender"),
self.get_person, self.get_gender),
2005-12-06 12:08:09 +05:30
'data_byear': ("Birth year", _("Birth year"),
self.get_birth, self.get_year),
2005-12-06 12:08:09 +05:30
'data_dyear': ("Death year", _("Death year"),
self.get_death, self.get_year),
2005-12-06 12:08:09 +05:30
'data_bmonth': ("Birth month", _("Birth month"),
self.get_birth, self.get_month),
2005-12-06 12:08:09 +05:30
'data_dmonth': ("Death month", _("Death month"),
self.get_death, self.get_month),
2005-12-06 12:08:09 +05:30
'data_bplace': ("Birth place", _("Birth place"),
self.get_birth, self.get_place),
2005-12-06 12:08:09 +05:30
'data_dplace': ("Death place", _("Death place"),
self.get_death, self.get_place),
2005-12-06 12:08:09 +05:30
'data_mplace': ("Marriage place", _("Marriage place"),
self.get_marriage_handles, self.get_places),
'data_mcount': ("Number of relationships", _("Number of relationships"),
self.get_family_handles, self.get_handle_count),
2005-12-06 12:08:09 +05:30
'data_fchild': ("Age when first child born", _("Age when first child born"),
self.get_child_handles, self.get_first_child_age),
2005-12-06 12:08:09 +05:30
'data_lchild': ("Age when last child born", _("Age when last child born"),
self.get_child_handles, self.get_last_child_age),
'data_ccount': ("Number of children", _("Number of children"),
self.get_child_handles, self.get_handle_count),
2005-12-06 12:08:09 +05:30
'data_mage': ("Age at marriage", _("Age at marriage"),
self.get_marriage_handles, self.get_event_ages),
2005-12-06 12:08:09 +05:30
'data_dage': ("Age at death", _("Age at death"),
self.get_person, self.get_death_age),
2005-12-06 12:08:09 +05:30
'data_age': ("Age", _("Age"),
self.get_person, self.get_person_age),
2005-12-06 12:08:09 +05:30
'data_etypes': ("Event type", _("Event type"),
self.get_event_handles, self.get_event_type)
}
2005-01-11 05:58:53 +05:30
# ----------------- data extraction methods --------------------
# take an object and return a list of strings
2005-01-11 05:58:53 +05:30
def get_title(self, person):
"return title for given person"
# TODO: return all titles, not just primary ones...
2005-01-11 05:58:53 +05:30
title = person.get_primary_name().get_title()
if title:
return [title]
else:
return [_("(Preferred) title missing")]
2005-01-11 05:58:53 +05:30
def get_forename(self, person):
"return forenames for given person"
# TODO: return all forenames, not just primary ones...
2005-01-11 05:58:53 +05:30
firstnames = person.get_primary_name().get_first_name().strip()
if firstnames:
return firstnames.split()
2005-01-11 05:58:53 +05:30
else:
return [_("(Preferred) forename missing")]
def get_surname(self, person):
"return surnames for given person"
# TODO: return all surnames, not just primary ones...
surnames = person.get_primary_name().get_surname().strip()
if surnames:
return surnames.split()
else:
return [_("(Preferred) surname missing")]
def get_gender(self, person):
"return gender for given person"
2005-01-11 05:58:53 +05:30
# TODO: why there's no Person.getGenderName?
# It could be used by getDisplayInfo & this...
if person.gender == Person.MALE:
return [_("Men")]
if person.gender == Person.FEMALE:
return [_("Women")]
return [_("Gender unknown")]
def get_year(self, event):
"return year for given event"
date = event.get_date_object()
if date:
year = date.get_year()
if year:
return [str(year)]
return [_("Date(s) missing")]
def get_month(self, event):
"return month for given event"
date = event.get_date_object()
if date:
month = date.get_month()
if month:
return [DateHandler.displayer.long_months[month]]
return [_("Date(s) missing")]
def get_place(self, event):
"return place for given event"
place_handle = event.get_place_handle()
if place_handle:
place = self.db.get_place_from_handle(place_handle).get_title()
if place:
return [place]
return [_("Place missing")]
def get_places(self, data):
"return places for given (person,event_handles)"
places = []
person, event_handles = data
for event_handle in event_handles:
event = self.db.get_event_from_handle(event_handle)
place_handle = event.get_place_handle()
if place_handle:
place = self.db.get_place_from_handle(place_handle).get_title()
if place:
places.append(place)
else:
places.append(_("Place missing"))
return places
def get_person_age(self, person):
"return age for given person, if alive"
death_ref = person.get_death_ref()
if not death_ref:
return [self.estimate_age(person)]
return [_("Already dead")]
def get_death_age(self, person):
"return age at death for given person, if dead"
death_ref = person.get_death_ref()
if death_ref:
return [self.estimate_age(person, death_ref.ref)]
return [_("Still alive")]
def get_event_ages(self, data):
"return ages at given (person,event_handles)"
person, event_handles = data
ages = [self.estimate_age(person, h) for h in event_handles]
if ages:
return ages
return [_("Events missing")]
def get_event_type(self, data):
"return event types at given (person,event_handles)"
types = []
person, event_handles = data
for event_handle in event_handles:
event = self.db.get_event_from_handle(event_handle)
evtType = str(event.get_type())
types.append(evtType)
if types:
return types
return [_("Events missing")]
def get_first_child_age(self, data):
"return age when first child in given (person,child_handles) was born"
ages, errors = self.get_sorted_child_ages(data)
if ages:
errors.append(ages[0])
return errors
return [_("Children missing")]
def get_last_child_age(self, data):
"return age when last child in given (person,child_handles) was born"
ages, errors = self.get_sorted_child_ages(data)
if ages:
errors.append(ages[-1])
return errors
return [_("Children missing")]
def get_handle_count(self, data):
"return number of handles in given (person, handle_list) used for child count, family count"
return ["%3d" % len(data[1])]
# ------------------- utility methods -------------------------
def get_sorted_child_ages(self, data):
"return (sorted_ages,errors) for given (person,child_handles)"
ages = []
errors = []
person, child_handles = data
for child_handle in child_handles:
child = self.db.get_person_from_handle(child_handle)
birth_ref = child.get_birth_ref()
if birth_ref:
ages.append(self.estimate_age(person, birth_ref.ref))
else:
errors.append(_("Birth missing"))
continue
ages.sort()
return (ages, errors)
def estimate_age(self, person, end=None, begin=None):
"""return estimated age (range) for given person or error message.
age string is padded with spaces so that it can be sorted"""
age = estimate_age(self.db, person, end, begin)
if age[0] < 0 or age[1] < 0:
# inadequate information
return _("Date(s) missing")
if age[0] == age[1]:
# exact year
return "%3d" % age[0]
else:
# minimum and maximum
return "%3d-%d" % (age[0], age[1])
# ------------------- type methods -------------------------
# take db and person and return suitable gramps object(s)
def get_person(self, person):
"return person"
return person
def get_birth(self, person):
"return birth event for given person or None"
birth_ref = person.get_birth_ref()
if birth_ref:
return self.db.get_event_from_handle(birth_ref.ref)
return None
def get_death(self, person):
"return death event for given person or None"
death_ref = person.get_death_ref()
if death_ref:
return self.db.get_event_from_handle(death_ref.ref)
return None
def get_child_handles(self, person):
"return list of child handles for given person or None"
children = []
for fam_handle in person.get_family_handle_list():
fam = self.db.get_family_from_handle(fam_handle)
for child_ref in fam.get_child_ref_list():
children.append(child_ref.ref)
# TODO: it would be good to return only biological children,
# but GRAMPS doesn't offer any efficient way to check that
# (I don't want to check each children's parent family mother
# and father relations as that would make this *much* slower)
if children:
return (person, children)
return None
def get_marriage_handles(self, person):
"return list of marriage event handles for given person or None"
marriages = []
for family_handle in person.get_family_handle_list():
family = self.db.get_family_from_handle(family_handle)
if int(family.get_relationship()) == FamilyRelType.MARRIED:
for event_ref in family.get_event_ref_list():
event = self.db.get_event_from_handle(event_ref.ref)
if event.get_type() == EventType.MARRIAGE and \
(event_ref.get_role() == EventRoleType.FAMILY or
event_ref.get_role() == EventRoleType.PRIMARY ):
marriages.append(event_ref.ref)
if marriages:
return (person, marriages)
return None
def get_family_handles(self, person):
"return list of family handles for given person or None"
families = person.get_family_handle_list()
if families:
return (person, families)
return None
def get_event_handles(self, person):
"return list of event handles for given person or None"
events = [ref.ref for ref in person.get_event_ref_list()]
if events:
return (person, events)
return None
# ----------------- data collection methods --------------------
def get_person_data(self, person, collect):
"""Add data from the database to 'collect' for the given person,
using methods rom the 'collect' data dict tuple
"""
for chart in collect:
# get the information
type_func = chart[2]
data_func = chart[3]
obj = type_func(person) # e.g. get_date()
if obj:
value = data_func(obj) # e.g. get_year()
else:
value = [_("Personal information missing")]
# list of information found
for key in value:
if key in chart[1]:
chart[1][key] += 1
else:
chart[1][key] = 1
def collect_data(self, db, filter_func, menu, genders,
year_from, year_to, no_years):
"""goes through the database and collects the selected personal
2005-01-11 05:58:53 +05:30
data persons fitting the filter and birth year criteria. The
arguments are:
db - the GRAMPS database
filter_func - filtering function selected by the StatisticsDialog
options - report options_dict which sets which methods are used
genders - which gender(s) to include into statistics
year_from - use only persons who've born this year of after
year_to - use only persons who've born this year or before
2005-05-24 18:38:06 +05:30
no_years - use also people without known birth year
Returns an array of tuple of:
- Extraction method title
- Dict of values with their counts
(- Method)
2005-01-11 05:58:53 +05:30
"""
self.db = db # store for use by methods
data = []
ext = self.extractors
# which methods to use
for name in self.extractors:
option = menu.get_option_by_name(name)
if option.get_value() == True:
# localized data title, value dict, type and data method
data.append((ext[name][1], {}, ext[name][2], ext[name][3]))
2005-01-11 05:58:53 +05:30
# go through the people and collect data
for person_handle in filter_func.apply(db, db.iter_person_handles()):
2005-01-11 05:58:53 +05:30
person = db.get_person_from_handle(person_handle)
# check whether person has suitable gender
if person.gender != genders and genders != Person.UNKNOWN:
2005-01-11 05:58:53 +05:30
continue
# check whether birth year is within required range
birth = self.get_birth(person)
if birth:
birthdate = birth.get_date_object()
if birthdate.get_year_valid():
year = birthdate.get_year()
2005-01-11 05:58:53 +05:30
if not (year >= year_from and year <= year_to):
continue
else:
# if death before range, person's out of range too...
death = self.get_death(person)
if death:
deathdate = death.get_date_object()
if deathdate.get_year_valid() and deathdate.get_year() < year_from:
2005-01-11 05:58:53 +05:30
continue
if not no_years:
# do not accept people who are not known to be in range
continue
else:
continue
else:
continue
2005-01-11 05:58:53 +05:30
self.get_person_data(person, data)
return data
2005-01-12 02:01:51 +05:30
# GLOBAL: required so that we get access to _Extract.extractors[]
# Unfortunately class variables cannot reference instance methods :-/
_Extract = Extract()
#------------------------------------------------------------------------
#
# Statistics report
#
#------------------------------------------------------------------------
class StatisticsChart(Report):
def __init__(self, database, options_class):
"""
Create the Statistics object that produces the report.
2005-01-11 05:58:53 +05:30
Uses the Extractor class to extract the data from the database.
The arguments are:
database - the GRAMPS database instance
person - currently selected person
options_class - instance of the Options class for this report
2005-01-11 05:58:53 +05:30
To see what the options are, check the options help in the options class.
"""
Report.__init__(self, database, options_class)
menu = options_class.menu
2005-01-11 05:58:53 +05:30
self.filter_option = menu.get_option_by_name('filter')
self.filter = self.filter_option.get_filter()
self.bar_items = menu.get_option_by_name('bar_items').get_value()
year_from = menu.get_option_by_name('year_from').get_value()
year_to = menu.get_option_by_name('year_to').get_value()
gender = menu.get_option_by_name('gender').get_value()
2005-01-11 05:58:53 +05:30
# title needs both data extraction method name + gender name
if gender == Person.MALE:
genders = _("Men")
elif gender == Person.FEMALE:
genders = _("Women")
2005-01-11 05:58:53 +05:30
else:
genders = None
# needed for keyword based localization
mapping = {
'genders': genders,
'year_from': year_from,
'year_to': year_to
}
self.progress = ProgressMeter(_('Statistics Charts'))
2005-01-11 05:58:53 +05:30
# extract requested items from the database and count them
self.progress.set_pass(_('Collecting data...'), 1)
tables = _Extract.collect_data(database, self.filter, menu,
gender, year_from, year_to,
menu.get_option_by_name('no_years').get_value())
self.progress.step()
2005-01-11 05:58:53 +05:30
self.progress.set_pass(_('Sorting data...'), len(tables))
self.data = []
sortby = menu.get_option_by_name('sortby').get_value()
reverse = menu.get_option_by_name('reverse').get_value()
for table in tables:
# generate sorted item lookup index index
lookup = self.index_items(table[1], sortby, reverse)
# document heading
mapping['chart_title'] = table[0]
if genders:
heading = _("%(genders)s born %(year_from)04d-%(year_to)04d: %(chart_title)s") % mapping
else:
heading = _("Persons born %(year_from)04d-%(year_to)04d: %(chart_title)s") % mapping
2005-12-06 12:08:09 +05:30
self.data.append((heading, table[0], table[1], lookup))
self.progress.step()
#DEBUG
#print heading
#print table[1]
def lookup_compare(self, a, b):
"compare given keys according to corresponding lookup values"
return cmp(self.lookup_items[a], self.lookup_items[b])
def index_items(self, data, sort, reverse):
2005-01-11 05:58:53 +05:30
"""creates & stores a sorted index for the items"""
# sort by item keys
index = sorted(data, reverse=True if reverse else False)
2005-01-11 05:58:53 +05:30
if sort == _options.SORT_VALUE:
2005-01-11 05:58:53 +05:30
# set for the sorting function
self.lookup_items = data
2005-01-11 05:58:53 +05:30
# then sort by value
index.sort(self.lookup_compare,
reverse=True if reverse else False)
2005-01-11 05:58:53 +05:30
return index
2005-01-11 05:58:53 +05:30
def write_report(self):
2005-01-11 05:58:53 +05:30
"output the selected statistics..."
self.progress.set_pass(_('Saving charts...'), len(self.data))
for data in self.data:
self.doc.start_page()
if len(data[2]) < self.bar_items:
2009-07-03 21:23:28 +05:30
self.output_piechart(*data[:4])
else:
2009-07-03 21:23:28 +05:30
self.output_barchart(*data[:4])
2005-12-06 12:08:09 +05:30
self.doc.end_page()
self.progress.step()
self.progress.close()
2005-12-06 12:08:09 +05:30
def output_piechart(self, title, typename, data, lookup):
# set layout variables
middle_w = self.doc.get_usable_width() / 2
middle_h = self.doc.get_usable_height() / 2
middle = min(middle_w,middle_h)
# start output
self.doc.center_text('SC-title', title, middle_w, 0)
style_sheet = self.doc.get_style_sheet()
pstyle = style_sheet.get_paragraph_style('SC-Title')
yoffset = ReportUtils.pt2cm(pstyle.get_font().get_size())
# collect data for output
color = 0
chart_data = []
for key in lookup:
style = "SC-color-%d" % color
text = "%s (%d)" % (key, data[key])
# graphics style, value, and it's label
2005-12-06 12:08:09 +05:30
chart_data.append((style, data[key], text))
color = (color+1) % 7 # There are only 7 color styles defined
margin = 1.0
legendx = 2.0
# output data...
2005-12-06 12:08:09 +05:30
radius = middle - 2*margin
yoffset += margin + radius
draw_pie_chart(self.doc, middle_w, yoffset, radius, chart_data, -90)
yoffset += radius + 2*margin
if middle == middle_h: # Landscape
legendx = 1.0
yoffset = margin
text = _("%s (persons):") % typename
draw_legend(self.doc, legendx, yoffset, chart_data, text,'SC-legend')
2005-12-06 12:08:09 +05:30
def output_barchart(self, title, typename, data, lookup):
pt2cm = ReportUtils.pt2cm
style_sheet = self.doc.get_style_sheet()
pstyle = style_sheet.get_paragraph_style('SC-Text')
font = pstyle.get_font()
2005-01-11 05:58:53 +05:30
# set layout variables
width = self.doc.get_usable_width()
row_h = pt2cm(font.get_size())
2005-01-11 05:58:53 +05:30
max_y = self.doc.get_usable_height() - row_h
pad = row_h * 0.5
2005-12-06 12:08:09 +05:30
# check maximum value
max_value = max(data[k] for k in lookup) if lookup else 0
2005-01-11 05:58:53 +05:30
# horizontal area for the gfx bars
margin = 1.0
middle = width/2.0
textx = middle + margin/2.0
stopx = middle - margin/2.0
2005-12-06 12:08:09 +05:30
maxsize = stopx - margin
2005-01-11 05:58:53 +05:30
# start output
2005-12-06 12:08:09 +05:30
self.doc.center_text('SC-title', title, middle, 0)
pstyle = style_sheet.get_paragraph_style('SC-Title')
yoffset = pt2cm(pstyle.get_font().get_size())
#print title
2005-01-11 05:58:53 +05:30
# header
yoffset += (row_h + pad)
text = _("%s (persons):") % typename
self.doc.draw_text('SC-text', text, textx, yoffset)
2005-12-06 12:08:09 +05:30
for key in lookup:
2005-01-11 05:58:53 +05:30
yoffset += (row_h + pad)
if yoffset > max_y:
# for graphical report, page_break() doesn't seem to work
self.doc.end_page()
self.doc.start_page()
yoffset = 0
2005-12-06 12:08:09 +05:30
# right align bar to the text
value = data[key]
2005-12-06 12:08:09 +05:30
startx = stopx - (maxsize * value / max_value)
self.doc.draw_box('SC-bar',"",startx,yoffset,stopx-startx,row_h)
# text after bar
text = "%s (%d)" % (key, data[key])
2005-12-06 12:08:09 +05:30
self.doc.draw_text('SC-text', text, textx, yoffset)
#print key + ":",
2005-01-11 05:58:53 +05:30
return
#------------------------------------------------------------------------
#
# StatisticsChartOptions
#
#------------------------------------------------------------------------
class StatisticsChartOptions(MenuReportOptions):
def __init__(self, name, dbase):
self.__pid = None
self.__filter = None
self.__db = dbase
MenuReportOptions.__init__(self, name, dbase)
def add_menu_options(self, menu):
"""
Add options to the menu for the statistics report.
"""
category_name = _("Report Options")
self.__filter = FilterOption(_("Filter"), 0)
self.__filter.set_help(
_("Determines what people are included in the report."))
menu.add_option(category_name, "filter", self.__filter)
self.__filter.connect('value-changed', self.__filter_changed)
self.__pid = PersonOption(_("Filter Person"))
self.__pid.set_help(_("The center person for the filter."))
menu.add_option(category_name, "pid", self.__pid)
self.__pid.connect('value-changed', self.__update_filters)
self.__update_filters()
sortby = EnumeratedListOption(_('Sort chart items by'),
_options.SORT_VALUE )
for item_idx in range(len(_options.sorts)):
item = _options.sorts[item_idx]
sortby.add_item(item_idx,item[2])
sortby.set_help( _("Select how the statistical data is sorted."))
menu.add_option(category_name,"sortby",sortby)
reverse = BooleanOption(_("Sort in reverse order"), False)
reverse.set_help(_("Check to reverse the sorting order."))
menu.add_option(category_name,"reverse", reverse)
this_year = time.localtime()[0]
year_from = NumberOption(_("People Born After"),
1700, 1, this_year)
year_from.set_help(_("Birth year from which to include people."))
menu.add_option(category_name,"year_from", year_from)
year_to = NumberOption(_("People Born Before"),
this_year, 1, this_year)
year_to.set_help(_("Birth year until which to include people"))
menu.add_option(category_name,"year_to", year_to)
no_years = BooleanOption(_("Include people without known birth years"),
False)
no_years.set_help(_("Whether to include people without "
"known birth years."))
menu.add_option(category_name,"no_years", no_years)
gender = EnumeratedListOption(_('Genders included'),
Person.UNKNOWN )
for item_idx in range(len(_options.genders)):
item = _options.genders[item_idx]
gender.add_item(item[0],item[2])
gender.set_help( _("Select which genders are included into "
"statistics."))
menu.add_option(category_name,"gender",gender)
bar_items = NumberOption(_("Max. items for a pie"), 8, 0, 20)
bar_items.set_help(_("With fewer items pie chart and legend will be "
"used instead of a bar chart."))
menu.add_option(category_name,"bar_items", bar_items)
# -------------------------------------------------
# List of available charts on separate option tabs
idx = 0
half = (len(_Extract.extractors))/2
self.charts = {}
for key in _Extract.extractors:
if idx < half:
category_name = _("Charts 1")
else:
category_name = _("Charts 2")
opt = BooleanOption(_Extract.extractors[key][1], False)
opt.set_help(_("Include charts with indicated data."))
menu.add_option(category_name,key, opt)
idx += 1
# Enable a couple of charts by default
menu.get_option_by_name("data_gender").set_value(True)
menu.get_option_by_name("data_ccount").set_value(True)
menu.get_option_by_name("data_bmonth").set_value(True)
def __update_filters(self):
"""
Update the filter list based on the selected person
"""
gid = self.__pid.get_value()
person = self.__db.get_person_from_gramps_id(gid)
filter_list = ReportUtils.get_person_filters(person, False)
self.__filter.set_filters(filter_list)
def __filter_changed(self):
"""
Handle filter change. If the filter is not specific to a person,
disable the person option
"""
filter_value = self.__filter.get_value()
if filter_value in [1, 2, 3, 4]:
# Filters 1, 2, 3 and 4 rely on the center person
self.__pid.set_available(True)
else:
# The rest don't
self.__pid.set_available(False)
def make_default_style(self, default_style):
2005-01-11 05:58:53 +05:30
"""Make the default output style for the Statistics report."""
# Paragraph Styles
f = FontStyle()
2005-01-11 05:58:53 +05:30
f.set_size(10)
f.set_type_face(FONT_SERIF)
p = ParagraphStyle()
2005-01-11 05:58:53 +05:30
p.set_font(f)
p.set_alignment(PARA_ALIGN_LEFT)
2005-01-11 05:58:53 +05:30
p.set_description(_("The style used for the items and values."))
default_style.add_paragraph_style("SC-Text",p)
2005-01-11 05:58:53 +05:30
f = FontStyle()
2005-01-11 05:58:53 +05:30
f.set_size(14)
f.set_type_face(FONT_SANS_SERIF)
p = ParagraphStyle()
2005-01-11 05:58:53 +05:30
p.set_font(f)
p.set_alignment(PARA_ALIGN_CENTER)
2005-01-11 05:58:53 +05:30
p.set_description(_("The style used for the title of the page."))
default_style.add_paragraph_style("SC-Title",p)
"""
Graphic Styles:
SC-title - Contains the SC-Title paragraph style used for
the title of the document
SC-text - Contains the SC-Name paragraph style used for
the individual's name
SC-color-N - The colors for drawing pies.
SC-bar - A red bar with 0.5pt black line.
"""
g = GraphicsStyle()
g.set_paragraph_style("SC-Title")
g.set_color((0,0,0))
g.set_fill_color((255,255,255))
g.set_line_width(0)
default_style.add_draw_style("SC-title",g)
g = GraphicsStyle()
g.set_paragraph_style("SC-Text")
g.set_color((0,0,0))
g.set_fill_color((255,255,255))
g.set_line_width(0)
default_style.add_draw_style("SC-text",g)
width = 0.8
# red
g = GraphicsStyle()
g.set_paragraph_style('SC-Text')
g.set_color((0,0,0))
g.set_fill_color((255,0,0))
g.set_line_width(width)
default_style.add_draw_style("SC-color-0",g)
# orange
g = GraphicsStyle()
g.set_paragraph_style('SC-Text')
g.set_color((0,0,0))
g.set_fill_color((255,158,33))
g.set_line_width(width)
default_style.add_draw_style("SC-color-1",g)
# green
g = GraphicsStyle()
g.set_paragraph_style('SC-Text')
g.set_color((0,0,0))
g.set_fill_color((0,178,0))
g.set_line_width(width)
default_style.add_draw_style("SC-color-2",g)
# violet
g = GraphicsStyle()
g.set_paragraph_style('SC-Text')
g.set_color((0,0,0))
g.set_fill_color((123,0,123))
g.set_line_width(width)
default_style.add_draw_style("SC-color-3",g)
# yellow
g = GraphicsStyle()
g.set_paragraph_style('SC-Text')
g.set_color((0,0,0))
g.set_fill_color((255,255,0))
g.set_line_width(width)
default_style.add_draw_style("SC-color-4",g)
# blue
g = GraphicsStyle()
g.set_paragraph_style('SC-Text')
g.set_color((0,0,0))
g.set_fill_color((0,105,214))
g.set_line_width(width)
default_style.add_draw_style("SC-color-5",g)
# gray
g = GraphicsStyle()
g.set_paragraph_style('SC-Text')
g.set_color((0,0,0))
g.set_fill_color((210,204,210))
g.set_line_width(width)
default_style.add_draw_style("SC-color-6",g)
g = GraphicsStyle()
g.set_color((0,0,0))
g.set_fill_color((255,0,0))
g.set_line_width(width)
default_style.add_draw_style("SC-bar",g)
# legend
g = GraphicsStyle()
g.set_paragraph_style('SC-Text')
g.set_color((0,0,0))
g.set_fill_color((255,255,255))
g.set_line_width(0)
default_style.add_draw_style("SC-legend",g)