diff options
author | accayetano <almiracayetano@gmail.com> | 2012-01-22 21:39:34 (GMT) |
---|---|---|
committer | accayetano <almiracayetano@gmail.com> | 2012-01-22 21:39:34 (GMT) |
commit | 0f4bc904a9506c371efe33a056d2454d10ac7a64 (patch) | |
tree | b65784f3be7496b1e67cd333919fe7222ec137d0 /pygtk_chart |
Initial upload
Diffstat (limited to 'pygtk_chart')
-rw-r--r-- | pygtk_chart/__init__.py | 53 | ||||
-rw-r--r-- | pygtk_chart/__init__.pyc | bin | 0 -> 1268 bytes | |||
-rw-r--r-- | pygtk_chart/bar_chart.py | 729 | ||||
-rw-r--r-- | pygtk_chart/bar_chart.pyc | bin | 0 -> 26455 bytes | |||
-rw-r--r-- | pygtk_chart/basics.py | 133 | ||||
-rw-r--r-- | pygtk_chart/basics.pyc | bin | 0 -> 4669 bytes | |||
-rw-r--r-- | pygtk_chart/chart.py | 592 | ||||
-rw-r--r-- | pygtk_chart/chart.pyc | bin | 0 -> 22045 bytes | |||
-rw-r--r-- | pygtk_chart/chart_object.py | 155 | ||||
-rw-r--r-- | pygtk_chart/chart_object.pyc | bin | 0 -> 5372 bytes | |||
-rw-r--r-- | pygtk_chart/data/tango.color | 7 | ||||
-rw-r--r-- | pygtk_chart/label.py | 744 | ||||
-rw-r--r-- | pygtk_chart/label.pyc | bin | 0 -> 24623 bytes | |||
-rw-r--r-- | pygtk_chart/multi_bar_chart.py | 649 | ||||
-rw-r--r-- | pygtk_chart/multi_bar_chart.pyc | bin | 0 -> 25001 bytes | |||
-rw-r--r-- | pygtk_chart/pie_chart.py | 474 |
16 files changed, 3536 insertions, 0 deletions
diff --git a/pygtk_chart/__init__.py b/pygtk_chart/__init__.py new file mode 100644 index 0000000..891e603 --- /dev/null +++ b/pygtk_chart/__init__.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# +# __init__.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. +""" +This package contains four pygtk widgets for drawing simple charts: + - line_chart.LineChart for line charts, + - pie_chart.PieChart for pie charts, + - bar_chart.BarChart for bar charts, + - bar_chart.MultiBarChart for charts with groups of bars. +""" +__docformat__ = "epytext" + +__version__ = "beta" +__author__ = "Sven Festersen, John Dickinson" +__license__ = "GPL" +__url__ = "http://notmyname.github.com/pygtkChart/" + +import os +from pygtk_chart.basics import gdk_color_list_from_file +COLOR_AUTO = 0 +COLORS = gdk_color_list_from_file(os.sep.join([os.path.dirname(__file__), "data", "tango.color"])) + +#line style +LINE_STYLE_SOLID = 0 +LINE_STYLE_DOTTED = 1 +LINE_STYLE_DASHED = 2 +LINE_STYLE_DASHED_ASYMMETRIC = 3 + +#point styles +POINT_STYLE_CIRCLE = 0 +POINT_STYLE_SQUARE = 1 +POINT_STYLE_CROSS = 2 +POINT_STYLE_TRIANGLE_UP = 3 +POINT_STYLE_TRIANGLE_DOWN = 4 +POINT_STYLE_DIAMOND = 5 + diff --git a/pygtk_chart/__init__.pyc b/pygtk_chart/__init__.pyc Binary files differnew file mode 100644 index 0000000..a4ba455 --- /dev/null +++ b/pygtk_chart/__init__.pyc diff --git a/pygtk_chart/bar_chart.py b/pygtk_chart/bar_chart.py new file mode 100644 index 0000000..d1e8879 --- /dev/null +++ b/pygtk_chart/bar_chart.py @@ -0,0 +1,729 @@ +# Copyright 2009 John Dickinson <john@johnandkaren.com> +# 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 BarChart widget. + +Author: John Dickinson (john@johnandkaren.com), +Sven Festersen (sven@sven-festersen.de) +""" +__docformat__ = "epytext" +import cairo +import gtk +import gobject +import os +import math + +import pygtk_chart +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 + +MODE_VERTICAL = 0 +MODE_HORIZONTAL = 1 + +def draw_rounded_rectangle(context, x, y, width, height, radius=0): + """ + Draws a rectangle with rounded corners to context. radius specifies + the corner radius in px. + + @param context: the context to draw on + @type context: CairoContext + @param x: x coordinate of the upper left corner + @type x: float + @param y: y coordinate of the upper left corner + @type y: float + @param width: width of the rectangle in px + @type width: float + @param height: height of the rectangle in px + @type height: float + @param radius: corner radius in px (default: 0) + @type radius: float. + """ + if radius == 0: + context.rectangle(x, y, width, height) + else: + context.move_to(x, y + radius) + context.arc(x + radius, y + radius, radius, math.pi, 1.5 * math.pi) + context.rel_line_to(width - 2 * radius, 0) + context.arc(x + width - radius, y + radius, radius, 1.5 * math.pi, 2 * math.pi) + context.rel_line_to(0, height - 2 * radius) + context.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * math.pi) + context.rel_line_to(-(width - 2 * radius), 0) + context.arc(x + radius, y + height - radius, radius, 0.5 * math.pi, math.pi) + context.close_path() + + +class Bar(chart.Area): + """ + A class that represents a bar on a bar chart. + + Properties + ========== + The Bar class inherits properties from chart.Area. + Additional properties: + - corner-radius (radius of the bar's corners, in px; type: float) + + Signals + ======= + The Bar class inherits signals from chart.Area. + """ + + __gproperties__ = {"corner-radius": (gobject.TYPE_INT, "bar corner radius", + "The radius of the bar's rounded corner.", + 0, 100, 0, gobject.PARAM_READWRITE)} + + def __init__(self, name, value, title=""): + chart.Area.__init__(self, name, value, title) + self._label_object = label.Label((0, 0), title) + self._value_label_object = label.Label((0, 0), "") + + self._corner_radius = 0 + + def do_get_property(self, property): + if property.name == "visible": + return self._show + elif property.name == "antialias": + return self._antialias + elif property.name == "name": + return self._name + elif property.name == "value": + return self._value + elif property.name == "color": + return self._color + elif property.name == "label": + return self._label + elif property.name == "highlighted": + return self._highlighted + elif property.name == "corner-radius": + return self._corner_radius + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "visible": + self._show = value + elif property.name == "antialias": + self._antialias = value + elif property.name == "value": + self._value = value + elif property.name == "color": + self._color = value + elif property.name == "label": + self._label = value + elif property.name == "highlighted": + self._highlighted = value + elif property.name == "corner-radius": + self._corner_radius = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def _do_draw(self, context, rect, n, i, mode, max_value, bar_padding, value_label_size, label_size, draw_labels): + if mode == MODE_VERTICAL: + self._do_draw_single_vertical(context, rect, n, i, mode, max_value, bar_padding, value_label_size, label_size, draw_labels) + elif mode == MODE_HORIZONTAL: + self._do_draw_single_horizontal(context, rect, n, i, mode, max_value, bar_padding, value_label_size, label_size, draw_labels) + + def _do_draw_single_vertical(self, context, rect, n, i, mode, max_value, bar_padding, value_label_size, label_size, draw_labels): + bar_width = (rect.width - (n - 1) * bar_padding) / n + bar_height = (rect.height - value_label_size - label_size) * self._value / max_value + bar_x = rect.x + i * (bar_width + bar_padding) + bar_y = rect.y + rect.height - bar_height - label_size + context.set_source_rgb(*color_gdk_to_cairo(self._color)) + draw_rounded_rectangle(context, bar_x, bar_y, bar_width, bar_height, self._corner_radius) + context.fill() + + if self._highlighted: + context.set_source_rgba(1, 1, 1, 0.1) + draw_rounded_rectangle(context, bar_x, bar_y, bar_width, bar_height, self._corner_radius) + context.fill() + + if draw_labels: + #draw the value label + self._value_label_object.set_text(str(self._value)) + self._value_label_object.set_color(self._color) + self._value_label_object.set_max_width(bar_width) + self._value_label_object.set_position((bar_x + bar_width / 2, bar_y - 3)) + self._value_label_object.set_anchor(label.ANCHOR_BOTTOM_CENTER) + self._value_label_object.draw(context, rect) + context.fill() + + #draw label + self._label_object.set_text(self._label) + self._label_object.set_color(self._color) + self._label_object.set_max_width(bar_width) + self._label_object.set_position((bar_x + bar_width / 2, bar_y + bar_height + 3)) + self._label_object.set_anchor(label.ANCHOR_TOP_CENTER) + self._label_object.draw(context, rect) + context.fill() + + chart.add_sensitive_area(chart.AREA_RECTANGLE, (bar_x, bar_y, bar_width, bar_height), self) + + def _do_draw_single_horizontal(self, context, rect, n, i, mode, max_value, bar_padding, value_label_size, label_size, draw_labels): + bar_width = (rect.width - value_label_size - label_size) * self._value / max_value + bar_height = (rect.height - (n - 1) * bar_padding) / n + bar_x = rect.x + label_size + bar_y = rect.y + i * (bar_height + bar_padding) + context.set_source_rgb(*color_gdk_to_cairo(self._color)) + draw_rounded_rectangle(context, bar_x, bar_y, bar_width, bar_height, self._corner_radius) + context.fill() + + if self._highlighted: + context.set_source_rgba(1, 1, 1, 0.1) + draw_rounded_rectangle(context, bar_x, bar_y, bar_width, bar_height, self._corner_radius) + context.fill() + + if draw_labels: + #draw the value label + self._value_label_object.set_text(str(self._value)) + self._value_label_object.set_color(self._color) + self._value_label_object.set_position((bar_x + bar_width + 3, bar_y + bar_height / 2)) + self._value_label_object.set_anchor(label.ANCHOR_LEFT_CENTER) + self._value_label_object.draw(context, rect) + context.fill() + + #draw label + self._label_object.set_text(self._label) + self._label_object.set_color(self._color) + self._label_object.set_max_width(0.25 * rect.width) + self._label_object.set_position((bar_x - 3, bar_y + bar_height / 2)) + self._label_object.set_anchor(label.ANCHOR_RIGHT_CENTER) + self._label_object.draw(context, rect) + context.fill() + + chart.add_sensitive_area(chart.AREA_RECTANGLE, (bar_x, bar_y, bar_width, bar_height), self) + + def get_value_label_size(self, context, rect, mode, n, bar_padding): + if mode == MODE_VERTICAL: + bar_width = (rect.width - (n - 1) * bar_padding) / n + self._value_label_object.set_max_width(bar_width) + self._value_label_object.set_text(str(self._value)) + return self._value_label_object.get_calculated_dimensions(context, rect)[1] + elif mode == MODE_HORIZONTAL: + self._value_label_object.set_wrap(False) + self._value_label_object.set_fixed(True) + self._value_label_object.set_text(str(self._value)) + return self._value_label_object.get_calculated_dimensions(context, rect)[0] + + def get_label_size(self, context, rect, mode, n, bar_padding): + if mode == MODE_VERTICAL: + bar_width = (rect.width - (n - 1) * bar_padding) / n + self._label_object.set_max_width(bar_width) + self._label_object.set_text(self._label) + return self._label_object.get_calculated_dimensions(context, rect)[1] + elif mode == MODE_HORIZONTAL: + self._label_object.set_max_width(0.25 * rect.width) + self._label_object.set_text(self._label) + return self._label_object.get_calculated_dimensions(context, rect)[0] + + def set_corner_radius(self, radius): + """ + Set the radius of the bar's corners in px (default: 0). + + @param radius: radius of the corners + @type radius: int in [0, 100]. + """ + self.set_property("corner-radius", radius) + self.emit("appearance_changed") + + def get_corner_radius(self): + """ + Returns the current radius of the bar's corners in px. + + @return: int in [0, 100] + """ + return self.get_property("corner-radius") + + +class Grid(ChartObject): + """ + This class represents the grid on BarChart and MultiBarChart + widgets. + + Properties + ========== + bar_chart.Grid inherits properties from ChartObject. + Additional properties: + - line-style (the style of the grid lines, type: a line style + constant) + - color (the color of the grid lines, type: gtk.gdk.Color) + - show-values (sets whether values should be shown at the grid + lines, type: boolean) + - padding (the grid's padding in px, type: int in [0, 100]). + + Signals + ======= + The Grid class inherits signal from chart_object.ChartObject. + """ + + __gproperties__ = {"show-values": (gobject.TYPE_BOOLEAN, "show values", + "Set whether to show grid values.", + True, gobject.PARAM_READWRITE), + "color": (gobject.TYPE_PYOBJECT, "color", + "The color of the grid lines.", + gobject.PARAM_READWRITE), + "line-style": (gobject.TYPE_INT, "line style", + "The grid's line style", 0, 3, 0, + gobject.PARAM_READWRITE), + "padding": (gobject.TYPE_INT, "padding", + "The grid's padding", 0, 100, 6, + gobject.PARAM_READWRITE)} + + def __init__(self): + ChartObject.__init__(self) + #private properties: + self._show_values = True + self._color = gtk.gdk.color_parse("#dedede") + self._line_style = pygtk_chart.LINE_STYLE_SOLID + self._padding = 6 + + def do_get_property(self, property): + if property.name == "visible": + return self._show + elif property.name == "antialias": + return self._antialias + elif property.name == "show-values": + return self._show_values + elif property.name == "color": + return self._color + elif property.name == "line-style": + return self._line_style + elif property.name == "padding": + return self._padding + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "visible": + self._show = value + elif property.name == "antialias": + self._antialias = value + elif property.name == "show-values": + self._show_values = value + elif property.name == "color": + self._color = value + elif property.name == "line-style": + self._line_style = value + elif property.name == "padding": + self._padding = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def _do_draw(self, context, rect, mode, maximum_value, value_label_size, label_size): + n = maximum_value / (10 ** int(math.log10(maximum_value))) + context.set_antialias(cairo.ANTIALIAS_NONE) + set_context_line_style(context, self._line_style) + labels = [] + if mode == MODE_VERTICAL: + delta = (rect.height - value_label_size - label_size) / n + if self._show_values: + max_label_size = 0 + for i in range(0, int(n + 1)): + y = rect.y + rect.height - i * delta - label_size + value = maximum_value * float(i) / n + value_label = label.Label((rect.x, y), str(value)) + max_label_size = max(max_label_size, value_label.get_calculated_dimensions(context, rect)[0]) + labels.append(value_label) + max_label_size += 3 + rect = gtk.gdk.Rectangle(int(rect.x + max_label_size), rect.y, int(rect.width - max_label_size), rect.height) + for i in range(0, len(labels)): + y = rect.y + rect.height - i * delta - label_size + value_label = labels[i] + value_label.set_position((rect.x - 3, y)) + value_label.set_anchor(label.ANCHOR_RIGHT_CENTER) + value_label.draw(context, rect) + context.fill() + + for i in range(0, int(n + 1)): + y = rect.y + rect.height - i * delta - label_size + context.set_source_rgb(*color_gdk_to_cairo(self._color)) + context.move_to(rect.x, y) + context.rel_line_to(rect.width, 0) + context.stroke() + rect = gtk.gdk.Rectangle(rect.x + self._padding, rect.y, rect.width - 2 * self._padding, rect.height) + elif mode == MODE_HORIZONTAL: + delta = (rect.width - value_label_size - label_size) / n + + if self._show_values: + max_label_size = 0 + for i in range(0, int(n + 1)): + x = rect.x + i * delta + label_size + value = maximum_value * float(i) / n + value_label = label.Label((x, rect.y + rect.height), str(value)) + max_label_size = max(max_label_size, value_label.get_calculated_dimensions(context, rect)[1]) + labels.append(value_label) + max_label_size += 3 + rect = gtk.gdk.Rectangle(rect.x, rect.y, rect.width, int(rect.height - max_label_size)) + for i in range(0, len(labels)): + x = rect.x + i * delta + label_size + value_label = labels[i] + value_label.set_position((x, rect.y + rect.height + 3)) + value_label.set_anchor(label.ANCHOR_TOP_CENTER) + value_label.draw(context, rect) + context.fill() + + for i in range(0, int(n + 1)): + x = rect.x + i * delta + label_size + context.set_source_rgb(*color_gdk_to_cairo(self._color)) + context.move_to(x, rect.y) + context.rel_line_to(0, rect.height) + context.stroke() + rect = gtk.gdk.Rectangle(rect.x, rect.y + self._padding, rect.width, rect.height - 2 * self._padding) + return rect + + #set and get methods + def set_show_values(self, show): + """ + Set whether values should be shown. + + @type show: boolean. + """ + self.set_property("show-values", show) + self.emit("appearance_changed") + + def get_show_values(self): + """ + Returns True if grid values are shown. + + @return: boolean. + """ + return self.get_property("show-values") + + def set_color(self, color): + """ + Set the color of the grid lines. + + @param color: the grid lines' color + @type color: gtk.gdk.Color. + """ + self.set_property("color", color) + self.emit("appearance_changed") + + def get_color(self): + """ + Returns the current color of the grid lines. + + @return: gtk.gdk.Color. + """ + return self.get_property("color") + + def set_line_style(self, style): + """ + Set the style of the grid lines. style has to be one of + - pygtk_chart.LINE_STYLE_SOLID (default) + - pygtk_chart.LINE_STYLE_DOTTED + - pygtk_chart.LINE_STYLE_DASHED + - pygtk_chart.LINE_STYLE_DASHED_ASYMMETRIC + + @param style: the new line style + @type style: one of the constants above. + """ + self.set_property("line-style", style) + self.emit("appearance_changed") + + def get_line_style(self): + """ + Returns the current grid's line style. + + @return: a line style constant. + """ + return self.get_property("line-style") + + def set_padding(self, padding): + """ + Set the grid's padding. + + @type padding: int in [0, 100]. + """ + self.set_property("padding", padding) + self.emit("appearance_changed") + + def get_padding(self): + """ + Returns the grid's padding. + + @return: int in [0, 100]. + """ + return self.get_property("padding") + + + +class BarChart(chart.Chart): + """ + This is a widget that show a simple BarChart. + + Properties + ========== + The BarChart class inherits properties from chart.Chart. + Additional properites: + - draw-labels (set wether to draw bar label, type: boolean) + - enable-mouseover (set whether to show a mouseover effect, type: + boolean) + - mode (the mode of the bar chart, type: one of MODE_VERTICAL, + MODE_HORIZONTAL) + - bar-padding (the sace between bars in px, type: int in [0, 100]). + + Signals + ======= + The BarChart class inherits signals from chart.Chart. + Additional signals: + - bar-clicked: emitted when a bar on the bar chart was clicked + callback signature: + def bar_clicked(chart, bar). + + """ + + __gsignals__ = {"bar-clicked": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,))} + + __gproperties__ = {"bar-padding": (gobject.TYPE_INT, "bar padding", + "The distance between two bars.", + 0, 100, 16, + gobject.PARAM_READWRITE), + "mode": (gobject.TYPE_INT, "mode", + "The chart's mode.", 0, 1, 0, + gobject.PARAM_READWRITE), + "draw-labels": (gobject.TYPE_BOOLEAN, + "draw labels", "Set whether to draw labels on bars.", + True, gobject.PARAM_READWRITE), + "enable-mouseover": (gobject.TYPE_BOOLEAN, "enable mouseover", + "Set whether to enable mouseover effect.", + True, gobject.PARAM_READWRITE)} + + def __init__(self): + super(BarChart, self).__init__() + #private properties: + self._bars = [] + #gobject properties: + self._bar_padding = 16 + self._mode = MODE_VERTICAL + self._draw_labels = True + self._mouseover = True + #public attributes: + self.grid = Grid() + #connect callbacks: + self.grid.connect("appearance_changed", self._cb_appearance_changed) + + def do_get_property(self, property): + if property.name == "padding": + return self._padding + elif property.name == "bar-padding": + return self._bar_padding + elif property.name == "mode": + return self._mode + elif property.name == "draw-labels": + return self._draw_labels + elif property.name == "enable-mouseover": + return self._mouseover + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "padding": + self._padding = value + elif property.name == "bar-padding": + self._bar_padding = value + elif property.name == "mode": + self._mode = value + elif property.name == "draw-labels": + self._draw_labels = value + elif property.name == "enable-mouseover": + self._mouseover = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + #drawing methods + 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() + rect = gtk.gdk.Rectangle(0, 0, rect.width, rect.height) #transform rect to context coordinates + context.set_line_width(1) + + rect = self.draw_basics(context, rect) + maximum_value = max(bar.get_value() for bar in self._bars) + #find out the size of the value labels + value_label_size = 0 + if self._draw_labels: + for bar in self._bars: + value_label_size = max(value_label_size, bar.get_value_label_size(context, rect, self._mode, len(self._bars), self._bar_padding)) + value_label_size += 3 + + #find out the size of the labels: + label_size = 0 + if self._draw_labels: + for bar in self._bars: + label_size = max(label_size, bar.get_label_size(context, rect, self._mode, len(self._bars), self._bar_padding)) + label_size += 3 + + rect = self._do_draw_grid(context, rect, maximum_value, value_label_size, label_size) + self._do_draw_bars(context, rect, maximum_value, value_label_size, label_size) + + label.finish_drawing() + + if self._mode == MODE_VERTICAL: + n = len(self._bars) + minimum_width = rect.x + self._padding + (n - 1) * self._bar_padding + n * 10 + minimum_height = 100 + self._padding + rect.y + elif self._mode == MODE_HORIZONTAL: + n = len(self._bars) + minimum_width = rect.x + self._bar_padding + 100 + minimum_height = rect.y + self._padding + (n - 1) * self._bar_padding + n * 10 + self.set_size_request(minimum_width, minimum_height) + + def draw_basics(self, context, rect): + """ + Draw basic things that every plot has (background, title, ...). + + @type context: cairo.Context + @param context: The context to draw on. + @type rect: gtk.gdk.Rectangle + @param rect: A rectangle representing the charts area. + """ + self.background.draw(context, rect) + self.title.draw(context, rect, self._padding) + + #calculate the rectangle that's available for drawing the chart + title_height = self.title.get_real_dimensions()[1] + rect_height = int(rect.height - 3 * self._padding - title_height) + rect_width = int(rect.width - 2 * self._padding) + rect_x = int(rect.x + self._padding) + rect_y = int(rect.y + title_height + 2 * self._padding) + return gtk.gdk.Rectangle(rect_x, rect_y, rect_width, rect_height) + + def _do_draw_grid(self, context, rect, maximum_value, value_label_size, label_size): + if self.grid.get_visible(): + return self.grid.draw(context, rect, self._mode, maximum_value, value_label_size, label_size) + else: + return rect + + def _do_draw_bars(self, context, rect, maximum_value, value_label_size, label_size): + if self._bars == []: + return + + #draw the bars + chart.init_sensitive_areas() + for i, bar in enumerate(self._bars): + bar.draw(context, rect, len(self._bars), i, self._mode, maximum_value, self._bar_padding, value_label_size, label_size, self._draw_labels) + + #other methods + def add_bar(self, bar): + if bar.get_color() == COLOR_AUTO: + bar.set_color(COLORS[len(self._bars) % len(COLORS)]) + self._bars.append(bar) + bar.connect("appearance_changed", self._cb_appearance_changed) + + #callbacks + def _cb_motion_notify(self, widget, event): + if not self._mouseover: return + bars = chart.get_sensitive_areas(event.x, event.y) + if bars == []: return + for bar in self._bars: + bar.set_property("highlighted", bar in bars) + self.queue_draw() + + def _cb_button_pressed(self, widget, event): + bars = chart.get_sensitive_areas(event.x, event.y) + for bar in bars: + self.emit("bar-clicked", bar) + + #set and get methods + def set_bar_padding(self, padding): + """ + Set the space between two bars in px. + + @param padding: space between bars in px + @type padding: int in [0, 100]. + """ + self.set_property("bar-padding", padding) + self.queue_draw() + + def get_bar_padding(self): + """ + Returns the space between bars in px. + + @return: int in [0, 100]. + """ + return self.get_property("bar-padding") + + def set_mode(self, mode): + """ + Set the mode (vertical or horizontal) of the BarChart. mode has + to be bar_chart.MODE_VERTICAL (default) or + bar_chart.MODE_HORIZONTAL. + + @param mode: the new mode of the chart + @type mode: one of the mode constants above. + """ + self.set_property("mode", mode) + self.queue_draw() + + def get_mode(self): + """ + Returns the current mode of the chart: bar_chart.MODE_VERTICAL + or bar_chart.MODE_HORIZONTAL. + + @return: a mode constant. + """ + return self.get_property("mode") + + def set_draw_labels(self, draw): + """ + Set whether labels should be drawn on bars. + + @type draw: boolean. + """ + self.set_property("draw-labels", draw) + self.queue_draw() + + def get_draw_labels(self): + """ + Returns True if labels are drawn on bars. + + @return: boolean. + """ + return self.get_property("draw-labels") + + def set_enable_mouseover(self, mouseover): + """ + Set whether a mouseover effect should be shown when the pointer + enters a bar. + + @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") + diff --git a/pygtk_chart/bar_chart.pyc b/pygtk_chart/bar_chart.pyc Binary files differnew file mode 100644 index 0000000..ef91f75 --- /dev/null +++ b/pygtk_chart/bar_chart.pyc diff --git a/pygtk_chart/basics.py b/pygtk_chart/basics.py new file mode 100644 index 0000000..5fdcd10 --- /dev/null +++ b/pygtk_chart/basics.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# +# misc.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. +""" +This module contains simple functions needed by all other modules. + +Author: Sven Festersen (sven@sven-festersen.de) +""" +__docformat__ = "epytext" +import cairo +import gtk +import os + +import pygtk_chart + +def is_in_range(x, (xmin, xmax)): + """ + Use this method to test whether M{xmin <= x <= xmax}. + + @type x: number + @type xmin: number + @type xmax: number + """ + return (xmin <= x and xmax >= x) + +def intersect_ranges(range_a, range_b): + min_a, max_a = range_a + min_b, max_b = range_b + return max(min_a, min_b), min(max_a, max_b) + +def get_center(rect): + """ + Find the center point of a rectangle. + + @type rect: gtk.gdk.Rectangle + @param rect: The rectangle. + @return: A (x, y) tuple specifying the center point. + """ + return rect.width / 2, rect.height / 2 + +def color_gdk_to_cairo(color): + """ + Convert a gtk.gdk.Color to cairo color. + + @type color: gtk.gdk.Color + @param color: the color to convert + @return: a color in cairo format. + """ + return (color.red / 65535.0, color.green / 65535.0, color.blue / 65535.0) + +def color_cairo_to_gdk(r, g, b): + return gtk.gdk.Color(int(65535 * r), int(65535 * g), int(65535 * b)) + +def color_rgb_to_cairo(color): + """ + Convert a 8 bit RGB value to cairo color. + + @type color: a triple of integers between 0 and 255 + @param color: The color to convert. + @return: A color in cairo format. + """ + return (color[0] / 255.0, color[1] / 255.0, color[2] / 255.0) + +def color_html_to_cairo(color): + """ + Convert a html (hex) RGB value to cairo color. + + @type color: html color string + @param color: The color to convert. + @return: A color in cairo format. + """ + if color[0] == '#': + color = color[1:] + (r, g, b) = (int(color[:2], 16), + int(color[2:4], 16), + int(color[4:], 16)) + return color_rgb_to_cairo((r, g, b)) + +def color_list_from_file(filename): + """ + Read a file with one html hex color per line and return a list + of cairo colors. + """ + result = [] + if os.path.exists(filename): + f = open(filename, "r") + for line in f.readlines(): + line = line.strip() + result.append(color_html_to_cairo(line)) + return result + +def gdk_color_list_from_file(filename): + """ + Read a file with one html hex color per line and return a list + of gdk colors. + """ + result = [] + if os.path.exists(filename): + f = open(filename, "r") + for line in f.readlines(): + line = line.strip() + result.append(gtk.gdk.color_parse(line)) + return result + +def set_context_line_style(context, style): + """ + The the line style for a context. + """ + if style == pygtk_chart.LINE_STYLE_SOLID: + context.set_dash([]) + elif style == pygtk_chart.LINE_STYLE_DASHED: + context.set_dash([5]) + elif style == pygtk_chart.LINE_STYLE_DASHED_ASYMMETRIC: + context.set_dash([6, 6, 2, 6]) + elif style == pygtk_chart.LINE_STYLE_DOTTED: + context.set_dash([1]) diff --git a/pygtk_chart/basics.pyc b/pygtk_chart/basics.pyc Binary files differnew file mode 100644 index 0000000..5b1d2e8 --- /dev/null +++ b/pygtk_chart/basics.pyc diff --git a/pygtk_chart/chart.py b/pygtk_chart/chart.py new file mode 100644 index 0000000..8b4a283 --- /dev/null +++ b/pygtk_chart/chart.py @@ -0,0 +1,592 @@ +#!/usr/bin/env python +# +# plot.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. + +""" +Module Contents +=============== +This is the main module. It contains the base classes for chart widgets. + - class Chart: base class for all chart widgets. + - class Background: background of a chart widget. + - class Title: title of a chart. + +Colors +------ +All colors that pygtkChart uses are gtk.gdk.Colors as used by PyGTK. + +Author: Sven Festersen (sven@sven-festersen.de) +""" +__docformat__ = "epytext" +import cairo +import gobject +import gtk +import os +import pango +import pangocairo +import pygtk + +from pygtk_chart.chart_object import ChartObject +from pygtk_chart.basics import * +from pygtk_chart import label + +COLOR_AUTO = 0 +AREA_CIRCLE = 0 +AREA_RECTANGLE = 1 +CLICK_SENSITIVE_AREAS = [] + + +def init_sensitive_areas(): + global CLICK_SENSITIVE_AREAS + CLICK_SENSITIVE_AREAS = [] + +def add_sensitive_area(type, coords, data): + global CLICK_SENSITIVE_AREAS + CLICK_SENSITIVE_AREAS.append((type, coords, data)) + +def get_sensitive_areas(x, y): + res = [] + global CLICK_SENSITIVE_AREAS + for type, coords, data in CLICK_SENSITIVE_AREAS: + if type == AREA_CIRCLE: + ax, ay, radius = coords + if (ax - x) ** 2 + (ay - y) ** 2 <= radius ** 2: + res.append(data) + elif type == AREA_RECTANGLE: + ax, ay, width, height = coords + if ax <= x <= ax + width and ay <= y <= ay + height: + res.append(data) + return res + + +class Chart(gtk.DrawingArea): + """ + This is the base class for all chart widgets. + + Properties + ========== + The Chart class inherits properties from gtk.DrawingArea. + Additional properties: + - padding (the amount of free white space between the chart's + content and its border in px, type: int in [0, 100]. + + Signals + ======= + The Chart class inherits signals from gtk.DrawingArea. + """ + + __gproperties__ = {"padding": (gobject.TYPE_INT, "padding", + "The chart's padding.", 0, 100, 16, + gobject.PARAM_READWRITE)} + + def __init__(self): + gtk.DrawingArea.__init__(self) + #private properties: + self._padding = 16 + #objects needed for every chart: + self.background = Background() + self.background.connect("appearance-changed", self._cb_appearance_changed) + self.title = Title() + self.title.connect("appearance-changed", self._cb_appearance_changed) + + self.add_events(gtk.gdk.BUTTON_PRESS_MASK|gtk.gdk.SCROLL_MASK|gtk.gdk.POINTER_MOTION_MASK) + self.connect("expose_event", self._cb_expose_event) + self.connect("button_press_event", self._cb_button_pressed) + self.connect("motion-notify-event", self._cb_motion_notify) + + def do_get_property(self, property): + if property.name == "padding": + return self._padding + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "padding": + self._padding = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def _cb_appearance_changed(self, object): + """ + This method is called after the appearance of an object changed + and forces a redraw. + """ + self.queue_draw() + + def _cb_button_pressed(self, widget, event): + pass + + def _cb_motion_notify(self, widget, event): + pass + + def _cb_expose_event(self, widget, event): + """ + This method is called when an instance of Chart receives + the gtk expose_event. + + @type widget: gtk.Widget + @param widget: The widget that received the event. + @type event: gtk.Event + @param event: The event. + """ + self.context = widget.window.cairo_create() + self.context.rectangle(event.area.x, event.area.y, \ + event.area.width, event.area.height) + self.context.clip() + self.draw(self.context) + return False + + def draw_basics(self, context, rect): + """ + Draw basic things that every plot has (background, title, ...). + + @type context: cairo.Context + @param context: The context to draw on. + @type rect: gtk.gdk.Rectangle + @param rect: A rectangle representing the charts area. + """ + self.background.draw(context, rect) + self.title.draw(context, rect) + + #calculate the rectangle that's available for drawing the chart + title_height = self.title.get_real_dimensions()[1] + rect_height = int(rect.height - 3 * self._padding - title_height) + rect_width = int(rect.width - 2 * self._padding) + rect_x = int(rect.x + self._padding) + rect_y = int(rect.y + title_height + 2 * self._padding) + return gtk.gdk.Rectangle(rect_x, rect_y, rect_width, rect_height) + + 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. + """ + rect = self.get_allocation() + rect = gtk.gdk.Rectangle(0, 0, rect.width, rect.height) #transform rect to context coordinates + context.set_line_width(1) + rect = self.draw_basics(context, rect) + + def export_svg(self, filename, size=None): + """ + Saves the contents of the widget to svg file. The size of the image + will be the size of the widget. + + @type filename: string + @param filename: The path to the file where you want the chart to be saved. + @type size: tuple + @param size: Optional parameter to give the desired height and width of the image. + """ + if size is None: + rect = self.get_allocation() + width = rect.width + height = rect.height + else: + width, height = size + old_alloc = self.get_allocation + self.get_allocation = lambda: gtk.gdk.Rectangle(0, 0, width, height) + surface = cairo.SVGSurface(filename, width, height) + ctx = cairo.Context(surface) + context = pangocairo.CairoContext(ctx) + self.draw(context) + surface.finish() + if size is not None: + self.get_allocation = old_alloc + + def export_png(self, filename, size=None): + """ + Saves the contents of the widget to png file. The size of the image + will be the size of the widget. + + @type filename: string + @param filename: The path to the file where you want the chart to be saved. + @type size: tuple + @param size: Optional parameter to give the desired height and width of the image. + """ + if size is None: + rect = self.get_allocation() + width = rect.width + height = rect.height + else: + width, height = size + old_alloc = self.get_allocation + self.get_allocation = lambda: gtk.gdk.Rectangle(0, 0, width, height) + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + ctx = cairo.Context(surface) + context = pangocairo.CairoContext(ctx) + self.set_size_request(width, height) + self.draw(context) + surface.write_to_png(filename) + if size is not None: + self.get_allocation = old_alloc + + + def set_padding(self, padding): + """ + Set the chart's padding. + + @param padding: the padding in px + @type padding: int in [0, 100] (default: 16). + """ + self.set_property("padding", padding) + self.queue_draw() + + def get_padding(self): + """ + Returns the chart's padding. + + @return: int in [0, 100]. + """ + return self.get_property("padding") + + +class Background(ChartObject): + """ + The background of a chart. + + Properties + ========== + This class inherits properties from chart_object.ChartObject. + Additional properties: + - color (the background color, type: gtk.gdk.Color) + - gradient (the background gradient, type: a pair of gtk.gdk.Color) + - image (path to the background image file, type: string) + + Signals + ======= + The Background class inherits signals from chart_object.ChartObject. + """ + + __gproperties__ = {"color": (gobject.TYPE_PYOBJECT, + "background color", + "The color of the backround.", + gobject.PARAM_READWRITE), + "gradient": (gobject.TYPE_PYOBJECT, + "background gradient", + "A background gardient. (first_color, second_color)", + gobject.PARAM_READWRITE), + "image": (gobject.TYPE_STRING, + "background image file", + "Path to the image file to use as background.", + "", gobject.PARAM_READWRITE)} + + def __init__(self): + ChartObject.__init__(self) + self._color = gtk.gdk.color_parse("#ffffff") #the backgound is filled white by default + self._gradient = None + self._image = "" + self._pixbuf = None + + def do_get_property(self, property): + if property.name == "visible": + return self._show + elif property.name == "antialias": + return self._antialias + elif property.name == "gradient": + return self._gradient + elif property.name == "color": + return self._color + elif property.name == "image": + return self._image + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "visible": + self._show = value + elif property.name == "antialias": + self._antialias = value + elif property.name == "gradient": + self._gradient = value + elif property.name == "color": + self._color = value + elif property.name == "image": + self._image = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def _do_draw(self, context, rect): + """ + Do all the drawing stuff. + + @type context: cairo.Context + @param context: The context to draw on. + @type rect: gtk.gdk.Rectangle + @param rect: A rectangle representing the charts area. + """ + if self._color != None: + #set source color + context.set_source_rgb(*color_gdk_to_cairo(self._color)) + elif self._gradient != None: + #set source gradient + cs = color_gdk_to_cairo(self._gradient[0]) + ce = color_gdk_to_cairo(self._gradient[1]) + gradient = cairo.LinearGradient(0, 0, 0, rect.height) + gradient.add_color_stop_rgb(0, cs[0], cs[1], cs[2]) + gradient.add_color_stop_rgb(1, ce[0], ce[1], ce[2]) + context.set_source(gradient) + elif self._pixbuf: + context.set_source_pixbuf(self._pixbuf, 0, 0) + else: + context.set_source_rgb(1, 1, 1) #fallback to white bg + #create the background rectangle and fill it: + context.rectangle(0, 0, rect.width, rect.height) + context.fill() + + def set_color(self, color): + """ + The set_color() method can be used to change the color of the + background. + + @type color: gtk.gdk.Color + @param color: Set the background to be filles with this color. + """ + self.set_property("color", color) + self.set_property("gradient", None) + self.set_property("image", "") + self.emit("appearance_changed") + + def get_color(self): + """ + Returns the background's color. + + @return: gtk.gdk.Color. + """ + return self.get_property("color") + + def set_gradient(self, color_start, color_end): + """ + Use set_gradient() to define a vertical gradient as the background. + + @type color_start: gtk.gdk.Color + @param color_start: The starting (top) color of the gradient. + @type color_end: gtk.gdk.Color + @param color_end: The ending (bottom) color of the gradient. + """ + self.set_property("color", None) + self.set_property("gradient", (color_start, color_end)) + self.set_property("image", "") + self.emit("appearance_changed") + + def get_gradient(self): + """ + Returns the gradient of the background or None. + + @return: A (gtk.gdk.Color, gtk.gdk.Color) tuple or None. + """ + return self.get_property("gradient") + + def set_image(self, filename): + """ + The set_image() method sets the background to be filled with an + image. + + @type filename: string + @param filename: Path to the file you want to use as background + image. If the file does not exists, the background is set to white. + """ + try: + self._pixbuf = gtk.gdk.pixbuf_new_from_file(filename) + except: + self._pixbuf = None + + self.set_property("color", None) + self.set_property("gradient", None) + self.set_property("image", filename) + self.emit("appearance_changed") + + def get_image(self): + return self.get_property("image") + + +class Title(label.Label): + """ + The title of a chart. The title will be drawn centered at the top of the + chart. + + Properties + ========== + The Title class inherits properties from label.Label. + + Signals + ======= + The Title class inherits signals from label.Label. + """ + + def __init__(self, text=""): + label.Label.__init__(self, (0, 0), text, weight=pango.WEIGHT_BOLD, anchor=label.ANCHOR_TOP_CENTER, fixed=True) + + def _do_draw(self, context, rect, top=-1): + if top == -1: top = rect.height / 80 + self._size = max(8, int(rect.height / 50.0)) + self._position = rect.width / 2, top + self._do_draw_label(context, rect) + + +class Area(ChartObject): + """ + This is a base class for classes that represent areas, e.g. the + pie_chart.PieArea class and the bar_chart.Bar class. + + Properties + ========== + The Area class inherits properties from chart_object.ChartObject. + Additional properties: + - name (a unique name for the area, type: string, read only) + - value (the value of the area, type: float) + - color (the area's color, type: gtk.gdk.Color) + - label (a label for the area, type: string) + - highlighted (set whether the area should be highlighted, + type: boolean). + + Signals + ======= + The Area class inherits signals from chart_object.ChartObject. + """ + + __gproperties__ = {"name": (gobject.TYPE_STRING, "area name", + "A unique name for the area.", + "", gobject.PARAM_READABLE), + "value": (gobject.TYPE_FLOAT, + "value", + "The value.", + 0.0, 9999999999.0, 0.0, gobject.PARAM_READWRITE), + "color": (gobject.TYPE_PYOBJECT, "area color", + "The color of the area.", + gobject.PARAM_READWRITE), + "label": (gobject.TYPE_STRING, "area label", + "The label for the area.", "", + gobject.PARAM_READWRITE), + "highlighted": (gobject.TYPE_BOOLEAN, "area is higlighted", + "Set whether the area should be higlighted.", + False, gobject.PARAM_READWRITE)} + + def __init__(self, name, value, title=""): + ChartObject.__init__(self) + self._name = name + self._value = value + self._label = title + self._color = COLOR_AUTO + self._highlighted = False + + def do_get_property(self, property): + if property.name == "visible": + return self._show + elif property.name == "antialias": + return self._antialias + elif property.name == "name": + return self._name + elif property.name == "value": + return self._value + elif property.name == "color": + return self._color + elif property.name == "label": + return self._label + elif property.name == "highlighted": + return self._highlighted + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "visible": + self._show = value + elif property.name == "antialias": + self._antialias = value + elif property.name == "value": + self._value = value + elif property.name == "color": + self._color = value + elif property.name == "label": + self._label = value + elif property.name == "highlighted": + self._highlighted = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def set_value(self, value): + """ + Set the value of the area. + + @type value: float. + """ + self.set_property("value", value) + self.emit("appearance_changed") + + def get_value(self): + """ + Returns the current value of the area. + + @return: float. + """ + return self.get_property("value") + + def set_color(self, color): + """ + Set the color of the area. + + @type color: gtk.gdk.Color. + """ + self.set_property("color", color) + self.emit("appearance_changed") + + def get_color(self): + """ + Returns the current color of the area or COLOR_AUTO. + + @return: gtk.gdk.Color or COLOR_AUTO. + """ + return self.get_property("color") + + def set_label(self, label): + """ + Set the label for the area. + + @param label: the new label + @type label: string. + """ + self.set_property("label", label) + self.emit("appearance_changed") + + def get_label(self): + """ + Returns the current label of the area. + + @return: string. + """ + return self.get_property("label") + + def set_highlighted(self, highlighted): + """ + Set whether the area should be highlighted. + + @type highlighted: boolean. + """ + self.set_property("highlighted", highlighted) + self.emit("appearance_changed") + + def get_highlighted(self): + """ + Returns True if the area is currently highlighted. + + @return: boolean. + """ + return self.get_property("highlighted") diff --git a/pygtk_chart/chart.pyc b/pygtk_chart/chart.pyc Binary files differnew file mode 100644 index 0000000..a098bcf --- /dev/null +++ b/pygtk_chart/chart.pyc diff --git a/pygtk_chart/chart_object.py b/pygtk_chart/chart_object.py new file mode 100644 index 0000000..8e4cd7e --- /dev/null +++ b/pygtk_chart/chart_object.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python +# +# chart_object.py +# +# Copyright 2009 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. +""" +This module contains the ChartObject class. + +Author: Sven Festersen (sven@sven-festersen.de) +""" +import cairo +import gobject + +class ChartObject(gobject.GObject): + """ + This is the base class for all things that can be drawn on a chart + widget. + It emits the signal 'appearance-changed' when it needs to be + redrawn. + + Properties + ========== + ChartObject inherits properties from gobject.GObject. + Additional properties: + - visible (sets whether the object should be visible, + type: boolean) + - antialias (sets whether the object should be antialiased, + type: boolean). + + Signals + ======= + ChartObject inherits signals from gobject.GObject, + Additional signals: + - appearance-changed (emitted if the object needs to be redrawn). + """ + + __gsignals__ = {"appearance-changed": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [])} + + + __gproperties__ = {"visible": (gobject.TYPE_BOOLEAN, + "visibilty of the object", + "Set whether to draw the object or not.", + True, gobject.PARAM_READWRITE), + "antialias": (gobject.TYPE_BOOLEAN, + "use antialiasing", + "Set whether to use antialiasing when drawing the object.", + True, gobject.PARAM_READWRITE)} + + def __init__(self): + gobject.GObject.__init__(self) + self._show = True + self._antialias = True + + def do_get_property(self, property): + if property.name == "visible": + return self._show + elif property.name == "antialias": + return self._antialias + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "visible": + self._show = value + elif property.name == "antialias": + self._antialias = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def _do_draw(self, context, rect): + """ + A derived class should override this method. The drawing stuff + should happen here. + + @type context: cairo.Context + @param context: The context to draw on. + @type rect: gtk.gdk.Rectangle + @param rect: A rectangle representing the charts area. + """ + pass + + def draw(self, context, rect, *args): + """ + This method is called by the parent Chart instance. It + calls _do_draw. + + @type context: cairo.Context + @param context: The context to draw on. + @type rect: gtk.gdk.Rectangle + @param rect: A rectangle representing the charts area. + """ + res = None + if self._show: + if not self._antialias: + context.set_antialias(cairo.ANTIALIAS_NONE) + res = self._do_draw(context, rect, *args) + context.set_antialias(cairo.ANTIALIAS_DEFAULT) + return res + + def set_antialias(self, antialias): + """ + This method sets the antialiasing mode of the ChartObject. Antialiasing + is enabled by default. + + @type antialias: boolean + @param antialias: If False, antialiasing is disabled for this + ChartObject. + """ + self.set_property("antialias", antialias) + self.emit("appearance_changed") + + def get_antialias(self): + """ + Returns True if antialiasing is enabled for the object. + + @return: boolean. + """ + return self.get_property("antialias") + + def set_visible(self, visible): + """ + Use this method to set whether the ChartObject should be visible or + not. + + @type visible: boolean + @param visible: If False, the PlotObject won't be drawn. + """ + self.set_property("visible", visible) + self.emit("appearance_changed") + + def get_visible(self): + """ + Returns True if the object is visble. + + @return: boolean. + """ + return self.get_property("visible") + + +gobject.type_register(ChartObject) diff --git a/pygtk_chart/chart_object.pyc b/pygtk_chart/chart_object.pyc Binary files differnew file mode 100644 index 0000000..df4f462 --- /dev/null +++ b/pygtk_chart/chart_object.pyc diff --git a/pygtk_chart/data/tango.color b/pygtk_chart/data/tango.color new file mode 100644 index 0000000..6231528 --- /dev/null +++ b/pygtk_chart/data/tango.color @@ -0,0 +1,7 @@ +#cc0000 +#3465a4 +#73d216 +#f57900 +#75507b +#c17d11 +#edd400 diff --git a/pygtk_chart/label.py b/pygtk_chart/label.py new file mode 100644 index 0000000..75b523c --- /dev/null +++ b/pygtk_chart/label.py @@ -0,0 +1,744 @@ +#!/usr/bin/env python +# +# text.py +# +# Copyright 2009 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 Label class. + +Author: Sven Festersen (sven@sven-festersen.de) +""" +import cairo +import gobject +import gtk +import math +import pango +import pygtk + +from pygtk_chart import basics +from pygtk_chart.chart_object import ChartObject + + +ANCHOR_BOTTOM_LEFT = 0 +ANCHOR_TOP_LEFT = 1 +ANCHOR_TOP_RIGHT = 2 +ANCHOR_BOTTOM_RIGHT = 4 +ANCHOR_CENTER = 5 +ANCHOR_TOP_CENTER = 6 +ANCHOR_BOTTOM_CENTER = 7 +ANCHOR_LEFT_CENTER = 8 +ANCHOR_RIGHT_CENTER = 9 + +UNDERLINE_NONE = pango.UNDERLINE_NONE +UNDERLINE_SINGLE = pango.UNDERLINE_SINGLE +UNDERLINE_DOUBLE = pango.UNDERLINE_DOUBLE +UNDERLINE_LOW = pango.UNDERLINE_LOW + +STYLE_NORMAL = pango.STYLE_NORMAL +STYLE_OBLIQUE = pango.STYLE_OBLIQUE +STYLE_ITALIC = pango.STYLE_ITALIC + +WEIGHT_ULTRALIGHT = pango.WEIGHT_ULTRALIGHT +WEIGHT_LIGHT = pango.WEIGHT_LIGHT +WEIGHT_NORMAL = pango.WEIGHT_NORMAL +WEIGHT_BOLD = pango.WEIGHT_BOLD +WEIGHT_ULTRABOLD = pango.WEIGHT_ULTRABOLD +WEIGHT_HEAVY = pango.WEIGHT_HEAVY + + +DRAWING_INITIALIZED = False +REGISTERED_LABELS = [] + + +def begin_drawing(): + global DRAWING_INITIALIZED + DRAWING_INITIALIZED = True + +def finish_drawing(): + global REGISTERED_LABELS + global DRAWING_INITIALIZED + REGISTERED_LABELS = [] + DRAWING_INITIALIZED = False + +def register_label(label): + if DRAWING_INITIALIZED: + REGISTERED_LABELS.append(label) + +def get_registered_labels(): + if DRAWING_INITIALIZED: + return REGISTERED_LABELS + return [] + + +class Label(ChartObject): + """ + This class is used for drawing all the text on the chart widgets. + It uses the pango layout engine. + + Properties + ========== + The Label class inherits properties from chart_object.ChartObject. + Additional properties: + - color (the label's color, type: gtk.gdk.Color) + - text (text to display, type: string) + - position (the label's position, type: pair of float) + - anchor (the anchor that should be used to position the label, + type: an anchor constant) + - underline (sets the type of underline, type; an underline + constant) + - max-width (the maximum width of the label in px, type: int) + - rotation (angle of rotation in degrees, type: int) + - size (the size of the label's text in px, type: int) + - slant (the font slant, type: a slant style constant) + - weight (the font weight, type: a font weight constant) + - fixed (sets whether the position of the label may be changed + dynamicly or not, type: boolean) + - wrap (sets whether the label's text should be wrapped if it's + longer than max-width, type: boolean). + + Signals + ======= + The Label class inherits signals from chart_object.ChartObject. + """ + + __gproperties__ = {"color": (gobject.TYPE_PYOBJECT, + "label color", + "The color of the label (a gtk.gdkColor)", + gobject.PARAM_READWRITE), + "text": (gobject.TYPE_STRING, + "label text", + "The text to show on the label.", + "", gobject.PARAM_READWRITE), + "position": (gobject.TYPE_PYOBJECT, + "label position", + "A pair of x,y coordinates.", + gobject.PARAM_READWRITE), + "anchor": (gobject.TYPE_INT, "label anchor", + "The anchor of the label.", 0, 9, 0, + gobject.PARAM_READWRITE), + "underline": (gobject.TYPE_PYOBJECT, + "underline text", + "Set whether to underline the text.", + gobject.PARAM_READWRITE), + "max-width": (gobject.TYPE_INT, "maximum width", + "The maximum width of the label.", + 1, 99999, 99999, + gobject.PARAM_READWRITE), + "rotation": (gobject.TYPE_INT, "rotation of the label", + "The angle that the label should be rotated by in degrees.", + 0, 360, 0, gobject.PARAM_READWRITE), + "size": (gobject.TYPE_INT, "text size", + "The size of the text.", 0, 1000, 8, + gobject.PARAM_READWRITE), + "slant": (gobject.TYPE_PYOBJECT, "font slant", + "The font slant style.", + gobject.PARAM_READWRITE), + "weight": (gobject.TYPE_PYOBJECT, "font weight", + "The font weight.", gobject.PARAM_READWRITE), + "fixed": (gobject.TYPE_BOOLEAN, "fixed", + "Set whether the position of the label should be forced.", + False, gobject.PARAM_READWRITE), + "wrap": (gobject.TYPE_BOOLEAN, "wrap text", + "Set whether text should be wrapped.", + False, gobject.PARAM_READWRITE)} + + def __init__(self, position, text, size=None, + slant=pango.STYLE_NORMAL, + weight=pango.WEIGHT_NORMAL, + underline=pango.UNDERLINE_NONE, + anchor=ANCHOR_BOTTOM_LEFT, max_width=99999, + fixed=False): + ChartObject.__init__(self) + self._position = position + self._text = text + self._size = size + self._slant = slant + self._weight = weight + self._underline = underline + self._anchor = anchor + self._rotation = 0 + self._color = gtk.gdk.Color() + self._max_width = max_width + self._fixed = fixed + self._wrap = True + + self._real_dimensions = (0, 0) + self._real_position = (0, 0) + self._line_count = 1 + + self._context = None + self._layout = None + + def do_get_property(self, property): + if property.name == "visible": + return self._show + elif property.name == "antialias": + return self._antialias + elif property.name == "text": + return self._text + elif property.name == "color": + return self._color + elif property.name == "position": + return self._position + elif property.name == "anchor": + return self._anchor + elif property.name == "underline": + return self._underline + elif property.name == "max-width": + return self._max_width + elif property.name == "rotation": + return self._rotation + elif property.name == "size": + return self._size + elif property.name == "slant": + return self._slant + elif property.name == "weight": + return self._weight + elif property.name == "fixed": + return self._fixed + elif property.name == "wrap": + return self._wrap + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "visible": + self._show = value + elif property.name == "antialias": + self._antialias = value + elif property.name == "text": + self._text = value + elif property.name == "color": + self._color = value + elif property.name == "position": + self._position = value + elif property.name == "anchor": + self._anchor = value + elif property.name == "underline": + self._underline = value + elif property.name == "max-width": + self._max_width = value + elif property.name == "rotation": + self._rotation = value + elif property.name == "size": + self._size = value + elif property.name == "slant": + self._slant = value + elif property.name == "weight": + self._weight = value + elif property.name == "fixed": + self._fixed = value + elif property.name == "wrap": + self._wrap = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def _do_draw(self, context, rect): + self._do_draw_label(context, rect) + + def _do_draw_label(self, context, rect): + angle = 2 * math.pi * self._rotation / 360.0 + + if self._context == None: + label = gtk.Label() + self._context = label.create_pango_context() + pango_context = self._context + + attrs = pango.AttrList() + attrs.insert(pango.AttrWeight(self._weight, 0, len(self._text))) + attrs.insert(pango.AttrStyle(self._slant, 0, len(self._text))) + attrs.insert(pango.AttrUnderline(self._underline, 0, + len(self._text))) + if self._size != None: + attrs.insert(pango.AttrSize(1000 * self._size, 0, + len(self._text))) + + if self._layout == None: + self._layout = pango.Layout(pango_context) + layout = self._layout + layout.set_text(self._text) + layout.set_attributes(attrs) + + #find out where to draw the layout and calculate the maximum width + width = rect.width + if self._anchor in [ANCHOR_BOTTOM_LEFT, ANCHOR_TOP_LEFT, + ANCHOR_LEFT_CENTER]: + width = rect.width - self._position[0] + elif self._anchor in [ANCHOR_BOTTOM_RIGHT, ANCHOR_TOP_RIGHT, + ANCHOR_RIGHT_CENTER]: + width = self._position[0] + + text_width, text_height = layout.get_pixel_size() + width = width * math.cos(angle) + width = min(width, self._max_width) + + if self._wrap: + layout.set_wrap(pango.WRAP_WORD_CHAR) + layout.set_width(int(1000 * width)) + + x, y = get_text_pos(layout, self._position, self._anchor, angle) + + if not self._fixed: + #Find already drawn labels that would intersect with the current one + #and adjust position to avoid intersection. + text_width, text_height = layout.get_pixel_size() + real_width = abs(text_width * math.cos(angle)) + abs(text_height * math.sin(angle)) + real_height = abs(text_height * math.cos(angle)) + abs(text_width * math.sin(angle)) + + other_labels = get_registered_labels() + this_rect = gtk.gdk.Rectangle(int(x), int(y), int(real_width), int(real_height)) + for label in other_labels: + label_rect = label.get_allocation() + intersection = this_rect.intersect(label_rect) + if intersection.width == 0 and intersection.height == 0: + continue + + y_diff = 0 + if label_rect.y <= y and label_rect.y + label_rect.height >= y: + y_diff = y - label_rect.y + label_rect.height + elif label_rect.y > y and label_rect.y < y + real_height: + y_diff = label_rect.y - real_height - y + y += y_diff + + #draw layout + context.move_to(x, y) + context.rotate(angle) + context.set_source_rgb(*basics.color_gdk_to_cairo(self._color)) + context.show_layout(layout) + context.rotate(-angle) + context.stroke() + + #calculate the real dimensions + text_width, text_height = layout.get_pixel_size() + real_width = abs(text_width * math.cos(angle)) + abs(text_height * math.sin(angle)) + real_height = abs(text_height * math.cos(angle)) + abs(text_width * math.sin(angle)) + self._real_dimensions = real_width, real_height + self._real_position = x, y + self._line_count = layout.get_line_count() + + register_label(self) + + def get_calculated_dimensions(self, context, rect): + angle = 2 * math.pi * self._rotation / 360.0 + + if self._context == None: + label = gtk.Label() + self._context = label.create_pango_context() + pango_context = self._context + + attrs = pango.AttrList() + attrs.insert(pango.AttrWeight(self._weight, 0, len(self._text))) + attrs.insert(pango.AttrStyle(self._slant, 0, len(self._text))) + attrs.insert(pango.AttrUnderline(self._underline, 0, + len(self._text))) + if self._size != None: + attrs.insert(pango.AttrSize(1000 * self._size, 0, + len(self._text))) + + if self._layout == None: + self._layout = pango.Layout(pango_context) + layout = self._layout + + layout.set_text(self._text) + layout.set_attributes(attrs) + + #find out where to draw the layout and calculate the maximum width + width = rect.width + if self._anchor in [ANCHOR_BOTTOM_LEFT, ANCHOR_TOP_LEFT, + ANCHOR_LEFT_CENTER]: + width = rect.width - self._position[0] + elif self._anchor in [ANCHOR_BOTTOM_RIGHT, ANCHOR_TOP_RIGHT, + ANCHOR_RIGHT_CENTER]: + width = self._position[0] + + text_width, text_height = layout.get_pixel_size() + width = width * math.cos(angle) + width = min(width, self._max_width) + + if self._wrap: + layout.set_wrap(pango.WRAP_WORD_CHAR) + layout.set_width(int(1000 * width)) + + x, y = get_text_pos(layout, self._position, self._anchor, angle) + + if not self._fixed: + #Find already drawn labels that would intersect with the current one + #and adjust position to avoid intersection. + text_width, text_height = layout.get_pixel_size() + real_width = abs(text_width * math.cos(angle)) + abs(text_height * math.sin(angle)) + real_height = abs(text_height * math.cos(angle)) + abs(text_width * math.sin(angle)) + + other_labels = get_registered_labels() + this_rect = gtk.gdk.Rectangle(int(x), int(y), int(real_width), int(real_height)) + for label in other_labels: + label_rect = label.get_allocation() + intersection = this_rect.intersect(label_rect) + if intersection.width == 0 and intersection.height == 0: + continue + + y_diff = 0 + if label_rect.y <= y and label_rect.y + label_rect.height >= y: + y_diff = y - label_rect.y + label_rect.height + elif label_rect.y > y and label_rect.y < y + real_height: + y_diff = label_rect.y - real_height - y + y += y_diff + + #calculate the dimensions + text_width, text_height = layout.get_pixel_size() + real_width = abs(text_width * math.cos(angle)) + abs(text_height * math.sin(angle)) + real_height = abs(text_height * math.cos(angle)) + abs(text_width * math.sin(angle)) + return real_width, real_height + + def set_text(self, text): + """ + Use this method to set the text that should be displayed by + the label. + + @param text: the text to display. + @type text: string + """ + self.set_property("text", text) + self.emit("appearance_changed") + + def get_text(self): + """ + Returns the text currently displayed. + + @return: string. + """ + return self.get_property("text") + + def set_color(self, color): + """ + Set the color of the label. color has to be a gtk.gdk.Color. + + @param color: the color of the label + @type color: gtk.gdk.Color. + """ + self.set_property("color", color) + self.emit("appearance_changed") + + def get_color(self): + """ + Returns the current color of the label. + + @return: gtk.gdk.Color. + """ + return self.get_property("color") + + def set_position(self, pos): + """ + Set the position of the label. pos has to be a x,y pair of + absolute pixel coordinates on the widget. + The position is not the actual position but the position of the + Label's anchor point (see L{set_anchor} for details). + + @param pos: new position of the label + @type pos: pair of (x, y). + """ + self.set_property("position", pos) + self.emit("appearance_changed") + + def get_position(self): + """ + Returns the current position of the label. + + @return: pair of (x, y). + """ + return self.get_property("position") + + def set_anchor(self, anchor): + """ + Set the anchor point of the label. The anchor point is the a + point on the label's edge that has the position you set with + set_position(). + anchor has to be one of the following constants: + + - label.ANCHOR_BOTTOM_LEFT + - label.ANCHOR_TOP_LEFT + - label.ANCHOR_TOP_RIGHT + - label.ANCHOR_BOTTOM_RIGHT + - label.ANCHOR_CENTER + - label.ANCHOR_TOP_CENTER + - label.ANCHOR_BOTTOM_CENTER + - label.ANCHOR_LEFT_CENTER + - label.ANCHOR_RIGHT_CENTER + + The meaning of the constants is illustrated below::: + + + ANCHOR_TOP_LEFT ANCHOR_TOP_CENTER ANCHOR_TOP_RIGHT + * * * + ##################### + ANCHOR_LEFT_CENTER * # * # * ANCHOR_RIGHT_CENTER + ##################### + * * * + ANCHOR_BOTTOM_LEFT ANCHOR_BOTTOM_CENTER ANCHOR_BOTTOM_RIGHT + + The point in the center is of course referred to by constant + label.ANCHOR_CENTER. + + @param anchor: the anchor point of the label + @type anchor: one of the constants described above. + """ + + self.set_property("anchor", anchor) + self.emit("appearance_changed") + + def get_anchor(self): + """ + Returns the current anchor point that's used to position the + label. See L{set_anchor} for details. + + @return: one of the anchor constants described in L{set_anchor}. + """ + return self.get_property("anchor") + + def set_underline(self, underline): + """ + Set the underline style of the label. underline has to be one + of the following constants: + + - label.UNDERLINE_NONE: do not underline the text + - label.UNDERLINE_SINGLE: draw a single underline (the normal + underline method) + - label.UNDERLINE_DOUBLE: draw a double underline + - label.UNDERLINE_LOW; draw a single low underline. + + @param underline: the underline style + @type underline: one of the constants above. + """ + self.set_property("underline", underline) + self.emit("appearance_changed") + + def get_underline(self): + """ + Returns the current underline style. See L{set_underline} for + details. + + @return: an underline constant (see L{set_underline}). + """ + return self.get_property("underline") + + def set_max_width(self, width): + """ + Set the maximum width of the label in pixels. + + @param width: the maximum width + @type width: integer. + """ + self.set_property("max-width", width) + self.emit("appearance_changed") + + def get_max_width(self): + """ + Returns the maximum width of the label. + + @return: integer. + """ + return self.get_property("max-width") + + def set_rotation(self, angle): + """ + Use this method to set the rotation of the label in degrees. + + @param angle: the rotation angle + @type angle: integer in [0, 360]. + """ + self.set_property("rotation", angle) + self.emit("appearance_changed") + + def get_rotation(self): + """ + Returns the current rotation angle. + + @return: integer in [0, 360]. + """ + return self.get_property("rotation") + + def set_size(self, size): + """ + Set the size of the text in pixels. + + @param size: size of the text + @type size: integer. + """ + self.set_property("size", size) + self.emit("appearance_changed") + + def get_size(self): + """ + Returns the current size of the text in pixels. + + @return: integer. + """ + return self.get_property("size") + + def set_slant(self, slant): + """ + Set the font slant. slat has to be one of the following: + + - label.STYLE_NORMAL + - label.STYLE_OBLIQUE + - label.STYLE_ITALIC + + @param slant: the font slant style + @type slant: one of the constants above. + """ + self.set_property("slant", slant) + self.emit("appearance_changed") + + def get_slant(self): + """ + Returns the current font slant style. See L{set_slant} for + details. + + @return: a slant style constant. + """ + return self.get_property("slant") + + def set_weight(self, weight): + """ + Set the font weight. weight has to be one of the following: + + - label.WEIGHT_ULTRALIGHT + - label.WEIGHT_LIGHT + - label.WEIGHT_NORMAL + - label.WEIGHT_BOLD + - label.WEIGHT_ULTRABOLD + - label.WEIGHT_HEAVY + + @param weight: the font weight + @type weight: one of the constants above. + """ + self.set_property("weight", weight) + self.emit("appearance_changed") + + def get_weight(self): + """ + Returns the current font weight. See L{set_weight} for details. + + @return: a font weight constant. + """ + return self.get_property("weight") + + def set_fixed(self, fixed): + """ + Set whether the position of the label should be forced + (fixed=True) or if it should be positioned avoiding intersection + with other labels. + + @type fixed: boolean. + """ + self.set_property("fixed", fixed) + self.emit("appearance_changed") + + def get_fixed(self): + """ + Returns True if the label's position is forced. + + @return: boolean + """ + return self.get_property("fixed") + + def set_wrap(self, wrap): + """ + Set whether too long text should be wrapped. + + @type wrap: boolean. + """ + self.set_property("wrap", wrap) + self.emit("appearance_changed") + + def get_wrap(self): + """ + Returns True if too long text should be wrapped. + + @return: boolean. + """ + return self.get_property("wrap") + + def get_real_dimensions(self): + """ + This method returns a pair (width, height) with the dimensions + the label was drawn with. Call this method I{after} drawing + the label. + + @return: a (width, height) pair. + """ + return self._real_dimensions + + def get_real_position(self): + """ + Returns the position of the label where it was really drawn. + + @return: a (x, y) pair. + """ + return self._real_position + + def get_allocation(self): + """ + Returns an allocation rectangle. + + @return: gtk.gdk.Rectangle. + """ + x, y = self._real_position + w, h = self._real_dimensions + return gtk.gdk.Rectangle(int(x), int(y), int(w), int(h)) + + def get_line_count(self): + """ + Returns the number of lines. + + @return: int. + """ + return self._line_count + + +def get_text_pos(layout, pos, anchor, angle): + """ + This function calculates the position of bottom left point of the + layout respecting the given anchor point. + + @return: (x, y) pair + """ + text_width_n, text_height_n = layout.get_pixel_size() + text_width = text_width_n * abs(math.cos(angle)) + text_height_n * abs(math.sin(angle)) + text_height = text_height_n * abs(math.cos(angle)) + text_width_n * abs(math.sin(angle)) + height_delta = text_height - text_height_n + x, y = pos + ref = (0, -text_height) + if anchor == ANCHOR_TOP_LEFT: + ref = (0, 0) + elif anchor == ANCHOR_TOP_RIGHT: + ref = (-text_width, height_delta) + elif anchor == ANCHOR_BOTTOM_RIGHT: + ref = (-text_width, -text_height) + elif anchor == ANCHOR_CENTER: + ref = (-text_width / 2, -text_height / 2) + elif anchor == ANCHOR_TOP_CENTER: + ref = (-text_width / 2, 0) + elif anchor == ANCHOR_BOTTOM_CENTER: + ref = (-text_width / 2, -text_height) + elif anchor == ANCHOR_LEFT_CENTER: + ref = (0, -text_height / 2) + elif anchor == ANCHOR_RIGHT_CENTER: + ref = (-text_width, -text_height / 2) + x += ref[0] + y += ref[1] + return x, y diff --git a/pygtk_chart/label.pyc b/pygtk_chart/label.pyc Binary files differnew file mode 100644 index 0000000..d0af0d8 --- /dev/null +++ b/pygtk_chart/label.pyc diff --git a/pygtk_chart/multi_bar_chart.py b/pygtk_chart/multi_bar_chart.py new file mode 100644 index 0000000..09e6a14 --- /dev/null +++ b/pygtk_chart/multi_bar_chart.py @@ -0,0 +1,649 @@ +# Copyright 2009 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 MultiBarChart widget. + +Author: Sven Festersen (sven@sven-festersen.de) +""" +__docformat__ = "epytext" +import cairo +import gtk +import gobject +import os +import math + +import pygtk_chart +from pygtk_chart.basics import * +from pygtk_chart import bar_chart +from pygtk_chart.chart_object import ChartObject +from pygtk_chart import chart +from pygtk_chart import label + +MODE_VERTICAL = 0 +MODE_HORIZONTAL = 1 + +COLOR_AUTO = 0 +COLORS = gdk_color_list_from_file(os.sep.join([os.path.dirname(__file__), "data", "tango.color"])) + + +class Bar(bar_chart.Bar): + """ + This is a special version of the bar_chart.Bar class that draws the + bars on a MultiBarChart widget. + + Properties + ========== + This class inherits properties from bar_chart.Bar. + + Signals + ======= + This class inherits signals from bar_chart.Bar. + """ + + def __init__(self, name, value, title=""): + bar_chart.Bar.__init__(self, name, value, title) + + #drawing methods + def _do_draw(self, context, rect, group, bar_count, n, i, m, j, mode, group_padding, bar_padding, maximum_value, group_end, value_label_size, label_size, label_rotation, draw_labels): + if mode == MODE_VERTICAL: + return self._do_draw_multi_vertical(context, rect, group, bar_count, n, i, m, j, mode, group_padding, bar_padding, maximum_value, group_end, value_label_size, label_size, label_rotation, draw_labels) + elif mode == MODE_HORIZONTAL: + return self._do_draw_multi_horizontal(context, rect, group, bar_count, n, i, m, j, mode, group_padding, bar_padding, maximum_value, group_end, value_label_size, label_size, label_rotation, draw_labels) + + def _do_draw_multi_vertical(self, context, rect, group, bar_count, n, i, m, j, mode, group_padding, bar_padding, maximum_value, group_end, value_label_size, label_size, label_rotation, draw_labels): + bar_width = (rect.width - (bar_count - n) * bar_padding - (n - 1) * group_padding) / bar_count + bar_height = (rect.height - value_label_size - label_size) * self._value / maximum_value + bar_x = group_end + j * (bar_width + bar_padding) + bar_y = rect.y + rect.height - bar_height - label_size + context.set_source_rgb(*color_gdk_to_cairo(self._color)) + bar_chart.draw_rounded_rectangle(context, bar_x, bar_y, bar_width, bar_height, self._corner_radius) + context.fill() + + chart.add_sensitive_area(chart.AREA_RECTANGLE, (bar_x, bar_y, bar_width, bar_height), (group, self)) + + if self._highlighted: + context.set_source_rgba(1, 1, 1, 0.1) + bar_chart.draw_rounded_rectangle(context, bar_x, bar_y, bar_width, bar_height, self._corner_radius) + context.fill() + + if draw_labels: + #draw the value label + self._value_label_object.set_max_width(bar_width) + self._value_label_object.set_text(str(self._value)) + self._value_label_object.set_color(self._color) + self._value_label_object.set_position((bar_x + bar_width / 2, bar_y - 3)) + self._value_label_object.set_anchor(label.ANCHOR_BOTTOM_CENTER) + self._value_label_object.draw(context, rect) + context.fill() + + #draw label + self._label_object.set_rotation(label_rotation) + self._label_object.set_wrap(False) + self._label_object.set_color(self._color) + self._label_object.set_fixed(True) + self._label_object.set_max_width(3 * bar_width) + self._label_object.set_text(self._label) + self._label_object.set_position((bar_x + bar_width / 2 + 5, bar_y + bar_height + 8)) + self._label_object.set_anchor(label.ANCHOR_TOP_RIGHT) + self._label_object.draw(context, rect) + context.fill() + + return bar_x + bar_width + + def _do_draw_multi_horizontal(self, context, rect, group, bar_count, n, i, m, j, mode, group_padding, bar_padding, maximum_value, group_end, value_label_size, label_size, label_rotation, draw_labels): + bar_height = (rect.height - (bar_count - n) * bar_padding - (n - 1) * group_padding) / bar_count + bar_width = (rect.width - value_label_size - label_size) * self._value / maximum_value + bar_x = rect.x + label_size + bar_y = group_end + j * (bar_height + bar_padding) + context.set_source_rgb(*color_gdk_to_cairo(self._color)) + bar_chart.draw_rounded_rectangle(context, bar_x, bar_y, bar_width, bar_height, self._corner_radius) + context.fill() + + chart.add_sensitive_area(chart.AREA_RECTANGLE, (bar_x, bar_y, bar_width, bar_height), (group, self)) + + if self._highlighted: + context.set_source_rgba(1, 1, 1, 0.1) + bar_chart.draw_rounded_rectangle(context, bar_x, bar_y, bar_width, bar_height, self._corner_radius) + context.fill() + + if draw_labels: + #draw the value label + self._value_label_object.set_text(str(self._value)) + self._value_label_object.set_wrap(False) + self._value_label_object.set_color(self._color) + self._value_label_object.set_position((bar_x + bar_width + 3, bar_y + bar_height / 2)) + self._value_label_object.set_anchor(label.ANCHOR_LEFT_CENTER) + self._value_label_object.draw(context, rect) + context.fill() + + #draw label + self._label_object.set_rotation(0) + self._label_object.set_wrap(False) + self._label_object.set_color(self._color) + self._label_object.set_fixed(True) + self._label_object.set_max_width(0.25 * rect.width) + self._label_object.set_text(self._label) + self._label_object.set_position((bar_x - 3, bar_y + bar_height / 2)) + self._label_object.set_anchor(label.ANCHOR_RIGHT_CENTER) + self._label_object.draw(context, rect) + context.fill() + + return bar_y + bar_height + + def get_value_label_size(self, context, rect, mode, bar_count, n, group_padding, bar_padding): + if mode == MODE_VERTICAL: + bar_width = (rect.width - (bar_count - n) * bar_padding - (n - 1) * group_padding) / bar_count + self._value_label_object.set_max_width(bar_width) + self._value_label_object.set_text(str(self._value)) + return self._value_label_object.get_calculated_dimensions(context, rect)[1] + elif mode == MODE_HORIZONTAL: + self._value_label_object.set_wrap(False) + self._value_label_object.set_fixed(True) + self._value_label_object.set_text(str(self._value)) + return self._value_label_object.get_calculated_dimensions(context, rect)[0] + + def get_label_size(self, context, rect, mode, bar_count, n, group_padding, bar_padding, label_rotation): + if mode == MODE_VERTICAL: + bar_width = (rect.width - (bar_count - n) * bar_padding - (n - 1) * group_padding) / bar_count + self._label_object.set_rotation(label_rotation) + self._label_object.set_wrap(False) + self._label_object.set_fixed(True) + self._label_object.set_max_width(3 * bar_width) + self._label_object.set_text(self._label) + return self._label_object.get_calculated_dimensions(context, rect)[1] + elif mode == MODE_HORIZONTAL: + self._label_object.set_max_width(0.25 * rect.width) + self._label_object.set_text(self._label) + return self._label_object.get_calculated_dimensions(context, rect)[0] + + + +class BarGroup(ChartObject): + """ + This class represents a group of bars on the MultiBarChart widget. + + Properties + ========== + This class has the following properties: + - name (a unique identifier for the group, type: string) + - title (a title for the group, type: string) + - bar-padding (the space between two bars of the group in px, + type: int in [0, 100]) + - bars (a list of the bars in the group, read only) + - maximum-value (the maximum value of the bars in the group, read + only) + - bar-count (the number of bars in the group, read only). + + Signals + ======= + The BarGroup class inherits signals from chart_object.ChartObject. + """ + + __gproperties__ = {"name": (gobject.TYPE_STRING, "group name", + "A unique identifier for this group.", + "", gobject.PARAM_READABLE), + "title": (gobject.TYPE_STRING, "group title", + "The group's title.", "", + gobject.PARAM_READWRITE), + "bar-padding": (gobject.TYPE_INT, "bar padding", + "The space between bars in this group.", + 0, 100, 2, gobject.PARAM_READWRITE), + "bars": (gobject.TYPE_PYOBJECT, "bars in the group", + "A list of bars in this group.", + gobject.PARAM_READABLE), + "maximum-value": (gobject.TYPE_FLOAT, "max value", + "The maximum value of the bars in this group.", + 0, 9999999, 0, gobject.PARAM_READABLE), + "bar-count": (gobject.TYPE_INT, "bar count", + "The number of bars in this group.", + 0, 100, 0, gobject.PARAM_READWRITE)} + + def __init__(self, name, title=""): + ChartObject.__init__(self) + #private properties: + self._group_label_object = label.Label((0, 0), title) + #gobject properties: + self._bars = [] + self._name = name + self._title = title + self._bar_padding = 2 + + #gobject set_* and get_* methods + def do_get_property(self, property): + if property.name == "visible": + return self._show + elif property.name == "antialias": + return self._antialias + elif property.name == "name": + return self._name + elif property.name == "title": + return self._title + elif property.name == "bar-padding": + return self._bar_padding + elif property.name == "bars": + return self._bars + elif property.name == "maximum-value": + return max(bar.get_value() for bar in self._bars) + elif property.name == "bar-count": + return len(self._bars) + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "visible": + self._show = value + elif property.name == "antialias": + self._antialias = value + elif property.name == "name": + self._name = value + elif property.name == "title": + self._title = value + elif property.name == "bar-padding": + self._bar_padding = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def get_bar_count(self): + """ + Returns the number of bars in this group. + + @return: int in [0, 100]. + """ + return self.get_property("bar-count") + + def get_maximum_value(self): + """ + Returns the maximum value of the bars in this group. + + @return: float. + """ + return self.get_property("maximum-value") + + def get_bars(self): + """ + Returns a list of the bars in this group. + + @return: list of multi_bar_chart.Bar. + """ + return self.get_property("bars") + + def get_name(self): + """ + Returns the name (a unique identifier) of this group. + + @return: string. + """ + return self.get_property("name") + + def set_title(self, title): + """ + Set the title of the group. + + @param title: the new title + @type title: string. + """ + self.set_property("title", title) + self.emit("appearance_changed") + + def get_title(self): + """ + Returns the title of the group. + + @return: string. + """ + return self.get_property("title") + + def get_label(self): + """ + Alias for get_title. + + @return: string. + """ + return self.get_title() + + def set_bar_padding(self, padding): + """ + Set the distance between two bars in this group (in px). + + @param padding: the padding in px + @type padding: int in [0, 100]. + """ + self.set_property("bar-padding", padding) + self.emit("appearance_changed") + + def get_bar_padding(self): + """ + Returns the distance of two bars in the group (in px). + + @return: int in [0, 100]. + """ + return self.get_property("bar-padding") + + #drawing methods + def _do_draw(self, context, rect, bar_count, n, i, mode, group_padding, maximum_value, group_end, value_label_size, label_size, label_rotation, draw_labels, rotate_label_horizontal): + end = group_end + for j, bar in enumerate(self._bars): + end = bar.draw(context, rect, self, bar_count, n, i, len(self._bars), j, mode, group_padding, self._bar_padding, maximum_value, group_end, value_label_size, label_size, label_rotation, draw_labels) + + if draw_labels and mode == MODE_VERTICAL: + context.set_source_rgb(0, 0, 0) + group_width = end - group_end + self._group_label_object.set_text(self._title) + self._group_label_object.set_fixed(True) + self._group_label_object.set_max_width(group_width) + self._group_label_object.set_position((group_end + group_width / 2, rect.y + rect.height)) + self._group_label_object.set_anchor(label.ANCHOR_BOTTOM_CENTER) + self._group_label_object.draw(context, rect) + context.fill() + elif draw_labels and mode == MODE_HORIZONTAL: + context.set_source_rgb(0, 0, 0) + group_height = end - group_end + if rotate_label_horizontal: + self._group_label_object.set_rotation(90) + offset = self.get_group_label_size(context, rect, mode, rotate_label_horizontal) #fixes postioning bug + else: + self._group_label_object.set_rotation(0) + offset = 0 + self._group_label_object.set_text(self._title) + self._group_label_object.set_wrap(False) + self._group_label_object.set_fixed(True) + self._group_label_object.set_position((rect.x + offset, group_end + group_height / 2)) + self._group_label_object.set_anchor(label.ANCHOR_LEFT_CENTER) + self._group_label_object.draw(context, rect) + context.fill() + + return end + group_padding + + #other methods + def add_bar(self, bar): + """ + Add a bar to the group. + + @param bar: the bar to add + @type bar: multi_bar_chart.Bar. + """ + if bar.get_color() == COLOR_AUTO: + bar.set_color(COLORS[len(self._bars) % len(COLORS)]) + self._bars.append(bar) + self.emit("appearance_changed") + + def get_value_label_size(self, context, rect, mode, bar_count, n, group_padding, bar_padding): + value_label_size = 0 + for bar in self._bars: + value_label_size = max(value_label_size, bar.get_value_label_size(context, rect, mode, bar_count, n, group_padding, bar_padding)) + return value_label_size + + def get_label_size(self, context, rect, mode, bar_count, n, group_padding, bar_padding, label_rotation): + label_size = 0 + for bar in self._bars: + label_size = max(label_size, bar.get_label_size(context, rect, mode, bar_count, n, group_padding, bar_padding, label_rotation)) + return label_size + + def get_group_label_size(self, context, rect, mode, rotate_label_horizontal): + self._group_label_object.set_text(self._title) + if mode == MODE_VERTICAL: + return self._group_label_object.get_calculated_dimensions(context, rect)[1] + elif mode == MODE_HORIZONTAL: + if rotate_label_horizontal: + self._group_label_object.set_rotation(90) + else: + self._group_label_object.set_rotation(0) + self._group_label_object.set_wrap(False) + return self._group_label_object.get_calculated_dimensions(context, rect)[0] + + +class MultiBarChart(bar_chart.BarChart): + """ + The MultiBarChart widget displays groups of bars. + Usage: create multi_bar_chart.BarGroups and + add multi_bar_chart.Bars. The add the bar groups to MultiBarChart. + + Properties + ========== + The MultiBarChart class inherits properties from bar_chart.BarChart + (except bar-padding). Additional properties: + - group-padding (the space between two bar groups in px, type: int + in [0, 100], default: 16) + - label-rotation (the angle (in degrees) that should be used to + rotate bar labels in vertical mode, type: int in [0, 360], + default: 300) + - rotate-group-labels (sets whether group labels should be roteated + by 90 degrees in horizontal mode, type: boolean, default: False). + + Signals + ======= + The MultiBarChart class inherits the signal 'bar-clicked' from + bar_chart.BarChart. Additional signals: + - group-clicked: emitted when a bar is clicked, callback signature: + def group_clicked(chart, group, bar). + """ + + __gsignals__ = {"group-clicked": (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT))} + + __gproperties__ = {"group-padding": (gobject.TYPE_INT, "group padding", + "The space between two bar groups.", + 0, 100, 16, gobject.PARAM_READWRITE), + "label-rotation": (gobject.TYPE_INT, "label rotation", + "The angle that should bar labels be rotated by in vertical mode.", + 0, 360, 300, gobject.PARAM_READWRITE), + "rotate-group-labels": (gobject.TYPE_BOOLEAN, + "rotate group label", + "Sets whether the group label should be rotated by 90 degrees in horizontal mode.", + False, gobject.PARAM_READWRITE), + "mode": (gobject.TYPE_INT, "mode", + "The chart's mode.", 0, 1, 0, + gobject.PARAM_READWRITE), + "draw-labels": (gobject.TYPE_BOOLEAN, + "draw labels", "Set whether to draw labels on bars.", + True, gobject.PARAM_READWRITE), + "enable-mouseover": (gobject.TYPE_BOOLEAN, "enable mouseover", + "Set whether to enable mouseover effect.", + True, gobject.PARAM_READWRITE)} + + def __init__(self): + bar_chart.BarChart.__init__(self) + #private properties: + self._groups = [] + #gobject properties: + self._group_padding = 16 + self._label_rotation = 300 + self._rotate_group_label_in_horizontal_mode = False + + #gobject set_* and get_* methods + def do_get_property(self, property): + if property.name == "group-padding": + return self._group_padding + elif property.name == "label-rotation": + return self._label_rotation + elif property.name == "rotate-group-labels": + return self._rotate_group_label_in_horizontal_mode + elif property.name == "mode": + return self._mode + elif property.name == "draw-labels": + return self._draw_labels + elif property.name == "enable-mouseover": + return self._mouseover + else: + raise AttributeError, "Property %s does not exist." % property.name + + def do_set_property(self, property, value): + if property.name == "group-padding": + self._group_padding = value + elif property.name == "label-rotation": + self._label_rotation = value + elif property.name == "rotate-group-labels": + self._rotate_group_label_in_horizontal_mode = value + elif property.name == "mode": + self._mode = value + elif property.name == "draw-labels": + self._draw_labels = value + elif property.name == "enable-mouseover": + self._mouseover = value + else: + raise AttributeError, "Property %s does not exist." % property.name + + def set_group_padding(self, padding): + """ + Set the amount of free space between bar groups (in px, + default: 16). + + @param padding: the padding + @type padding: int in [0, 100]. + """ + self.set_property("group-padding", padding) + self.queue_draw() + + def get_group_padding(self): + """ + Returns the amount of free space between two bar groups (in px). + + @return: int in [0, 100]. + """ + return self.get_property("group-padding") + + def set_label_rotation(self, angle): + """ + Set the abgle (in degrees) that should be used to rotate the + bar labels in vertical mode (defualt: 300 degrees). + + @type angle: int in [0, 360]. + """ + self.set_property("label-rotation", angle) + self.queue_draw() + + def get_label_rotation(self): + """ + Returns the angle by which bar labels are rotated in vertical + mode. + + @return: int in [0, 350]. + """ + return self.get_property("label-rotation") + + def set_rotate_group_labels(self, rotate): + """ + Set wether the groups' labels should be rotated by 90 degrees in + horizontal mode (default: False). + + @type rotate: boolean. + """ + self.set_property("rotate-group-labels", rotate) + self.queue_draw() + + def get_rotate_group_labels(self): + """ + Returns True if group labels should be rotated by 90 degrees + in horizontal mode. + + @return: boolean. + """ + return self.get_property("rotate-group-labels") + + #callbacks + def _cb_motion_notify(self, widget, event): + if not self._mouseover: return + active = chart.get_sensitive_areas(event.x, event.y) + if active == []: return + for group in self._groups: + for bar in group.get_bars(): + bar.set_highlighted((group, bar) in active) + self.queue_draw() + + def _cb_button_pressed(self, widget, event): + active = chart.get_sensitive_areas(event.x, event.y) + for group, bar in active: + self.emit("group-clicked", group, bar) + self.emit("bar-clicked", bar) + + #drawing methods + def _do_draw_groups(self, context, rect, maximum_value, value_label_size, label_size, bar_count): + if self._groups == []: return + + if self._mode == MODE_VERTICAL: + group_end = rect.x + else: + group_end = rect.y + + for i, group in enumerate(self._groups): + group_end = group.draw(context, rect, bar_count, len(self._groups), i, self._mode, self._group_padding, maximum_value, group_end, value_label_size, label_size, self._label_rotation, self._draw_labels, self._rotate_group_label_in_horizontal_mode) + + 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() + chart.init_sensitive_areas() + + rect = self.get_allocation() + rect = gtk.gdk.Rectangle(0, 0, rect.width, rect.height) #transform rect to context coordinates + context.set_line_width(1) + + rect = self.draw_basics(context, rect) + + maximum_value = max(group.get_maximum_value() for group in self._groups) + bar_count = 0 + for group in self._groups: bar_count += group.get_bar_count() + + value_label_size = 0 + if self._draw_labels: + for group in self._groups: + value_label_size = max(value_label_size, group.get_value_label_size(context, rect, self._mode, bar_count, len(self._groups), self._group_padding, self._bar_padding)) + + label_size = 0 + if self._draw_labels: + for group in self._groups: + label_size = max(label_size, group.get_label_size(context, rect, self._mode, bar_count, len(self._groups), self._group_padding, self._bar_padding, self._label_rotation)) + label_size += 10 + label_size += group.get_group_label_size(context, rect, self._mode, self._rotate_group_label_in_horizontal_mode) + + rect = self._do_draw_grid(context, rect, maximum_value, value_label_size, label_size) + self._do_draw_groups(context, rect, maximum_value, value_label_size, label_size, bar_count) + + label.finish_drawing() + n = len(self._groups) + if self._mode == MODE_VERTICAL: + minimum_width = rect.x + self._padding + bar_count * 10 + n * self._group_padding + minimum_height = rect.y + self._padding + 200 + elif self._mode == MODE_HORIZONTAL: + minimum_width = rect.x + self._padding + 200 + minimum_height = rect.y + self._padding + bar_count * 10 + n * self._group_padding + self.set_size_request(minimum_width, minimum_height) + + #other methods + def add_group(self, group): + """ + Add a BarGroup to the chart. + + @type group: multi_bar_chart.BarGroup. + """ + self._groups.append(group) + self.queue_draw() + + def add_bar(self, bar): + """ + Alias for add_group. + This method is deprecated. Use add_group instead. + """ + print "MultiBarChart.add_bar is deprecated. Use add_group instead." + self.add_group(bar) diff --git a/pygtk_chart/multi_bar_chart.pyc b/pygtk_chart/multi_bar_chart.pyc Binary files differnew file mode 100644 index 0000000..bceafe1 --- /dev/null +++ b/pygtk_chart/multi_bar_chart.pyc 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") + |