class TimeLine(Report): """ TimeLine Report """ def __init__(self, database, options, user): """ Create the Timeline object that produces the report. The arguments are: database - the GRAMPS database instance options - instance of the Options class for this report user - instance of gen.user.User() This report needs the following parameters (class variables) that come in the options class. filter - Filter to be applied to the people of the database. The option class carries its number, and the function returning the list of filters. sortby - Sorting method to be used. name_format - Preferred format to display names incl_private - Whether to include private data living_people - How to handle living people years_past_death - Consider as living this many years after death """ Report.__init__(self, database, options, user) self._user = user menu = options.menu lang = options.menu.get_option_by_name('trans').get_value() rlocale = self.set_locale(lang) stdoptions.run_private_data_option(self, menu) living_opt = stdoptions.run_living_people_option(self, menu, rlocale) self.database = CacheProxyDb(self.database) self.filter = menu.get_option_by_name('filter').get_filter() self.fil_name = "(%s)" % self.filter.get_name(rlocale) living_value = menu.get_option_by_name('living_people').get_value() for (value, description) in living_opt.get_items(xml_items=True): if value == living_value: living_desc = self._(description) break self.living_desc = self._("(Living people: %(option_name)s)") % { 'option_name': living_desc } stdoptions.run_name_format_option(self, menu) sort_func_num = menu.get_option_by_name('sortby').get_value() sort_functions = _get_sort_functions(Sort(self.database)) self.sort_name = self._(sort_functions[sort_func_num][0]) self.sort_func = sort_functions[sort_func_num][1] self.calendar = config.get('preferences.calendar-format-report') self.plist = [] self.header = 2.6 def write_report(self): # Apply the filter with self._user.progress(_('Timeline'), _('Applying filter...'), self.database.get_number_of_people()) as step: self.plist = self.filter.apply(self.database, self.database.iter_person_handles(), step) # Find the range of dates to include (low, high) = self.find_year_range() # Generate the actual timeline self.generate_timeline(low, high) def generate_timeline(self, low, high): """ generate the timeline """ st_size = self.name_size() style_sheet = self.doc.get_style_sheet() font = style_sheet.get_paragraph_style('TLG-Name').get_font() incr = utils.pt2cm(font.get_size()) pad = incr * 0.75 _x1, _x2, _y1, _y2 = (0, 0, 0, 0) start = st_size + 0.5 stop = self.doc.get_usable_width() - 0.5 size = stop - start self.header = 2.6 # Sort the people as requested with self._user.progress(_('Timeline'), _('Sorting dates...'), 0) as step: self.plist.sort(key=self.sort_func) self.doc.start_page() self.build_grid(low, high, start, stop, True) index = 1 current = 1 length = len(self.plist) with self._user.progress(_('Timeline'), _('Calculating timeline...'), length) as step: for p_id in self.plist: person = self.database.get_person_from_handle(p_id) birth = get_birth_or_fallback(self.database, person) if birth: bth = birth.get_date_object() bth = bth.to_calendar(self.calendar).get_year() else: bth = None death = get_death_or_fallback(self.database, person) if death: dth = death.get_date_object() dth = dth.to_calendar(self.calendar).get_year() else: dth = None dname = self._name_display.display(person) mark = utils.get_person_mark(self.database, person) self.doc.draw_text('TLG-text', dname, incr + pad, self.header + (incr + pad) * index, mark) _y1 = self.header + (pad + incr) * index _y2 = self.header + ((pad + incr) * index) + incr _y3 = (_y1 + _y2) / 2.0 w05 = 0.05 if bth: start_offset = ((float(bth - low) / float(high - low)) * size) _x1 = start + start_offset path = [(_x1, _y1), (_x1 + w05, _y3), (_x1, _y2), (_x1 - w05, _y3)] self.doc.draw_path('TLG-line', path) if dth: start_offset = ((float(dth - low) / float(high - low)) * size) _x1 = start + start_offset path = [(_x1, _y1), (_x1 + w05, _y3), (_x1, _y2), (_x1 - w05, _y3)] self.doc.draw_path('TLG-solid', path) if bth and dth: start_offset = ( (float(bth - low) / float(high - low)) * size) + w05 stop_offset = ( (float(dth - low) / float(high - low)) * size) - w05 _x1 = start + start_offset _x2 = start + stop_offset self.doc.draw_line('open', _x1, _y3, _x2, _y3) if (_y2 + incr) >= self.doc.get_usable_height(): if current != length: self.doc.end_page() self.doc.start_page() self.build_grid(low, high, start, stop) index = 1 _x1, _x2, _y1, _y2 = (0, 0, 0, 0) else: index += 1 current += 1 step() self.doc.end_page() def build_grid(self, year_low, year_high, start_pos, stop_pos, toc=False): """ Draws the grid outline for the chart. Sets the document label, draws the vertical lines, and adds the year labels. Arguments are: year_low - lowest year on the chart year_high - highest year on the chart start_pos - x position of the lowest leftmost grid line stop_pos - x position of the rightmost grid line """ self.draw_title(toc) self.draw_columns(start_pos, stop_pos) if year_high is not None and year_low is not None: self.draw_year_headings(year_low, year_high, start_pos, stop_pos) else: self.draw_no_date_heading() def draw_columns(self, start_pos, stop_pos): """ Draws the columns out of vertical lines. start_pos - x position of the lowest leftmost grid line stop_pos - x position of the rightmost grid line """ top_y = self.header bottom_y = self.doc.get_usable_height() delta = (stop_pos - start_pos) / 5 for val in range(0, 6): xpos = start_pos + (val * delta) self.doc.draw_line('TLG-grid', xpos, top_y, xpos, bottom_y) def draw_title(self, toc): """ Draws the title for the page. """ width = self.doc.get_usable_width() title = "%(str1)s -- %(str2)s" % { 'str1': self._("Timeline Chart"), # feature request 2356: avoid genitive form 'str2': self._("Sorted by %s") % self.sort_name } title3 = self.living_desc mark = None if toc: mark = IndexMark(title, INDEX_TYPE_TOC, 1) self.doc.center_text('TLG-title', title, width / 2.0, 0, mark) style_sheet = self.doc.get_style_sheet() title_font = style_sheet.get_paragraph_style('TLG-Title').get_font() title_y = 1.2 - (utils.pt2cm(title_font.get_size()) * 1.2) self.doc.center_text('TLG-title', self.fil_name, width / 2.0, title_y) title_y = 1.8 - (utils.pt2cm(title_font.get_size()) * 1.2) self.doc.center_text('TLG-title', title3, width / 2.0, title_y) def draw_year_headings(self, year_low, year_high, start_pos, stop_pos): """ Draws the column headings (years) for the page. """ style_sheet = self.doc.get_style_sheet() label_font = style_sheet.get_paragraph_style('TLG-Label').get_font() label_y = self.header - (utils.pt2cm(label_font.get_size()) * 1.2) incr = (year_high - year_low) / 5 delta = (stop_pos - start_pos) / 5 for val in range(0, 6): xpos = start_pos + (val * delta) year_str = str(int(year_low + (incr * val))) self.doc.center_text('TLG-label', year_str, xpos, label_y) def draw_no_date_heading(self): """ Draws a single heading that says "No Date Information" """ width = self.doc.get_usable_width() style_sheet = self.doc.get_style_sheet() label_font = style_sheet.get_paragraph_style('TLG-Label').get_font() label_y = self.header - (utils.pt2cm(label_font.get_size()) * 1.2) self.doc.center_text('TLG-label', self._("No Date Information"), width / 2.0, label_y) def find_year_range(self): """ Finds the range of years that will be displayed on the chart. Returns a tuple of low and high years. If no dates are found, the function returns (None, None). """ low = None high = None def min_max_year(low, high, year): """ convenience function """ if year is not None and year != 0: if low is not None: low = min(low, year) else: low = year if high is not None: high = max(high, year) else: high = year return (low, high) with self._user.progress(_('Timeline'), _('Finding date range...'), len(self.plist)) as step: for p_id in self.plist: person = self.database.get_person_from_handle(p_id) birth = get_birth_or_fallback(self.database, person) if birth: bth = birth.get_date_object() bth = bth.to_calendar(self.calendar).get_year() (low, high) = min_max_year(low, high, bth) death = get_death_or_fallback(self.database, person) if death: dth = death.get_date_object() dth = dth.to_calendar(self.calendar).get_year() (low, high) = min_max_year(low, high, dth) step() # round the dates to the nearest decade if low is not None: low = int((low / 10)) * 10 else: low = high if high is not None: high = int(((high + 9) / 10)) * 10 else: high = low # Make sure the difference is a multiple of 50 so # all year ranges land on a decade. if low is not None and high is not None: low -= 50 - ((high - low) % 50) return (low, high) def name_size(self): """ get the length of the name """ self.plist = self.filter.apply(self.database, self.database.iter_person_handles()) style_sheet = self.doc.get_style_sheet() gstyle = style_sheet.get_draw_style('TLG-text') pname = gstyle.get_paragraph_style() pstyle = style_sheet.get_paragraph_style(pname) font = pstyle.get_font() size = 0 for p_id in self.plist: person = self.database.get_person_from_handle(p_id) dname = self._name_display.display(person) size = max(self.doc.string_width(font, dname), size) return utils.pt2cm(size)
class StatisticsChart(Report): """ StatisticsChart report """ def __init__(self, database, options, user): """ Create the Statistics object that produces the report. Uses the Extractor class to extract the data from the database. The arguments are: database - the GRAMPS database instance options - instance of the Options class for this report user - a gen.user.User() instance incl_private - Whether to include private data living_people - How to handle living people years_past_death - Consider as living this many years after death """ Report.__init__(self, database, options, user) menu = options.menu self._user = user lang = menu.get_option_by_name('trans').get_value() rlocale = self.set_locale(lang) # override default gettext, or English output will have "person|Title" self._ = rlocale.translation.sgettext stdoptions.run_private_data_option(self, menu) living_opt = stdoptions.run_living_people_option(self, menu, rlocale) self.database = CacheProxyDb(self.database) get_option_by_name = menu.get_option_by_name get_value = lambda name: get_option_by_name(name).get_value() filter_opt = get_option_by_name('filter') self.filter = filter_opt.get_filter() self.fil_name = "(%s)" % self.filter.get_name(rlocale) self.bar_items = get_value('bar_items') year_from = get_value('year_from') year_to = get_value('year_to') gender = get_value('gender') living_value = get_value('living_people') for (value, description) in living_opt.get_items(xml_items=True): if value == living_value: living_desc = self._(description) break self.living_desc = self._("(Living people: %(option_name)s)") % { 'option_name': living_desc } # title needs both data extraction method name + gender name if gender == Person.MALE: genders = self._("Men") elif gender == Person.FEMALE: genders = self._("Women") else: genders = None # needed for keyword based localization mapping = { 'genders': genders, 'year_from': year_from, 'year_to': year_to } if genders: span_string = self._("%(genders)s born " "%(year_from)04d-%(year_to)04d") % mapping else: span_string = self._("Persons born " "%(year_from)04d-%(year_to)04d") % mapping # extract requested items from the database and count them self._user.begin_progress(_('Statistics Charts'), _('Collecting data...'), self.database.get_number_of_people()) tables = _Extract.collect_data(self.database, self.filter, menu, gender, year_from, year_to, get_value('no_years'), self._user.step_progress, rlocale) self._user.end_progress() self._user.begin_progress(_('Statistics Charts'), _('Sorting data...'), len(tables)) self.data = [] sortby = get_value('sortby') reverse = get_value('reverse') for table in tables: # generate sorted item lookup index index lookup = self.index_items(table[1], sortby, reverse) # document heading heading = "%(str1)s -- %(str2)s" % { 'str1': self._(table[0]), 'str2': span_string } self.data.append((heading, table[0], table[1], lookup)) self._user.step_progress() self._user.end_progress() def index_items(self, data, sort, reverse): """creates & stores a sorted index for the items""" # sort by item keys index = sorted(data, reverse=True if reverse else False) if sort == _options.SORT_VALUE: # set for the sorting function self.lookup_items = data # then sort by value index.sort(key=lambda x: self.lookup_items[x], reverse=True if reverse else False) return index def write_report(self): "output the selected statistics..." mark = IndexMark(self._('Statistics Charts'), INDEX_TYPE_TOC, 1) self._user.begin_progress(_('Statistics Charts'), _('Saving charts...'), len(self.data)) for data in sorted(self.data): self.doc.start_page() if mark: self.doc.draw_text('SC-title', '', 0, 0, mark) # put it in TOC mark = None # crock, but we only want one of them if len(data[3]) < self.bar_items: self.output_piechart(*data[:4]) else: self.output_barchart(*data[:4]) self.doc.end_page() self._user.step_progress() self._user.end_progress() def output_piechart(self, title1, 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 style_sheet = self.doc.get_style_sheet() pstyle = style_sheet.get_paragraph_style('SC-Title') mark = IndexMark(title1, INDEX_TYPE_TOC, 2) self.doc.center_text('SC-title', title1, middle_w, 0, mark) yoffset = utils.pt2cm(pstyle.get_font().get_size()) self.doc.center_text('SC-title', self.fil_name, middle_w, yoffset) yoffset = 2 * utils.pt2cm(pstyle.get_font().get_size()) self.doc.center_text('SC-title', self.living_desc, middle_w, yoffset) # collect data for output color = 0 chart_data = [] for key in lookup: style = "SC-color-%d" % color text = "%s (%d)" % (self._(key), data[key]) # graphics style, value, and it's label 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... 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 = self._("%s (persons):") % self._(typename) draw_legend(self.doc, legendx, yoffset, chart_data, text, 'SC-legend') def output_barchart(self, title1, typename, data, lookup): pt2cm = utils.pt2cm style_sheet = self.doc.get_style_sheet() pstyle = style_sheet.get_paragraph_style('SC-Text') font = pstyle.get_font() # set layout variables width = self.doc.get_usable_width() row_h = pt2cm(font.get_size()) max_y = self.doc.get_usable_height() - row_h pad = row_h * 0.5 # check maximum value max_value = max(data[k] for k in lookup) if lookup else 0 # horizontal area for the gfx bars margin = 1.0 middle = width / 2.0 textx = middle + margin / 2.0 stopx = middle - margin / 2.0 maxsize = stopx - margin # start output pstyle = style_sheet.get_paragraph_style('SC-Title') mark = IndexMark(title1, INDEX_TYPE_TOC, 2) self.doc.center_text('SC-title', title1, middle, 0, mark) yoffset = pt2cm(pstyle.get_font().get_size()) self.doc.center_text('SC-title', self.fil_name, middle, yoffset) yoffset = 2 * pt2cm(pstyle.get_font().get_size()) self.doc.center_text('SC-title', self.living_desc, middle, yoffset) yoffset = 3 * pt2cm(pstyle.get_font().get_size()) # header yoffset += (row_h + pad) text = self._("%s (persons):") % self._(typename) self.doc.draw_text('SC-text', text, textx, yoffset) for key in lookup: 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 # right align bar to the text value = data[key] startx = stopx - (maxsize * value / max_value) self.doc.draw_box('SC-bar', "", startx, yoffset, stopx - startx, row_h) # text after bar text = "%s (%d)" % (self._(key), data[key]) self.doc.draw_text('SC-text', text, textx, yoffset) return