diff options
Diffstat (limited to 'pygtk_chart/pie_chart.py')
-rw-r--r-- | pygtk_chart/pie_chart.py | 474 |
1 files changed, 474 insertions, 0 deletions
diff --git a/pygtk_chart/pie_chart.py b/pygtk_chart/pie_chart.py new file mode 100644 index 0000000..3e3871d --- /dev/null +++ b/pygtk_chart/pie_chart.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python +# +# pie_chart.py +# +# Copyright 2008 Sven Festersen <sven@sven-festersen.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +""" +Contains the PieChart widget. + +Author: Sven Festersen (sven@sven-festersen.de) +""" +__docformat__ = "epytext" +import cairo +import gobject +import gtk +import math +import os + +from pygtk_chart.basics import * +from pygtk_chart.chart_object import ChartObject +from pygtk_chart import chart +from pygtk_chart import label +from pygtk_chart import COLORS, COLOR_AUTO + +def draw_sector(context, cx, cy, radius, angle, angle_offset): + context.move_to(cx, cy) + context.arc(cx, cy, radius, angle_offset, angle_offset + angle) + context.close_path() + context.fill() + + +class PieArea(chart.Area): + """ + This class represents the sector of a pie chart. + + Properties + ========== + The PieArea class inherits properties from chart.Area. + + Signals + ======= + The PieArea class inherits signals from chart.Area. + """ + + def __init__(self, name, value, title=""): + chart.Area.__init__(self, name, value, title) + self._label_object = label.Label((0, 0), title) + + def _do_draw(self, context, rect, cx, cy, radius, angle, angle_offset, draw_label, draw_percentage, draw_value): + context.set_source_rgb(*color_gdk_to_cairo(self._color)) + draw_sector(context, cx, cy, radius, angle, angle_offset) + if self._highlighted: + context.set_source_rgba(1, 1, 1, 0.1) + draw_sector(context, cx, cy, radius, angle, angle_offset) + + if draw_label: + title = self._label + title_extra = "" + fraction = angle / (2 * math.pi) + if draw_percentage and not draw_value: + title_extra = " (%s%%)" % round(100 * fraction, 2) + elif not draw_percentage and draw_value: + title_extra = " (%s)" % self._value + elif draw_percentage and draw_value: + title_extra = " (%s, %s%%)" % (self._value, round(100 * fraction, 2)) + title += title_extra + + label_angle = angle_offset + angle / 2 + label_angle = label_angle % (2 * math.pi) + x = cx + (radius + 10) * math.cos(label_angle) + y = cy + (radius + 10) * math.sin(label_angle) + + ref = label.ANCHOR_BOTTOM_LEFT + if 0 <= label_angle <= math.pi / 2: + ref = label.ANCHOR_TOP_LEFT + elif math.pi / 2 <= label_angle <= math.pi: + ref = label.ANCHOR_TOP_RIGHT + elif math.pi <= label_angle <= 1.5 * math.pi: + ref = label.ANCHOR_BOTTOM_RIGHT + + if self._highlighted: + self._label_object.set_underline(label.UNDERLINE_SINGLE) + else: + self._label_object.set_underline(label.UNDERLINE_NONE) + self._label_object.set_color(self._color) + self._label_object.set_text(title) + self._label_object.set_position((x, y)) + self._label_object.set_anchor(ref) + self._label_object.draw(context, rect) + + +class PieChart(chart.Chart): + """ + This is the pie chart class. + + Properties + ========== + The PieChart class inherits properties from chart.Chart. + Additional properties: + - rotate (the angle that the pie chart should be rotated by in + degrees, type: int in [0, 360]) + - draw-shadow (sets whther to draw a shadow under the pie chart, + type: boolean) + - draw-labels (sets whether to draw area labels, type: boolean) + - show-percentage (sets whether to show percentage in area labels, + type: boolean) + - show-values (sets whether to show values in area labels, + type: boolean) + - enable-scroll (sets whether the pie chart can be rotated by + scrolling with the mouse wheel, type: boolean) + - enable-mouseover (sets whether a mouse over effect should be + added to areas, type: boolean). + + Signals + ======= + The PieChart class inherits signals from chart.Chart. + Additional signals: + - area-clicked (emitted when an area is clicked) + callback signature: + def callback(piechart, area). + """ + + __gproperties__ = {"rotate": (gobject.TYPE_INT, + "rotation", + "The angle to rotate the chart in degrees.", + 0, 360, 0, gobject.PARAM_READWRITE), + "draw-shadow": (gobject.TYPE_BOOLEAN, + "draw pie shadow", + "Set whether to draw pie shadow.", + True, gobject.PARAM_READWRITE), + "draw-labels": (gobject.TYPE_BOOLEAN, + "draw area labels", + "Set whether to draw area labels.", + True, gobject.PARAM_READWRITE), + "show-percentage": (gobject.TYPE_BOOLEAN, + "show percentage", + "Set whether to show percentage in the areas' labels.", + False, gobject.PARAM_READWRITE), + "show-values": (gobject.TYPE_BOOLEAN, + "show values", + "Set whether to show values in the areas' labels.", + True, gobject.PARAM_READWRITE), + "enable-scroll": (gobject.TYPE_BOOLEAN, + "enable scroll", + "If True, the pie can be rotated by scrolling with the mouse wheel.", + True, gobject.PARAM_READWRITE), + "enable-mouseover": (gobject.TYPE_BOOLEAN, + "enable mouseover", + "Set whether a mouseover effect should be visible if moving the mouse over a pie area.", + True, gobject.PARAM_READWRITE)} + + __gsignals__ = {"area-clicked": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))} + + def __init__(self): + chart.Chart.__init__(self) + self._areas = [] + self._rotate = 0 + self._shadow = True + self._labels = True + self._percentage = False + self._values = True + self._enable_scroll = True + self._enable_mouseover = True + + self.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.SCROLL_MASK|gtk.gdk.POINTER_MOTION_MASK) + self.connect("button_press_event", self._cb_button_pressed) + self.connect("scroll-event", self._cb_scroll_event) + self.connect("motion-notify-event", self._cb_motion_notify) + + def do_get_property(self, property): + if property.name == "rotate": + return self._rotate + elif property.name == "draw-shadow": + return self._shadow + elif property.name == "draw-labels": + return self._labels + elif property.name == "show-percentage": + return self._percentage + elif property.name == "show-values": + return self._values + elif property.name == "enable-scroll": + return self._enable_scroll + elif property.name == "enable-mouseover": + return self._enable_mouseover + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "rotate": + self._rotate = value + elif property.name == "draw-shadow": + self._shadow = value + elif property.name == "draw-labels": + self._labels = value + elif property.name == "show-percentage": + self._percentage = value + elif property.name == "show-values": + self._values = value + elif property.name == "enable-scroll": + self._enable_scroll = value + elif property.name == "enable-mouseover": + self._enable_mouseover = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def _cb_appearance_changed(self, widget): + self.queue_draw() + + def _cb_motion_notify(self, widget, event): + if not self._enable_mouseover: return + area = self._get_area_at_pos(event.x, event.y) + for a in self._areas: + a.set_property("highlighted", a == area) + self.queue_draw() + + def _cb_button_pressed(self, widget, event): + area = self._get_area_at_pos(event.x, event.y) + if area: + self.emit("area-clicked", area) + + def _get_area_at_pos(self, x, y): + rect = self.get_allocation() + center = rect.width / 2, rect.height / 2 + x = x - center[0] + y = y - center[1] + + #calculate angle + angle = math.atan2(x, -y) + angle -= math.pi / 2 + angle -= 2 * math.pi * self.get_rotate() / 360.0 + while angle < 0: + angle += 2 * math.pi + + #calculate radius + radius_squared = math.pow(int(0.4 * min(rect.width, rect.height)), 2) + clicked_radius_squared = x*x + y*y + + if clicked_radius_squared <= radius_squared: + #find out area that was clicked + sum = 0 + for area in self._areas: + if area.get_visible(): + sum += area.get_value() + + current_angle_position = 0 + for area in self._areas: + area_angle = 2 * math.pi * area.get_value() / sum + + if current_angle_position <= angle <= current_angle_position + area_angle: + return area + + current_angle_position += area_angle + return None + + def _cb_scroll_event(self, widget, event): + if not self._enable_scroll: return + if event.direction in [gtk.gdk.SCROLL_UP, gtk.gdk.SCROLL_RIGHT]: + delta = 360.0 / 32 + elif event.direction in [gtk.gdk.SCROLL_DOWN, gtk.gdk.SCROLL_LEFT]: + delta = - 360.0 / 32 + else: + delta = 0 + rotate = self.get_rotate() + delta + rotate = rotate % 360.0 + if rotate < 0: rotate += 360 + self.set_rotate(rotate) + + def draw(self, context): + """ + Draw the widget. This method is called automatically. Don't call it + yourself. If you want to force a redrawing of the widget, call + the queue_draw() method. + + @type context: cairo.Context + @param context: The context to draw on. + """ + label.begin_drawing() + + rect = self.get_allocation() + #initial context settings: line width & font + context.set_line_width(1) + font = gtk.Label().style.font_desc.get_family() + context.select_font_face(font, cairo.FONT_SLANT_NORMAL, \ + cairo.FONT_WEIGHT_NORMAL) + + self.draw_basics(context, rect) + self._do_draw_shadow(context, rect) + self._do_draw_areas(context, rect) + + label.finish_drawing() + + def _do_draw_areas(self, context, rect): + center = rect.width / 2, rect.height / 2 + radius = int(0.4 * min(rect.width, rect.height)) + sum = 0 + + for area in self._areas: + if area.get_visible(): + sum += area.get_value() + + current_angle_position = 2 * math.pi * self.get_rotate() / 360.0 + for i, area in enumerate(self._areas): + area_angle = 2 * math.pi * area.get_value() / sum + area.draw(context, rect, center[0], center[1], radius, area_angle, current_angle_position, self._labels, self._percentage, self._values) + current_angle_position += area_angle + + def _do_draw_shadow(self, context, rect): + if not self._shadow: return + center = rect.width / 2, rect.height / 2 + radius = int(0.4 * min(rect.width, rect.height)) + + gradient = cairo.RadialGradient(center[0], center[1], radius, center[0], center[1], radius + 10) + gradient.add_color_stop_rgba(0, 0, 0, 0, 0.5) + gradient.add_color_stop_rgba(0.5, 0, 0, 0, 0) + + context.set_source(gradient) + context.arc(center[0], center[1], radius + 10, 0, 2 * math.pi) + context.fill() + + def add_area(self, area): + color = area.get_color() + if color == COLOR_AUTO: area.set_color(COLORS[len(self._areas) % len(COLORS)]) + self._areas.append(area) + area.connect("appearance_changed", self._cb_appearance_changed) + + def get_pie_area(self, name): + """ + Returns the PieArea with the id 'name' if it exists, None + otherwise. + + @type name: string + @param name: the id of a PieArea + + @return: a PieArea or None. + """ + for area in self._areas: + if area.get_name() == name: + return area + return None + + def set_rotate(self, angle): + """ + Set the rotation angle of the PieChart in degrees. + + @param angle: angle in degrees 0 - 360 + @type angle: integer. + """ + self.set_property("rotate", angle) + self.queue_draw() + + def get_rotate(self): + """ + Get the current rotation angle in degrees. + + @return: integer. + """ + return self.get_property("rotate") + + def set_draw_shadow(self, draw): + """ + Set whether to draw the pie chart's shadow. + + @type draw: boolean. + """ + self.set_property("draw-shadow", draw) + self.queue_draw() + + def get_draw_shadow(self): + """ + Returns True if pie chart currently has a shadow. + + @return: boolean. + """ + return self.get_property("draw-shadow") + + def set_draw_labels(self, draw): + """ + Set whether to draw the labels of the pie areas. + + @type draw: boolean. + """ + self.set_property("draw-labels", draw) + self.queue_draw() + + def get_draw_labels(self): + """ + Returns True if area labels are shown. + + @return: boolean. + """ + return self.get_property("draw-labels") + + def set_show_percentage(self, show): + """ + Set whether to show the percentage an area has in its label. + + @type show: boolean. + """ + self.set_property("show-percentage", show) + self.queue_draw() + + def get_show_percentage(self): + """ + Returns True if percentages are shown. + + @return: boolean. + """ + return self.get_property("show-percentage") + + def set_enable_scroll(self, scroll): + """ + Set whether the pie chart can be rotated by scrolling with + the mouse wheel. + + @type scroll: boolean. + """ + self.set_property("enable-scroll", scroll) + + def get_enable_scroll(self): + """ + Returns True if the user can rotate the pie chart by scrolling. + + @return: boolean. + """ + return self.get_property("enable-scroll") + + def set_enable_mouseover(self, mouseover): + """ + Set whether a mouseover effect should be shown when the pointer + enters a pie area. + + @type mouseover: boolean. + """ + self.set_property("enable-mouseover", mouseover) + + def get_enable_mouseover(self): + """ + Returns True if the mouseover effect is enabled. + + @return: boolean. + """ + return self.get_property("enable-mouseover") + + def set_show_values(self, show): + """ + Set whether the area's value should be shown in its label. + + @type show: boolean. + """ + self.set_property("show-values", show) + self.queue_draw() + + def get_show_values(self): + """ + Returns True if the value of a pie area is shown in its label. + + @return: boolean. + """ + return self.get_property("show-values") + |