diff --git a/configure.ac b/configure.ac
index b3b3a84170..09cd073ea7 100644
--- a/configure.ac
+++ b/configure.ac
@@ -73,6 +73,7 @@ src/jarabe/Makefile
src/jarabe/model/Makefile
src/jarabe/model/update/Makefile
src/jarabe/util/Makefile
+src/jarabe/util/sugarpycha/Makefile
src/jarabe/util/telepathy/Makefile
src/jarabe/view/Makefile
src/jarabe/webservice/Makefile
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 54a306c2ec..327ec3c6cc 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -62,6 +62,7 @@ src/jarabe/frame/devicestray.py
src/jarabe/frame/zoomtoolbar.py
src/jarabe/intro/window.py
src/jarabe/intro/agepicker.py
+src/jarabe/journal/dashboardview.py
src/jarabe/journal/detailview.py
src/jarabe/journal/expandedentry.py
src/jarabe/journal/journalactivity.py
diff --git a/src/jarabe/journal/Makefile.am b/src/jarabe/journal/Makefile.am
index 31ce3b005a..59e88d4751 100644
--- a/src/jarabe/journal/Makefile.am
+++ b/src/jarabe/journal/Makefile.am
@@ -2,6 +2,7 @@ sugardir = $(pythondir)/jarabe/journal
sugar_PYTHON = \
__init__.py \
bundlelauncher.py \
+ dashboardview.py \
detailview.py \
expandedentry.py \
iconview.py \
diff --git a/src/jarabe/journal/dashboardview.py b/src/jarabe/journal/dashboardview.py
new file mode 100644
index 0000000000..f8c473d96d
--- /dev/null
+++ b/src/jarabe/journal/dashboardview.py
@@ -0,0 +1,744 @@
+# Copyright (C) 2019 Hrishi Patel
+# 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import gi
+gi.require_version('Gtk', '3.0')
+gi.require_version('Gdk', '3.0')
+from gi.repository import Gtk, Gdk, Pango, GObject, GdkPixbuf
+import logging
+
+from gettext import gettext as _
+
+from sugar3.activity.activity import launch_bundle
+from sugar3.activity.activity import get_activity_root
+from sugar3.graphics.toolbarbox import ToolbarBox
+from sugar3.graphics.icon import CellRendererIcon
+from sugar3.graphics import style
+from sugar3.activity.widgets import ActivityToolbarButton
+from sugar3.activity.widgets import StopButton
+from sugar3.graphics.toolbutton import ToolButton
+from sugar3.datastore import datastore
+from sugar3 import profile
+
+from jarabe.model import bundleregistry
+from jarabe.journal import misc
+
+from jarabe.util.charts import Chart
+from jarabe.util.readers import JournalReader
+from jarabe.util.utils import get_user_fill_color
+
+import os
+import datetime
+import locale
+
+
+COLOR1 = get_user_fill_color('str')
+
+
+class DashboardView(Gtk.ScrolledWindow):
+
+ def __init__(self, **kwargs):
+
+ Gtk.ScrolledWindow.__init__(self)
+
+ self.current_chart = None
+ self.x_label = ""
+ self.y_label = ""
+ self.chart_data = []
+ self.mime_types = ['image/bmp', 'image/gif', 'image/jpeg',
+ 'image/png', 'image/tiff',
+ 'application/pdf',
+ 'application/vnd.olpc-sugar',
+ 'application/rtf', 'text/rtf',
+ 'application/epub+zip', 'text/html',
+ 'application/x-pdf']
+
+ # Detect if device is a XO
+ if os.path.exists('/etc/olpc-release') or \
+ os.path.exists('/sys/power/olpc-pm'):
+ COLUMN_SPACING = 1
+ STATS_WIDTH = 30
+ TP_WIDTH = 45
+ HMAP_WIDTH = 90
+ else:
+ COLUMN_SPACING = 2
+ STATS_WIDTH = 50
+ TP_WIDTH = 75
+ HMAP_WIDTH = 150
+
+ # ScrolledWindow as the main container
+ self.set_can_focus(False)
+ self.set_policy(Gtk.PolicyType.AUTOMATIC,
+ Gtk.PolicyType.AUTOMATIC)
+ self.set_shadow_type(Gtk.ShadowType.NONE)
+ self.show_all()
+
+ grid = Gtk.Grid(column_spacing=6, row_spacing=3.5)
+ grid.set_border_width(20)
+ grid.set_halign(Gtk.Align.CENTER)
+ self.add_with_viewport(grid)
+
+ # VBoxes for total activities, journal entries and total files
+ vbox_total_activities = Gtk.VBox()
+ vbox_journal_entries = Gtk.VBox()
+ vbox_total_contribs = Gtk.VBox()
+ vbox_tree = Gtk.VBox()
+ vbox_heatmap = Gtk.VBox()
+ self.vbox_pie = Gtk.VBox()
+
+ eb_total_activities = Gtk.EventBox()
+ eb_journal_entries = Gtk.EventBox()
+ eb_total_contribs = Gtk.EventBox()
+ eb_heatmap = Gtk.EventBox()
+ eb_tree = Gtk.EventBox()
+ eb_pie = Gtk.EventBox()
+
+ eb_total_activities.add(vbox_total_activities)
+ eb_journal_entries.add(vbox_journal_entries)
+ eb_total_contribs.add(vbox_total_contribs)
+ eb_heatmap.add(vbox_heatmap)
+ eb_pie.add(self.vbox_pie)
+ eb_tree.add(vbox_tree)
+
+ # change eventbox color
+ eb_total_activities.modify_bg(Gtk.StateType.NORMAL,
+ Gdk.color_parse("#ffffff"))
+ eb_journal_entries.modify_bg(Gtk.StateType.NORMAL,
+ Gdk.color_parse("#ffffff"))
+ eb_total_contribs.modify_bg(Gtk.StateType.NORMAL,
+ Gdk.color_parse("#ffffff"))
+ eb_heatmap.modify_bg(Gtk.StateType.NORMAL,
+ Gdk.color_parse("#ffffff"))
+ eb_pie.modify_bg(Gtk.StateType.NORMAL,
+ Gdk.color_parse("#ffffff"))
+ eb_tree.modify_bg(Gtk.StateType.NORMAL,
+ Gdk.color_parse("#ffffff"))
+
+ label_dashboard = Gtk.Label()
+ text_dashboard = "{0}".format(_("Dashboard"))
+ label_dashboard.set_markup(text_dashboard)
+
+ # label for total activities
+ label_TA = Gtk.Label()
+ text_TA = "{0}".format(_("Activities Installed"))
+ label_TA.set_markup(text_TA)
+ vbox_total_activities.add(label_TA)
+
+ self.label_total_activities = Gtk.Label()
+ vbox_total_activities.add(self.label_total_activities)
+
+ # label for total journal entries
+ label_JE = Gtk.Label()
+ text_JE = "{0}".format(_("Journal Entries"))
+ label_JE.set_markup(text_JE)
+ vbox_journal_entries.add(label_JE)
+
+ self.label_journal_entries = Gtk.Label()
+ vbox_journal_entries.add(self.label_journal_entries)
+
+ # label for files
+ label_CE = Gtk.Label()
+ text_CE = "{0}".format(_("Total Files"))
+ label_CE.set_markup(text_CE)
+ vbox_total_contribs.add(label_CE)
+
+ # label for pie
+ label_PIE = Gtk.Label()
+ text_PIE = "{0}".format(_("Most used activities"))
+ label_PIE.set_markup(text_PIE)
+ self.vbox_pie.pack_start(label_PIE, False, True, 5)
+
+ self.label_contribs = Gtk.Label()
+ vbox_total_contribs.add(self.label_contribs)
+
+ # pie chart
+ self.labels_and_values = ChartData(self)
+ self.eventbox = Gtk.EventBox()
+ self.charts_area = ChartArea(self)
+ self.charts_area.connect('size-allocate', self._chart_size_allocate_cb)
+ self.eventbox.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse("white"))
+ self.eventbox.add(self.charts_area)
+ self.vbox_pie.pack_start(self.eventbox, True, True, 0)
+ self.eventbox.connect('button-press-event', self._pie_opened)
+ self.charts_area.set_tooltip_text(_("Click for more information"))
+
+ # pie chart window
+ self.window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
+ self.window.set_border_width(2)
+ self.window.set_position(Gtk.WindowPosition.CENTER_ALWAYS)
+ self.window.set_decorated(True)
+ self.window.set_resizable(False)
+ self.window.set_modal(True)
+ self.window.set_keep_above(True)
+ self.window.set_size_request(800, 600)
+ self.window.set_title("Pie Chart")
+ self.window.connect('delete-event', self._hide_window)
+
+ eb_image_holder = Gtk.EventBox()
+ eb_image_holder.modify_bg(Gtk.StateType.NORMAL,
+ Gdk.color_parse("ffffff"))
+ self.window.modify_bg(Gtk.StateType.NORMAL,
+ Gdk.color_parse("#282828"))
+
+ vbox_image = Gtk.VBox()
+ eb_image_holder.add(vbox_image)
+
+ # scrolled window for details window
+ scrolled_window = Gtk.ScrolledWindow()
+ scrolled_window.set_can_focus(False)
+ scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC,
+ Gtk.PolicyType.AUTOMATIC)
+ scrolled_window.set_shadow_type(Gtk.ShadowType.NONE)
+ scrolled_window.show()
+
+ # load pie image
+ # not using get_activity_root for now
+ self.image = Gtk.Image()
+ self.image.set_from_file("/tmp/screenshot.png")
+ vbox_image.add(self.image)
+
+ self.vbox_holder = Gtk.VBox()
+ self.vbox_holder.pack_start(eb_image_holder, True, True, 0)
+ self.vbox_holder.pack_start(self.labels_and_values, False, False, 0)
+ self.window.add(scrolled_window)
+ scrolled_window.add_with_viewport(self.vbox_holder)
+
+ reader = JournalReader()
+ self._graph_from_reader(reader)
+ self.current_chart = Chart("pie")
+ self.update_chart()
+
+ # font
+ font_main = Pango.FontDescription("Granada 12")
+ label_JE.modify_font(font_main)
+ label_CE.modify_font(font_main)
+ label_TA.modify_font(font_main)
+
+ font_actual = Pango.FontDescription("12")
+ self.label_journal_entries.modify_font(font_actual)
+ self.label_total_activities.modify_font(font_actual)
+ self.label_contribs.modify_font(font_actual)
+ label_dashboard.modify_font(font_actual)
+
+ self.treeview_list = []
+ self.files_list = []
+ self.old_list = []
+ self.heatmap_list = []
+ self.journal_entries = 0
+
+ # treeview for Journal entries
+ self.liststore = Gtk.ListStore(str, str, str, object, str,
+ datastore.DSMetadata, str, str)
+ self.treeview = Gtk.TreeView(self.liststore)
+ self.treeview.set_headers_visible(False)
+ self.treeview.set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL)
+ self._load_data()
+
+ for i, col_title in enumerate(["Recently Opened Activities"]):
+
+ renderer_title = Gtk.CellRendererText()
+ renderer_time = Gtk.CellRendererText()
+ icon_renderer = CellRendererActivityIcon()
+
+ renderer_title.set_property('ellipsize', Pango.EllipsizeMode.END)
+ renderer_title.set_property('ellipsize-set', True)
+
+ column1 = Gtk.TreeViewColumn("Icon")
+ column1.pack_start(icon_renderer, True)
+ column1.add_attribute(icon_renderer, 'file-name',
+ 1)
+ column1.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column1.add_attribute(icon_renderer, 'xo-color',
+ 3)
+ column2 = Gtk.TreeViewColumn(col_title, renderer_title, text=0)
+ column2.set_min_width(200)
+ column3 = Gtk.TreeViewColumn(col_title, renderer_time, text=6)
+
+ self.treeview.set_tooltip_column(0)
+ self.treeview.append_column(column1)
+ self.treeview.append_column(column2)
+ self.treeview.append_column(column3)
+
+ # combobox for sort selection
+ cbox_store = Gtk.ListStore(str)
+ cbox_entries = [_("Newest"), _("Files"), _("Oldest")]
+
+ for item in cbox_entries:
+ cbox_store.append([item])
+
+ combobox = Gtk.ComboBox.new_with_model(cbox_store)
+ combobox.set_halign(Gtk.Align.END)
+ combobox.connect('changed', self._on_name_combo_changed_cb)
+ renderer_text = Gtk.CellRendererText()
+ combobox.pack_start(renderer_text, True)
+ combobox.add_attribute(renderer_text, "text", 0)
+ combobox.set_active(0)
+
+ self._add_to_treeview(self.treeview_list)
+
+ selected_row = self.treeview.get_selection()
+ selected_row.connect('changed', self._item_select_cb)
+
+ scrolled_window = Gtk.ScrolledWindow()
+ scrolled_window.set_can_focus(False)
+ scrolled_window.set_policy(Gtk.PolicyType.NEVER,
+ Gtk.PolicyType.AUTOMATIC)
+ scrolled_window.set_shadow_type(Gtk.ShadowType.NONE)
+ scrolled_window.show()
+
+ hbox_tree2 = Gtk.HBox()
+ text_treeview = "{0}".format(_("Journal Entries"))
+ self.label_treeview = Gtk.Label(text_treeview)
+ hbox_tree2.pack_start(self.label_treeview, False, True, 10)
+ hbox_tree2.pack_start(combobox, True, True, 10)
+
+ vbox_tree.pack_start(hbox_tree2, False, False, 5)
+ scrolled_window.add_with_viewport(self.treeview)
+
+ # label for recent activities
+ label_rec = Gtk.Label(expand=False)
+ text_treeview = "{0}".format(_("Recently Opened Activities"))
+ label_rec.set_markup(text_treeview)
+
+ vbox_tree.add(scrolled_window)
+
+ # heatmap
+ label_heatmap = Gtk.Label(_("User Activity"))
+ grid_heatmap = Gtk.Grid(column_spacing=COLUMN_SPACING,
+ row_spacing=2)
+ grid_heatmap.set_halign(Gtk.Align.CENTER)
+ vbox_heatmap.pack_start(label_heatmap, False, True, 5)
+ vbox_heatmap.pack_start(grid_heatmap, False, True, 5)
+
+ self.dates, self.dates_a, months = self._generate_dates()
+ self._build_heatmap(grid_heatmap, self.dates, self.dates_a, months)
+
+ self.heatmap_liststore = Gtk.ListStore(str, str, str, object, str,
+ datastore.DSMetadata, str, str)
+ heatmap_treeview = Gtk.TreeView(self.heatmap_liststore)
+ heatmap_treeview.set_headers_visible(False)
+ heatmap_treeview.set_grid_lines(Gtk.TreeViewGridLines.HORIZONTAL)
+
+ for i, col_title in enumerate(["Activity"]):
+ renderer_title = Gtk.CellRendererText()
+ icon_renderer = CellRendererActivityIcon()
+ renderer_time = Gtk.CellRendererText()
+
+ column1 = Gtk.TreeViewColumn("Icon")
+ column1.pack_start(icon_renderer, True)
+ column1.add_attribute(icon_renderer, 'file-name',
+ 1)
+ column1.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
+ column1.add_attribute(icon_renderer, 'xo-color',
+ 3)
+ column2 = Gtk.TreeViewColumn(col_title, renderer_title, text=0)
+ column3 = Gtk.TreeViewColumn(col_title, renderer_time, text=6)
+
+ heatmap_treeview.append_column(column1)
+ heatmap_treeview.append_column(column2)
+ heatmap_treeview.append_column(column3)
+
+ vbox_heatmap.pack_start(heatmap_treeview, False, True, 5)
+
+ selected_row_heatmap = heatmap_treeview.get_selection()
+ selected_row_heatmap.connect('changed', self._item_select_cb)
+
+ # add views to grid
+ grid.attach(label_dashboard, 1, 2, 1, 1)
+ grid.attach_next_to(eb_total_activities, label_dashboard,
+ Gtk.PositionType.BOTTOM, STATS_WIDTH, 35)
+ grid.attach_next_to(eb_journal_entries, eb_total_activities,
+ Gtk.PositionType.RIGHT, STATS_WIDTH, 35)
+ grid.attach_next_to(eb_total_contribs, eb_journal_entries,
+ Gtk.PositionType.RIGHT, STATS_WIDTH, 35)
+ grid.attach_next_to(eb_tree, eb_total_activities,
+ Gtk.PositionType.BOTTOM, TP_WIDTH, 90)
+ grid.attach_next_to(eb_pie, eb_tree,
+ Gtk.PositionType.RIGHT, TP_WIDTH, 90)
+ grid.attach_next_to(eb_heatmap, eb_tree,
+ Gtk.PositionType.BOTTOM, HMAP_WIDTH, 75)
+ grid.show_all()
+
+ def _load_data(self, widget=None):
+ del self.treeview_list[:]
+ del self.files_list[:]
+ del self.old_list[:]
+ del self.heatmap_list[:]
+
+ dsobjects, journal_entries = datastore.find({})
+ for dsobject in dsobjects:
+ new = []
+ new.append(dsobject.metadata['title'])
+ new.append(misc.get_icon_name(dsobject.metadata))
+ new.append(dsobject.metadata['activity_id'])
+ new.append(profile.get_color())
+ new.append(dsobject.get_object_id())
+ new.append(dsobject.metadata)
+ new.append(misc.get_date(dsobject.metadata))
+ new.append(dsobject.metadata['mtime'])
+ self.treeview_list.append(new)
+ self.old_list.append(new)
+
+ # determine if a file
+ if dsobject.metadata['mime_type'] in self.mime_types:
+ new2 = []
+ new2.append(dsobject.metadata['title'])
+ new2.append(misc.get_icon_name(dsobject.metadata))
+ new2.append(dsobject.metadata['activity_id'])
+ new2.append(profile.get_color())
+ new2.append(dsobject.get_object_id())
+ new2.append(dsobject.metadata)
+ new2.append(misc.get_date(dsobject.metadata))
+ new2.append(dsobject.metadata['mtime'])
+ self.files_list.append(new2)
+
+ self.old_list = sorted(self.old_list, key=lambda x: x[7])
+ self.journal_entries = journal_entries
+ self._add_to_treeview(self.treeview_list)
+
+ # get number of activities installed
+ registry = bundleregistry.get_registry()
+
+ self.label_total_activities.set_text(str(len(registry)))
+ self.label_journal_entries.set_text(str(self.journal_entries))
+ self.label_contribs.set_text(str(len(self.files_list)))
+
+ def _pie_opened(self, widget, event):
+ self.update_chart(300)
+ self.vbox_holder.pack_start(self.labels_and_values, False, False, 0)
+ pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale("/tmp/screenshot.png",
+ 800, 550, True)
+ self.image.set_from_pixbuf(pixbuf)
+ self.window.show()
+ self.window.show_all()
+
+ def _hide_window(self, *args):
+ self.vbox_holder.remove(self.labels_and_values)
+ self.window.hide()
+ self.update_chart(0)
+ return Gtk.true
+
+ def _build_heatmap(self, grid, dates, dates_a, months):
+ j = 1
+ k = 1
+ counter_days = 0
+ counter_weeks = 0
+ week_list = [0, 5, 9, 13, 18, 22, 26, 31, 35, 39, 44, 49]
+ months_dict = {}
+
+ # populate dictionary
+ for i, item in enumerate(week_list):
+ months_dict[item] = months[i]
+
+ for i in range(0, 365):
+ if (i % 7 == 0):
+ j = j + 1
+ k = 0
+ k = k + 1
+ count = 0
+ for x in range(0, len(self.old_list)):
+ date = self.old_list[x][7][:-16]
+ if date == dates[i]:
+ count = count + 1
+ box = HeatMapBlock(dates_a[i], count, i)
+ box.connect('on-clicked', self._on_clicked_cb)
+ lab_days = Gtk.Label()
+ lab_months = Gtk.Label()
+
+ # for weekdays
+ if(k % 2 == 0 and counter_days < 3):
+ day = ''
+ if(counter_days == 0):
+ day = dates_a[8][:-13]
+ lab_days.set_text(_(day))
+ if(counter_days == 1):
+ day = dates_a[10][:-13]
+ lab_days.set_text(_(day))
+ if(counter_days == 2):
+ day = dates_a[12][:-13]
+ lab_days.set_text(_(day))
+
+ grid.attach(lab_days, 0, k, 1, 1)
+ counter_days = counter_days + 1
+
+ # for months
+ if(k % 4 == 0 and counter_weeks < 54):
+ for key, value in months_dict.items():
+ if counter_weeks == key:
+ lab_months.set_text(str(value))
+
+ if counter_weeks in week_list:
+ grid.attach(lab_months, j, 0, 2, 1)
+
+ counter_weeks = counter_weeks + 1
+
+ grid.attach(box, j, k, 1, 1)
+
+ def _on_clicked_cb(self, i, index):
+ self.heatmap_liststore.clear()
+ del self.heatmap_list[:]
+
+ for y in range(0, len(self.old_list)):
+ date = self.old_list[y][7][:-16]
+ if date == self.dates[index]:
+ self.heatmap_list.append(self.old_list[y])
+
+ for item in self.heatmap_list:
+ self.heatmap_liststore.append(item)
+
+ def _generate_dates(self):
+ year = datetime.date.today().year
+
+ dt = datetime.datetime(year, 1, 1)
+ end = datetime.datetime(year, 12, 31, 23, 59, 59)
+ step = datetime.timedelta(days=1)
+
+ result = []
+ result_a = []
+ months = []
+
+ while dt < end:
+ result_a.append(dt.strftime('%a, %b %d %Y'))
+ result.append(dt.strftime('%Y-%m-%d'))
+ dt += step
+
+ for i in range(1, 13):
+ month_abre = datetime.date(year, i, 1).strftime('%b')
+ months.append(month_abre)
+
+ return result, result_a, months
+
+ def _add_to_treeview(self, tlist):
+ self.liststore.clear()
+ for item in tlist:
+ self.liststore.append(item)
+
+ def _on_name_combo_changed_cb(self, combo):
+ tree_iter = combo.get_active_iter()
+ if tree_iter is not None:
+ model = combo.get_model()
+ selected_item = model[tree_iter][0]
+ if selected_item == "Files":
+ self._add_to_treeview(self.files_list)
+ elif selected_item == "Newest":
+ self._add_to_treeview(self.treeview_list)
+ elif selected_item == "Oldest":
+ self._add_to_treeview(self.old_list)
+
+ def _item_select_cb(self, selection):
+ model, row = selection.get_selected()
+
+ if row is not None:
+ metadata = model[row][5]
+ bundle_id = metadata.get('activity', '')
+ launch_bundle(bundle_id, model[row][4])
+
+ def _chart_size_allocate_cb(self, widget, allocation):
+ self._render_chart()
+
+ def _render_chart(self, extra_size=0):
+ if self.current_chart is None or self.charts_area is None:
+ return
+
+ # Resize the chart for all the screen sizes
+ alloc = self.vbox_pie.get_allocation()
+ new_width = alloc.width + extra_size
+ new_height = alloc.height + extra_size
+
+ self.current_chart.width = new_width
+ self.current_chart.height = new_height
+
+ try:
+ if self.current_chart.type == "pie":
+ self.current_chart.render(self)
+ else:
+ self.current_chart.render()
+ self.charts_area.queue_draw()
+ surface = self.charts_area.get_surface()
+ surface.write_to_png('/tmp/screenshot.png')
+ except (ZeroDivisionError, ValueError):
+ pass
+
+ return False
+
+ def _graph_from_reader(self, reader):
+ self.labels_and_values.model.clear()
+ self.chart_data = []
+
+ chart_data = reader.get_chart_data()
+
+ # Load the data
+ for row in chart_data:
+ self._add_value(None,
+ label=row[0], value=float(row[1]))
+
+ self.update_chart()
+
+ def _add_value(self, widget, label="", value="0.0"):
+ data = (label, float(value))
+ if data not in self.chart_data:
+ pos = self.labels_and_values.add_value(label, value)
+ self.chart_data.insert(pos, data)
+ self._update_chart_data()
+
+ def update_chart(self, extra_size=0):
+ if self.current_chart:
+ self.current_chart.data_set(self.chart_data)
+ self.current_chart.set_x_label(self.x_label)
+ self.current_chart.set_y_label(self.y_label)
+ self._render_chart(extra_size)
+
+ def _update_chart_data(self):
+ if self.current_chart is None:
+ return
+ self.current_chart.data_set(self.chart_data)
+ self._update_chart_labels()
+
+ def _update_chart_labels(self, title=""):
+ if self.current_chart is None:
+ return
+ self.current_chart.set_title(title)
+ self.current_chart.set_x_label(self.x_label)
+ self.current_chart.set_y_label(self.y_label)
+ self._render_chart()
+
+
+class ChartArea(Gtk.DrawingArea):
+
+ def __init__(self, parent):
+ """A class for Draw the chart"""
+ super(ChartArea, self).__init__()
+ self._parent = parent
+ self.add_events(Gdk.EventMask.EXPOSURE_MASK |
+ Gdk.EventMask.VISIBILITY_NOTIFY_MASK)
+ self.connect('draw', self._draw_cb)
+
+ def _draw_cb(self, widget, context):
+ alloc = self.get_allocation()
+
+ # White Background:
+ context.rectangle(0, 0, alloc.width, alloc.height)
+ context.set_source_rgb(255, 255, 255)
+ context.fill()
+
+ # Paint the chart:
+ chart_width = self._parent.current_chart.width
+ chart_height = self._parent.current_chart.height
+
+ cxpos = alloc.width / 2 - chart_width / 2
+ cypos = alloc.height / 2 - chart_height / 2
+
+ context.set_source_surface(self._parent.current_chart.surface,
+ cxpos,
+ cypos)
+ context.paint()
+
+ def get_surface(self):
+ return self._parent.current_chart.surface
+
+
+class ChartData(Gtk.TreeView):
+
+ def __init__(self, activity):
+ GObject.GObject.__init__(self)
+
+ self.model = Gtk.ListStore(str, str)
+ self.set_model(self.model)
+
+ self._selection = self.get_selection()
+ self._selection.set_mode(Gtk.SelectionMode.SINGLE)
+
+ # Label column
+ column = Gtk.TreeViewColumn(_("Activities"))
+ label = Gtk.CellRendererText()
+
+ column.pack_start(label, True)
+ column.add_attribute(label, 'text', 0)
+ self.append_column(column)
+
+ # Value column
+ column = Gtk.TreeViewColumn(_("Number of Instances"))
+ value = Gtk.CellRendererText()
+
+ column.pack_start(value, True)
+ column.add_attribute(value, 'text', 1)
+
+ self.append_column(column)
+ self.set_enable_search(False)
+
+ self.show_all()
+
+ def add_value(self, label, value):
+ treestore, selected = self._selection.get_selected()
+ if not selected:
+ path = 0
+
+ elif selected:
+ path = int(str(self.model.get_path(selected))) + 1
+ try:
+ _iter = self.model.insert(path, [label, str(value)])
+ except ValueError:
+ _iter = self.model.append([label, str(value)])
+
+ self.set_cursor(self.model.get_path(_iter),
+ self.get_column(1),
+ True)
+
+ return path
+
+
+class CellRendererActivityIcon(CellRendererIcon):
+ __gtype_name__ = 'DashboardCellRendererActivityIcon'
+
+ def __init__(self):
+ CellRendererIcon.__init__(self)
+
+ self.props.width = style.GRID_CELL_SIZE
+ self.props.height = style.GRID_CELL_SIZE
+ self.props.size = style.STANDARD_ICON_SIZE
+ self.props.mode = Gtk.CellRendererMode.ACTIVATABLE
+
+
+class HeatMapBlock(Gtk.EventBox):
+
+ __gsignals__ = {
+ 'on-clicked': (GObject.SignalFlags.RUN_FIRST, None,
+ (int,)),
+ }
+
+ def __init__(self, date, contribs, index):
+ Gtk.EventBox.__init__(self)
+
+ self._i = index
+
+ label = Gtk.Label(" ")
+ tooltip = date + "\nContributions: " + str(contribs)
+ label.set_tooltip_text(tooltip)
+
+ if contribs == 0:
+ self.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse("#cdcfd3"))
+ elif contribs <= 2 and contribs > 0:
+ self.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse("#5fce68"))
+ elif contribs <= 5 and contribs > 2:
+ self.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse("#47a94f"))
+ elif contribs >= 6:
+ self.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse("#38853e"))
+
+ self.add(label)
+ self.set_events(Gdk.EventType.BUTTON_PRESS)
+ self.connect('button-press-event', self._on_mouse_cb)
+
+ def _on_mouse_cb(self, widget, event):
+ self.emit('on-clicked', self._i)
diff --git a/src/jarabe/journal/journalactivity.py b/src/jarabe/journal/journalactivity.py
index d00c7ff465..9fdb164702 100644
--- a/src/jarabe/journal/journalactivity.py
+++ b/src/jarabe/journal/journalactivity.py
@@ -19,7 +19,6 @@
import uuid
import time
-
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkX11
@@ -37,8 +36,10 @@
from jarabe.journal.journaltoolbox import MainToolbox
from jarabe.journal.journaltoolbox import AddNewBar
from jarabe.journal.journaltoolbox import DetailToolbox
+from jarabe.journal.journaltoolbox import DashboardToolBox
from jarabe.journal.journaltoolbox import EditToolbox
from jarabe.journal.projectview import ProjectView
+from jarabe.journal.dashboardview import DashboardView
from jarabe.journal.listview import ListView
from jarabe.journal.detailview import DetailView
@@ -186,6 +187,7 @@ class JournalViews(object):
MAIN = 1
DETAIL = 2
PROJECT = 3
+ DASHBOARD = 4
class JournalActivity(JournalWindow):
@@ -212,6 +214,7 @@ def __init__(self):
self._setup_main_view()
self._setup_secondary_view()
self._setup_project_view()
+ self._setup_dashboard_view()
self.add_events(Gdk.EventMask.ALL_EVENTS_MASK)
self._realized_sid = self.connect('realize', self.__realize_cb)
@@ -281,6 +284,8 @@ def _setup_main_view(self):
self._main_toolbox = MainToolbox()
self._edit_toolbox = EditToolbox(self)
self._main_view = Gtk.VBox()
+ self._main_toolbox.connect('dashboard-clicked',
+ self._show_dashboard_view)
self._add_new_box = AddNewBar(_('Add new project'))
self._add_new_box.activate.connect(self.__add_project_activate_cb)
@@ -320,6 +325,24 @@ def _setup_project_view(self):
project_vbox.pack_start(self._list_view_project, True, True, 0)
self._list_view_project.show()
+ def _setup_dashboard_view(self):
+ self._dashboard_toolbox = DashboardToolBox()
+ self._dashboard_toolbox.connect('refresh-clicked',
+ self._refresh_clicked_cb)
+ self._dashboard_toolbox.connect('journal-clicked',
+ self._journal_clicked_cb)
+ self._dashboard_holder = Gtk.VBox()
+ self._dashboard_view = DashboardView()
+ self._dashboard_holder.show_all()
+ self._dashboard_holder.pack_start(self._dashboard_view, True, True, 0)
+
+ def _refresh_clicked_cb(self, i):
+ self._dashboard_view._load_data()
+
+ def _journal_clicked_cb(self, i):
+ self._main_toolbox.clear_query()
+ self.show_main_view()
+
def get_add_new_box(self):
return self._add_new_box
@@ -490,6 +513,16 @@ def _show_secondary_view(self, object_id):
self.set_canvas(self._secondary_view)
self._secondary_view.show()
+ def _show_dashboard_view(self, i):
+ self._active_view = JournalViews.DASHBOARD
+ if self.canvas != self._dashboard_holder:
+ self.set_canvas(self._dashboard_holder)
+ self._dashboard_holder.show()
+ self._toolbox = self._dashboard_toolbox
+ self.set_toolbar_box(self._dashboard_toolbox)
+ else:
+ self.set_canvas(self._main_view)
+
def show_object(self, object_id):
metadata = model.get(object_id)
if metadata is None:
@@ -571,7 +604,8 @@ def set_active_volume(self, mount):
def show_journal(self):
"""Become visible and show main view"""
self.reveal()
- self.show_main_view()
+ if self._active_view != JournalViews.DASHBOARD:
+ self.show_main_view()
def get_total_number_of_entries(self):
list_view_model = self.get_list_view().get_model()
diff --git a/src/jarabe/journal/journaltoolbox.py b/src/jarabe/journal/journaltoolbox.py
index 72281f5d47..a7871ce696 100644
--- a/src/jarabe/journal/journaltoolbox.py
+++ b/src/jarabe/journal/journaltoolbox.py
@@ -76,6 +76,11 @@
class MainToolbox(ToolbarBox):
+ __gsignals__ = {
+ 'dashboard-clicked': (GObject.SignalFlags.RUN_FIRST, None,
+ ([])),
+ }
+
query_changed_signal = GObject.Signal('query-changed',
arg_types=([object]))
@@ -114,6 +119,13 @@ def __init__(self, default_what_filter=None, default_filter_type=None):
self.toolbar.insert(self._proj_list_button, -1)
self._proj_list_button.show()
+ self._dashboard_button = ToggleToolButton('activity-journal')
+ self._dashboard_button.set_tooltip(_('Dashboard'))
+ self._dashboard_button.connect('toggled',
+ self._dashboard_clicked_cb)
+ self.toolbar.insert(self._dashboard_button, -1)
+ self._dashboard_button.show()
+
if not self._proj_list_button.props.active:
self._what_widget_contents = None
self._what_widget = Gtk.ToolItem()
@@ -482,6 +494,9 @@ def _proj_list_button_clicked_cb(self, proj_list_button):
self._what_search_button.show()
self._update_if_needed()
+ def _dashboard_clicked_cb(self, dashboard_button):
+ self.emit('dashboard-clicked')
+
def __favorite_button_toggled_cb(self, favorite_button):
self._update_if_needed()
@@ -517,6 +532,9 @@ def clear_query(self):
self._what_search_button.show()
self._proj_list_button.props.active = False
+ if self._dashboard_button.props.active:
+ self._dashboard_button.props.active = False
+
self._update_if_needed()
@@ -770,6 +788,37 @@ def get_current_sort(self):
return (self._property, self._order)
+class DashboardToolBox(ToolbarBox):
+
+ __gsignals__ = {
+ 'refresh-clicked': (GObject.SignalFlags.RUN_FIRST, None,
+ ([])),
+ 'journal-clicked': (GObject.SignalFlags.RUN_FIRST, None,
+ ([])),
+ }
+
+ def __init__(self):
+ ToolbarBox.__init__(self)
+
+ self.journal_button = ToolButton('activity-journal')
+ self.journal_button.connect('clicked', self._journal_clicked_cb)
+ self.journal_button.show()
+ self.toolbar.insert(self.journal_button, -1)
+
+ refresh_button = ToolButton('view-refresh')
+ refresh_button.set_tooltip_text(_("Refresh Data"))
+ refresh_button.connect('clicked', self._load_data)
+ refresh_button.show()
+ self.toolbar.insert(refresh_button, -1)
+ self.show()
+
+ def _load_data(self, widget=None):
+ self.emit('refresh-clicked')
+
+ def _journal_clicked_cb(self, widget=None):
+ self.emit('journal-clicked')
+
+
class EditToolbox(ToolbarBox):
def __init__(self, journalactivity):
diff --git a/src/jarabe/util/Makefile.am b/src/jarabe/util/Makefile.am
index b6553ced65..e929834593 100644
--- a/src/jarabe/util/Makefile.am
+++ b/src/jarabe/util/Makefile.am
@@ -1,9 +1,13 @@
SUBDIRS = \
- telepathy
+ telepathy \
+ sugarpycha
sugardir = $(pythondir)/jarabe/util
sugar_PYTHON = \
__init__.py \
+ charts.py \
downloader.py \
httprange.py \
- normalize.py
+ normalize.py \
+ readers.py \
+ utils.py
diff --git a/src/jarabe/util/charts.py b/src/jarabe/util/charts.py
new file mode 100644
index 0000000000..53d78b81ac
--- /dev/null
+++ b/src/jarabe/util/charts.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# charts.py by:
+# Agustin Zubiaga
+# Gonzalo Odiard
+# Manuel QuiƱones
+
+# Copyright (C) 2019 Hrishi Patel
+# 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 3 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+
+from jarabe.util.sugarpycha import bar
+from jarabe.util.sugarpycha import line
+from jarabe.util.sugarpycha import pie
+
+import gi
+from gi.repository import GObject
+import cairo
+
+
+class Chart(GObject.GObject):
+ def __init__(self, type="vertical", width=600, height=460):
+ GObject.GObject.__init__(self)
+
+ self.dataSet = None
+ self.options = None
+ self.surface = None
+
+ self.type = type
+ self.width = width
+ self.height = height
+
+ def data_set(self, data):
+ """Set chart data (dataSet)"""
+
+ self.dataSet = (
+ ('Puntos', [(i, l[1]) for i, l in enumerate(data)]),
+ )
+
+ self.options = {
+ 'legend': {'hide': True},
+ 'titleFontSize': 16,
+ 'axis': {
+ 'tickFontSize': 12,
+ 'labelFontSize': 14,
+ 'lineColor': '#b3b3b3',
+ 'x': {
+ 'ticks': [dict(v=i, label=l[0]) for i,
+ l in enumerate(data)],
+ 'label': 'X',
+ },
+ 'y': {
+ 'tickCount': 5,
+ 'label': 'Y',
+ }
+ },
+ 'stroke': {
+ 'width': 3
+ },
+ 'background': {
+ 'chartColor': '#FFFFFF',
+ 'lineColor': '#CCCCCC'
+ },
+ 'colorScheme': {
+ 'name': 'gradient',
+ 'args': {
+ 'initialColor': 'blue',
+ },
+ },
+ }
+
+ def set_color_scheme(self, color='blue'):
+ """Set the chart color scheme"""
+ self.options["colorScheme"]["args"] = {'initialColor': color}
+
+ def set_line_color(self, color='#000000'):
+ """Set the chart line color"""
+ self.options["stroke"]["color"] = color
+
+ def set_x_label(self, text="X"):
+ """Set the X Label"""
+ self.options["axis"]["x"]["label"] = str(text)
+
+ def set_y_label(self, text="Y"):
+ """Set the Y Label"""
+ self.options["axis"]["y"]["label"] = str(text)
+
+ def set_type(self, type="vertical"):
+ """Set chart type (vertical, horizontal, line, pie)"""
+ self.type = type
+
+ def set_title(self, title="Chart"):
+ """Set the chart title"""
+ self.options["title"] = title
+
+ def render(self, sg=None):
+ """Draw the chart
+ Use the self.surface variable for show the chart"""
+ self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
+ self.width,
+ self.height)
+
+ if self.type == "vbar":
+ chart = bar.VerticalBarChart(self.surface, self.options)
+
+ elif self.type == "hbar":
+ chart = bar.HorizontalBarChart(self.surface,
+ self.options)
+
+ elif self.type == "line":
+ chart = line.LineChart(self.surface, self.options)
+
+ elif self.type == "pie":
+ self.options["legend"] = {"hide": "False"}
+ chart = pie.PieChart(self.surface, self.options)
+ self.dataSet = [(data[0],
+ [[0, data[1]]]) for data in sg.chart_data]
+
+ else:
+ chart = bar.HorizontalBarChart(self.surface,
+ self.options)
+
+ chart.addDataset(self.dataSet)
+ chart.render()
+
+ def as_png(self, file):
+ """Save the chart as png image"""
+ self.surface.write_to_png(file)
diff --git a/src/jarabe/util/readers.py b/src/jarabe/util/readers.py
new file mode 100644
index 0000000000..610a10a015
--- /dev/null
+++ b/src/jarabe/util/readers.py
@@ -0,0 +1,288 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# readers.py by:
+# Agustin Zubiaga
+# Walter Bender
+
+# Copyright (C) 2019 Hrishi Patel
+# 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 3 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+
+import os
+import glob
+import statvfs
+
+from gettext import gettext as _
+
+from sugar3 import env
+from sugar3 import profile
+from sugar3.datastore import datastore
+
+
+class FreeSpaceReader():
+ """Reader for Free Space
+ Measure free space on disk.
+ """
+
+ def __init__(self):
+ """Import chart data from file."""
+
+ space = self._get_space()
+ self._reader = ((_('Free space'), space[0]),
+ (_('Used space'), space[1]))
+ self.xlabel = ""
+ self.ylabel = ""
+
+ def get_chart_data(self):
+ """Return data suitable for pyCHA."""
+
+ chart_data = []
+
+ for row in self._reader:
+ label, value = row[0], row[1]
+
+ if label == "XLabel":
+ self.xlabel = value
+
+ elif label == "YLabel":
+ self.ylabel = value
+
+ else:
+ chart_data.append((label, float(value)))
+
+ return chart_data
+
+ def _get_space(self):
+ stat = os.statvfs(env.get_profile_path())
+ free_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL]
+ total_space = stat[statvfs.F_BSIZE] * stat[statvfs.F_BLOCKS]
+
+ free_space = self._get_MBs(free_space)
+ total_space = self._get_MBs(total_space)
+ used_space = total_space - free_space
+
+ return free_space, used_space, total_space
+
+ def _get_MBs(self, space):
+ space = space / (1024 * 1024)
+
+ return space
+
+ def _get_GBs(self, space):
+ space = space / 1024
+
+ return space
+
+ def get_labels_name(self):
+ """Return the h_label and y_label names."""
+
+ return self.xlabel, self.ylabel
+
+
+class TurtleReader():
+ """Reader for Journal activity
+
+ Import chart data from journal activity analysis
+ """
+
+ TACAT = {'clean':'forward', 'forward':'forward', 'back':'forward',
+ 'left':'forward', 'right':'forward', 'arc': 'arc',
+ 'xcor': 'coord', 'ycor': 'coord', 'heading': 'coord',
+ 'setxy2': 'setxy', 'seth': 'setxy', 'penup': 'pen', 'pendown': 'pen',
+ 'setpensize': 'pen', 'setcolor': 'pen', 'pensize': 'pen',
+ 'color': 'pen', 'setshade': 'pen', 'setgray': 'pen', 'shade': 'pen',
+ 'gray': 'pen', 'fillscreen': 'pen', 'startfill': 'fill',
+ 'stopfill': 'fill', 'plus2': 'number', 'minus2': 'number',
+ 'product2': 'number', 'division2': 'number', 'remainder2': 'number',
+ 'sqrt': 'number', 'identity2': 'number', 'and2': 'boolean',
+ 'or2': 'boolean', 'not': 'boolean', 'greater2': 'boolean',
+ 'less2': 'boolean', 'equal2': 'boolean', 'random': 'random',
+ 'repeat': 'repeat', 'forever': 'repeat', 'if': 'ifthen',
+ 'ifelse': 'ifthen', 'while': 'ifthen', 'until': 'ifthen',
+ 'hat': 'action', 'stack': 'action', 'storein': 'box', 'box': 'box',
+ 'luminance': 'sensor', 'mousex': 'sensor', 'mousey': 'sensor',
+ 'mousebutton2': 'sensor', 'keyboard': 'sensor', 'kbinput': 'sensor',
+ 'readpixel': 'sensor', 'see': 'sensor', 'time': 'sensor',
+ 'sound': 'sensor', 'volume': 'sensor', 'pitch': 'sensor',
+ 'resistance': 'sensor', 'voltage': 'sensor', 'video': 'media',
+ 'wait': 'media', 'camera': 'media', 'journal': 'media',
+ 'audio': 'media', 'show': 'media', 'setscale': 'media',
+ 'savepix': 'media', 'savesvg': 'media', 'mediawait': 'media',
+ 'mediapause': 'media', 'mediastop': 'media', 'mediaplay': 'media',
+ 'speak': 'media', 'sinewave': 'media', 'description': 'media',
+ 'push':'extras', 'pop':'extras', 'printheap':'extras',
+ 'clearheap':'extras', 'isheapempty2':'extras', 'chr':'extras',
+ 'int':'extras', 'myfunction': 'python', 'userdefined': 'python',
+ 'loadblock': 'python', 'loadpalette': 'python'}
+ TAPAL = {'forward': 'turtlep', 'arc': 'turtlep', 'coord': 'turtlep',
+ 'setxy': 'turtlep', 'pen': 'penp', 'fill': 'penp', 'number': 'numberp',
+ 'random': 'numberp', 'boolean': 'numberp', 'repeat': 'flowp',
+ 'ifthen': 'flowp', 'action': 'boxp', 'box': 'boxp',
+ 'sensor': 'sensorp', 'media': 'mediap', 'extras': 'extrasp',
+ 'python': 'extrasp'}
+ TASCORE = {'forward': 3, 'arc': 3, 'setxy': 2.5, 'coord': 4, 'turtlep': 5,
+ 'pen': 2.5, 'fill': 2.5, 'penp': 5,
+ 'number': 2.5, 'boolean': 2.5, 'random': 2.5, 'numberp': 0,
+ 'repeat': 2.5, 'ifthen': 7.5, 'flowp': 10,
+ 'box': 7.5, 'action': 7.5, 'boxp': 0,
+ 'media': 5, 'mediap': 0,
+ 'python': 5, 'extras': 5, 'extrasp': 0,
+ 'sensor': 5, 'sensorp': 0}
+ PALS = ['turtlep', 'penp', 'numberp', 'flowp', 'boxp', 'sensorp', 'mediap',
+ 'extrasp']
+ PALNAMES = [_('turtle'), _('pen'), _('number'), _('flow'), _('box'),
+ _('sensor'), _('media'), _('extras')]
+
+ def hasturtleblocks(self, path):
+ ''' Parse turtle block data and generate score based on rubric '''
+
+ fd = open(path)
+ blocks = []
+ # block name is second token in each line
+ for line in fd:
+ tokens = line.split(',')
+ if len(tokens) > 1:
+ token = tokens[1].strip('" [')
+ blocks.append(token)
+
+ score = []
+ for i in range(len(self.PALS)):
+ score.append([self.PALNAMES[i], 0])
+ cats = []
+ pals = []
+
+ for b in blocks:
+ if b in self.TACAT:
+ if not self.TACAT[b] in cats:
+ cats.append(self.TACAT[b])
+ for c in cats:
+ if c in self.TAPAL:
+ if not self.TAPAL[c] in pals:
+ pals.append(self.TAPAL[c])
+
+ for c in cats:
+ if c in self.TASCORE:
+ score[self.PALS.index(self.TAPAL[c])][1] += self.TASCORE[c]
+
+ for p in pals:
+ if p in self.TASCORE:
+ score[self.PALS.index(p)][1] += self.TASCORE[p]
+
+ return score
+
+ def __init__(self, file):
+
+ self._reader = self.hasturtleblocks(file)
+ self.xlabel = ""
+ self.ylabel = ""
+
+ def get_chart_data(self):
+ """Return data suitable for pyCHA."""
+
+ chart_data = []
+
+ for row in self._reader:
+ label, value = row[0], row[1]
+
+ if label == "XLabel":
+ self.xlabel = value
+
+ elif label == "YLabel":
+ self.ylabel = value
+
+ else:
+ chart_data.append((label, float(value)))
+
+ return chart_data
+
+ def get_labels_name(self):
+ """Return the h_label and y_label names."""
+
+ return self.xlabel, self.ylabel
+
+
+MAX = 19
+class ParseJournal():
+ ''' Simple parser of datastore '''
+
+ def __init__(self):
+ self._dsdict = {}
+ self._activity_name = []
+ self._activity_count = []
+
+ dsobjects, journal_entries = datastore.find({})
+ for dobj in dsobjects:
+ name = dobj.metadata['activity']
+ activity_name = name.split('.')[-1]
+ if not activity_name.isdigit():
+ self._activity_name.append(activity_name)
+ self._activity_count.append(1)
+
+ def get_sorted(self):
+ activity_tuples = []
+ for i in range(len(self._activity_name)):
+ activity_tuples.append((self._activity_name[i].replace('Activity',
+ ''),
+ self._activity_count[i]))
+ sorted_tuples = sorted(activity_tuples, key=lambda x: x[1])
+ activity_list = []
+ count = 0
+ length = len(sorted_tuples)
+ for i in range(length):
+ if i < MAX:
+ activity_list.append([sorted_tuples[length - i - 1][0],
+ sorted_tuples[length - i - 1][1]])
+ else:
+ count += sorted_tuples[length - i - 1][1]
+ if count > 0:
+ activity_list.append([_('Others'), count])
+ return activity_list
+
+
+class JournalReader():
+ """Reader for Journal activity
+
+ Import chart data from journal activity analysis
+ """
+
+ def __init__(self):
+
+ self._reader = ParseJournal().get_sorted()
+ self.xlabel = ""
+ self.ylabel = ""
+
+ def get_chart_data(self):
+ """Return data suitable for pyCHA."""
+
+ chart_data = []
+
+ for row in self._reader:
+ label, value = row[0], row[1]
+
+ if label == "XLabel":
+ self.xlabel = value
+
+ elif label == "YLabel":
+ self.ylabel = value
+
+ else:
+ chart_data.append((label, float(value)))
+
+ return chart_data
+
+ def get_labels_name(self):
+ """Return the h_label and y_label names."""
+
+ return self.xlabel, self.ylabel
diff --git a/src/jarabe/util/sugarpycha/Makefile.am b/src/jarabe/util/sugarpycha/Makefile.am
new file mode 100644
index 0000000000..29edf690e3
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/Makefile.am
@@ -0,0 +1,13 @@
+sugardir = $(pythondir)/jarabe/util/sugarpycha
+sugar_PYTHON = \
+ __init__.py \
+ bar.py \
+ chart.py \
+ color.py \
+ line.py \
+ pie.py \
+ polygonal.py \
+ radial.py \
+ scatter.py \
+ stackedbar.py \
+ utils.py
diff --git a/src/jarabe/util/sugarpycha/__init__.py b/src/jarabe/util/sugarpycha/__init__.py
new file mode 100644
index 0000000000..35bba091e4
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/__init__.py
@@ -0,0 +1,18 @@
+# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+version = "0.6.1dev"
diff --git a/src/jarabe/util/sugarpycha/bar.py b/src/jarabe/util/sugarpycha/bar.py
new file mode 100644
index 0000000000..40c734f22b
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/bar.py
@@ -0,0 +1,318 @@
+# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+from jarabe.util.sugarpycha.chart import Chart, uniqueIndices
+from jarabe.util.sugarpycha.color import hex2rgb
+from jarabe.util.sugarpycha.utils import safe_unicode
+
+
+class BarChart(Chart):
+
+ def __init__(self, surface=None, options={}, debug=False):
+ super(BarChart, self).__init__(surface, options, debug)
+ self.bars = []
+ self.minxdelta = 0.0
+ self.barWidthForSet = 0.0
+ self.barMargin = 0.0
+
+ def _updateXY(self):
+ super(BarChart, self)._updateXY()
+ # each dataset is centered around a line segment. that's why we
+ # need n + 1 divisions on the x axis
+ self.xscale = 1 / (self.xrange + 1.0)
+
+ def _updateChart(self):
+ """Evaluates measures for vertical bars"""
+ stores = self._getDatasetsValues()
+ uniqx = uniqueIndices(stores)
+
+ if len(uniqx) == 1:
+ self.minxdelta = 1.0
+ else:
+ self.minxdelta = min([abs(uniqx[j] - uniqx[j - 1])
+ for j in range(1, len(uniqx))])
+
+ k = self.minxdelta * self.xscale
+ barWidth = k * self.options.barWidthFillFraction
+ self.barWidthForSet = barWidth / len(stores)
+ self.barMargin = k * (1.0 - self.options.barWidthFillFraction) / 2
+
+ self.bars = []
+
+ def _renderChart(self, cx):
+ """Renders a horizontal/vertical bar chart"""
+
+ def drawBar(bar):
+ stroke_width = self.options.stroke.width
+ ux, uy = cx.device_to_user_distance(stroke_width, stroke_width)
+ if ux < uy:
+ ux = uy
+ cx.set_line_width(ux)
+
+ # gather bar proportions
+ x = self.layout.chart.x + self.layout.chart.w * bar.x
+ y = self.layout.chart.y + self.layout.chart.h * bar.y
+ w = self.layout.chart.w * bar.w
+ h = self.layout.chart.h * bar.h
+
+ if (w < 1 or h < 1) and self.options.yvals.skipSmallValues:
+ return # don't draw when the bar is too small
+
+ if self.options.stroke.shadow:
+ cx.set_source_rgba(0, 0, 0, 0.15)
+ rectangle = self._getShadowRectangle(x, y, w, h)
+ cx.rectangle(*rectangle)
+ cx.fill()
+
+ if self.options.shouldFill or (not self.options.stroke.hide):
+
+ if self.options.shouldFill:
+ cx.set_source_rgb(*self.colorScheme[bar.name])
+ cx.rectangle(x, y, w, h)
+ cx.fill()
+
+ if not self.options.stroke.hide:
+ cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
+ cx.rectangle(x, y, w, h)
+ cx.stroke()
+
+ if bar.yerr:
+ self._renderError(cx, x, y, w, h, bar.yval, bar.yerr)
+
+ # render yvals above/beside bars
+ if self.options.yvals.show:
+ cx.save()
+ cx.set_font_size(self.options.yvals.fontSize)
+ cx.set_source_rgb(*hex2rgb(self.options.yvals.fontColor))
+
+ if callable(self.options.yvals.renderer):
+ label = safe_unicode(self.options.yvals.renderer(bar),
+ self.options.encoding)
+ else:
+ label = safe_unicode(bar.yval, self.options.encoding)
+ extents = cx.text_extents(label)
+ labelW = extents[2]
+ labelH = extents[3]
+
+ self._renderYVal(cx, label, labelW, labelH, x, y, w, h)
+
+ cx.restore()
+
+ cx.save()
+ for bar in self.bars:
+ drawBar(bar)
+ cx.restore()
+
+ def _renderYVal(self, cx, label, width, height, x, y, w, h):
+ raise NotImplementedError
+
+
+class VerticalBarChart(BarChart):
+
+ def _updateChart(self):
+ """Evaluates measures for vertical bars"""
+ super(VerticalBarChart, self)._updateChart()
+ for i, (name, store) in enumerate(self.datasets):
+ for item in store:
+ if len(item) == 3:
+ xval, yval, yerr = item
+ else:
+ xval, yval = item
+
+ x = (((xval - self.minxval) * self.xscale)
+ + self.barMargin + (i * self.barWidthForSet))
+ w = self.barWidthForSet
+ h = abs(yval) * self.yscale
+ if yval > 0:
+ y = (1.0 - h) - self.origin
+ else:
+ y = 1 - self.origin
+ rect = Rect(x, y, w, h, xval, yval, name)
+
+ if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
+ self.bars.append(rect)
+
+ def _updateTicks(self):
+ """Evaluates bar ticks"""
+ super(BarChart, self)._updateTicks()
+ offset = (self.minxdelta * self.xscale) / 2
+ self.xticks = [(tick[0] + offset, tick[1]) for tick in self.xticks]
+
+ def _getShadowRectangle(self, x, y, w, h):
+ return (x - 2, y - 2, w + 4, h + 2)
+
+ def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH):
+ x = barX + (barW / 2.0) - (labelW / 2.0)
+ if self.options.yvals.snapToOrigin:
+ y = barY + barH - 0.5 * labelH
+ elif self.options.yvals.inside:
+ y = barY + (1.5 * labelH)
+ else:
+ y = barY - 0.5 * labelH
+
+ # if the label doesn't fit below the bar, put it above the bar
+ if y > (barY + barH):
+ y = barY - 0.5 * labelH
+
+ cx.move_to(x, y)
+ cx.show_text(label)
+
+ def _renderError(self, cx, barX, barY, barW, barH, value, error):
+ center = barX + (barW / 2.0)
+ errorWidth = max(barW * 0.1, 5.0)
+ left = center - errorWidth
+ right = center + errorWidth
+ errorSize = barH * error / value
+ top = barY + errorSize
+ bottom = barY - errorSize
+
+ cx.set_source_rgb(0, 0, 0)
+ cx.move_to(left, top)
+ cx.line_to(right, top)
+ cx.stroke()
+ cx.move_to(center, top)
+ cx.line_to(center, bottom)
+ cx.stroke()
+ cx.move_to(left, bottom)
+ cx.line_to(right, bottom)
+ cx.stroke()
+
+
+class HorizontalBarChart(BarChart):
+
+ def _updateChart(self):
+ """Evaluates measures for horizontal bars"""
+ super(HorizontalBarChart, self)._updateChart()
+
+ for i, (name, store) in enumerate(self.datasets):
+ for item in store:
+ if len(item) == 3:
+ xval, yval, yerr = item
+ else:
+ xval, yval = item
+ yerr = 0.0
+
+ y = (((xval - self.minxval) * self.xscale)
+ + self.barMargin + (i * self.barWidthForSet))
+ h = self.barWidthForSet
+ w = abs(yval) * self.yscale
+ if yval > 0:
+ x = self.origin
+ else:
+ x = self.origin - w
+ rect = Rect(x, y, w, h, xval, yval, name, yerr)
+
+ if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
+ self.bars.append(rect)
+
+ def _updateTicks(self):
+ """Evaluates bar ticks"""
+ super(BarChart, self)._updateTicks()
+ offset = (self.minxdelta * self.xscale) / 2
+ tmp = self.xticks
+ self.xticks = [(1.0 - tick[0], tick[1]) for tick in self.yticks]
+ self.yticks = [(tick[0] + offset, tick[1]) for tick in tmp]
+
+ def _renderLines(self, cx):
+ """Aux function for _renderBackground"""
+ if self.options.axis.y.showLines and self.yticks:
+ for tick in self.xticks:
+ self._renderLine(cx, tick, True)
+ if self.options.axis.x.showLines and self.xticks:
+ for tick in self.yticks:
+ self._renderLine(cx, tick, False)
+
+ def _getShadowRectangle(self, x, y, w, h):
+ return (x, y - 2, w + 2, h + 4)
+
+ def _renderXAxisLabel(self, cx, labelText):
+ labelText = self.options.axis.x.label
+ super(HorizontalBarChart, self)._renderYAxisLabel(cx, labelText)
+
+ def _renderXAxis(self, cx):
+ """Draws the horizontal line representing the X axis"""
+ cx.new_path()
+ cx.move_to(self.layout.chart.x,
+ self.layout.chart.y + self.layout.chart.h)
+ cx.line_to(self.layout.chart.x + self.layout.chart.w,
+ self.layout.chart.y + self.layout.chart.h)
+ cx.close_path()
+ cx.stroke()
+
+ def _renderYAxisLabel(self, cx, labelText):
+ labelText = self.options.axis.y.label
+ super(HorizontalBarChart, self)._renderXAxisLabel(cx, labelText)
+
+ def _renderYAxis(self, cx):
+ # draws the vertical line representing the Y axis
+ cx.new_path()
+ cx.move_to(self.layout.chart.x + self.origin * self.layout.chart.w,
+ self.layout.chart.y)
+ cx.line_to(self.layout.chart.x + self.origin * self.layout.chart.w,
+ self.layout.chart.y + self.layout.chart.h)
+ cx.close_path()
+ cx.stroke()
+
+ def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH):
+ y = barY + (barH / 2.0) + (labelH / 2.0)
+ if self.options.yvals.snapToOrigin:
+ x = barX + 2
+ elif self.options.yvals.inside:
+ x = barX + barW - (1.2 * labelW)
+ else:
+ x = barX + barW + 0.2 * labelW
+
+ # if the label doesn't fit to the left of the bar, put it to the right
+ if x < barX:
+ x = barX + barW + 0.2 * labelW
+
+ cx.move_to(x, y)
+ cx.show_text(label)
+
+ def _renderError(self, cx, barX, barY, barW, barH, value, error):
+ center = barY + (barH / 2.0)
+ errorHeight = max(barH * 0.1, 5.0)
+ top = center + errorHeight
+ bottom = center - errorHeight
+ errorSize = barW * error / value
+ right = barX + barW + errorSize
+ left = barX + barW - errorSize
+
+ cx.set_source_rgb(0, 0, 0)
+ cx.move_to(left, top)
+ cx.line_to(left, bottom)
+ cx.stroke()
+ cx.move_to(left, center)
+ cx.line_to(right, center)
+ cx.stroke()
+ cx.move_to(right, top)
+ cx.line_to(right, bottom)
+ cx.stroke()
+
+
+class Rect(object):
+
+ def __init__(self, x, y, w, h, xval, yval, name, yerr=0.0):
+ self.x, self.y, self.w, self.h = x, y, w, h
+ self.xval, self.yval, self.yerr = xval, yval, yerr
+ self.name = name
+
+ def __str__(self):
+ return (""
+ % (self.x, self.y, self.w, self.h,
+ self.xval, self.yval, self.yerr,
+ self.name))
diff --git a/src/jarabe/util/sugarpycha/chart.py b/src/jarabe/util/sugarpycha/chart.py
new file mode 100644
index 0000000000..e69cc48b91
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/chart.py
@@ -0,0 +1,887 @@
+# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+import copy
+import inspect
+import math
+
+import cairo
+
+from jarabe.util.sugarpycha.color import ColorScheme, hex2rgb, DEFAULT_COLOR
+from jarabe.util.sugarpycha.utils import safe_unicode
+from functools import reduce
+
+
+class Chart(object):
+
+ def __init__(self, surface, options={}, debug=False):
+ # this flag is useful to reuse this chart for drawing different data
+ # or use different options
+ self.resetFlag = False
+
+ # initialize storage
+ self.datasets = []
+
+ # computed values used in several methods
+ self.layout = Layout()
+ self.minxval = None
+ self.maxxval = None
+ self.minyval = None
+ self.maxyval = None
+ self.xscale = 1.0
+ self.yscale = 1.0
+ self.xrange = None
+ self.yrange = None
+ self.origin = 0.0
+
+ self.xticks = []
+ self.yticks = []
+
+ # set the default options
+ self.options = copy.deepcopy(DEFAULT_OPTIONS)
+ if options:
+ self.options.merge(options)
+
+ # initialize the surface
+ self._initSurface(surface)
+
+ self.colorScheme = None
+
+ # debug mode to draw aditional hints
+ self.debug = debug
+
+ def addDataset(self, dataset):
+ """Adds an object containing chart data to the storage hash"""
+ self.datasets += dataset
+
+ def _getDatasetsKeys(self):
+ """Return the name of each data set"""
+ return [d[0] for d in self.datasets]
+
+ def _getDatasetsValues(self):
+ """Return the data (value) of each data set"""
+ return [d[1] for d in self.datasets]
+
+ def setOptions(self, options={}):
+ """Sets options of this chart"""
+ self.options.merge(options)
+
+ def getSurfaceSize(self):
+ cx = cairo.Context(self.surface)
+ x, y, w, h = cx.clip_extents()
+ return w, h
+
+ def reset(self):
+ """Resets options and datasets.
+
+ In the next render the surface will be cleaned before any drawing.
+ """
+ self.resetFlag = True
+ self.options = copy.deepcopy(DEFAULT_OPTIONS)
+ self.datasets = []
+
+ def render(self, surface=None, options={}):
+ """Renders the chart with the specified options.
+
+ The optional parameters can be used to render a chart in a different
+ surface with new options.
+ """
+ self._update(options)
+ if surface:
+ self._initSurface(surface)
+
+ cx = cairo.Context(self.surface)
+
+ # calculate area data
+ surface_width, surface_height = self.getSurfaceSize()
+ self.layout.update(cx, self.options, surface_width, surface_height,
+ self.xticks, self.yticks)
+
+ self._renderBackground(cx)
+ if self.debug:
+ self.layout.render(cx)
+ self._renderChart(cx)
+ self._renderAxis(cx)
+ self._renderTitle(cx)
+ self._renderLegend(cx)
+
+ def clean(self):
+ """Clears the surface with a white background."""
+ cx = cairo.Context(self.surface)
+ cx.save()
+ cx.set_source_rgb(1, 1, 1)
+ cx.paint()
+ cx.restore()
+
+ def _setColorscheme(self):
+ """Sets the colorScheme used for the chart using the
+ options.colorScheme option
+ """
+ name = self.options.colorScheme.name
+ keys = self._getDatasetsKeys()
+ colorSchemeClass = ColorScheme.getColorScheme(name, None)
+ if colorSchemeClass is None:
+ raise ValueError('Color scheme "%s" is invalid!' % name)
+
+ # Remove invalid args before calling the constructor
+ kwargs = dict(self.options.colorScheme.args)
+ validArgs = inspect.getargspec(colorSchemeClass.__init__)[0]
+ kwargs = dict([(k, v) for k, v in list(kwargs.items()) if k in validArgs])
+ self.colorScheme = colorSchemeClass(keys, **kwargs)
+
+ def _initSurface(self, surface):
+ self.surface = surface
+
+ if self.resetFlag:
+ self.resetFlag = False
+ self.clean()
+
+ def _update(self, options={}):
+ """Update all the information needed to render the chart"""
+ self.setOptions(options)
+ self._setColorscheme()
+ self._updateXY()
+ self._updateChart()
+ self._updateTicks()
+
+ def _updateXY(self):
+ """Calculates all kinds of metrics for the x and y axis"""
+ x_range_is_defined = self.options.axis.x.range is not None
+ y_range_is_defined = self.options.axis.y.range is not None
+
+ if not x_range_is_defined or not y_range_is_defined:
+ stores = self._getDatasetsValues()
+
+ # gather data for the x axis
+ if x_range_is_defined:
+ self.minxval, self.maxxval = self.options.axis.x.range
+ else:
+ xdata = [pair[0] for pair in reduce(lambda a, b: a + b, stores)]
+ self.minxval = float(min(xdata))
+ self.maxxval = float(max(xdata))
+ if self.minxval * self.maxxval > 0 and self.minxval > 0:
+ self.minxval = 0.0
+
+ self.xrange = self.maxxval - self.minxval
+ if self.xrange == 0:
+ self.xscale = 1.0
+ else:
+ self.xscale = 1.0 / self.xrange
+
+ # gather data for the y axis
+ if y_range_is_defined:
+ self.minyval, self.maxyval = self.options.axis.y.range
+ else:
+ ydata = [pair[1] for pair in reduce(lambda a, b: a + b, stores)]
+ self.minyval = float(min(ydata))
+ self.maxyval = float(max(ydata))
+ if self.minyval * self.maxyval > 0 and self.minyval > 0:
+ self.minyval = 0.0
+
+ self.yrange = self.maxyval - self.minyval
+ if self.yrange == 0:
+ self.yscale = 1.0
+ else:
+ self.yscale = 1.0 / self.yrange
+
+ if self.minyval * self.maxyval < 0: # different signs
+ self.origin = abs(self.minyval) * self.yscale
+ else:
+ self.origin = 0.0
+
+ def _updateChart(self):
+ raise NotImplementedError
+
+ def _updateTicks(self):
+ """Evaluates ticks for x and y axis.
+
+ You should call _updateXY before because that method computes the
+ values of xscale, minxval, yscale, and other attributes needed for
+ this method.
+ """
+ stores = self._getDatasetsValues()
+
+ # evaluate xTicks
+ self.xticks = []
+ if self.options.axis.x.ticks:
+ for tick in self.options.axis.x.ticks:
+ if not isinstance(tick, Option):
+ tick = Option(tick)
+ if tick.label is None:
+ label = str(tick.v)
+ else:
+ label = tick.label
+ pos = self.xscale * (tick.v - self.minxval)
+ if 0.0 <= pos <= 1.0:
+ self.xticks.append((pos, label))
+
+ elif self.options.axis.x.interval > 0:
+ interval = self.options.axis.x.interval
+ label = (divmod(self.minxval, interval)[0] + 1) * interval
+ pos = self.xscale * (label - self.minxval)
+ prec = self.options.axis.x.tickPrecision
+ while 0.0 <= pos <= 1.0:
+ pretty_label = round(label, prec)
+ if prec == 0:
+ pretty_label = int(pretty_label)
+ self.xticks.append((pos, pretty_label))
+ label += interval
+ pos = self.xscale * (label - self.minxval)
+
+ elif self.options.axis.x.tickCount > 0:
+ uniqx = list(range(len(uniqueIndices(stores)) + 1))
+ roughSeparation = self.xrange / self.options.axis.x.tickCount
+ i = j = 0
+ while i < len(uniqx) and j < self.options.axis.x.tickCount:
+ if (uniqx[i] - self.minxval) >= (j * roughSeparation):
+ pos = self.xscale * (uniqx[i] - self.minxval)
+ if 0.0 <= pos <= 1.0:
+ self.xticks.append((pos, uniqx[i]))
+ j += 1
+ i += 1
+
+ # evaluate yTicks
+ self.yticks = []
+ if self.options.axis.y.ticks:
+ for tick in self.options.axis.y.ticks:
+ if not isinstance(tick, Option):
+ tick = Option(tick)
+ if tick.label is None:
+ label = str(tick.v)
+ else:
+ label = tick.label
+ pos = 1.0 - (self.yscale * (tick.v - self.minyval))
+ if 0.0 <= pos <= 1.0:
+ self.yticks.append((pos, label))
+
+ elif self.options.axis.y.interval > 0:
+ interval = self.options.axis.y.interval
+ label = (divmod(self.minyval, interval)[0] + 1) * interval
+ pos = 1.0 - (self.yscale * (label - self.minyval))
+ prec = self.options.axis.y.tickPrecision
+ while 0.0 <= pos <= 1.0:
+ pretty_label = round(label, prec)
+ if prec == 0:
+ pretty_label = int(pretty_label)
+ self.yticks.append((pos, pretty_label))
+ label += interval
+ pos = 1.0 - (self.yscale * (label - self.minyval))
+
+ elif self.options.axis.y.tickCount > 0:
+ prec = self.options.axis.y.tickPrecision
+ num = self.yrange / self.options.axis.y.tickCount
+ if (num < 1 and prec == 0):
+ roughSeparation = 1
+ else:
+ roughSeparation = round(num, prec)
+
+ for i in range(self.options.axis.y.tickCount + 1):
+ yval = self.minyval + (i * roughSeparation)
+ pos = 1.0 - ((yval - self.minyval) * self.yscale)
+ if 0.0 <= pos <= 1.0:
+ pretty_label = round(yval, prec)
+ if prec == 0:
+ pretty_label = int(pretty_label)
+ self.yticks.append((pos, pretty_label))
+
+ def _renderBackground(self, cx):
+ """Renders the background area of the chart"""
+ if self.options.background.hide:
+ return
+
+ cx.save()
+
+ if self.options.background.baseColor:
+ cx.set_source_rgb(*hex2rgb(self.options.background.baseColor))
+ cx.paint()
+
+ if self.options.background.chartColor:
+ cx.set_source_rgb(*hex2rgb(self.options.background.chartColor))
+ surface_width, surface_height = self.getSurfaceSize()
+ cx.rectangle(self.options.padding.left, self.options.padding.top,
+ surface_width - (self.options.padding.left
+ + self.options.padding.right),
+ surface_height - (self.options.padding.top
+ + self.options.padding.bottom))
+ cx.fill()
+
+ if self.options.background.lineColor:
+ cx.set_source_rgb(*hex2rgb(self.options.background.lineColor))
+ cx.set_line_width(self.options.axis.lineWidth)
+ self._renderLines(cx)
+
+ cx.restore()
+
+ def _renderLines(self, cx):
+ """Aux function for _renderBackground"""
+ if self.options.axis.y.showLines and self.yticks:
+ for tick in self.yticks:
+ self._renderLine(cx, tick, False)
+ if self.options.axis.x.showLines and self.xticks:
+ for tick in self.xticks:
+ self._renderLine(cx, tick, True)
+
+ def _renderLine(self, cx, tick, horiz):
+ """Aux function for _renderLines"""
+ x1, x2, y1, y2 = (0, 0, 0, 0)
+ if horiz:
+ x1 = x2 = tick[0] * self.layout.chart.w + self.layout.chart.x
+ y1 = self.layout.chart.y
+ y2 = y1 + self.layout.chart.h
+ else:
+ x1 = self.layout.chart.x
+ x2 = x1 + self.layout.chart.w
+ y1 = y2 = tick[0] * self.layout.chart.h + self.layout.chart.y
+
+ cx.new_path()
+ cx.move_to(x1, y1)
+ cx.line_to(x2, y2)
+ cx.close_path()
+ cx.stroke()
+
+ def _renderChart(self, cx):
+ raise NotImplementedError
+
+ def _renderTick(self, cx, tick, x, y, x2, y2, rotate, text_position):
+ """Aux method for _renderXTick and _renderYTick"""
+ if callable(tick):
+ return
+
+ cx.new_path()
+ cx.move_to(x, y)
+ cx.line_to(x2, y2)
+ cx.close_path()
+ cx.stroke()
+ cx.set_source_rgb(*hex2rgb('#000000'))
+ cx.select_font_face(self.options.axis.tickFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_NORMAL)
+ cx.set_font_size(self.options.axis.tickFontSize)
+
+ label = safe_unicode(tick[1], self.options.encoding)
+ xb, yb, width, height, xa, ya = cx.text_extents(label)
+
+ x, y = text_position
+
+ if rotate:
+ cx.save()
+ cx.translate(x, y)
+ cx.rotate(math.radians(rotate))
+ x = -width / 2.0
+ y = -height / 2.0
+ cx.move_to(x - xb, y - yb)
+ cx.show_text(label)
+ cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
+ if self.debug:
+ cx.rectangle(x, y, width, height)
+ cx.stroke()
+ cx.restore()
+ else:
+ x -= width / 2.0
+ y -= height / 2.0
+ cx.move_to(x - xb, y - yb)
+ cx.show_text(label)
+ cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
+ if self.debug:
+ cx.rectangle(x, y, width, height)
+ cx.stroke()
+
+ return label
+
+ def _renderYTick(self, cx, tick):
+ """Aux method for _renderAxis"""
+ x = self.layout.y_ticks.x + self.layout.y_ticks.w
+ y = self.layout.y_ticks.y + tick[0] * self.layout.y_ticks.h
+
+ text_position = ((self.layout.y_tick_labels.x
+ + self.layout.y_tick_labels.w / 2.0 - 5), y)
+
+ return self._renderTick(cx, tick,
+ x, y,
+ x - self.options.axis.tickSize, y,
+ self.options.axis.y.rotate,
+ text_position)
+
+ def _renderXTick(self, cx, tick):
+ """Aux method for _renderAxis"""
+
+ x = self.layout.x_ticks.x + tick[0] * self.layout.x_ticks.w
+ y = self.layout.x_ticks.y
+
+ text_position = (x, (self.layout.x_tick_labels.y + 5
+ + self.layout.x_tick_labels.h / 2.0))
+
+ return self._renderTick(cx, tick,
+ x, y,
+ x, y + self.options.axis.tickSize,
+ self.options.axis.x.rotate,
+ text_position)
+
+ def _renderAxisLabel(self, cx, label, x, y, vertical=False):
+ cx.save()
+ cx.select_font_face(self.options.axis.labelFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_BOLD)
+ cx.set_font_size(self.options.axis.labelFontSize)
+ cx.set_source_rgb(*hex2rgb(self.options.axis.labelColor))
+
+ xb, yb, width, height, xa, ya = cx.text_extents(label)
+
+ if vertical:
+ y = y + width / 2.0
+ cx.move_to(x - xb, y - yb)
+ cx.translate(x, y)
+ cx.rotate(-math.radians(90))
+ cx.move_to(-xb, -yb)
+ cx.show_text(label)
+ if self.debug:
+ cx.rectangle(0, 0, width, height)
+ cx.stroke()
+ else:
+ x = x - width / 2.0
+ cx.move_to(x - xb, y - yb)
+ cx.show_text(label)
+ if self.debug:
+ cx.rectangle(x, y, width, height)
+ cx.stroke()
+ cx.restore()
+
+ def _renderYAxisLabel(self, cx, label_text):
+ label = safe_unicode(label_text, self.options.encoding)
+ x = self.layout.y_label.x
+ y = self.layout.y_label.y + self.layout.y_label.h / 2.0
+ self._renderAxisLabel(cx, label, x, y, True)
+
+ def _renderYAxis(self, cx):
+ """Draws the vertical line represeting the Y axis"""
+ cx.new_path()
+ cx.move_to(self.layout.chart.x, self.layout.chart.y)
+ cx.line_to(self.layout.chart.x,
+ self.layout.chart.y + self.layout.chart.h)
+ cx.close_path()
+ cx.stroke()
+
+ def _renderXAxisLabel(self, cx, label_text):
+ label = safe_unicode(label_text, self.options.encoding)
+ x = self.layout.x_label.x + self.layout.x_label.w / 2.0
+ y = self.layout.x_label.y
+ self._renderAxisLabel(cx, label, x, y, False)
+
+ def _renderXAxis(self, cx):
+ """Draws the horizontal line representing the X axis"""
+ cx.new_path()
+ y = self.layout.chart.y + (1.0 - self.origin) * self.layout.chart.h
+ cx.move_to(self.layout.chart.x, y)
+ cx.line_to(self.layout.chart.x + self.layout.chart.w, y)
+ cx.close_path()
+ cx.stroke()
+
+ def _renderAxis(self, cx):
+ """Renders axis"""
+ if self.options.axis.x.hide and self.options.axis.y.hide:
+ return
+
+ cx.save()
+ cx.set_line_width(self.options.axis.lineWidth)
+
+ if not self.options.axis.y.hide:
+ if self.yticks:
+ for tick in self.yticks:
+ self._renderYTick(cx, tick)
+
+ if self.options.axis.y.label:
+ self._renderYAxisLabel(cx, self.options.axis.y.label)
+
+ cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
+ self._renderYAxis(cx)
+
+ if not self.options.axis.x.hide:
+ if self.xticks:
+ for tick in self.xticks:
+ self._renderXTick(cx, tick)
+
+ if self.options.axis.x.label:
+ self._renderXAxisLabel(cx, self.options.axis.x.label)
+
+ cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
+ self._renderXAxis(cx)
+
+ cx.restore()
+
+ def _renderTitle(self, cx):
+ if self.options.title:
+ cx.save()
+ cx.select_font_face(self.options.titleFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_BOLD)
+ cx.set_font_size(self.options.titleFontSize)
+ cx.set_source_rgb(*hex2rgb(self.options.titleColor))
+
+ title = safe_unicode(self.options.title, self.options.encoding)
+ extents = cx.text_extents(title)
+ title_width = extents[2]
+
+ x = (self.layout.title.x
+ + self.layout.title.w / 2.0
+ - title_width / 2.0)
+ y = self.layout.title.y - extents[1] - 10
+
+ cx.move_to(x, y)
+ cx.show_text(title)
+
+ cx.restore()
+
+ def _renderLegend(self, cx):
+ """This function adds a legend to the chart"""
+ if self.options.legend.hide:
+ return
+
+ surface_width, surface_height = self.getSurfaceSize()
+
+ # Compute legend dimensions
+ padding = 4
+ bullet = 15
+ width = 0
+ height = padding
+ keys = self._getDatasetsKeys()
+ cx.select_font_face(self.options.legend.legendFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_NORMAL)
+ cx.set_font_size(self.options.legend.legendFontSize)
+ for key in keys:
+ key = safe_unicode(key, self.options.encoding)
+ extents = cx.text_extents(key)
+ width = max(extents[2], width)
+ height += max(extents[3], bullet) + padding
+ width = padding + bullet + padding + width + padding
+
+ # Compute legend position
+ legend = self.options.legend
+ if legend.position.right is not None:
+ legend.position.left = (surface_width
+ - legend.position.right
+ - width)
+ if legend.position.bottom is not None:
+ legend.position.top = (surface_height
+ - legend.position.bottom
+ - height)
+
+ # Draw the legend
+ cx.save()
+ cx.rectangle(self.options.legend.position.left,
+ self.options.legend.position.top,
+ width, height)
+ cx.set_source_rgba(1, 1, 1, self.options.legend.opacity)
+ cx.fill_preserve()
+ cx.set_line_width(self.options.legend.borderWidth)
+ cx.set_source_rgb(*hex2rgb(self.options.legend.borderColor))
+ cx.stroke()
+
+ def drawKey(key, x, y, text_height):
+ cx.rectangle(x, y, bullet, bullet)
+ cx.set_source_rgb(*self.colorScheme[key])
+ cx.fill_preserve()
+ cx.set_source_rgb(0, 0, 0)
+ cx.stroke()
+ cx.move_to(x + bullet + padding,
+ y + bullet / 2.0 + text_height / 2.0)
+ cx.show_text(key)
+
+ cx.set_line_width(1)
+ x = self.options.legend.position.left + padding
+ y = self.options.legend.position.top + padding
+ for key in keys:
+ extents = cx.text_extents(key)
+ drawKey(key, x, y, extents[3])
+ y += max(extents[3], bullet) + padding
+
+ cx.restore()
+
+
+def uniqueIndices(arr):
+ """Return a list with the indexes of the biggest element of arr"""
+ return list(range(max([len(a) for a in arr])))
+
+
+class Area(object):
+ """Simple rectangle to hold an area coordinates and dimensions"""
+
+ def __init__(self, x=0.0, y=0.0, w=0.0, h=0.0):
+ self.x, self.y, self.w, self.h = x, y, w, h
+
+ def __str__(self):
+ msg = ""
+ return msg % (self.x, self.y, self.w, self.h)
+
+
+def get_text_extents(cx, text, font, font_size, encoding):
+ if text:
+ cx.save()
+ cx.select_font_face(font,
+ cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
+ cx.set_font_size(font_size)
+ safe_text = safe_unicode(text, encoding)
+ extents = cx.text_extents(safe_text)
+ cx.restore()
+ return extents[2:4]
+ return (0.0, 0.0)
+
+
+class Layout(object):
+ """Set of chart areas"""
+
+ def __init__(self):
+ self.title = Area()
+ self.x_label = Area()
+ self.y_label = Area()
+ self.x_tick_labels = Area()
+ self.y_tick_labels = Area()
+ self.x_ticks = Area()
+ self.y_ticks = Area()
+ self.chart = Area()
+
+ self._areas = (
+ (self.title, (1, 126 / 255.0, 0)), # orange
+ (self.y_label, (41 / 255.0, 91 / 255.0, 41 / 255.0)), # grey
+ (self.x_label, (41 / 255.0, 91 / 255.0, 41 / 255.0)), # grey
+ (self.y_tick_labels, (0, 115 / 255.0, 0)), # green
+ (self.x_tick_labels, (0, 115 / 255.0, 0)), # green
+ (self.y_ticks, (229 / 255.0, 241 / 255.0, 18 / 255.0)), # yellow
+ (self.x_ticks, (229 / 255.0, 241 / 255.0, 18 / 255.0)), # yellow
+ (self.chart, (75 / 255.0, 75 / 255.0, 1.0)), # blue
+ )
+
+ def update(self, cx, options, width, height, xticks, yticks):
+ self.title.x = options.padding.left
+ self.title.y = options.padding.top
+ self.title.w = width - (options.padding.left + options.padding.right)
+ self.title.h = get_text_extents(cx,
+ options.title,
+ options.titleFont,
+ options.titleFontSize,
+ options.encoding)[1]
+ x_axis_label_height = get_text_extents(cx,
+ options.axis.x.label,
+ options.axis.labelFont,
+ options.axis.labelFontSize,
+ options.encoding)[1]
+ y_axis_label_width = get_text_extents(cx,
+ options.axis.y.label,
+ options.axis.labelFont,
+ options.axis.labelFontSize,
+ options.encoding)[1]
+
+ x_axis_tick_labels_height = self._getAxisTickLabelsSize(cx, options,
+ options.axis.x,
+ xticks)[1]
+ y_axis_tick_labels_width = self._getAxisTickLabelsSize(cx, options,
+ options.axis.y,
+ yticks)[0]
+
+ self.y_label.x = options.padding.left
+ self.y_label.y = options.padding.top + self.title.h
+ self.y_label.w = y_axis_label_width
+ self.y_label.h = height - (options.padding.bottom
+ + options.padding.top
+ + x_axis_label_height
+ + x_axis_tick_labels_height
+ + options.axis.tickSize
+ + self.title.h)
+ self.x_label.x = (options.padding.left
+ + y_axis_label_width
+ + y_axis_tick_labels_width
+ + options.axis.tickSize)
+ self.x_label.y = height - (options.padding.bottom
+ + x_axis_label_height)
+ self.x_label.w = width - (options.padding.left
+ + options.padding.right
+ + options.axis.tickSize
+ + y_axis_label_width
+ + y_axis_tick_labels_width)
+ self.x_label.h = x_axis_label_height
+
+ self.y_tick_labels.x = self.y_label.x + self.y_label.w
+ self.y_tick_labels.y = self.y_label.y
+ self.y_tick_labels.w = y_axis_tick_labels_width
+ self.y_tick_labels.h = self.y_label.h
+
+ self.x_tick_labels.x = self.x_label.x
+ self.x_tick_labels.y = self.x_label.y - x_axis_tick_labels_height
+ self.x_tick_labels.w = self.x_label.w
+ self.x_tick_labels.h = x_axis_tick_labels_height
+
+ self.y_ticks.x = self.y_tick_labels.x + self.y_tick_labels.w
+ self.y_ticks.y = self.y_tick_labels.y
+ self.y_ticks.w = options.axis.tickSize
+ self.y_ticks.h = self.y_label.h
+
+ self.x_ticks.x = self.x_tick_labels.x
+ self.x_ticks.y = self.x_tick_labels.y - options.axis.tickSize
+ self.x_ticks.w = self.x_label.w
+ self.x_ticks.h = options.axis.tickSize
+
+ self.chart.x = self.y_ticks.x + self.y_ticks.w
+ self.chart.y = self.title.y + self.title.h
+ self.chart.w = self.x_ticks.w
+ self.chart.h = self.y_ticks.h
+
+ def render(self, cx):
+
+ def draw_area(area, r, g, b):
+ cx.rectangle(area.x, area.y, area.w, area.h)
+ cx.set_source_rgba(r, g, b, 0.5)
+ cx.fill()
+
+ cx.save()
+ for area, color in self._areas:
+ draw_area(area, *color)
+ cx.restore()
+
+ def _getAxisTickLabelsSize(self, cx, options, axis, ticks):
+ cx.save()
+ cx.select_font_face(options.axis.tickFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_NORMAL)
+ cx.set_font_size(options.axis.tickFontSize)
+
+ max_width = max_height = 0.0
+ if not axis.hide:
+ extents = [cx.text_extents(safe_unicode(
+ tick[1], options.encoding,
+ ))[2:4] # get width and height as a tuple
+ for tick in ticks]
+ if extents:
+ widths, heights = list(zip(*extents))
+ max_width, max_height = max(widths), max(heights)
+ if axis.rotate:
+ radians = math.radians(axis.rotate)
+ sin = math.sin(radians)
+ cos = math.cos(radians)
+ max_width, max_height = (
+ max_width * cos + max_height * sin,
+ max_width * sin + max_height * cos,
+ )
+ cx.restore()
+ return max_width, max_height
+
+
+class Option(dict):
+ """Useful dict that allow attribute-like access to its keys"""
+
+ def __getattr__(self, name):
+ if name in list(self.keys()):
+ return self[name]
+ else:
+ raise AttributeError(name)
+
+ def merge(self, other):
+ """Recursive merge with other Option or dict object"""
+ for key, value in list(other.items()):
+ if key in self:
+ if isinstance(self[key], Option):
+ self[key].merge(other[key])
+ else:
+ self[key] = other[key]
+
+
+DEFAULT_OPTIONS = Option(
+ axis=Option(
+ lineWidth=1.0,
+ lineColor='#0f0000',
+ tickSize=3.0,
+ labelColor='#666666',
+ labelFont='Tahoma',
+ labelFontSize=9,
+ tickFont='Tahoma',
+ tickFontSize=9,
+ x=Option(
+ hide=False,
+ ticks=None,
+ tickCount=10,
+ tickPrecision=1,
+ range=None,
+ rotate=None,
+ label=None,
+ interval=0,
+ showLines=False,
+ ),
+ y=Option(
+ hide=False,
+ ticks=None,
+ tickCount=10,
+ tickPrecision=1,
+ range=None,
+ rotate=None,
+ label=None,
+ interval=0,
+ showLines=True,
+ ),
+ ),
+ background=Option(
+ hide=False,
+ baseColor=None,
+ chartColor='#f5f5f5',
+ lineColor='#ffffff',
+ lineWidth=1.5,
+ ),
+ legend=Option(
+ opacity=0.8,
+ borderColor='#000000',
+ borderWidth=2,
+ hide=False,
+ legendFont='Tahoma',
+ legendFontSize=9,
+ position=Option(top=20, left=40, bottom=None, right=None),
+ ),
+ padding=Option(
+ left=10,
+ right=10,
+ top=10,
+ bottom=10,
+ ),
+ stroke=Option(
+ color='#000000',
+ hide=False,
+ shadow=True,
+ width=1
+ ),
+ yvals=Option(
+ show=False,
+ inside=False,
+ fontSize=11,
+ fontColor='#000000',
+ skipSmallValues=True,
+ snapToOrigin=False,
+ renderer=None
+ ),
+ fillOpacity=1.0,
+ shouldFill=True,
+ barWidthFillFraction=0.75,
+ pieRadius=0.4,
+ colorScheme=Option(
+ name='gradient',
+ args=Option(
+ initialColor=DEFAULT_COLOR,
+ colors=None,
+ ),
+ ),
+ title=None,
+ titleColor='#000000',
+ titleFont='Tahoma',
+ titleFontSize=12,
+ encoding='utf-8',
+)
diff --git a/src/jarabe/util/sugarpycha/color.py b/src/jarabe/util/sugarpycha/color.py
new file mode 100644
index 0000000000..5e38bd87b4
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/color.py
@@ -0,0 +1,203 @@
+# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
+# 2009 by Yaco S.L.
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+import math
+
+from jarabe.util.sugarpycha.utils import clamp
+
+
+DEFAULT_COLOR = '#3c581a'
+
+
+def hex2rgb(hexstring, digits=2):
+ """Converts a hexstring color to a rgb tuple.
+
+ Example: #ff0000 -> (1.0, 0.0, 0.0)
+
+ digits is an integer number telling how many characters should be
+ interpreted for each component in the hexstring.
+ """
+ if isinstance(hexstring, (tuple, list)):
+ return hexstring
+
+ top = float(int(digits * 'f', 16))
+ r = int(hexstring[1:digits + 1], 16)
+ g = int(hexstring[digits + 1:digits * 2 + 1], 16)
+ b = int(hexstring[digits * 2 + 1:digits * 3 + 1], 16)
+ return r / top, g / top, b / top
+
+
+def rgb2hsv(r, g, b):
+ """Converts a RGB color into a HSV one
+
+ See http://en.wikipedia.org/wiki/HSV_color_space
+ """
+ maximum = max(r, g, b)
+ minimum = min(r, g, b)
+ if maximum == minimum:
+ h = 0.0
+ elif maximum == r:
+ h = 60.0 * ((g - b) / (maximum - minimum)) + 360.0
+ if h >= 360.0:
+ h -= 360.0
+ elif maximum == g:
+ h = 60.0 * ((b - r) / (maximum - minimum)) + 120.0
+ elif maximum == b:
+ h = 60.0 * ((r - g) / (maximum - minimum)) + 240.0
+
+ if maximum == 0.0:
+ s = 0.0
+ else:
+ s = 1.0 - (minimum / maximum)
+
+ v = maximum
+
+ return h, s, v
+
+
+def hsv2rgb(h, s, v):
+ """Converts a HSV color into a RGB one
+
+ See http://en.wikipedia.org/wiki/HSV_color_space
+ """
+ hi = int(math.floor(h / 60.0)) % 6
+ f = (h / 60.0) - hi
+ p = v * (1 - s)
+ q = v * (1 - f * s)
+ t = v * (1 - (1 - f) * s)
+
+ if hi == 0:
+ r, g, b = v, t, p
+ elif hi == 1:
+ r, g, b = q, v, p
+ elif hi == 2:
+ r, g, b = p, v, t
+ elif hi == 3:
+ r, g, b = p, q, v
+ elif hi == 4:
+ r, g, b = t, p, v
+ elif hi == 5:
+ r, g, b = v, p, q
+
+ return r, g, b
+
+
+def lighten(r, g, b, amount):
+ """Return a lighter version of the color (r, g, b)"""
+ return (clamp(0.0, 1.0, r + amount),
+ clamp(0.0, 1.0, g + amount),
+ clamp(0.0, 1.0, b + amount))
+
+
+basicColors = dict(
+ red='#6d1d1d',
+ green=DEFAULT_COLOR,
+ blue='#224565',
+ grey='#444444',
+ black='#000000',
+ darkcyan='#305755',
+ )
+
+
+class ColorSchemeMetaclass(type):
+ """This metaclass is used to autoregister all ColorScheme classes"""
+
+ def __new__(mcs, name, bases, dict):
+ klass = type.__new__(mcs, name, bases, dict)
+ klass.registerColorScheme()
+ return klass
+
+
+class ColorScheme(dict):
+ """A color scheme is a dictionary where the keys match the keys
+ constructor argument and the values are colors"""
+ __metaclass__ = ColorSchemeMetaclass
+ __registry__ = {}
+
+ def __init__(self, keys):
+ super(ColorScheme, self).__init__()
+
+ @classmethod
+ def registerColorScheme(cls):
+ key = cls.__name__.replace('ColorScheme', '').lower()
+ if key:
+ cls.__registry__[key] = cls
+
+ @classmethod
+ def getColorScheme(cls, name, default=None):
+ return cls.__registry__.get(name, default)
+
+
+class GradientColorScheme(ColorScheme):
+ """In this color scheme each color is a lighter version of initialColor.
+
+ This difference is computed based on the number of keys.
+
+ The initialColor is given in a hex string format.
+ """
+
+ def __init__(self, keys, initialColor=DEFAULT_COLOR):
+ super(GradientColorScheme, self).__init__(keys)
+ if initialColor in basicColors:
+ initialColor = basicColors[initialColor]
+
+ r, g, b = hex2rgb(initialColor)
+ light = 1.0 / (len(keys) * 2)
+
+ for i, key in enumerate(keys):
+ self[key] = lighten(r, g, b, light * i)
+
+
+class FixedColorScheme(ColorScheme):
+ """In this color scheme fixed colors are used.
+
+ These colors are provided as a list argument in the constructor.
+ """
+
+ def __init__(self, keys, colors=[]):
+ super(FixedColorScheme, self).__init__(keys)
+
+ if len(keys) != len(colors):
+ raise ValueError("You must provide as many colors as datasets "
+ "for the fixed color scheme")
+
+ for i, key in enumerate(keys):
+ self[key] = hex2rgb(colors[i])
+
+
+class RainbowColorScheme(ColorScheme):
+ """In this color scheme the rainbow is divided in N pieces
+ where N is the number of datasets.
+
+ So each dataset gets a color of the rainbow.
+ """
+
+ def __init__(self, keys, initialColor=DEFAULT_COLOR):
+ super(RainbowColorScheme, self).__init__(keys)
+ if initialColor in basicColors:
+ initialColor = basicColors[initialColor]
+
+ r, g, b = hex2rgb(initialColor)
+ h, s, v = rgb2hsv(r, g, b)
+
+ angleDelta = 360.0 / (len(keys) + 1)
+ for key in keys:
+ self[key] = hsv2rgb(h, s, v)
+ h += angleDelta
+ if h >= 360.0:
+ h -= 360.0
diff --git a/src/jarabe/util/sugarpycha/line.py b/src/jarabe/util/sugarpycha/line.py
new file mode 100644
index 0000000000..a4e25e5036
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/line.py
@@ -0,0 +1,129 @@
+# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+from jarabe.util.sugarpycha.chart import Chart
+from jarabe.util.sugarpycha.color import hex2rgb
+
+
+class LineChart(Chart):
+
+ def __init__(self, surface=None, options={}, debug=False):
+ super(LineChart, self).__init__(surface, options, debug)
+ self.points = []
+
+ def _updateChart(self):
+ """Evaluates measures for line charts"""
+ self.points = []
+
+ for i, (name, store) in enumerate(self.datasets):
+ for item in store:
+ xval, yval = item
+ x = (xval - self.minxval) * self.xscale
+ y = 1.0 - (yval - self.minyval) * self.yscale
+ point = Point(x, y, xval, yval, name)
+
+ if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0:
+ self.points.append(point)
+
+ def _renderChart(self, cx):
+ """Renders a line chart"""
+
+ def preparePath(storeName):
+ cx.new_path()
+ firstPoint = True
+ lastX = None
+ if self.options.shouldFill:
+ # Go to the (0,0) coordinate to start drawing the area
+ #cx.move_to(self.layout.chart.x,
+ # self.layout.chart.y + self.layout.chart.h)
+ offset = (1.0 - self.origin) * self.layout.chart.h
+ cx.move_to(self.layout.chart.x, self.layout.chart.y + offset)
+
+ for point in self.points:
+ if point.name == storeName:
+ if not self.options.shouldFill and firstPoint:
+ # starts the first point of the line
+ cx.move_to(point.x * self.layout.chart.w
+ + self.layout.chart.x,
+ point.y * self.layout.chart.h
+ + self.layout.chart.y)
+ firstPoint = False
+ continue
+ cx.line_to(point.x * self.layout.chart.w
+ + self.layout.chart.x,
+ point.y * self.layout.chart.h
+ + self.layout.chart.y)
+ # we remember the last X coordinate to close the area
+ # properly. See bug #4
+ lastX = point.x
+
+ if self.options.shouldFill:
+ # Close the path to the start point
+ y = ((1.0 - self.origin) * self.layout.chart.h
+ + self.layout.chart.y)
+ cx.line_to(lastX * self.layout.chart.w
+ + self.layout.chart.x, y)
+ cx.line_to(self.layout.chart.x, y)
+ cx.close_path()
+ else:
+ cx.set_source_rgb(*self.colorScheme[storeName])
+ cx.stroke()
+
+ cx.save()
+ cx.set_line_width(self.options.stroke.width)
+ if self.options.shouldFill:
+
+ def drawLine(storeName):
+ if self.options.stroke.shadow:
+ # draw shadow
+ cx.save()
+ cx.set_source_rgba(0, 0, 0, 0.15)
+ cx.translate(2, -2)
+ preparePath(storeName)
+ cx.fill()
+ cx.restore()
+
+ # fill the line
+ cx.set_source_rgb(*self.colorScheme[storeName])
+ preparePath(storeName)
+ cx.fill()
+
+ if not self.options.stroke.hide:
+ # draw stroke
+ cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
+ preparePath(storeName)
+ cx.stroke()
+
+ # draw the lines
+ for key in self._getDatasetsKeys():
+ drawLine(key)
+ else:
+ for key in self._getDatasetsKeys():
+ preparePath(key)
+
+ cx.restore()
+
+
+class Point(object):
+
+ def __init__(self, x, y, xval, yval, name):
+ self.x, self.y = x, y
+ self.xval, self.yval = xval, yval
+ self.name = name
+
+ def __str__(self):
+ return "" % (self.x, self.y)
diff --git a/src/jarabe/util/sugarpycha/pie.py b/src/jarabe/util/sugarpycha/pie.py
new file mode 100644
index 0000000000..e09f0dd972
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/pie.py
@@ -0,0 +1,368 @@
+# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+import math
+
+import cairo
+
+from jarabe.util.sugarpycha.chart import Chart, Option, Layout, Area, get_text_extents
+from jarabe.util.sugarpycha.color import hex2rgb
+
+
+class PieChart(Chart):
+
+ def __init__(self, surface=None, options={}, debug=False):
+ super(PieChart, self).__init__(surface, options, debug)
+ self.slices = []
+ self.centerx = 0
+ self.centery = 0
+ self.layout = PieLayout(self.slices)
+
+ self.colors = [[0, 255, 0],
+ [0, 200, 204],
+ [120, 203, 0],
+ [107, 0, 202],
+ [194, 0, 0],
+ ]
+
+ def _updateChart(self):
+ """Evaluates measures for pie charts"""
+ slices = [dict(name=key,
+ value=(i, value[0][1]))
+ for i, (key, value) in enumerate(self.datasets)]
+
+ s = float(sum([slice['value'][1] for slice in slices]))
+
+ fraction = angle = 0.0
+
+ del self.slices[:]
+ for slice in slices:
+ if slice['value'][1] > 0:
+ angle += fraction
+ fraction = slice['value'][1] / s
+ self.slices.append(Slice(slice['name'], fraction,
+ slice['value'][0], slice['value'][1],
+ angle))
+
+ def _updateTicks(self):
+ """Evaluates pie ticks"""
+ self.xticks = []
+ if self.options.axis.x.ticks:
+ lookup = dict([(_slice.xval, _slice) for _slice in self.slices])
+ for tick in self.options.axis.x.ticks:
+ if not isinstance(tick, Option):
+ tick = Option(tick)
+ _slice = lookup.get(tick.v, None)
+ label = tick.label or str(tick.v)
+ if _slice is not None:
+ label += ' (%.0f%%)' % (_slice.fraction * 100)
+ self.xticks.append((tick.v, label))
+ else:
+ for _slice in self.slices:
+ label = '%s (%.1f%%)' % (_slice.name, _slice.fraction * 100)
+ self.xticks.append((_slice.xval, label))
+
+ def _renderLines(self, cx):
+ """Aux function for _renderBackground"""
+ # there are no lines in a Pie Chart
+
+ def _renderChart(self, cx):
+ """Renders a pie chart"""
+ self.centerx = self.layout.chart.x + self.layout.chart.w * 0.5
+ self.centery = self.layout.chart.y + self.layout.chart.h * 0.5
+
+ cx.set_line_join(cairo.LINE_JOIN_ROUND)
+
+ if self.options.stroke.shadow and False:
+ cx.save()
+ cx.set_source_rgba(0, 0, 0, 0.15)
+
+ cx.new_path()
+ cx.move_to(self.centerx, self.centery)
+ cx.arc(self.centerx + 1, self.centery + 2,
+ self.layout.radius + 1, 0, math.pi * 2)
+ cx.line_to(self.centerx, self.centery)
+ cx.close_path()
+ cx.fill()
+ cx.restore()
+
+ cx.save()
+
+ ctr = 0
+ color_len = len(self.colors)
+
+ for slice in self.slices:
+ if slice.isBigEnough():
+ if ctr == color_len - 1:
+ ctr = 0
+ else:
+ ctr = ctr + 1
+ cx.set_source_rgb(*self.colors[ctr])
+
+ if self.options.shouldFill:
+ slice.draw(cx, self.centerx, self.centery,
+ self.layout.radius)
+ cx.fill()
+
+ if not self.options.stroke.hide:
+ slice.draw(cx, self.centerx, self.centery,
+ self.layout.radius)
+ cx.set_line_width(self.options.stroke.width)
+ cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
+ #cx.stroke()
+
+ cx.restore()
+
+ if self.debug:
+ cx.set_source_rgba(1, 0, 0, 0.5)
+ px = max(cx.device_to_user_distance(1, 1))
+ for x, y in self.layout._lines:
+ cx.arc(x, y, 5 * px, 0, 2 * math.pi)
+ cx.fill()
+ cx.new_path()
+ cx.move_to(self.centerx, self.centery)
+ cx.line_to(x, y)
+ cx.stroke()
+
+ def _renderAxis(self, cx):
+ """Renders the axis for pie charts"""
+ if self.options.axis.x.hide or not self.xticks:
+ return
+
+ self.xlabels = []
+
+ if self.debug:
+ px = max(cx.device_to_user_distance(1, 1))
+ cx.set_source_rgba(0, 0, 1, 0.5)
+ for x, y, w, h in self.layout.ticks:
+ cx.rectangle(x, y, w, h)
+ cx.stroke()
+ cx.arc(x + w / 2.0, y + h / 2.0, 5 * px, 0, 2 * math.pi)
+ cx.fill()
+ cx.arc(x, y, 2 * px, 0, 2 * math.pi)
+ cx.fill()
+
+ cx.select_font_face(self.options.axis.tickFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_NORMAL)
+ cx.set_font_size(10)
+
+ cx.set_source_rgb(*hex2rgb(self.options.axis.labelColor))
+
+ for i, tick in enumerate(self.xticks):
+ label = tick[1]
+ x, y, w, h = self.layout.ticks[i]
+
+ xb, yb, width, height, xa, ya = cx.text_extents(label)
+
+ # draw label with text tick[1]
+ cx.move_to(x - xb, y - yb)
+ cx.show_text(label)
+ self.xlabels.append(label)
+
+
+class Slice(object):
+
+ def __init__(self, name, fraction, xval, yval, angle):
+ self.name = name
+ self.fraction = fraction
+ self.xval = xval
+ self.yval = yval
+ self.startAngle = 2 * angle * math.pi
+ self.endAngle = 2 * (angle + fraction) * math.pi
+
+ def __str__(self):
+ return ("" %
+ (self.startAngle, self.endAngle, self.fraction))
+
+ def isBigEnough(self):
+ return abs(self.startAngle - self.endAngle) > 0.001
+
+ def draw(self, cx, centerx, centery, radius):
+ cx.new_path()
+ cx.move_to(centerx, centery)
+ cx.arc(centerx, centery, radius - 10, -self.endAngle, -self.startAngle)
+ cx.close_path()
+
+ def getNormalisedAngle(self):
+ normalisedAngle = (self.startAngle + self.endAngle) / 2
+
+ if normalisedAngle > math.pi * 2:
+ normalisedAngle -= math.pi * 2
+ elif normalisedAngle < 0:
+ normalisedAngle += math.pi * 2
+
+ return normalisedAngle
+
+
+class PieLayout(Layout):
+ """Set of chart areas for pie charts"""
+
+ def __init__(self, slices):
+ self.slices = slices
+
+ self.title = Area()
+ self.chart = Area()
+
+ self.ticks = []
+ self.radius = 0
+
+ self._areas = (
+ (self.title, (1, 126 / 255.0, 0)), # orange
+ (self.chart, (75 / 255.0, 75 / 255.0, 1.0)), # blue
+ )
+
+ self._lines = []
+
+ def update(self, cx, options, width, height, xticks, yticks):
+ self.title.x = options.padding.left
+ self.title.y = options.padding.top
+ self.title.w = width - (options.padding.left + options.padding.right)
+ self.title.h = get_text_extents(cx,
+ options.title,
+ options.titleFont,
+ options.titleFontSize,
+ options.encoding)[1]
+
+ lookup = dict([(slice.xval, slice) for slice in self.slices])
+
+ self.chart.x = self.title.x
+ self.chart.y = self.title.y + self.title.h
+ self.chart.w = self.title.w
+ self.chart.h = height - self.title.h - (options.padding.top
+ + options.padding.bottom)
+
+ centerx = self.chart.x + self.chart.w * 0.5
+ centery = self.chart.y + self.chart.h * 0.5
+
+ self.radius = min(self.chart.w / 2.0, self.chart.h / 2.0)
+ for tick in xticks:
+ _slice = lookup.get(tick[0], None)
+ width, height = get_text_extents(cx, tick[1],
+ options.axis.tickFont,
+ options.axis.tickFontSize,
+ options.encoding)
+ angle = _slice.getNormalisedAngle()
+ radius = self._get_min_radius(angle, centerx, centery,
+ width, height)
+ self.radius = min(self.radius, radius)
+
+ # Now that we now the radius we move the ticks as close as we can
+ # to the circle
+ for i, tick in enumerate(xticks):
+ _slice = lookup.get(tick[0], None)
+ angle = _slice.getNormalisedAngle()
+ self.ticks[i] = self._get_tick_position(self.radius, angle,
+ self.ticks[i],
+ centerx, centery)
+
+ def _get_min_radius(self, angle, centerx, centery, width, height):
+ min_radius = None
+
+ # precompute some common values
+ tan = math.tan(angle)
+ half_width = width / 2.0
+ half_height = height / 2.0
+ offset_x = half_width * tan
+ offset_y = half_height / tan
+
+ def intersect_horizontal_line(y):
+ return centerx + (centery - y) / tan
+
+ def intersect_vertical_line(x):
+ return centery - tan * (x - centerx)
+
+ # computes the intersection between the rect that has
+ # that angle with the X axis and the bounding chart box
+ if 0.25 * math.pi <= angle < 0.75 * math.pi:
+ # intersects with the top rect
+ y = self.chart.y
+ x = intersect_horizontal_line(y)
+ self._lines.append((x, y))
+
+ x1 = x - half_width - offset_y
+ self.ticks.append((x1, self.chart.y, width, height))
+
+ min_radius = abs((y + height) - centery)
+ elif 0.75 * math.pi <= angle < 1.25 * math.pi:
+ # intersects with the left rect
+ x = self.chart.x
+ y = intersect_vertical_line(x)
+ self._lines.append((x, y))
+
+ y1 = y - half_height - offset_x
+ self.ticks.append((x, y1, width, height))
+
+ min_radius = abs(centerx - (x + width))
+ elif 1.25 * math.pi <= angle < 1.75 * math.pi:
+ # intersects with the bottom rect
+ y = self.chart.y + self.chart.h
+ x = intersect_horizontal_line(y)
+ self._lines.append((x, y))
+
+ x1 = x - half_width + offset_y
+ self.ticks.append((x1, y - height, width, height))
+
+ min_radius = abs((y - height) - centery)
+ else:
+ # intersects with the right rect
+ x = self.chart.x + self.chart.w
+ y = intersect_vertical_line(x)
+ self._lines.append((x, y))
+
+ y1 = y - half_height + offset_x
+ self.ticks.append((x - width, y1, width, height))
+
+ min_radius = abs((x - width) - centerx)
+
+ return min_radius
+
+ def _get_tick_position(self, radius, angle, tick, centerx, centery):
+ text_width, text_height = tick[2:4]
+ half_width = text_width / 2.0
+ half_height = text_height / 2.0
+
+ if 0 <= angle < 0.5 * math.pi:
+ # first quadrant
+ k1 = j1 = k2 = 1
+ j2 = -1
+ elif 0.5 * math.pi <= angle < math.pi:
+ # second quadrant
+ k1 = k2 = -1
+ j1 = j2 = 1
+ elif math.pi <= angle < 1.5 * math.pi:
+ # third quadrant
+ k1 = j1 = k2 = -1
+ j2 = 1
+ elif 1.5 * math.pi <= angle < 2 * math.pi:
+ # fourth quadrant
+ k1 = k2 = 1
+ j1 = j2 = -1
+
+ cx = radius * math.cos(angle) + k1 * half_width
+ cy = radius * math.sin(angle) + j1 * half_height
+
+ radius2 = math.sqrt(cx * cx + cy * cy)
+
+ tan = math.tan(angle)
+ x = math.sqrt((radius2 * radius2) / (1 + tan * tan))
+ y = tan * x
+
+ x = centerx + k2 * x
+ y = centery + j2 * y
+
+ return x - half_width, y - half_height, text_width, text_height
diff --git a/src/jarabe/util/sugarpycha/polygonal.py b/src/jarabe/util/sugarpycha/polygonal.py
new file mode 100644
index 0000000000..54460bcd25
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/polygonal.py
@@ -0,0 +1,372 @@
+# Copyright(c) 2011 by Roberto Garcia Carvajal
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+import math
+
+import cairo
+
+from jarabe.util.sugarpycha.chart import Chart
+from jarabe.util.sugarpycha.line import Point
+from jarabe.util.sugarpycha.color import hex2rgb
+from jarabe.util.sugarpycha.utils import safe_unicode
+
+
+class PolygonalChart(Chart):
+
+ def __init__(self, surface=None, options={}):
+ super(PolygonalChart, self).__init__(surface, options)
+ self.points = []
+
+ def _updateChart(self):
+ """Evaluates measures for polygonal charts"""
+ self.points = []
+
+ for i, (name, store) in enumerate(self.datasets):
+ for item in store:
+ xval, yval = item
+ x = (xval - self.minxval) * self.xscale
+ y = 1.0 - (yval - self.minyval) * self.yscale
+ point = Point(x, y, xval, yval, name)
+
+ if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0:
+ self.points.append(point)
+
+ def _renderBackground(self, cx):
+ """Renders the background area of the chart"""
+ if self.options.background.hide:
+ return
+
+ cx.save()
+
+ if self.options.background.baseColor:
+ cx.set_source_rgb(*hex2rgb(self.options.background.baseColor))
+ cx.paint()
+
+ if self.options.background.chartColor:
+ cx.set_source_rgb(*hex2rgb(self.options.background.chartColor))
+ cx.set_line_width(10.0)
+ cx.new_path()
+ init = None
+ count = len(self.xticks)
+ for index, tick in enumerate(self.xticks):
+ ang = math.pi / 2 - index * 2 * math.pi / count
+ x = (self.layout.chart.x + self.layout.chart.w / 2
+ - math.cos(ang)
+ * min(self.layout.chart.w / 2, self.layout.chart.h / 2))
+ y = (self.layout.chart.y + self.layout.chart.h / 2
+ - math.sin(ang)
+ * min(self.layout.chart.w / 2, self.layout.chart.h / 2))
+ if init is None:
+ cx.move_to(x, y)
+ init = (x, y)
+ else:
+ cx.line_to(x, y)
+ cx.line_to(init[0], init[1])
+ cx.close_path()
+ cx.fill()
+
+ if self.options.background.lineColor:
+ cx.set_source_rgb(*hex2rgb(self.options.background.lineColor))
+ cx.set_line_width(self.options.axis.lineWidth)
+ self._renderLines(cx)
+
+ cx.restore()
+
+ def _renderLine(self, cx, tick, horiz):
+ """Aux function for _renderLines"""
+
+ rad = (self.layout.chart.h / 2) * (1 - tick[0])
+ cx.new_path()
+ init = None
+ count = len(self.xticks)
+ for index, tick in enumerate(self.xticks):
+ ang = math.pi / 2 - index * 2 * math.pi / count
+ x = (self.layout.chart.x + self.layout.chart.w / 2
+ - math.cos(ang) * rad)
+ y = (self.layout.chart.y + self.layout.chart.h / 2
+ - math.sin(ang) * rad)
+ if init is None:
+ cx.move_to(x, y)
+ init = (x, y)
+ else:
+ cx.line_to(x, y)
+ cx.line_to(init[0], init[1])
+ cx.close_path()
+ cx.stroke()
+
+ def _renderXAxis(self, cx):
+ """Draws the horizontal line representing the X axis"""
+
+ count = len(self.xticks)
+
+ centerx = self.layout.chart.x + self.layout.chart.w / 2
+ centery = self.layout.chart.y + self.layout.chart.h / 2
+
+ for i in range(0, count):
+ offset1 = i * 2 * math.pi / count
+ offset = math.pi / 2 - offset1
+
+ rad = self.layout.chart.h / 2
+ (r1, r2) = (0, rad + 5)
+
+ x1 = centerx - math.cos(offset) * r1
+ x2 = centerx - math.cos(offset) * r2
+ y1 = centery - math.sin(offset) * r1
+ y2 = centery - math.sin(offset) * r2
+
+ cx.new_path()
+ cx.move_to(x1, y1)
+ cx.line_to(x2, y2)
+ cx.close_path()
+ cx.stroke()
+
+ def _renderYTick(self, cx, tick, center):
+ """Aux method for _renderAxis"""
+
+ i = tick
+ tick = self.yticks[i]
+
+ count = len(self.yticks)
+
+ if callable(tick):
+ return
+
+ x = center[0]
+ y = center[1] - i * (self.layout.chart.h / 2) / count
+
+ cx.new_path()
+ cx.move_to(x, y)
+ cx.line_to(x - self.options.axis.tickSize, y)
+ cx.close_path()
+ cx.stroke()
+
+ cx.select_font_face(self.options.axis.tickFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_NORMAL)
+ cx.set_font_size(self.options.axis.tickFontSize)
+
+ label = safe_unicode(tick[1], self.options.encoding)
+ extents = cx.text_extents(label)
+ labelWidth = extents[2]
+ labelHeight = extents[3]
+
+ if self.options.axis.y.rotate:
+ radians = math.radians(self.options.axis.y.rotate)
+ cx.move_to(x - self.options.axis.tickSize
+ - (labelWidth * math.cos(radians))
+ - 4,
+ y + (labelWidth * math.sin(radians))
+ + labelHeight / (2.0 / math.cos(radians)))
+ cx.rotate(-radians)
+ cx.show_text(label)
+ cx.rotate(radians) # this is probably faster than a save/restore
+ else:
+ cx.move_to(x - self.options.axis.tickSize - labelWidth - 4,
+ y + labelHeight / 2.0)
+ cx.rel_move_to(0.0, -labelHeight / 2.0)
+ cx.show_text(label)
+
+ return label
+
+ def _renderYAxis(self, cx):
+ """Draws the vertical line for the Y axis"""
+
+ centerx = self.layout.chart.x + self.layout.chart.w / 2
+ centery = self.layout.chart.y + self.layout.chart.h / 2
+
+ offset = math.pi / 2
+
+ r1 = self.layout.chart.h / 2
+
+ x1 = centerx - math.cos(offset) * r1
+ y1 = centery - math.sin(offset) * r1
+
+ cx.new_path()
+ cx.move_to(centerx, centery)
+ cx.line_to(x1, y1)
+ cx.close_path()
+ cx.stroke()
+
+ def _renderAxis(self, cx):
+ """Renders axis"""
+ if self.options.axis.x.hide and self.options.axis.y.hide:
+ return
+
+ cx.save()
+ cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
+ cx.set_line_width(self.options.axis.lineWidth)
+
+ centerx = self.layout.chart.x + self.layout.chart.w / 2
+ centery = self.layout.chart.y + self.layout.chart.h / 2
+
+ if not self.options.axis.y.hide:
+ if self.yticks:
+
+ count = len(self.yticks)
+
+ for i in range(0, count):
+ self._renderYTick(cx, i, (centerx, centery))
+
+ if self.options.axis.y.label:
+ self._renderYAxisLabel(cx, self.options.axis.y.label)
+
+ self._renderYAxis(cx)
+
+ if not self.options.axis.x.hide:
+ fontAscent = cx.font_extents()[0]
+ if self.xticks:
+
+ count = len(self.xticks)
+
+ for i in range(0, count):
+ self._renderXTick(cx, i, fontAscent, (centerx, centery))
+
+ if self.options.axis.x.label:
+ self._renderXAxisLabel(cx, self.options.axis.x.label)
+
+ self._renderXAxis(cx)
+
+ cx.restore()
+
+ def _renderXTick(self, cx, i, fontAscent, center):
+ tick = self.xticks[i]
+ if callable(tick):
+ return
+
+ count = len(self.xticks)
+ cx.select_font_face(self.options.axis.tickFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_NORMAL)
+ cx.set_font_size(self.options.axis.tickFontSize)
+
+ label = safe_unicode(tick[1], self.options.encoding)
+ extents = cx.text_extents(label)
+ labelWidth = extents[2]
+ labelHeight = extents[3]
+
+ x, y = center
+ cx.move_to(x, y)
+
+ if self.options.axis.x.rotate:
+ radians = math.radians(self.options.axis.x.rotate)
+ cx.move_to(x - (labelHeight * math.cos(radians)),
+ y + self.options.axis.tickSize
+ + (labelHeight * math.cos(radians))
+ + 4.0)
+ cx.rotate(radians)
+ cx.show_text(label)
+ cx.rotate(-radians)
+ else:
+ offset1 = i * 2 * math.pi / count
+ offset = math.pi / 2 - offset1
+
+ rad = self.layout.chart.h / 2 + 10
+
+ x = center[0] - math.cos(offset) * rad
+ y = center[1] - math.sin(offset) * rad
+
+ cx.move_to(x, y)
+ cx.rotate(offset - math.pi / 2)
+
+ if math.sin(offset) < 0.0:
+ cx.rotate(math.pi)
+ cx.rel_move_to(0.0, 5.0)
+
+ cx.rel_move_to(-labelWidth / 2.0, 0)
+ cx.show_text(label)
+ if math.sin(offset) < 0.0:
+ cx.rotate(-math.pi)
+
+ cx.rotate(-(offset - math.pi / 2))
+ return label
+
+ def _renderChart(self, cx):
+ """Renders a polygonal chart"""
+ # draw the polygon.
+ def preparePath(storeName):
+ cx.new_path()
+ firstPoint = True
+
+ count = len(self.points) / len(self.datasets)
+ centerx = self.layout.chart.x + self.layout.chart.w / 2
+ centery = self.layout.chart.y + self.layout.chart.h / 2
+
+ firstPointCoord = None
+
+ for index, point in enumerate(self.points):
+ if point.name == storeName:
+ offset1 = index * 2 * math.pi / count
+ offset = math.pi / 2 - offset1
+
+ rad = (self.layout.chart.h / 2) * (1 - point.y)
+
+ x = centerx - math.cos(offset) * rad
+ y = centery - math.sin(offset) * rad
+
+ if firstPointCoord is None:
+ firstPointCoord = (x, y)
+
+ if not self.options.shouldFill and firstPoint:
+ # starts the first point of the line
+ cx.move_to(x, y)
+ firstPoint = False
+ continue
+ cx.line_to(x, y)
+
+ if not firstPointCoord is None:
+ cx.line_to(firstPointCoord[0], firstPointCoord[1])
+
+ if self.options.shouldFill:
+ # Close the path to the start point
+ y = ((1.0 - self.origin)
+ * self.layout.chart.h + self.layout.chart.y)
+ else:
+ cx.set_source_rgb(*self.colorScheme[storeName])
+ cx.stroke()
+
+ cx.save()
+ cx.set_line_width(self.options.stroke.width)
+ if self.options.shouldFill:
+
+ def drawLine(storeName):
+ if self.options.stroke.shadow:
+ # draw shadow
+ cx.save()
+ cx.set_source_rgba(0, 0, 0, 0.15)
+ cx.translate(2, -2)
+ preparePath(storeName)
+ cx.fill()
+ cx.restore()
+
+ # fill the line
+ cx.set_source_rgb(*self.colorScheme[storeName])
+ preparePath(storeName)
+ cx.fill()
+
+ if not self.options.stroke.hide:
+ # draw stroke
+ cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
+ preparePath(storeName)
+ cx.stroke()
+
+ # draw the lines
+ for key in self._getDatasetsKeys():
+ drawLine(key)
+ else:
+ for key in self._getDatasetsKeys():
+ preparePath(key)
+ cx.restore()
diff --git a/src/jarabe/util/sugarpycha/radial.py b/src/jarabe/util/sugarpycha/radial.py
new file mode 100644
index 0000000000..b1dadab29c
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/radial.py
@@ -0,0 +1,346 @@
+# Copyright(c) 2011 by Roberto Garcia Carvajal
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+import math
+
+import cairo
+
+from jarabe.util.sugarpycha.chart import Chart
+from jarabe.util.sugarpycha.line import Point
+from jarabe.util.sugarpycha.color import hex2rgb
+from jarabe.util.sugarpycha.utils import safe_unicode
+
+
+class RadialChart(Chart):
+
+ def __init__(self, surface=None, options={}):
+ super(RadialChart, self).__init__(surface, options)
+ self.points = []
+
+ def _updateChart(self):
+ """Evaluates measures for radial charts"""
+ self.points = []
+
+ for i, (name, store) in enumerate(self.datasets):
+ for item in store:
+ xval, yval = item
+ x = (xval - self.minxval) * self.xscale
+ y = 1.0 - (yval - self.minyval) * self.yscale
+ point = Point(x, y, xval, yval, name)
+
+ if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0:
+ self.points.append(point)
+
+ def _renderBackground(self, cx):
+ """Renders the background area of the chart"""
+ if self.options.background.hide:
+ return
+
+ cx.save()
+
+ if self.options.background.baseColor:
+ cx.set_source_rgb(*hex2rgb(self.options.background.baseColor))
+ cx.paint()
+
+ if self.options.background.chartColor:
+ cx.set_source_rgb(*hex2rgb(self.options.background.chartColor))
+ cx.set_line_width(10.0)
+ cx.arc(self.layout.chart.x + self.layout.chart.w / 2,
+ self.layout.chart.y + self.layout.chart.h / 2,
+ min(self.layout.chart.w / 2, self.layout.chart.h / 2),
+ 0, 2 * math.pi)
+ cx.fill()
+
+ if self.options.background.lineColor:
+ cx.set_source_rgb(*hex2rgb(self.options.background.lineColor))
+ cx.set_line_width(self.options.axis.lineWidth)
+ self._renderLines(cx)
+
+ cx.restore()
+
+ def _renderLine(self, cx, tick, horiz):
+ """Aux function for _renderLines"""
+
+ rad = (self.layout.chart.h / 2) * (1 - tick[0])
+ cx.arc(self.layout.chart.x + self.layout.chart.w / 2,
+ self.layout.chart.y + self.layout.chart.h / 2,
+ rad, 0, 2 * math.pi)
+ cx.stroke()
+
+ def _renderXAxis(self, cx):
+ """Draws the horizontal line representing the X axis"""
+
+ count = len(self.xticks)
+
+ centerx = self.layout.chart.x + self.layout.chart.w / 2
+ centery = self.layout.chart.y + self.layout.chart.h / 2
+
+ for i in range(0, count):
+ offset1 = i * 2 * math.pi / count
+ offset = math.pi / 2 - offset1
+
+ rad = self.layout.chart.h / 2
+ (r1, r2) = (0, rad + 5)
+
+ x1 = centerx - math.cos(offset) * r1
+ x2 = centerx - math.cos(offset) * r2
+ y1 = centery - math.sin(offset) * r1
+ y2 = centery - math.sin(offset) * r2
+
+ cx.new_path()
+ cx.move_to(x1, y1)
+ cx.line_to(x2, y2)
+ cx.close_path()
+ cx.stroke()
+
+ def _renderYTick(self, cx, tick, center):
+ """Aux method for _renderAxis"""
+
+ i = tick
+ tick = self.yticks[i]
+
+ count = len(self.yticks)
+
+ if callable(tick):
+ return
+
+ x = center[0]
+ y = center[1] - i * (self.layout.chart.h / 2) / count
+
+ cx.new_path()
+ cx.move_to(x, y)
+ cx.line_to(x - self.options.axis.tickSize, y)
+ cx.close_path()
+ cx.stroke()
+
+ cx.select_font_face(self.options.axis.tickFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_NORMAL)
+ cx.set_font_size(self.options.axis.tickFontSize)
+
+ label = safe_unicode(tick[1], self.options.encoding)
+ extents = cx.text_extents(label)
+ labelWidth = extents[2]
+ labelHeight = extents[3]
+
+ if self.options.axis.y.rotate:
+ radians = math.radians(self.options.axis.y.rotate)
+ cx.move_to(x - self.options.axis.tickSize
+ - (labelWidth * math.cos(radians))
+ - 4,
+ y + (labelWidth * math.sin(radians))
+ + labelHeight / (2.0 / math.cos(radians)))
+ cx.rotate(-radians)
+ cx.show_text(label)
+ cx.rotate(radians) # this is probably faster than a save/restore
+ else:
+ cx.move_to(x - self.options.axis.tickSize - labelWidth - 4,
+ y + labelHeight / 2.0)
+ cx.rel_move_to(0.0, -labelHeight / 2.0)
+ cx.show_text(label)
+
+ return label
+
+ def _renderYAxis(self, cx):
+ """Draws the vertical line for the Y axis"""
+
+ centerx = self.layout.chart.x + self.layout.chart.w / 2
+ centery = self.layout.chart.y + self.layout.chart.h / 2
+
+ offset = math.pi / 2
+
+ r1 = self.layout.chart.h / 2
+
+ x1 = centerx - math.cos(offset) * r1
+ y1 = centery - math.sin(offset) * r1
+
+ cx.new_path()
+ cx.move_to(centerx, centery)
+ cx.line_to(x1, y1)
+ cx.close_path()
+ cx.stroke()
+
+ def _renderAxis(self, cx):
+ """Renders axis"""
+ if self.options.axis.x.hide and self.options.axis.y.hide:
+ return
+
+ cx.save()
+ cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
+ cx.set_line_width(self.options.axis.lineWidth)
+
+ centerx = self.layout.chart.x + self.layout.chart.w / 2
+ centery = self.layout.chart.y + self.layout.chart.h / 2
+
+ if not self.options.axis.y.hide:
+ if self.yticks:
+
+ count = len(self.yticks)
+
+ for i in range(0, count):
+ self._renderYTick(cx, i, (centerx, centery))
+
+ if self.options.axis.y.label:
+ self._renderYAxisLabel(cx, self.options.axis.y.label)
+
+ self._renderYAxis(cx)
+
+ if not self.options.axis.x.hide:
+ fontAscent = cx.font_extents()[0]
+ if self.xticks:
+
+ count = len(self.xticks)
+
+ for i in range(0, count):
+ self._renderXTick(cx, i, fontAscent, (centerx, centery))
+
+ if self.options.axis.x.label:
+ self._renderXAxisLabel(cx, self.options.axis.x.label)
+
+ self._renderXAxis(cx)
+
+ cx.restore()
+
+ def _renderXTick(self, cx, i, fontAscent, center):
+ tick = self.xticks[i]
+ if callable(tick):
+ return
+
+ count = len(self.xticks)
+ cx.select_font_face(self.options.axis.tickFont,
+ cairo.FONT_SLANT_NORMAL,
+ cairo.FONT_WEIGHT_NORMAL)
+ cx.set_font_size(self.options.axis.tickFontSize)
+
+ label = safe_unicode(tick[1], self.options.encoding)
+ extents = cx.text_extents(label)
+ labelWidth = extents[2]
+ labelHeight = extents[3]
+
+ x, y = center
+ cx.move_to(x, y)
+
+ if self.options.axis.x.rotate:
+ radians = math.radians(self.options.axis.x.rotate)
+ cx.move_to(x - (labelHeight * math.cos(radians)),
+ y + self.options.axis.tickSize
+ + (labelHeight * math.cos(radians))
+ + 4.0)
+ cx.rotate(radians)
+ cx.show_text(label)
+ cx.rotate(-radians)
+ else:
+ offset1 = i * 2 * math.pi / count
+ offset = math.pi / 2 - offset1
+
+ rad = self.layout.chart.h / 2 + 10
+
+ x = center[0] - math.cos(offset) * rad
+ y = center[1] - math.sin(offset) * rad
+
+ cx.move_to(x, y)
+ cx.rotate(offset - math.pi / 2)
+
+ if math.sin(offset) < 0.0:
+ cx.rotate(math.pi)
+ cx.rel_move_to(0.0, 5.0)
+
+ cx.rel_move_to(-labelWidth / 2.0, 0)
+ cx.show_text(label)
+ if math.sin(offset) < 0.0:
+ cx.rotate(-math.pi)
+
+ cx.rotate(-(offset - math.pi / 2))
+ return label
+
+ def _renderChart(self, cx):
+ """Renders a line chart"""
+
+ # draw the circle
+ def preparePath(storeName):
+ cx.new_path()
+ firstPoint = True
+
+ count = len(self.points) / len(self.datasets)
+ centerx = self.layout.chart.x + self.layout.chart.w / 2
+ centery = self.layout.chart.y + self.layout.chart.h / 2
+
+ firstPointCoord = None
+
+ for index, point in enumerate(self.points):
+ if point.name == storeName:
+ offset1 = index * 2 * math.pi / count
+ offset = math.pi / 2 - offset1
+
+ rad = (self.layout.chart.h / 2) * (1 - point.y)
+
+ x = centerx - math.cos(offset) * rad
+ y = centery - math.sin(offset) * rad
+
+ if firstPointCoord is None:
+ firstPointCoord = (x, y)
+
+ if not self.options.shouldFill and firstPoint:
+ # starts the first point of the line
+ cx.move_to(x, y)
+ firstPoint = False
+ continue
+ cx.line_to(x, y)
+
+ if not firstPointCoord is None:
+ cx.line_to(firstPointCoord[0], firstPointCoord[1])
+
+ if self.options.shouldFill:
+ # Close the path to the start point
+ y = ((1.0 - self.origin)
+ * self.layout.chart.h + self.layout.chart.y)
+ else:
+ cx.set_source_rgb(*self.colorScheme[storeName])
+ cx.stroke()
+
+ cx.save()
+ cx.set_line_width(self.options.stroke.width)
+ if self.options.shouldFill:
+
+ def drawLine(storeName):
+ if self.options.stroke.shadow:
+ # draw shadow
+ cx.save()
+ cx.set_source_rgba(0, 0, 0, 0.15)
+ cx.translate(2, -2)
+ preparePath(storeName)
+ cx.fill()
+ cx.restore()
+
+ # fill the line
+ cx.set_source_rgb(*self.colorScheme[storeName])
+ preparePath(storeName)
+ cx.fill()
+
+ if not self.options.stroke.hide:
+ # draw stroke
+ cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
+ preparePath(storeName)
+ cx.stroke()
+
+ # draw the lines
+ for key in self._getDatasetsKeys():
+ drawLine(key)
+ else:
+ for key in self._getDatasetsKeys():
+ preparePath(key)
+ cx.restore()
diff --git a/src/jarabe/util/sugarpycha/scatter.py b/src/jarabe/util/sugarpycha/scatter.py
new file mode 100644
index 0000000000..3079952d99
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/scatter.py
@@ -0,0 +1,38 @@
+# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+import math
+
+from jarabe.util.sugarpycha.line import LineChart
+
+
+class ScatterplotChart(LineChart):
+
+ def _renderChart(self, cx):
+ """Renders a scatterplot"""
+
+ def drawSymbol(point, size):
+ ox = point.x * self.layout.chart.w + self.layout.chart.x
+ oy = point.y * self.layout.chart.h + self.layout.chart.y
+ cx.arc(ox, oy, size, 0.0, 2 * math.pi)
+ cx.fill()
+
+ for key in self._getDatasetsKeys():
+ cx.set_source_rgb(*self.colorScheme[key])
+ for point in self.points:
+ if point.name == key:
+ drawSymbol(point, self.options.stroke.width)
diff --git a/src/jarabe/util/sugarpycha/stackedbar.py b/src/jarabe/util/sugarpycha/stackedbar.py
new file mode 100644
index 0000000000..350b269667
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/stackedbar.py
@@ -0,0 +1,122 @@
+# Copyright(c) 2009 by Yaco S.L.
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+from jarabe.util.sugarpycha.bar import BarChart, VerticalBarChart, HorizontalBarChart, Rect
+from jarabe.util.sugarpycha.chart import uniqueIndices
+from functools import reduce
+
+
+class StackedBarChart(BarChart):
+
+ def __init__(self, surface=None, options={}, debug=False):
+ super(StackedBarChart, self).__init__(surface, options, debug)
+ self.barWidth = 0.0
+
+ def _updateXY(self):
+ super(StackedBarChart, self)._updateXY()
+ # each dataset is centered around a line segment. that's why we
+ # need n + 1 divisions on the x axis
+ self.xscale = 1 / (self.xrange + 1.0)
+
+ if self.options.axis.y.range is None:
+ # Fix the yscale as we accumulate the y values
+ stores = self._getDatasetsValues()
+ n_stores = len(stores)
+ flat_y = [pair[1] for pair in reduce(lambda a, b: a + b, stores)]
+ store_size = len(flat_y) / n_stores
+ accum = [sum(flat_y[j]for j in range(i,
+ i + store_size * n_stores,
+ store_size))
+ for i in range(len(flat_y) / n_stores)]
+ self.yrange = float(max(accum))
+ if self.yrange == 0:
+ self.yscale = 1.0
+ else:
+ self.yscale = 1.0 / self.yrange
+
+ def _updateChart(self):
+ """Evaluates measures for vertical bars"""
+ stores = self._getDatasetsValues()
+ uniqx = uniqueIndices(stores)
+
+ if len(uniqx) == 1:
+ self.minxdelta = 1.0
+ else:
+ self.minxdelta = min([abs(uniqx[j] - uniqx[j - 1])
+ for j in range(1, len(uniqx))])
+
+ k = self.minxdelta * self.xscale
+ self.barWidth = k * self.options.barWidthFillFraction
+ self.barMargin = k * (1.0 - self.options.barWidthFillFraction) / 2
+
+ self.bars = []
+
+
+class StackedVerticalBarChart(StackedBarChart, VerticalBarChart):
+
+ def _updateChart(self):
+ """Evaluates measures for vertical bars"""
+ super(StackedVerticalBarChart, self)._updateChart()
+
+ accumulated_heights = {}
+ for i, (name, store) in enumerate(self.datasets):
+ for item in store:
+ xval, yval = item
+ x = ((xval - self.minxval) * self.xscale) + self.barMargin
+ w = self.barWidth
+ h = abs(yval) * self.yscale
+ if yval > 0:
+ y = (1.0 - h) - self.origin
+ else:
+ y = 1 - self.origin
+
+ accumulated_height = accumulated_heights.setdefault(xval, 0)
+ y -= accumulated_height
+ accumulated_heights[xval] += h
+
+ rect = Rect(x, y, w, h, xval, yval, name)
+
+ if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
+ self.bars.append(rect)
+
+
+class StackedHorizontalBarChart(StackedBarChart, HorizontalBarChart):
+
+ def _updateChart(self):
+ """Evaluates measures for horizontal bars"""
+ super(StackedHorizontalBarChart, self)._updateChart()
+
+ accumulated_widths = {}
+ for i, (name, store) in enumerate(self.datasets):
+ for item in store:
+ xval, yval = item
+ y = ((xval - self.minxval) * self.xscale) + self.barMargin
+ h = self.barWidth
+ w = abs(yval) * self.yscale
+ if yval > 0:
+ x = self.origin
+ else:
+ x = self.origin - w
+
+ accumulated_width = accumulated_widths.setdefault(xval, 0)
+ x += accumulated_width
+ accumulated_widths[xval] += w
+
+ rect = Rect(x, y, w, h, xval, yval, name)
+
+ if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
+ self.bars.append(rect)
diff --git a/src/jarabe/util/sugarpycha/utils.py b/src/jarabe/util/sugarpycha/utils.py
new file mode 100644
index 0000000000..ccd014d7da
--- /dev/null
+++ b/src/jarabe/util/sugarpycha/utils.py
@@ -0,0 +1,40 @@
+# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
+# 2009-2010 by Yaco S.L.
+#
+# This file is part of PyCha.
+#
+# PyCha is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# PyCha 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 Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with PyCha. If not, see .
+
+
+def clamp(minValue, maxValue, value):
+ """Make sure value is between minValue and maxValue"""
+ if value < minValue:
+ return minValue
+ if value > maxValue:
+ return maxValue
+ return value
+
+
+def safe_unicode(obj, encoding=None):
+ """Return a unicode value from the argument"""
+ if isinstance(obj, str):
+ return obj
+ elif isinstance(obj, str):
+ if encoding is None:
+ return str(obj)
+ else:
+ return str(obj, encoding)
+ else:
+ # it may be an int or a float
+ return str(obj)
diff --git a/src/jarabe/util/utils.py b/src/jarabe/util/utils.py
new file mode 100644
index 0000000000..1b6d681d12
--- /dev/null
+++ b/src/jarabe/util/utils.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# utils.py by:
+# Agustin Zubiaga
+
+# Copyright (C) 2019 Hrishi Patel
+# 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 3 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 St, Fifth Floor, Boston, MA 02110-1301 USA
+import os
+
+from sugar3 import profile
+from sugar3.graphics.style import Color
+
+
+def rgb2html(color):
+ """Returns a html string from a Gdk color"""
+ red = "%x" % int(color.red / 65535.0 * 255)
+ if len(red) == 1:
+ red = "0%s" % red
+
+ green = "%x" % int(color.green / 65535.0 * 255)
+
+ if len(green) == 1:
+ green = "0%s" % green
+
+ blue = "%x" % int(color.blue / 65535.0 * 255)
+
+ if len(blue) == 1:
+ blue = "0%s" % blue
+
+ new_color = "#%s%s%s" % (red, green, blue)
+
+ return new_color
+
+
+def get_user_fill_color(type='gdk'):
+ """Returns the user fill color"""
+ color = profile.get_color()
+ if type == 'gdk':
+ rcolor = Color(color.get_fill_color()).get_gdk_color()
+
+ elif type == 'str':
+ rcolor = color.get_fill_color()
+
+ return rcolor
+
+
+def get_user_stroke_color(type='gdk'):
+ """Returns the user stroke color"""
+ color = profile.get_color()
+
+ if type == 'gdk':
+ rcolor = Color(color.get_stroke_color()).get_gdk_color()
+
+ elif type == 'str':
+ rcolor = color.get_stroke_color()
+
+ return rcolor
+
+
+def get_chart_file(activity_dir):
+ """Returns a path for write the chart in a png image"""
+ chart_file = os.path.join(activity_dir, "chart-1.png")
+ num = 0
+
+ while os.path.exists(chart_file):
+ num += 1
+ chart_file = os.path.join(activity_dir, "chart-" + str(num) + ".png")
+
+ return chart_file
+
+
+def get_decimals(number):
+ """Returns the decimals count of a number"""
+ return str(len(number.split('.')[1]))
+
+
+def get_channels():
+ path = os.path.join('/sys/class/dmi/id', 'product_version')
+ try:
+ product = open(path).readline().strip()
+ except:
+ product = None
+
+ if product == '1' or product == '1.0':
+ return 1
+ else:
+ return 2