# Copyright 2008 by Wade Brainerd.
# This file is part of Finance.
#
# Finance 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 3 of the License, or
# (at your option) any later version.
#
# Finance 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 Finance. If not, see .
# Import standard Python modules.
import math
import locale
import cairo
import logging
# Import activity module
import colors
from gettext import gettext as _
from gi.repository import Gtk
from gi.repository import GObject
from gi.repository import PangoCairo
from sugar3.graphics import style
# Set up localization.
locale.setlocale(locale.LC_ALL, '')
CHART_HELP = _(
'The Chart view shows the proportion of your expenses that is in each '
'category.\nYou can categorize transactions in the Register view.')
def _get_screen_dpi():
xft_dpi = Gtk.Settings.get_default().get_property('gtk-xft-dpi')
dpi = float(xft_dpi / 1024)
logging.error('Setting dpi to: %f', dpi)
return dpi
def _set_screen_dpi():
dpi = _get_screen_dpi()
font_map_default = PangoCairo.font_map_get_default()
font_map_default.set_resolution(dpi)
class ChartScreen(Gtk.VBox):
CHART_CREDIT = 'credit'
CHART_DEBIT = 'debit'
def __init__(self, activity):
GObject.GObject.__init__(self)
self.activity = activity
self.category_total = {}
self.sorted_categories = []
self._graph_mode = self.CHART_DEBIT
header = Gtk.EventBox()
header.modify_bg(Gtk.StateType.NORMAL,
style.Color('#666666').get_gdk_color())
header.set_size_request(-1, style.GRID_CELL_SIZE)
self._title_label = Gtk.Label()
self._title_label.set_halign(Gtk.Align.START)
self._title_label.props.margin_left = style.GRID_CELL_SIZE / 2
header.add(self._title_label)
self.area = Gtk.DrawingArea()
self.area.connect('draw', self.chart_draw_cb)
self.pack_start(header, False, False, 0)
self.pack_start(self.area, True, True, 0)
self.show_all()
def set_mode(self, mode):
self._graph_mode = mode
self.build()
def build(self):
if self._graph_mode == self.CHART_CREDIT:
self.title = _('Credit Categories')
elif self._graph_mode == self.CHART_DEBIT:
self.title = _('Debit Categories')
self._title_label.set_markup(
'%s' %
self.title)
# Build the category totals.
self.category_total = {}
for t in self.activity.visible_transactions:
cat = t['category']
amount = t['amount']
if t['type'] == self._graph_mode:
if cat not in self.category_total:
self.category_total[cat] = amount
else:
self.category_total[cat] += amount
# Generate a list of names sorted by total.
self.sorted_categories = self.category_total.keys()
# self.sorted_categories.sort(lamba a, b: cmp(self.category_total[a],
# self.category_total[b]))
self.area.queue_draw()
def generate_image(self, image_file, width, height):
image_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
context = cairo.Context(image_surface)
self.create_chart(context, width, height)
image_surface.flush()
image_surface.write_to_png(image_file)
def chart_draw_cb(self, widget, context):
# Draw pie chart.
bounds = widget.get_allocation()
self.create_chart(context, bounds.width, bounds.height)
def create_chart(self, context, image_width, image_height):
_set_screen_dpi()
scale = image_width / 1600.
context.rectangle(0, 0, image_width, image_height)
logging.debug('canvas size %s x %s - scale %s', image_width,
image_height, scale)
context.set_source_rgb(1, 1, 1)
context.fill()
margin_left = (style.GRID_CELL_SIZE / 2) * scale
margin_top = (style.GRID_CELL_SIZE / 2) * scale
padding = 20 * scale
# measure the descriptions
max_width_desc = 0
max_width_amount = 0
max_height = 0
context.select_font_face('Sans', cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
context.set_font_size(26 * scale)
for c in self.sorted_categories:
description = c
# If there is no category, display as Unknown
if c is '':
description = _('Unknown')
# need measure the description width to align the amounts
x_bearing, y_bearing, width, height, x_advance, y_advance = \
context.text_extents(description)
max_width_desc = max(max_width_desc, width)
max_height = max(max_height, height)
x_bearing, y_bearing, width, height, x_advance, y_advance = \
context.text_extents(locale.currency(self.category_total[c]))
max_height = max(max_height, height)
max_width_amount = max(max_width_amount, width)
# draw the labels
y = margin_top
context.save()
context.translate(margin_left, 0)
rectangles_width = max_width_desc + max_width_amount + padding * 3
for c in self.sorted_categories:
description = c
if c is '':
description = _('Unknown')
context.save()
context.translate(0, y)
context.rectangle(0, 0, rectangles_width, max_height + padding)
color = colors.get_category_color(c)
context.set_source_rgb(color[0], color[1], color[2])
context.fill()
if colors.is_too_light(colors.get_category_color_str(c)):
context.set_source_rgb(0, 0, 0)
else:
context.set_source_rgb(1, 1, 1)
context.save()
x_bearing, y_bearing, width, height, x_advance, y_advance = \
context.text_extents(description)
context.move_to(padding, padding * 2.5 + y_bearing)
context.show_text(description)
context.restore()
context.save()
text = locale.currency(self.category_total[c])
x_bearing, y_bearing, width, height, x_advance, y_advance = \
context.text_extents(text)
context.move_to(rectangles_width - x_advance - padding,
padding * 2.5 + y_bearing)
context.show_text(text)
context.restore()
y += max_height + padding * 2
context.restore()
context.restore()
# draw the pie
x = (image_width - rectangles_width) / 2 + rectangles_width
y = image_height / 2
r = min(image_width, image_height) / 2 - 10
total = 0
for c in self.sorted_categories:
total += self.category_total[c]
if total != 0:
angle = 0.0
for c in self.sorted_categories:
slice = 2 * math.pi * self.category_total[c] / total
color = colors.get_category_color(c)
context.move_to(x, y)
context.arc(x, y, r, angle, angle + slice)
context.close_path()
context.set_source_rgb(color[0], color[1], color[2])
context.fill()
angle += slice